Table of Contents

Software Localization Using dxgettext

Basic Setup

Extract Strings

  • Run application and test: The application now uses the translation when the Windows settings corresponds to the two-letter language code chosen. If a language needs to be used in spite of the Windows settings, use function call UseLanguage(). For example:
    UseLanguage('es');  // es = Spanish 

    This line is added earlier in the project, for example:

     program Project1;
     
    uses
      gnugettext in 'gnugettext.pas',
      Forms,
      Unit1 in 'Unit1.pas' {Form1};
     
    {$R *.res}
     
    begin
      // Add extra domain for runtime library translations
      AddDomainForResourceString ('delphi');
     
      // Force program to use Danish instead of the current Windows settings
      UseLanguage ('da');
     
      // Put ignores on the properties that cannot be translated
      TP_GlobalIgnoreClassProperty (TMyComponent1,'property1');
      TP_GlobalIgnoreClassProperty (TMyComponent2,'property2');
      TP_GlobalIgnoreClassProperty (TMyComponent3,'property3');
      TP_GlobalIgnoreClassProperty (TMyComponent4,'property4');
     
      Application.Initialize;
      Application.CreateForm(TForm1, Form1);
      Application.Run;
    end. 
  • For some languages (eg. Hebrew, Russian (Cyrillic)) even doing all of this does not change the ??? text (Delphi 7 bug). You would need to include the following in the Delphi project file (.dpr) at the beginning of the initialization code:
      // Localization: force to use Locale System Default so all text is displayed correctly
      // to fix Delphi7 bug (see Bug Report 2460: http://qc.borland.com/wc/qcmain.aspx?d=2460)
      // Not so much an issue with Latin characters, but with Cyrillic (Russian) characters.
      // For full list of locales, open registry HKEY_CLASSES_ROOT\MIME\DataBase\Rfc1766
      //ShowMessage(format('GetThreadLocale = %d, GetSystemDefaultLCID = %d, GetUserDefaultLCID = %d', 
      //    [GetThreadLocale, GetSystemDefaultLCID, GetUserDefaultLCID]));
      SetThreadLocale(LOCALE_SYSTEM_DEFAULT);   // same as SetThreadLocale($0419); 
  • If there is a need to change to Unicode, try the UTF-8 VCL utility which changes your application to Unicode without modifying your code: http://sourceforge.net/projects/utf8vcl. For example, in the project file:
    program UTF8Test;
     
    {%File 'MissingMethods-incomplete.txt'}         // used by UTF8
    {%File 'MissingWindowMessages-incomplete.txt'}  // used by UTF8
     
    uses
      ShareMem,
      UTF8VCLMessages in 'UTF8VCLMessages.pas',
      UTF8VCL in 'UTF8VCL.pas',
      UTF8VCLUtils in 'UTF8VCLUtils.pas',
      UTF8VCLControls in 'UTF8VCLControls.pas',
      Forms,
      TestForm in 'TestForm.pas' {Form1};
     
    {$R *.res}
     
    begin
      Application.Initialize;
      Application.CreateForm(TForm1, Form1);
      Application.Run;
    end.

    Then add the following to the form needing UTF8 support:

    uses
      UTF8VCL;

