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

From Lazarus wiki
(Translating at start of program)
(How to actually translate form elements)
Line 39: Line 39:
  
 
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.
 
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.
 +
 +
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
 +
 +
<Delphi>
 +
...
 +
uses
 +
  ...
 +
  LResources;
 +
...
 +
...
 +
initialization
 +
  LRSTranslator:=TPoTranslator.Create('/path/to/the/po/file');
 +
</Delphi>
 +
 +
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):
 +
 +
<Delphi>
 +
unit PoTranslator;
 +
 +
{$mode objfpc}{$H+}
 +
 +
interface
 +
 +
uses
 +
  Classes, SysUtils, LResources, typinfo, Translations;
 +
 +
type
 +
 +
{ 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
 +
 +
{ TPoTranslator }
 +
 +
constructor TPoTranslator.Create(POFileName: 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
 +
  s: String;
 +
begin
 +
  if not Assigned(FPOFile) then exit;
 +
  if not Assigned(PropInfo) then exit;
 +
{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.
 +
</Delphi>
 +
 +
Alternatively you can transform the .po file into .mo (using msgfmt) and simply use the DefaultTranslator unit
 +
 +
<Delphi>
 +
...
 +
uses
 +
  ...
 +
  DefaultTranslator;
 +
</Delphi>
 +
 +
which will automatically look in several standard places for a .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):
 +
 +
*  <Application Directory>/LANG/<Application Filename>.mo
 +
*  <Application Directory>/languages/LANG/<Application Filename>.mo
 +
*  <Application Directory>/locale/LANG/<Application Filename>.mo
 +
*  <Application Directory>/locale/LC_MESSAGES/LANG/<Application Filename>.mo
 +
 +
under unix-like systems it will also look in
 +
 +
*  /usr/share/locale/LANG/LC_MESSAGES/<Application Filename>.mo
 +
 +
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==
 
==Translating at start of program==

Revision as of 17:31, 21 February 2009

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

Overview

This is about how a program can use different strings for various languages like english, chinese, german, finnish, italian, ... . 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.

Resourcestrings

For example

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

These are like normal string constants, that means you can assign them to any string. For example

 Label1.Caption := HelloWorld1;

When fpc compiles them, it creates for each unit a file unitname.rst, containing the resourcestring data (name + content).

.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. Examples for free tools: kbabel, poedit.

Example 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. This means, 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 to brazilians/portugueses:: Lazarus IDE and LCL has only brazillian portuguese translations and these files are 'pb.po' extensions and not 'pt.po'.

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.

Translating Forms, Datamodules and Frames

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.

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

<Delphi> ... uses

 ...
 LResources;

... ... initialization

 LRSTranslator:=TPoTranslator.Create('/path/to/the/po/file');

</Delphi>

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):

<Delphi> unit PoTranslator;

{$mode objfpc}{$H+}

interface

uses

 Classes, SysUtils, LResources, typinfo, Translations;

type

{ 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

{ TPoTranslator }

constructor TPoTranslator.Create(POFileName: 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

 s: String;

begin

 if not Assigned(FPOFile) then exit;
 if not Assigned(PropInfo) then exit;

{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. </Delphi>

Alternatively you can transform the .po file into .mo (using msgfmt) and simply use the DefaultTranslator unit

<Delphi> ... uses

  ...
  DefaultTranslator;

</Delphi>

which will automatically look in several standard places for a .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):

  • <Application Directory>/LANG/<Application Filename>.mo
  • <Application Directory>/languages/LANG/<Application Filename>.mo
  • <Application Directory>/locale/LANG/<Application Filename>.mo
  • <Application Directory>/locale/LC_MESSAGES/LANG/<Application Filename>.mo

under unix-like systems it will also look in

  • /usr/share/locale/LANG/LC_MESSAGES/<Application Filename>.mo

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 of the LCL translations unit. For example:

<pascal>

   {First of all: add "gettext" and "translations" units in uses clause}
   procedure TForm1.FormCreate(Sender: TObject);
   var
     PODirectory, Lang, FallbackLang: String;
   begin
     PODirectory := '/path/to/lazarus/lcl/languages/';
     GetLanguageIDs(Lang, FallbackLang); // in unit gettext
     TranslateUnitResourceStrings('LCLStrConsts', PODirectory + 'lclstrconsts.%s.po', Lang, FallbackLang);
     MessageDlg('Title', 'Text', mtInformation, [mbOk, mbCancel, mbYes], 0);
   end;

</pascal>

Compiling po files into the executable

If you don't want to install the .po files, but put all files of the application into the executable, use the following:

  • Create a new unit (not a form!).
  • Convert the .po file(s) to .lrs using tools/lazres:
./lazres unit1.lrs unit1.de.po

This will create an include file unit1.lrs beginning with <pascal> LazarusResources.Add('unit1.de','PO',[

 ...

</pascal>

  • Add the code:

<pascal> uses LResources, Translations;

resourcestring

 MyCaption = 'Caption';

function TranslateUnitResourceStrings: boolean; var

 r: TLResource;
 POFile: TPOFile;

begin

 r:=LazarusResources.Find('unit1.de','PO');
 POFile:=TPOFile.Create;
 try
   POFile.ReadPOText(r.Value);
   Result:=Translations.TranslateUnitResourceStrings('unit1',POFile);
 finally
   POFile.Free;
 end;

end;

initialization

 {$I unit1.lrs}

</pascal>

  • Call TranslateUnitResourceStrings at the beginning of the program. You can do that in the initialization section if you like.

Future work / ToDos

IDE Development: Translations, i18n, lrt, po files


Daemons and Services