Difference between revisions of "Translations / i18n / localizations for programs"

From Free Pascal wiki
Jump to navigationJump to search
(looking for language files)
(24 intermediate revisions by 4 users not shown)
Line 1: Line 1:
 
{{Translations_/_i18n_/_localizations_for_programs}}
 
{{Translations_/_i18n_/_localizations_for_programs}}
==Overview==
 
  
This is about how a program can use different strings for various languages like english, chinese, german, finnish, italian, ... .
+
This is about how a program can use different strings for various languages like English, Chinese, German, Finnish, Italian and any other language.
Basically it works like this: Add a ''resourcestring'' for every caption, compile to get the .rst and/or .po files (the IDE can do this automatically), create one translated .po file for each language (there are free graphical tools) and use the functions of the LCL ''translations'' unit to load the right one at start of the program.
+
 
 +
* '''i18n''' is short for '''internationalization''' (18 being the number of characters between i and n of the word internationalization)
 +
* '''l10n''' is short for '''localization''' (10 being the number of characters between l and n of the word localization)
 +
 
  
 
==Quick i18n==
 
==Quick i18n==
  
This is intended as be a quick guide to introduce yourself in the world of translations and get things done quickly.
+
This is intended as be a quick guide to introduce yourself in the world of translations and get things done quickly. Check also [[Language Codes]] and [[BidiMode]].
 +
 
 +