Modify Source Code

  1. Add gnugettext to the project and the uses section in your source code. 2)
  2. Add the following to the OnFormCreate event in every form in the project:
     TranslateComponent (self);
  3. Add common ignores before the call to TranslatedComponent(self) function. Comment out the classes or properties not used in the current form or unit:
    //--------------------------------------
    // VCL, important ones
    //--------------------------------------
    TP_GlobalIgnoreClassProperty(TAction,'Category'); 
    TP_GlobalIgnoreClassProperty(TControl,'HelpKeyword');
    TP_GlobalIgnoreClassProperty(TNotebook,'Pages');
     
    //--------------------------------------
    // VCL, not so important
    //--------------------------------------
    // These are normally not needed.
    TP_GlobalIgnoreClassProperty(TControl,'ImeName');
    TP_GlobalIgnoreClass(TFont);
     
    //--------------------------------------
    // Database (DB unit)
    //--------------------------------------
    // Field names and table names often tend to have names that are also used 
    // for other purposes elsewhere in the program. Therefore, it is very wise 
    // to add this somewhere in your program if you are using databases.
    TP_GlobalIgnoreClassProperty(TField,'DefaultExpression');
    TP_GlobalIgnoreClassProperty(TField,'FieldName');
    TP_GlobalIgnoreClassProperty(TField,'KeyFields');
    TP_GlobalIgnoreClassProperty(TField,'DisplayName');
    TP_GlobalIgnoreClassProperty(TField,'LookupKeyFields');
    TP_GlobalIgnoreClassProperty(TField,'LookupResultField');
    TP_GlobalIgnoreClassProperty(TField,'Origin');
    TP_GlobalIgnoreClass(TParam);
    TP_GlobalIgnoreClassProperty(TFieldDef,'Name');
     
    //--------------------------------------
    // MIDAS/Datasnap
    //--------------------------------------
    TP_GlobalIgnoreClassProperty(TClientDataset,'CommandText');
    TP_GlobalIgnoreClassProperty(TClientDataset,'Filename');
    TP_GlobalIgnoreClassProperty(TClientDataset,'Filter');
    TP_GlobalIgnoreClassProperty(TClientDataset,'IndexFieldnames');
    TP_GlobalIgnoreClassProperty(TClientDataset,'IndexName');
    TP_GlobalIgnoreClassProperty(TClientDataset,'MasterFields');
    TP_GlobalIgnoreClassProperty(TClientDataset,'Params');
    TP_GlobalIgnoreClassProperty(TClientDataset,'ProviderName');
     
    //--------------------------------------
    // Database controls
    //--------------------------------------
    TP_GlobalIgnoreClassProperty(TDBComboBox,'DataField');
    TP_GlobalIgnoreClassProperty(TDBCheckBox,'DataField');
    TP_GlobalIgnoreClassProperty(TDBEdit,'DataField');
    TP_GlobalIgnoreClassProperty(TDBImage,'DataField');
    TP_GlobalIgnoreClassProperty(TDBListBox,'DataField');
    TP_GlobalIgnoreClassProperty(TDBLookupControl,'DataField');
    TP_GlobalIgnoreClassProperty(TDBLookupControl,'KeyField');
    TP_GlobalIgnoreClassProperty(TDBLookupControl,'ListField');
    TP_GlobalIgnoreClassProperty(TDBMemo,'DataField');
    TP_GlobalIgnoreClassProperty(TDBRadioGroup,'DataField');
    TP_GlobalIgnoreClassProperty(TDBRichEdit,'DataField');
    TP_GlobalIgnoreClassProperty(TDBText,'DataField');
     
    //--------------------------------------
    // Interbase Express (IBX)
    //--------------------------------------
    TP_GlobalIgnoreClass(TIBDatabase);
    TP_GlobalIgnoreClass(TIBDatabase);
    TP_GlobalIgnoreClass(TIBTransaction);
    TP_GlobalIgnoreClassProperty(TIBSQL,'UniqueRelationName');
     
    //--------------------------------------
    // Borland Database Engine
    //--------------------------------------
    TP_GlobalIgnoreClass(TSession);
    TP_GlobalIgnoreClass(TDatabase);
     
    //--------------------------------------
    // ADO components
    //--------------------------------------
    TP_GlobalIgnoreClass (TADOConnection);
    TP_GlobalIgnoreClassProperty(TADOQuery,'CommandText');
    TP_GlobalIgnoreClassProperty(TADOQuery,'ConnectionString');
    TP_GlobalIgnoreClassProperty(TADOQuery,'DatasetField');
    TP_GlobalIgnoreClassProperty(TADOQuery,'Filter');
    TP_GlobalIgnoreClassProperty(TADOQuery,'IndexFieldNames');
    TP_GlobalIgnoreClassProperty(TADOQuery,'IndexName');
    TP_GlobalIgnoreClassProperty(TADOQuery,'MasterFields');
    TP_GlobalIgnoreClassProperty(TADOTable,'IndexFieldNames');
    TP_GlobalIgnoreClassProperty(TADOTable,'IndexName');
    TP_GlobalIgnoreClassProperty(TADOTable,'MasterFields');
    TP_GlobalIgnoreClassProperty(TADOTable,'TableName');
    TP_GlobalIgnoreClassProperty(TADODataset,'CommandText');
    TP_GlobalIgnoreClassProperty(TADODataset,'ConnectionString');
    TP_GlobalIgnoreClassProperty(TADODataset,'DatasetField');
    TP_GlobalIgnoreClassProperty(TADODataset,'Filter');
    TP_GlobalIgnoreClassProperty(TADODataset,'IndexFieldNames');
    TP_GlobalIgnoreClassProperty(TADODataset,'IndexName');
    TP_GlobalIgnoreClassProperty(TADODataset,'MasterFields');
     
    //--------------------------------------
    // ActiveX stuff
    //--------------------------------------
    TP_GlobalIgnoreClass (TWebBrowser);
     
    //--------------------------------------
    // Other
    //--------------------------------------
    TP_GlobalIgnoreClassProperty(TwwKeyCombo,'Text');
    TP_GlobalIgnoreClassProperty(TdxBarManager, 'Categories');    // TdxBarManager prop generates memory leak
    TP_GlobalIgnoreClassProperty(TdxBarManager, 'IniFileName');   // TdxBarManager prop generates memory leak
    TP_GlobalIgnoreClassProperty(TdxBarManager, 'RegistryPath');  // TdxBarManager prop generates memory leak

