== Software Localization Using dxgettext == === Basic Setup === ==== Extract Strings ==== * Generate translation template: right-click the FOLDER that contains the Delphi source code, and choose "Extract translations to template". ((Online User Documentation, GNU gettext for Delphi, C++ Builder and Kylix 1.1.1, http://dybdahl.dk/dxgettext/docs/online/howtochapter.html#SCREENSHOT-EXTRACTFILES)). This will create a file named ''default.po'' with all the texts to be translated. Because it has no translations in it, it is named a "translation template". You should download poEdit and use this program to translate the messages in the po file. * Compile translation: right-click on the translation filename (in the Windows Explorer), and choose "Compile to mo file". This will compile the po file and generate the binary mo file needed by your application. * Place files in ''locale'' folder: Put the ''default.mo'' file in ''C:\my\program\path\locale\##\LC_MESSAGES\default.mo'' (assuming the application is located in ''C:\my\program\path\myprogram.exe''). In this path, ## represents the two-letter language code that you can find in the Section called ISO 639 language codes in the appendix called Standards. * 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 ==== # Add ''gnugettext'' to the project and the ''uses'' section in your source code. (("How to do a translation", GNU gettext for Delphi, C++ Builder and Kylix 1.1.1, http://dybdahl.dk/dxgettext/docs/howto.php)) # Add the following to the ''OnFormCreate'' event in every form in the project: TranslateComponent (self); # 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 ==== # Extract all strings from non-English program as it is now (Explorer > select FOLDER where executable is found > Extract translations to template). ((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)) # Translate your program to English and compile. # Extract all strings from the new English program. # 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 ==== # 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. ((Online User Documentation, GNU gettext for Delphi, C++ Builder and Kylix 1.1.1, http://dybdahl.dk/dxgettext/docs/online/msgen.html)) C:\> msgen english-empty-translation.po -o english-fuzzy-translation.po -s ==== Creating a translation file from a text import ==== # Create spreadsheet. # 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. # Verify that column ''msgstr'' is not empty in any row (to avoid "Error: No tabulator in line" message). # Save file as a text-file, tabulator separated with no text delimiters. # 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)