For your information the most used languages in the world ([http://www.redlinels.com/2014/01/10/most-widely-spoken-languages/ source]) are:
 +
 
 +
* 1 - Chinese: With more than 1.2 billion native speakers in the world
 +
* 2 - Spanish: Spanish occupies the No. 2 spot and is spoken in approximately 30 countries.
 +
* 3 - English: 335 million worldwide—about 5% of the world’s population. (This does not takes into account second language spoken).
 +
* 4 - Hindi: Spoken by over 260 million people.
 +
* 5 - Arabic: Spoken in almost 60 countries around the world.
 +
* 6 - Portuguese: Population of Brazil is over 200 million. The population of Portugal is just over 10 million.
 +
* 7 - Bengali: The main language of Bangladesh (population, 155 million) and one of India’s many official languages.
 +
* 8 - Russian and Japanese.
  
 
===poedit===
 
===poedit===
Line 15: Line 28:
 
===Translating Resourcestrings===
 
===Translating Resourcestrings===
  
This is the way to store a resourcestring in an unit:
+
This is the way to store a resourcestring in a unit:
<syntaxhighlight>resourcestring
+
 
 +
<syntaxhighlight lang="pascal">
 +
resourcestring
 
   Caption1 = 'Some text';
 
   Caption1 = 'Some text';
   HelloWorld1 = 'Hello World';</syntaxhighlight>
+
   HelloWorld1 = 'Hello World';
 +
</syntaxhighlight>
  
 
Resourcestrings are like normal string constants, that means you can assign them to any string:
 
Resourcestrings are like normal string constants, that means you can assign them to any string:
<syntaxhighlight>Label1.Caption := HelloWorld1;</syntaxhighlight>
+
 
 +
<syntaxhighlight lang="pascal">Label1.Caption := HelloWorld1;</syntaxhighlight>
  
 
===Compiling into .po files===
 
===Compiling into .po files===
Line 27: Line 44:
 
Resourcestrings are compiled into .po files if you enable i18n in the Lazarus IDE. Go to Project > Project Options > i18n > Enable i18n. When you recompile your application the .po files will be updated. You may also select a directory where .po files will be stored, recommended '''po_files'''.
 
Resourcestrings are compiled into .po files if you enable i18n in the Lazarus IDE. Go to Project > Project Options > i18n > Enable i18n. When you recompile your application the .po files will be updated. You may also select a directory where .po files will be stored, recommended '''po_files'''.
  
The default one is recommended to be in English or the default language of your application, since it will be loaded if no other translation file is found.
+
The default translation is recommended to be in English or the default language of your application, since it will be loaded if no other translation file is found.
  
When you have your '''project1.po''' copy and paste it and rename it to '''project1.es.po'''. Then you will have 2 languages: English and Spanish. Then you must send to the translator the files for translation.
+
When you have your '''project1.po''' copy and paste it and rename it to '''project1.es.po''' or one of the [[Language Codes]] of your choice. Then you will have 2 languages: English and Spanish. Then you must send to the translator the files for translation.
  
 
Your folder structure will look something like this:
 
Your folder structure will look something like this:
<syntaxhighlight>
+
 
 +
<syntaxhighlight lang="text">
 
project1\po_files\
 
project1\po_files\
 
project1\po_files\project1.po
 
project1\po_files\project1.po
project1\po_files\project1.po
+
project1\po_files\project1.es.po
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Line 41: Line 59:
  
 
When you have the translation finished in the .po file, you want to compile it to .mo to load it faster since is a binary format. To convert to .mo you can use poedit and go to File > Compile as .mo.
 
When you have the translation finished in the .po file, you want to compile it to .mo to load it faster since is a binary format. To convert to .mo you can use poedit and go to File > Compile as .mo.
 +
 +
Note that [[Everything else about translations]] has a section about where a Lazarus app looks for its language files.
  
 
===Automatic translation===
 
===Automatic translation===
Line 46: Line 66:
 
When you have the .mo files ready, put them in the '''locale''' or '''languages''' folder right to your application executable, then include the unit '''DefaultTranslator''' and that's all. The translation will be done automatically.
 
When you have the .mo files ready, put them in the '''locale''' or '''languages''' folder right to your application executable, then include the unit '''DefaultTranslator''' and that's all. The translation will be done automatically.
  
You want to distribute only the .mo files in the '''locale''' or '''languages''' directory, since .po files are usefull only for making the translation and compiling them into .mo.
+
<syntaxhighlight lang="pascal">
 +
uses
 +
  DefaultTranslator;
 +
</syntaxhighlight>
 +
 
 +
You want to distribute only the .mo files in the '''locale''' or '''languages''' directory, since .po files are usefull only for making the translation and compiling them into a .mo file.
  
 
Your folder structure will look something like this:
 
Your folder structure will look something like this:
<syntaxhighlight>
+
 
 +
<syntaxhighlight lang="text">
 
project1\project1.exe
 
project1\project1.exe
 
project1\locale\
 
project1\locale\
 
project1\locale\project1.mo
 
project1\locale\project1.mo
project1\locale\project1.mo
+
project1\locale\project1.es.mo
 
</syntaxhighlight>
 
</syntaxhighlight>
  
==Additional info==
+
===Testing translations===
  
===Date, time and number format===
+
When you have everything ready. you want to test if the translations looks fine in your application for each language you have. Automatic translation has a feature that you can use in order to test each language quickly.
  
Under Linux, BSD and Mac OSX there are several locales defining things like time and date format or the thousand separator. In order to initialize the RTL you need to include the '''clocale''' unit in the uses section of your program (.lpr file).
+
You must run your executable with the command line parameter '''--lang''' followed by the language code of your choice.
  
===gettext===
+
You will run your executable like this in order to test Spanish translation:
  
The main technology involved in the process of translations is [https://www.gnu.org/software/gettext/manual/gettext.html GNU gettext]. FPC comes with the gettext unit.
+
<syntaxhighlight lang="text">
 
+
project1.exe --lang es
<syntaxhighlight>uses
+
</syntaxhighlight>
gettext;</syntaxhighlight>
 
 
 
===PO===
 
 
 
PO – Portable Object. This is the file that you receive back from the translators. It’s a text file that includes the original texts and the translations.
 
 
 
===MO===
 
 
 
MO – Machine Object. The MO file includes the exact same contents as PO file. The two files differ in their format. While a PO file is a text file and is easy for humans to read, MO files are compiled and are easy for computers to read. The unit gettext implements TMOFile and has several procedures to do the translation from .mo files, if you want to use it.
 
 
 
<syntaxhighlight>
 
unit gettext;
 
 
 
...
 
  
TMOFile = class
+
And you will see the translated application.
  
...
+
You can do this with the IDE. Go to '''Run > Run Parameters ...'''. In that window in the input '''Command line parameters (without application name)''' write this:
  
  procedure GetLanguageIDs(var Lang, FallbackLang: string);
+
<syntaxhighlight lang="text">
  procedure TranslateResourceStrings(AFile: TMOFile);
+
--lang it
  procedure TranslateUnitResourceStrings(const AUnitName:string; AFile: TMOFile);
 
  procedure TranslateResourceStrings(const AFilename: String);
 
  procedure TranslateUnitResourceStrings(const AUnitName:string; const AFilename: String);
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
==Everything else about translations==
+
Then '''Run (F9)''' and you will see the translated application.
 
 
Here goes all the translations stuff you want to read if you want to make an advanced translation.
 
 
 
===.po Files===
 
 
 
There are many free graphical tools to edit .po files, which are simple text like the .rst files, but with some more options, like a header providing fields for author, encoding, language and date. Every FPC installation provides the tool '''rstconv''' (windows: rstconv.exe). This tool can be used to convert a .rst file into a .po file. The IDE can do this automatically.
 
Some free tools: kbabel, po-auto-translator, poedit, virtaal.
 
 
 
Virtaal has a translation memory containing source-target language pairs for items that you already translated once, and a translation suggestion function that shows already translated terms in various open source software packages. These function may save you a lot of work and improve consistency.
 
 
 
Example of using rstconv directly:
 
rstconv -i unit1.rst -o unit1.po
 
 
 
===Translating===
 
 
 
For every language the .po file must be copied and translated. The LCL translation unit uses the common language codes (en=english, de=german, it=italian, ...) to search. For example the German translation of unit1.po would be unit1.de.po. To achieve this, copy the unit1.po file to unit1.de.po, unit1.it.po, and whatever language you want to support and then the translators can edit their specific .po file.
 
 
 
{{Note|For Brazilians/Portuguese: Lazarus IDE and LCL only have a Brazilian Portuguese translation and these files have 'pt_BR.po' extensions}}
 
 
 
===IDE options for automatic updates of .po files===
 
 
 
*The unit containing the resource strings must be added to the package or project.
 
*You must provide a .po path, this means a separate directory. For example: create a sub directory ''language'' in the package / project directory. For projects go to the Project > Project Options. For packages go to Options > IDE integration.
 
 
 
When this options are enabled, the IDE generates or updates the base .po file using the information contained in .rst and .lrt files (rstconv tool is then not necesary). The update process begins by collecting all existing entries found in base .po file and in .rst and .lrt files and then applying the following features it finds and brings up to date any translated .xx.po file.  
 
  
====Removal of Obsolete entries====
+
===Final steps===
  
Entries in the base .po file that are not found in .rst and .lrt files are removed. Subsequently, all entries found in translated .xx.po files not found in the base .po file are also removed. This way, .po files are not cluttered with obsolete entries and translators don't have to translate entries that are not used.
+
These steps are to get a better result for your entire translation.
  
====Duplicate entries====
+
====Translate LCL====
  
Duplicate entries occur when for some reason the same text is used for different resource strings, a random example of this is the file lazarus/ide/lazarusidestrconst.pas for the 'Gutter' string:
+
To get everything translated you must include the LCL translations into your application '''locale''' folder.
<syntaxhighlight>  dlfMouseSimpleGutterSect = 'Gutter';
 
  dlgMouseOptNodeGutter = 'Gutter';
 
  dlgGutter = 'Gutter';
 
  dlgAddHiAttrGroupGutter = 'Gutter';
 
</syntaxhighlight>
 
A converted .rst file for this resource strings would look similar to this in a .po file:
 
  
#: lazarusidestrconsts.dlfmousesimpleguttersect
+
Copy everything inside the folder '''C:\lazarus\lcl\languages''' to your '''locale''' folder. Then you will have the LCL translated for your application.
msgid "Gutter"
 
msgstr ""
 
#: lazarusidestrconsts.dlgaddhiattrgroupgutter
 
msgid "Gutter"
 
msgstr ""
 
etc.
 
 
Where the lines starting with "#: " are considered comments and the tools used to translate this entries see the repeated msgid "Gutter" lines like duplicated entries and produce errors or warnings on loading or saving. Duplicate entries are considered a normal eventuality on .po files and they need to have some context attached to them. The msgctxt keyword is used to add context to duplicated entries and the automatic update tool use the entry ID (the text next to "#: " prefix) as the context, for the previous example it would produce something like this:
 
  
#: lazarusidestrconsts.dlfmousesimpleguttersect
+
====Format Settings====
msgctxt "lazarusidestrconsts.dlfmousesimpleguttersect"
 
msgid "Gutter"
 
msgstr ""
 
#: lazarusidestrconsts.dlgaddhiattrgroupgutter
 
msgctxt "lazarusidestrconsts.dlgaddhiattrgroupgutter"
 
msgid "Gutter"
 
msgstr ""
 
etc.
 
 
On translated .xx.po files the automatic tool does one additional check: if the duplicated entry was already translated, the new entry gets the old translation, so it appears like being translated automatically.
 
  
The automatic detection of duplicates is not yet perfect, duplicate detection is made as items are added to the list and it may happen that some untranslated entries are read first. So it may take several passes to get all duplicates automatically translated by the tool.
+
For Windows you must read [[Step-by-step instructions for creating multi-language applications#Format settings]] to get format settings in the right place.
  
====Fuzzy entries====
+
Under Linux, BSD and macOS there are several locales defining things like time and date format or the thousands separator. In order to initialize the RTL you need to include the '''clocale''' unit in the uses section of your program (.lpr file).
  
Changes in resource strings affect translations, for example if initially a resource string was defined like:
+
====BiDiMode====
<syntaxhighlight>dlgEdColor = 'Syntax highlight';</syntaxhighlight>
 
  
this would produce a .po entry similar to this
+
[[BidiMode]] is for languages like Arabic that read and write right to left. See also [http://www.w3.org/International/questions/qa-scripts.en Script direction and languages] for a list of LTR (Left To Right) and RTL (Right To Left) languages and most used languages in general.
  
#: lazarusidestrconsts.dlgedcolor
+
With this unit you can determine what BiDiMode is right for you. This code is tested in Lazarus Trunk and may or not work in current release (see the changes on this page to get the old source).
msgid "Syntax highlight"
 
msgstr ""
 
which if translated to Spanish (this sample was taken from lazarus history), may result in
 
#: lazarusidestrconsts.dlgedcolor
 
msgid "Syntax highlight"
 
msgstr "Color"
 
Suppose then that at a later time, the resource string has been changed to
 
<syntaxhighlight>
 
  dlgEdColor = 'Colors';
 
</syntaxhighlight>
 
the resulting .po entry may become
 
#: lazarusidestrconsts.dlgedcolor
 
msgid "Colors"
 
msgstr ""
 
Note that while the ID remained the same lazarusidestrconsts.dlgedcolor the string has changed from 'Syntax highlight' to 'Colors'. As the string was already translated the old translation may not match the new meaning. Indeed, for the new string probably 'Colores' may be a better translation.  
 
The automatic update tool notices this situation and produces an entry like this:
 
#: lazarusidestrconsts.dlgedcolor
 
#, fuzzy
 
#| msgid "Syntax highlight"
 
msgctxt "lazarusidestrconsts.dlgedcolor"
 
msgid "Colors"
 
msgstr "Color"
 
In terms of .po file format, the "#," prefix means the entry has a flag (fuzzy) and translator programs may present a special GUI to the translator user for this item. In this case, the flag would mean that the translation in its current state is doubtful and needs to be reviewed more carefully by translator. The "#|" prefix indicates what was the previous untranslated string of this entry and gives the translator a hint why the entry was marked as fuzzy.
 
  
===Translating Forms, Datamodules and Frames===
+
First save this unit as ubidimodetools.pas:
  
When the i18n option is enabled for the project / package then the IDE automatically creates .lrt files for every form. It creates the .lrt file on saving a unit. So, if you enable the option for the first time, you must open every form once, move it a little bit, so that it is modified, and save the form. For example if you save a form ''unit1.pas'' the IDE creates a ''unit1.lrt''. And on compile the IDE gathers all strings of all .lrt files and all .rst file into a single .po file (projectname.po or packagename.po) in the i18n directory.
+
<syntaxhighlight lang="pascal">
 
+
unit ubidimodetools;
For the forms to be actually translated at runtime, you have to assign a translator to LRSTranslator (defined in LResources) in the initialization section to one of your units
 
 
 
<syntaxhighlight>...
 
uses
 
  ...
 
  LResources;
 
...
 
...
 
initialization
 
  LRSTranslator := TPoTranslator.Create('/path/to/the/po/file');</syntaxhighlight>
 
 
 
<s>However there's no TPoTranslator class (i.e a class that translates using .po files) available in the LCL. This is a possible implementation (partly lifted from DefaultTranslator.pas in the LCL):</s> The following code isn't needed anymore if you use recent Lazarus 0.9.29 snapshots. Simply include DefaultTranslator in Uses clause.
 
 
 
<syntaxhighlight>unit PoTranslator;
 
  
 
{$mode objfpc}{$H+}
 
{$mode objfpc}{$H+}
Line 214: Line 136:
  
 
uses
 
uses
   Classes, SysUtils, LResources, typinfo, Translations;
+
   Classes, SysUtils;
  
type
+
function GetLang: string;
 
+
function GetBiDiMode: TBiDiMode;
{ TPoTranslator }
 
 
 
TPoTranslator=class(TAbstractTranslator)
 
private
 
  FPOFile:TPOFile;
 
public
 
  constructor Create(POFileName:string);
 
  destructor Destroy;override;
 
  procedure TranslateStringProperty(Sender:TObject;  
 
    const Instance: TPersistent; PropInfo: PPropInfo; var Content:string);override;
 
end;
 
  
 
implementation
 
implementation
  
{ TPoTranslator }
+
uses
 +
  FileUtil, LCLProc, LazUTF8;
  
constructor TPoTranslator.Create(POFileName: string);
+
function GetLang: string;
begin
 
  inherited Create;
 
  FPOFile:=TPOFile.Create(POFileName);
 
end;
 
 
 
destructor TPoTranslator.Destroy;
 
begin
 
  FPOFile.Free;
 
  inherited Destroy;
 
end;
 
 
 
procedure TPoTranslator.TranslateStringProperty(Sender: TObject;
 
  const Instance: TPersistent; PropInfo: PPropInfo; var Content: string);
 
 
var
 
var
   s: String;
+
   T: string; // unused FallBackLang
 +
  i: integer;
 
begin
 
begin
   if not Assigned(FPOFile) then exit;
+
   Result := '';
   if not Assigned(PropInfo) then exit;
+
   { We use the same method that is used in LCLTranslator unit }
{DO we really need this?}
 
  if Instance is TComponent then
 
  if csDesigning in (Instance as TComponent).ComponentState then exit;
 
{End DO :)}
 
  if (AnsiUpperCase(PropInfo^.PropType^.Name)<>'TTRANSLATESTRING') then exit;
 
  s:=FPOFile.Translate(Content, Content);
 
  if s<>'' then Content:=s;
 
end;
 
  
end.</syntaxhighlight>
+
  for i := 1 to Paramcount - 1 do
 +
    if (ParamStrUTF8(i) = '--LANG') or (ParamStrUTF8(i) = '-l') or
 +
      (ParamStrUTF8(i) = '--lang') then
 +
      Result := ParamStrUTF8(i + 1);
  
Alternatively you can transform the .po file into .mo using msgfmt (isn't needed anymore if you use recent 0.9.29 snapshot) and simply use the DefaultTranslator unit
+
  //Win32 user may decide to override locale with LANG variable.
 +
  if Result = '' then
 +
    Result := GetEnvironmentVariableUTF8('LANG');
  
<syntaxhighlight>...
+
  if Result = '' then
uses
+
    LazGetLanguageIDs(Result, {%H-}T);
  ...
+
end;
  DefaultTranslator;</syntaxhighlight>
 
 
 
which will automatically look in several standard places for a .po file (higher precedence) or .mo file <s>(the disadvantage is that you'll have to keep around both the .mo files for the DefaultTranslator unit and the .po files for TranslateUnitResourceStrings)</s>.
 
If you use DefaultTranslator, it will try to automatically detect the language based on the LANG environment variable (overridable using the --lang command line switch), then look in these places for the translation (LANG stands for the desired language, ext can be either po or mo):
 
 
 
*  <Application Directory>/<LANG>/<Application Filename>.<ext>
 
*  <Application Directory>/languages/<LANG>/<Application Filename>.<ext>
 
*  <Application Directory>/locale/<LANG>/<Application Filename>.<ext>
 
*  <Application Directory>/locale/LC_MESSAGES/<LANG/><Application Filename>.<ext>
 
 
 
under unix-like systems it will also look in
 
 
 
*  /usr/share/locale/<LANG>/LC_MESSAGES/<Application Filename>.<ext>
 
 
 
as well as using the short part of the language (e.g. if it is "es_ES" or "es_ES.UTF-8" and it doesn't exist it will also try "es")
 
 
 
===Translating at start of program===
 
 
 
For every .po file, you must call TranslateUnitResourceStrings. The LCL po file is lclstrconsts. For example you do this in FormCreate of your MainForm:
 
 
 
<syntaxhighlight>
 
uses
 
..., gettext, translations;
 
  
procedure TForm1.FormCreate(Sender: TObject);
+
function GetBiDiMode: TBiDiMode;
var
 
  PODirectory, Lang, FallbackLang: String;
 
 
begin
 
begin
   PODirectory := '/path/to/lazarus/lcl/languages/';
+
   case GetLang of
   GetLanguageIDs(Lang, FallbackLang);
+
    // Arabic
  Translations.TranslateUnitResourceStrings('LCLStrConsts', PODirectory + 'lclstrconsts.%s.po', Lang, FallbackLang);
+
    'ar': Result := bdRightToLeft;
 +
    else
 +
      Result := bdLeftToRight;
 +
   end;
 +
end;
  
  // the following dialog now shows translated buttons:
+
end.
  MessageDlg('Title', 'Text', mtInformation, [mbOk, mbCancel, mbYes], 0);
 
end;
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
===Compiling po files into the executable===
+
When you create a form do this:
  
If you don't want to install the .po files, but put all files of the application into the executable, use one the following methods.
+
<syntaxhighlight lang="pascal">
 +
BiDiMode := GetBiDiMode;
 +
</syntaxhighlight>
  
====FPC Resources (Recommended)====
+
'''Note:''' You must be sure that you have the proper translations in order to get it working.
Normal resources are now recommended for current FPC (including all recent Lazarus versions) [[Lazarus_Resources]]
 
  
*Add the resources (.po files) to executable with the Lazarus IDE (Project Options > Resources) as RCDATA.
+
'''Note 2:''' Maybe this is not neccesary at all since Windows can change the BiDiMode automatically.
  
<syntaxhighlight>
+
===Technical Details===
uses
 
LCLType
 
  
function Translate(Language: string): boolean;
+
====gettext====
var
 
  Res: TResourceStream;
 
  PoStringStream: TStringStream;
 
  PoFile: TPOFile;
 
begin
 
  Res := TResourceStream.Create(HInstance, 'project1.' + Language, RT_RCDATA);
 
  PoStringStream := TStringStream.Create('');
 
  Res.SaveToStream(PoStringStream);
 
  Res.Free;
 
  
  PoFile := TPOFile.Create(False);
+
The main technology involved in the process of translations is [https://www.gnu.org/software/gettext/manual/gettext.html GNU gettext]. FPC comes with the gettext unit.
  PoFile.ReadPOText(PoStringStream.DataString);
 
  PoStringStream.Free;
 
  
  Result := TranslateResourceStrings(PoFile);
+
<syntaxhighlight lang="pascal">
   PoFile.Free;
+
uses
end;
+
   gettext;
 
</syntaxhighlight>
 
</syntaxhighlight>
  
====Lazarus Resources====
+
====PO====
  
*Create a new unit (not a form!).
+
PO – Portable Object. This is the file that you receive back from the translators. It’s a text file that includes the original text and the translations.
*Convert the .po file(s) to .lrs using tools/lazres:
 
<pre>
 
./lazres unit1.lrs unit1.de.po
 
</pre>
 
  
This will create an include file unit1.lrs beginning with
+
====MO====
<syntaxhighlight>LazarusResources.Add('unit1.de','PO',[
 
  ...</syntaxhighlight>
 
  
*Add the code:
+
MO – Machine Object. The MO file includes the exact same contents as the PO file. The two files differ in their format. While a PO file is a text file and is easy for humans to read, MO files are compiled and are easy for computers to read. The unit gettext implements TMOFile and has several procedures to do the translation from .mo files, if you want to use it.
<syntaxhighlight>uses LResources, Translations;
 
  
resourcestring
+
<syntaxhighlight lang="pascal">
  MyCaption = 'Caption';
+
unit gettext;
  
function TranslateUnitResourceStrings: boolean;
+
...
var
 
  r: TLResource;
 
  POFile: TPOFile;
 
begin
 
  r:=LazarusResources.Find('unit1.de','PO');
 
  POFile:=TPOFile.Create(False);  //if Full=True then you can get a crash (Issue #0026021)
 
  try
 
    POFile.ReadPOText(r.Value);
 
    Result:=Translations.TranslateUnitResourceStrings('unit1',POFile);
 
  finally
 
    POFile.Free;
 
  end;
 
end;
 
  
initialization
+
TMOFile = class
  {$I unit1.lrs}</syntaxhighlight>
 
  
* Call TranslateUnitResourceStrings at the beginning of the program. You can do that in the initialization section if you like.
+
...
  
Unfortunately this code will not compile with Lazarus 1.2.2 and earlier.
+
   procedure GetLanguageIDs(var Lang, FallbackLang: string);
 
+
  procedure TranslateResourceStrings(AFile: TMOFile);
For these Lazarus versions you can use something like this:
+
   procedure TranslateUnitResourceStrings(const AUnitName:string; AFile: TMOFile);
<syntaxhighlight>
+
   procedure TranslateResourceStrings(const AFilename: String);
type
+
  procedure TranslateUnitResourceStrings(const AUnitName:string; const AFilename: String);
   TTranslateFromResourceResult = (trSuccess, trResourceNotFound, trTranslationError);
 
 
 
function TranslateFromResource(AResourceName, ALanguage : String): TTranslateFromResourceResult;
 
var
 
   LRes : TLResource;
 
  POFile : TPOFile = nil;
 
  SStream : TStringStream = nil;
 
begin
 
  Result := trResourceNotFound;
 
  LRes := LazarusResources.Find(AResourceName + '.' + ALanguage, 'PO');
 
   if LRes <> nil then
 
  try
 
    SStream := TStringStream.Create(LRes.Value);
 
    POFile := TPoFile.Create(SStream, False);
 
    try
 
      if TranslateUnitResourceStrings(AResourceName, POFile) then Result := trSuccess
 
      else Result := trTranslationError;
 
    except
 
      Result := trTranslationError;
 
    end;
 
  finally
 
    if Assigned(SStream) then SStream.Free;
 
    if Assigned(POFile) then POFile.Free;
 
  end;
 
end;
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Usage example:
+
==Everything else about translations==
 
 
<syntaxhighlight>
 
initialization
 
  {$I lclstrconsts.de.lrs}
 
  TranslateFromResource('lclstrconsts', 'de');
 
end.
 
</syntaxhighlight>
 
 
 
===Cross-platform method to determine system language===
 
 
 
The following function delivers a string that represents the language of the user interface. It supports Linux, Mac OS X and Windows.
 
 
 
<syntaxhighlight>
 
uses
 
  Classes, SysUtils {add additional units that may be needed by your code here}
 
  {$IFDEF win32}
 
  , Windows
 
  {$ELSE}
 
  , Unix
 
    {$IFDEF LCLCarbon}
 
  , MacOSAll
 
    {$ENDIF}
 
  {$ENDIF}
 
  ;
 
</syntaxhighlight>
 
 
 
<syntaxhighlight>
 
function GetOSLanguage: string;
 
{platform-independent method to read the language of the user interface}
 
var
 
  l, fbl: string;
 
  {$IFDEF LCLCarbon}
 
  theLocaleRef: CFLocaleRef;
 
  locale: CFStringRef;
 
  buffer: StringPtr;
 
  bufferSize: CFIndex;
 
  encoding: CFStringEncoding;
 
  success: boolean;
 
  {$ENDIF}
 
begin
 
  {$IFDEF LCLCarbon}
 
  theLocaleRef := CFLocaleCopyCurrent;
 
  locale := CFLocaleGetIdentifier(theLocaleRef);
 
  encoding := 0;
 
  bufferSize := 256;
 
  buffer := new(StringPtr);
 
  success := CFStringGetPascalString(locale, buffer, bufferSize, encoding);
 
  if success then
 
    l := string(buffer^)
 
  else
 
    l := '';
 
  fbl := Copy(l, 1, 2);
 
  dispose(buffer);
 
  {$ELSE}
 
  {$IFDEF LINUX}
 
  fbl := Copy(GetEnvironmentVariable('LC_CTYPE'), 1, 2);
 
    {$ELSE}
 
  GetLanguageIDs(l, fbl);
 
    {$ENDIF}
 
  {$ENDIF}
 
  Result := fbl;
 
end;
 
</syntaxhighlight>
 
 
 
===Translating the IDE===
 
 
 
====Files====
 
The .po files of the IDE are in the lazarus source directory:
 
*lazarus/languages strings for the IDE
 
*lazarus/lcl/languages/ strings for the LCL
 
*lazarus/components/ideintf/languages/ strings for the IDE interface
 
 
 
====Translators====
 
* The German translation is maintained by Swen Heinig.
 
* The Finnish translation is maintained by Seppo Suurtarla
 
* The Russian translation is maintained by Maxim Ganetsky
 
* The French translation is maintained by Gilles Vasseur
 
  
When you want to start a new translation, ask on the mailing if someone is already working on that.
+
Here is all the translation material that was here in the past. There are a lot of articles about translations that can cause trouble to novices. To keep it simple this other material has been moved to: [[Everything else about translations]]. Please keep this article as clean as possible.
  
Please read carefully: [[Lazarus_Documentation#Translating.2FInternationalization.2FLocalization|Translating/Internationalization/Localization]]
+
==See also==
  
===See also===
 
 
* [[IDE_Development#Translations.2C_i18n.2C_lrt_files.2C_po_files|IDE Development: Translations, i18n, lrt, po files]]
 
* [[IDE_Development#Translations.2C_i18n.2C_lrt_files.2C_po_files|IDE Development: Translations, i18n, lrt, po files]]
 
* [[Getting_translation_strings_right|Getting translation strings right]]
 
* [[Getting_translation_strings_right|Getting translation strings right]]

Revision as of 11:14, 15 November 2020

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) 한국어 (ko) polski (pl) português (pt) русский (ru) 中文(中国大陆)‎ (zh_CN)

This is about how a program can use different strings for various languages like English, Chinese, German, Finnish, Italian and any other language.

  • i18n is short for internationalization (18 being the number of characters between i and n of the word internationalization)
  • l10n is short for localization (10 being the number of characters between l and n of the word localization)


Quick i18n

This is intended as be a quick guide to introduce yourself in the world of translations and get things done quickly. Check also Language Codes and BidiMode.

For your information the most used languages in the world (source) are:

  • 1 - Chinese: With more than 1.2 billion native speakers in the world
  • 2 - Spanish: Spanish occupies the No. 2 spot and is spoken in approximately 30 countries.
  • 3 - English: 335 million worldwide—about 5% of the world’s population. (This does not takes into account second language spoken).
  • 4 - Hindi: Spoken by over 260 million people.
  • 5 - Arabic: Spoken in almost 60 countries around the world.
  • 6 - Portuguese: Population of Brazil is over 200 million. The population of Portugal is just over 10 million.
  • 7 - Bengali: The main language of Bangladesh (population, 155 million) and one of India’s many official languages.
  • 8 - Russian and Japanese.

poedit

The best known tool is a program called poedit. poedit is a tool for translators. It produces both PO and MO as output.

Translating Resourcestrings

This is the way to store a resourcestring in a unit:

resourcestring
  Caption1 = 'Some text';
  HelloWorld1 = 'Hello World';

Resourcestrings are like normal string constants, that means you can assign them to any string:

Label1.Caption := HelloWorld1;

Compiling into .po files

Resourcestrings are compiled into .po files if you enable i18n in the Lazarus IDE. Go to Project > Project Options > i18n > Enable i18n. When you recompile your application the .po files will be updated. You may also select a directory where .po files will be stored, recommended po_files.

The default translation is recommended to be in English or the default language of your application, since it will be loaded if no other translation file is found.

When you have your project1.po copy and paste it and rename it to project1.es.po or one of the Language Codes of your choice. Then you will have 2 languages: English and Spanish. Then you must send to the translator the files for translation.

Your folder structure will look something like this:

project1\po_files\
project1\po_files\project1.po
project1\po_files\project1.es.po

Converting .po files to .mo files

When you have the translation finished in the .po file, you want to compile it to .mo to load it faster since is a binary format. To convert to .mo you can use poedit and go to File > Compile as .mo.

Note that Everything else about translations has a section about where a Lazarus app looks for its language files.

Automatic translation

When you have the .mo files ready, put them in the locale or languages folder right to your application executable, then include the unit DefaultTranslator and that's all. The translation will be done automatically.

uses
  DefaultTranslator;

You want to distribute only the .mo files in the locale or languages directory, since .po files are usefull only for making the translation and compiling them into a .mo file.

Your folder structure will look something like this:

project1\project1.exe
project1\locale\
project1\locale\project1.mo
project1\locale\project1.es.mo

Testing translations

When you have everything ready. you want to test if the translations looks fine in your application for each language you have. Automatic translation has a feature that you can use in order to test each language quickly.

You must run your executable with the command line parameter --lang followed by the language code of your choice.

You will run your executable like this in order to test Spanish translation:

project1.exe --lang es

And you will see the translated application.

You can do this with the IDE. Go to Run > Run Parameters .... In that window in the input Command line parameters (without application name) write this:

--lang it

Then Run (F9) and you will see the translated application.

Final steps

These steps are to get a better result for your entire translation.

Translate LCL

To get everything translated you must include the LCL translations into your application locale folder.

Copy everything inside the folder C:\lazarus\lcl\languages to your locale folder. Then you will have the LCL translated for your application.

Format Settings

For Windows you must read Step-by-step instructions for creating multi-language applications#Format settings to get format settings in the right place.

Under Linux, BSD and macOS there are several locales defining things like time and date format or the thousands separator. In order to initialize the RTL you need to include the clocale unit in the uses section of your program (.lpr file).

BiDiMode

BidiMode is for languages like Arabic that read and write right to left. See also Script direction and languages for a list of LTR (Left To Right) and RTL (Right To Left) languages and most used languages in general.

With this unit you can determine what BiDiMode is right for you. This code is tested in Lazarus Trunk and may or not work in current release (see the changes on this page to get the old source).

First save this unit as ubidimodetools.pas:

unit ubidimodetools;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils;

function GetLang: string;
function GetBiDiMode: TBiDiMode;

implementation

uses
  FileUtil, LCLProc, LazUTF8;

function GetLang: string;
var
  T: string; // unused FallBackLang
  i: integer;
begin
  Result := '';
  { We use the same method that is used in LCLTranslator unit }

  for i := 1 to Paramcount - 1 do
    if (ParamStrUTF8(i) = '--LANG') or (ParamStrUTF8(i) = '-l') or
      (ParamStrUTF8(i) = '--lang') then
      Result := ParamStrUTF8(i + 1);

  //Win32 user may decide to override locale with LANG variable.
  if Result = '' then
    Result := GetEnvironmentVariableUTF8('LANG');

  if Result = '' then
    LazGetLanguageIDs(Result, {%H-}T);
end;

function GetBiDiMode: TBiDiMode;
begin
  case GetLang of
    // Arabic
    'ar': Result := bdRightToLeft;
    else
      Result := bdLeftToRight;
  end;
end;

end.

When you create a form do this:

BiDiMode := GetBiDiMode;

Note: You must be sure that you have the proper translations in order to get it working.

Note 2: Maybe this is not neccesary at all since Windows can change the BiDiMode automatically.

Technical Details

gettext

The main technology involved in the process of translations is GNU gettext. FPC comes with the gettext unit.

uses
  gettext;

PO

PO – Portable Object. This is the file that you receive back from the translators. It’s a text file that includes the original text and the translations.

MO

MO – Machine Object. The MO file includes the exact same contents as the PO file. The two files differ in their format. While a PO file is a text file and is easy for humans to read, MO files are compiled and are easy for computers to read. The unit gettext implements TMOFile and has several procedures to do the translation from .mo files, if you want to use it.

unit gettext;

...

TMOFile = class

...

  procedure GetLanguageIDs(var Lang, FallbackLang: string);
  procedure TranslateResourceStrings(AFile: TMOFile);
  procedure TranslateUnitResourceStrings(const AUnitName:string; AFile: TMOFile);
  procedure TranslateResourceStrings(const AFilename: String);
  procedure TranslateUnitResourceStrings(const AUnitName:string; const AFilename: String);

Everything else about translations

Here is all the translation material that was here in the past. There are a lot of articles about translations that can cause trouble to novices. To keep it simple this other material has been moved to: Everything else about translations. Please keep this article as clean as possible.

See also