Common Issues or Tasks

Automate Build Process

Perform the following steps to automate the build process of localization. These steps assume the existence of translation files translation-default.po and translation-ignore.po:

  • Generate new translation template (extract strings) from new compiled application. This creates a new default.po file.
    C:\> msg 
  • Verify that there are only unique msgid entries in default.po.
    C:\> msguniq -o output.po default.po
  • Fix broken lines generated by msguniq utility, by using poStripAllCR.exe (custom application). poStripAllCR.exe will strip unnecessary carriage returns (CR) characters that break '#: filename:line' lines.
  • Merge translation file into new translation template.
    C:\> msgmerge -o output.po def.po ref.pot
  • Copy new translation file to locale directory.
  • Optionally, run the application with a specified locale to test it.

Localizing a non-English program

  1. Extract all strings from non-English program as it is now (Explorer > select FOLDER where executable is found > Extract translations to template). 3)
  2. Translate your program to English and compile.
  3. Extract all strings from the new English program.
  4. Use the msgmergePOT tool to create an English→YourLanguage translation file.
C:\> msgmergePOT english.po russian.po english-russian.po

Now you have an English language program with a translation to the language that your program used before.

Creating a translation file where all translations are a copy of the original but fuzzy

  1. Use msgen, a program that creates an English translation file, where all translations are a copy of the original, but marked as fuzzy. A translator can then run through all messages and correct any typing errors or improve any words, if needed. 4)
C:\> msgen english-empty-translation.po -o english-fuzzy-translation.po -s

Creating a translation file from a text import

  1. Create spreadsheet.
  2. Add data in two that contain msgid and msgstr. Text in the msgstr must be formatted in UTF-8. NOTE: It does not support multiline msgstr values.
  3. Verify that column msgstr is not empty in any row (to avoid “Error: No tabulator in line” message).
  4. Save file as a text-file, tabulator separated with no text delimiters.
  5. Open the file in a UTF-8 text editor (such as Notepad++), and save the file as an UTF-8 text file with “no BOM” (Notepad++ > Format > Encode in ANSI > UTF-8 without BOM).
C:\> msgimport textfile.txt -o output.po

Avoiding Conflicts

To distinguish fields that need translation from the ones that do not, follow these guidelines:

  • Create unique database field names. Eg: User (bad name) → FUSER (better name)
1)
Online User Documentation, GNU gettext for Delphi, C++ Builder and Kylix 1.1.1, http://dybdahl.dk/dxgettext/docs/online/howtochapter.html#SCREENSHOT-EXTRACTFILES
2)
“How to do a translation”, GNU gettext for Delphi, C++ Builder and Kylix 1.1.1, http://dybdahl.dk/dxgettext/docs/howto.php
3)
Online User Documentation, Frequently Asked Questions, GNU gettext for Delphi, C++ Builder and Kylix 1.1.1, http://dybdahl.dk/dxgettext/docs/online/faq.html#AEN1841
4)
Online User Documentation, GNU gettext for Delphi, C++ Builder and Kylix 1.1.1, http://dybdahl.dk/dxgettext/docs/online/msgen.html