Sign in Join
Wiki copytemporary

Everything else about translations: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11### Introduction
22
33**Note:** This stuff was before in [[Translations / i18n / localizations for programs]]. If you think that something is really important move that stuff back. Please read both articles carefully in order to avoid duplicate stuff.
44
55### .po Files
66
77There 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.
88
99Virtaal 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.
1010
1111Example of using rstconv directly:
1212
1313`rstconv -i unit1.rst -o unit1.po`
1414
1515### Translating
1616
1717For 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.
1818
1919> **Note:** For Brazilians/Portuguese: Lazarus IDE and LCL only have a Brazilian Portuguese translation and these files have 'pt_BR.po' extensions
2020
2121### IDE options for automatic updates of .po files
2222
2323- The unit containing the resource strings must be added to the package or project.
2424- 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.
2525
2626When 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.
2727
2828#### Removal of Obsolete entries
2929
3030Entries 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.
3131
3232#### Duplicate entries
3333
3434Duplicate 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:
3535
3636``` pascal
3737 dlfMouseSimpleGutterSect = 'Gutter';
3838 dlgMouseOptNodeGutter = 'Gutter';
3939 dlgGutter = 'Gutter';
4040 dlgAddHiAttrGroupGutter = 'Gutter';
4141```
4242
4343A converted .rst file for this resource strings would look similar to this in a .po file:
4444
4545`#: lazarusidestrconsts.dlfmousesimpleguttersect`\
4646`msgid "Gutter"`\
4747`msgstr ""`\
4848`#: lazarusidestrconsts.dlgaddhiattrgroupgutter`\
4949`msgid "Gutter"`\
5050`msgstr ""`\
5151`etc.`\
5252
5353Where 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:
5454
5555`#: lazarusidestrconsts.dlfmousesimpleguttersect`\
5656`msgctxt "lazarusidestrconsts.dlfmousesimpleguttersect"`\
5757`msgid "Gutter"`\
5858`msgstr ""`\
5959`#: lazarusidestrconsts.dlgaddhiattrgroupgutter`\
6060`msgctxt "lazarusidestrconsts.dlgaddhiattrgroupgutter"`\
6161`msgid "Gutter"`\
6262`msgstr ""`\
6363`etc.`\
6464
6565On 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.
6666
6767The 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.
6868
6969#### Fuzzy entries
7070
7171Changes in resource strings affect translations, for example if initially a resource string was defined like:
7272
7373``` pascal
7474dlgEdColor = 'Syntax highlight';
7575```
7676
7777this would produce a .po entry similar to this
7878
7979`#: lazarusidestrconsts.dlgedcolor`\
8080`msgid "Syntax highlight"`\
8181`msgstr ""`
8282
8383which if translated to Spanish (this sample was taken from lazarus history), may result in
8484
8585`#: lazarusidestrconsts.dlgedcolor`\
8686`msgid "Syntax highlight"`\
8787`msgstr "Color"`
8888
8989Suppose then that at a later time, the resource string has been changed to
9090
9191``` pascal
9292 dlgEdColor = 'Colors';
9393```
9494
9595the resulting .po entry may become
9696
9797`#: lazarusidestrconsts.dlgedcolor`\
9898`msgid "Colors"`\
9999`msgstr ""`
100100
101101Note 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:
102102
103103`#: lazarusidestrconsts.dlgedcolor`\
104104`#, fuzzy`\
105105`#| msgid "Syntax highlight"`\
106106`msgctxt "lazarusidestrconsts.dlgedcolor"`\
107107`msgid "Colors"`\
108108`msgstr "Color"`
109109
110110In 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.
111111
112112### Translating Forms, Datamodules and Frames
113113
114114When 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.
115115
116116For 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
117117
118118``` pascal
119119...
120120uses
121121 ...
122122 LResources;
123123...
124124...
125125initialization
126126 LRSTranslator := TPoTranslator.Create('/path/to/the/po/file');
127127```
128128
129-<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.
129+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): The following code isn't needed anymore if you use recent Lazarus 0.9.29 snapshots. Simply include DefaultTranslator in Uses clause.
130130
131131``` pascal
132132unit PoTranslator;
133133
134134{$mode objfpc}{$H+}
135135
136136interface
137137
138138uses
139139 Classes, SysUtils, LResources, typinfo, Translations;
140140
141141type
142142
143143 { TPoTranslator }
144144
145145 TPoTranslator=class(TAbstractTranslator)
146146 private
147147 FPOFile:TPOFile;
148148 public
149149 constructor Create(POFileName:string);
150150 destructor Destroy;override;
151151 procedure TranslateStringProperty(Sender:TObject;
152152 const Instance: TPersistent; PropInfo: PPropInfo; var Content:string);override;
153153 end;
154154
155155implementation
156156
157157{ TPoTranslator }
158158
159159constructor TPoTranslator.Create(POFileName: string);
160160begin
161161 inherited Create;
162162 FPOFile:=TPOFile.Create(POFileName);
163163end;
164164
165165destructor TPoTranslator.Destroy;
166166begin
167167 FPOFile.Free;
168168 inherited Destroy;
169169end;
170170
171171procedure TPoTranslator.TranslateStringProperty(Sender: TObject;
172172 const Instance: TPersistent; PropInfo: PPropInfo; var Content: string);
173173var
174174 s: String;
175175begin
176176 if not Assigned(FPOFile) then exit;
177177 if not Assigned(PropInfo) then exit;
178178{DO we really need this?}
179179 if Instance is TComponent then
180180 if csDesigning in (Instance as TComponent).ComponentState then exit;
181181{End DO :)}
182182 if (AnsiUpperCase(PropInfo^.PropType^.Name)<>'TTRANSLATESTRING') then exit;
183183 s:=FPOFile.Translate(Content, Content);
184184 if s<>'' then Content:=s;
185185end;
186186
187187end.
188188```
189189
190190Alternatively 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
191191
192192``` pascal
193193...
194194uses
195195 ...
196196 DefaultTranslator;
197197```
198198
199-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):
199+which will automatically look in several standard places for a .po file (higher precedence) or .mo file (the disadvantage is that you'll have to keep around both the .mo files for the DefaultTranslator unit and the .po files for TranslateUnitResourceStrings). 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):
200200
201201- \<Application Directory\>/\<LANG\>/\<Application Filename\>.\<ext\>
202202- \<Application Directory\>/languages/\<LANG\>/\<Application Filename\>.\<ext\>
203203- \<Application Directory\>/locale/\<LANG\>/\<Application Filename\>.\<ext\>
204204- \<Application Directory\>/locale/LC_MESSAGES/\<LANG/\>\<Application Filename\>.\<ext\>
205205
206206under unix-like systems it will also look in
207207
208208- /usr/share/locale/\<LANG\>/LC_MESSAGES/\<Application Filename\>.\<ext\>
209209
210210as 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")
211211
212212### Translating at start of program
213213
214214For every .po file, you must call TranslateUnitResourceStrings. The LCL po file is lclstrconsts. For example you do this in FormCreate of your MainForm:
215215
216216``` pascal
217217uses
218218 ..., gettext, translations;
219219
220220procedure TForm1.FormCreate(Sender: TObject);
221221var
222222 PODirectory, Lang, FallbackLang: String;
223223begin
224224 PODirectory := '/path/to/lazarus/lcl/languages/';
225225 GetLanguageIDs(Lang, FallbackLang);
226226 Translations.TranslateUnitResourceStrings('LCLStrConsts', PODirectory + 'lclstrconsts.%s.po', Lang, FallbackLang);
227227
228228 // the following dialog now shows translated buttons:
229229 MessageDlg('Title', 'Text', mtInformation, [mbOk, mbCancel, mbYes], 0);
230230end;
231231```
232232
233233### Compiling po files into the executable
234234
235235If you don't want to install the .po files, but put all files of the application into the executable, use one the following methods.
236236
237237#### FPC Resources (Recommended)
238238
239239Normal resources are now recommended for current FPC (including all recent Lazarus versions) [[Lazarus Resources]]
240240
241241- Add the resources (.po files) to executable with the Lazarus IDE (Project Options \> Resources) as RCDATA.
242242
243243``` pascal
244244uses
245245 Classes, Translation, LCLType,
246246
247247function Translate(const Language: string): boolean;
248248var
249249 Res: TResourceStream;
250250 PoStringStream: TStringStream;
251251 PoFile: TPOFile;
252252begin
253253 Res := TResourceStream.Create(HInstance, 'project1.' + Language, RT_RCDATA);
254254 PoStringStream := TStringStream.Create('');
255255 Res.SaveToStream(PoStringStream);
256256 Res.Free;
257257
258258 PoFile := TPOFile.Create(False);
259259 PoFile.ReadPOText(PoStringStream.DataString);
260260 PoStringStream.Free;
261261
262262 Result := TranslateResourceStrings(PoFile);
263263 PoFile.Free;
264264end;
265265```
266266
267267#### Lazarus Resources
268268
269269- Create a new unit (not a form!).
270270- Convert the .po file(s) to .lrs using tools/lazres:
271271
272272``` bash
273273./lazres unit1.lrs unit1.de.po
274274```
275275
276276This will create an include file unit1.lrs beginning with
277277
278278``` pascal
279279LazarusResources.Add('unit1.de','PO',[
280280 ...
281281```
282282
283283- Add the code:
284284
285285``` pascal
286286uses LResources, Translations;
287287
288288resourcestring
289289 MyCaption = 'Caption';
290290
291291function TranslateUnitResourceStrings: boolean;
292292var
293293 r: TLResource;
294294 POFile: TPOFile;
295295begin
296296 r:=LazarusResources.Find('unit1.de','PO');
297297 POFile:=TPOFile.Create(False); //if Full=True then you can get a crash (Issue #0026021)
298298 try
299299 POFile.ReadPOText(r.Value);
300300 Result:=Translations.TranslateUnitResourceStrings('unit1',POFile);
301301 finally
302302 POFile.Free;
303303 end;
304304end;
305305
306306initialization
307307 {$I unit1.lrs}
308308```
309309
310310- Call TranslateUnitResourceStrings at the beginning of the program. You can do that in the initialization section if you like.
311311
312312Unfortunately this code will not compile with Lazarus 1.2.2 and earlier.
313313
314314For these Lazarus versions you can use something like this:
315315
316316``` pascal
317317type
318318 TTranslateFromResourceResult = (trSuccess, trResourceNotFound, trTranslationError);
319319
320320function TranslateFromResource(AResourceName, ALanguage : String): TTranslateFromResourceResult;
321321var
322322 LRes : TLResource;
323323 POFile : TPOFile = nil;
324324 SStream : TStringStream = nil;
325325begin
326326 Result := trResourceNotFound;
327327 LRes := LazarusResources.Find(AResourceName + '.' + ALanguage, 'PO');
328328 if LRes <> nil then
329329 try
330330 SStream := TStringStream.Create(LRes.Value);
331331 POFile := TPoFile.Create(SStream, False); // WARNING: This example relies on the bug in TStringStream.Create(...) that the
332332 // position in the stream is incorrectly set to 0, even though the stream
333333 // is initialized with LRes.Value i.e. assumed to be ready for *appending*!!!
334334 try
335335 if TranslateUnitResourceStrings(AResourceName, POFile) then Result := trSuccess
336336 else Result := trTranslationError;
337337 except
338338 Result := trTranslationError;
339339 end;
340340 finally
341341 if Assigned(SStream) then SStream.Free;
342342 if Assigned(POFile) then POFile.Free;
343343 end;
344344end;
345345```
346346
347347Usage example:
348348
349349``` pascal
350350initialization
351351 {$I lclstrconsts.de.lrs}
352352 TranslateFromResource('lclstrconsts', 'de');
353353end.
354354```
355355
356356### Where a Lazarus App looks for Language Files
357357
358358As a multi-language Lazarus app starts, it looks for a nominated language file in a number of places and looks for a number of different file names. The following Lists are for Linux, I believe pretty much the same for Windows but no '/usr/local' of course. The order is important, it stops looking as soon as it finds a suitable candidate. In all cases, the \$LA refers to the language code, es, fr etc. The \$APP is the application's name.
359359
360360**.po files** - these contain raw, editable text and are probably what arrives on your desk from your translators. Looked for first, perhaps as an aid to development ?
361361
362362``` bash
363363./$LA/$APP.po
364364./languages/$LA/$APP.po
365365./locale/$LA/$APP.po
366366./locale/$LA/LC_MESSAGES/$APP.po
367367/usr/share/locale/$LA/LC_MESSAGES/$APP.po
368368./$APP.$LA.po
369369./locale/$APP.$LA.po
370370./languages/$APP.$LA.po
371371/usr/share/locale/$LA/LC_MESSAGES/$APP.po
372372```
373373
374374**.pot files** - these are template files. Note that they don't identify their language, I wonder why they are even looked for ?
375375
376376``` bash
377377./$APP.pot
378378./locale/$APP.pot
379379./languages/$APP.pot
380380```
381381
382382**.mo files** - these are the machine readable ones, faster and smaller than text, these are the only files you should ship with a finished product.
383383
384384``` bash
385385./$LA/$APP.mo
386386./languages/$LA/$APP.mo
387387./locale/$LA/$APP.mo
388388./locale/$LA/LC_MESSAGES/$APP.mo
389389/usr/share/locale/$LA/LC_MESSAGES/$APP.mo
390390./$APP.$LA.mo
391391./locale/$APP.$LA.mo
392392./languages/$APP.$LA.mo
393393/usr/share/locale/$LA/LC_MESSAGES/$APP.mo
394394./$APP.mo
395395./locale/$APP.mo
396396./languages/$APP.mo
397397```
398398
399399**Note 1 :** Firstly, note that Lazarus looks first for .po and then .pot files. So, be very careful about leaving such files lying around in the 'wrong' place. If your app finds an old .po file of the right language, perhaps left there while doing some test, it won't look further for the lovely new .mo file you just made.
400400
401401**Notes 2 :** A linux system should always put its language files in /usr/local/somewhere. And really, you should only let your end users get a .mo file, even your beta testers.
402402
403403**Note 3 :** Above, where we say ./ we mean the directory where the binary is, not the current working directory.
404404
405405### Compiling po files into the executable and change language while running
406406
407407(State: Lazarus 1.6; last tested with Lazarus 2.0.10)
408408
409409With this method a standalone program is possible which can change language without restarting the program. Captions of LCL-components are translated and updated automatically, ressourcestrings change their contents according to the chosen language and can be used to reassign strings manually.
410410
411411Convert the .po file(s) to .lrs using tools/lazres:
412412
413413``` bash
414414./lazres language.lrs testprog1.de.po testprog1.en.po
415415```
416416
417417Create a new unit (not a form) and insert the below function:
418418
419419``` pascal
420420uses
421421 Forms, LResources, LCLTranslator, Translations;
422422
423423function TranslateTo(POFileName: string): boolean;
424424var
425425 res: TLResource;
426426 ii: Integer;
427427 POFile: TPOFile;
428428 LocalTranslator: TUpdateTranslator;
429429begin
430430 Result:= false;
431431 res:=LazarusResources.Find(POFileName,'PO');
432432 if res = nil then EXIT;
433433 POFile:=TPOFile.Create(False); //if Full=True then you can get a crash (Issue #0026021)
434434 try
435435 POFile.ReadPOText(res.Value);
436436 Result:= Translations.TranslateResourceStrings(POFile);
437437 if not Result then EXIT;
438438
439439 LocalTranslator := TPOTranslator.Create(POFile);
440440
441441 LRSTranslator := LocalTranslator;
442442 for ii := 0 to Screen.CustomFormCount-1
443443 do LocalTranslator.UpdateTranslation(Screen.CustomForms[ii]);
444444 for ii := 0 to Screen.DataModuleCount-1
445445 do LocalTranslator.UpdateTranslation(Screen.DataModules[ii]);
446446 LRSTranslator:= nil;
447447 LocalTranslator.Destroy;
448448 //POFile.Destroy; //DONT! Already done in LocalTranslator.Destroy
449449 finally end;
450450end;
451451```
452452
453453Also insert the include statement (here the above mentioned lrs-file is in subfolder "locale"):
454454
455455``` pascal
456456initialization
457457 {$I locale/language.lrs}
458458```
459459
460460Then the language can be changed (examples German and English) while runtime with
461461
462462``` pascal
463463TranslateTo('testprog1.de');
464464```
465465
466466and
467467
468468``` pascal
469469TranslateTo('testprog1.en');
470470```
471471
472472### Cross-platform method to determine system language
473473
474474The following function delivers a string that represents the language of the user interface. It supports Linux, Mac OS X and Windows.
475475
476476``` pascal
477477uses
478478 Classes, SysUtils {add additional units that may be needed by your code here}
479479 {$IFDEF win32}
480480 , Windows
481481 {$ELSE}
482482 , Unix
483483 {$IFDEF LCLCarbon}
484484 , MacOSAll
485485 {$ENDIF}
486486 {$ENDIF}
487487 ;
488488```
489489
490490``` pascal
491491function GetOSLanguage: string;
492492{platform-independent method to read the language of the user interface}
493493var
494494 l, fbl: string;
495495 {$IFDEF LCLCarbon}
496496 theLocaleRef: CFLocaleRef;
497497 locale: CFStringRef;
498498 buffer: StringPtr;
499499 bufferSize: CFIndex;
500500 encoding: CFStringEncoding;
501501 success: boolean;
502502 {$ENDIF}
503503begin
504504 {$IFDEF LCLCarbon}
505505 theLocaleRef := CFLocaleCopyCurrent;
506506 locale := CFLocaleGetIdentifier(theLocaleRef);
507507 encoding := 0;
508508 bufferSize := 256;
509509 buffer := new(StringPtr);
510510 success := CFStringGetPascalString(locale, buffer, bufferSize, encoding);
511511 if success then
512512 l := string(buffer^)
513513 else
514514 l := '';
515515 fbl := Copy(l, 1, 2);
516516 dispose(buffer);
517517 {$ELSE}
518518 {$IFDEF LINUX}
519519 fbl := Copy(GetEnvironmentVariable('LC_CTYPE'), 1, 2);
520520 {$ELSE}
521521 GetLanguageIDs(l, fbl);
522522 {$ENDIF}
523523 {$ENDIF}
524524 Result := fbl;
525525end;
526526```
527527
528528### Translating the IDE
529529
530530#### Files
531531
532532The .po files of the IDE are in the lazarus source directory:
533533
534534- lazarus/languages strings for the IDE
535535- lazarus/lcl/languages/ strings for the LCL
536536- lazarus/components/ideintf/languages/ strings for the IDE interface
537537
538538#### Translators
539539
540540- The German translation is maintained by Swen Heinig.
541541- The Finnish translation is maintained by Seppo Suurtarla
542542- The Russian translation is maintained by Maxim Ganetsky
543543- The French translation is maintained by Gilles Vasseur
544544
545545When you want to start a new translation, ask on the mailing if someone is already working on that.
546546
547547Please read carefully: [[Lazarus Documentation|Translating/Internationalization/Localization]]
548548
549549### Control design with internationalization capability
550550
551551Controls can easily be designed with i18n-support, e.g. a button with translatable caption. Therefore the control should be packed in a package and the published property of the regarding string has to be of type TTranslateString. After the package is installed in Lazarus, the control has the same translate capabilities as the known LCL controls.
552552
553553``` pascal
554554uses LCLType;
555555
556556type TFancyButton = class(TCustomControl)
557557 ...
558558 protected
559559 fButtonCaption: TTranslateString;
560560 procedure SetButtonCaption(ACaption: TTranslateString);
561561 published
562562 property Caption : TTranslateString read fButtonCaption write SetButtonCaption
563563```
564564
565565### See also
566566
567567[[Lazarus Documentation|Translating/Internationalization/Localization]] [[Translations / i18n / localizations for programs]]
568568
569569---
570570*From the Free Pascal wiki, page "Everything else about translations", as it stood on 2021-04-14 after a change by Kupferstecher, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
571571Category: Localization
572572Category: Tutorials

Back to history