Difference between revisions of "Step-by-step instructions for creating multi-language applications"

From Lazarus wiki
Jump to navigationJump to search
(→‎Use DefaultTranslator: Replace "DefaultTranslator.pas" by "LCLTranslator.pas")
(31 intermediate revisions by 6 users not shown)
Line 1: Line 1:
 +
{{Step-by-step instructions for creating multi-language applications}}
 +
 
== Introduction ==
 
== Introduction ==
  
Line 6: Line 8:
  
 
== Getting started ==
 
== Getting started ==
Before starting, I'd like to mention that I am writing this tutorial based on the trunk version of Lazarus which will largely be the basis of the upcoming version 1.2. You can use one of the "official" versions 1.0.x as well, but run-time switching of languages will not be working as shown below.
+
Before starting, I'd like to mention that this tutorial was initially written for Lazarus version 1.2 and now has been updated to version 1.4. There will be some notes here and here to identify the differences between the versions.
  
 
[[File:imgviewer_orig.png]]
 
[[File:imgviewer_orig.png]]
Line 26: Line 28:
  
 
==Enable translations==
 
==Enable translations==
 +
 
Only one modification of the project is required to enable translation. It is found in the project options as item "i18n". Strange word, isn't it? It is the abbreviation for "internationalization" and stands for "18 letters between the i and the n".
 
Only one modification of the project is required to enable translation. It is found in the project options as item "i18n". Strange word, isn't it? It is the abbreviation for "internationalization" and stands for "18 letters between the i and the n".
  
Put a checkmark in the checkbox <code>Enable i18n</code>. This activates the <code>i18n Options</code> underneath. Enter the name for the <code>PO Output Directory</code>. This is the folder where the files with translated texts will be stored. As you will see later, the translation files will have the extension .po. Let's use the folder <code>languages</code>. Note that this folder is relative to the folder containing the exe file. Be sure to keep this structure if you should later copy the exe to somewhere else.
+
Put a checkmark in the checkbox <code>Enable i18n</code>. This activates the <code>i18n Options</code> underneath. Enter the name for the <code>PO Output Directory</code> (use '''locale''' or '''languages''' as directory, so later it would be found automatically). This is the folder where the files with translated texts will be stored. As you will see later, the translation files will have the extension .po. Let's use the folder <code>languages</code>. Note that this folder is relative to the folder containing the exe file. Be sure to keep this structure if you should later copy the exe to somewhere else.
  
 
[[File:enable_i18n.png]]
 
[[File:enable_i18n.png]]
Line 36: Line 39:
 
If you compile the project again, you'll find two changes in the project folder: At first there is a new file with the extension <code>.rst</code>. This file type collects the resource strings declared in a unit. '''Resource strings''' are the key elements of the translation system: whenever you want a string to be translated declare it as a <code>resourcestring</code>, don't hard-code it, and don't declare it as <code>const</code>.
 
If you compile the project again, you'll find two changes in the project folder: At first there is a new file with the extension <code>.rst</code>. This file type collects the resource strings declared in a unit. '''Resource strings''' are the key elements of the translation system: whenever you want a string to be translated declare it as a <code>resourcestring</code>, don't hard-code it, and don't declare it as <code>const</code>.
  
As an example: In order to display an error message "File does not exist." don't call the <code>ShowMessage</code> procedure like
+
As an example: In order to display an error message "File does not exist." don't call the <code>ShowMessage</code> procedure like:
<source>
+
 
 +
<syntaxhighlight lang="pascal">
 
begin
 
begin
 
   ShowMessage('File does not exist.');
 
   ShowMessage('File does not exist.');
 
end;
 
end;
</source>
+
</syntaxhighlight>
 +
 
 
but declare the text as a <code>resourcestring</code> and use it as a parameter for the <code>ShowMessage</code>:
 
but declare the text as a <code>resourcestring</code> and use it as a parameter for the <code>ShowMessage</code>:
<source>
+
 
 +
<syntaxhighlight lang="pascal">
 
resourcestring
 
resourcestring
 
   SFileDoesNotExist = 'File does not exist.';
 
   SFileDoesNotExist = 'File does not exist.';
Line 49: Line 55:
 
   ShowMessage(SFileDoesNotExist);
 
   ShowMessage(SFileDoesNotExist);
 
end;
 
end;
</source>
+
</syntaxhighlight>
  
 
In the demo project there are four resourcestring declarations at the beginning of the implementation section of the main form (<code>frmmain</code>). In a larger project, it is conventient to collect all resourcestrings in a separate unit; you can access the strings from other forms and units and avoid cross-referencing of units this way.
 
In the demo project there are four resourcestring declarations at the beginning of the implementation section of the main form (<code>frmmain</code>). In a larger project, it is conventient to collect all resourcestrings in a separate unit; you can access the strings from other forms and units and avoid cross-referencing of units this way.
Line 57: Line 63:
 
We could open <code>imgview.de.po</code> and add translations in a straighforward way. But there is an even simpler way as we will see later. Before doing that let's apply another modification to the demo project.
 
We could open <code>imgview.de.po</code> and add translations in a straighforward way. But there is an even simpler way as we will see later. Before doing that let's apply another modification to the demo project.
  
== Use DefaultTranslator ==
+
== Use LCLTranslator unit (class TDefaultTranslator) ==
 
So far, the po files only contain resourcestrings that are explicitly declared as such. There are, however, a lot of other strings in our application which are not yet covered, like the entire menu and submenus, texts in labels or listboxes, etc.
 
So far, the po files only contain resourcestrings that are explicitly declared as such. There are, however, a lot of other strings in our application which are not yet covered, like the entire menu and submenus, texts in labels or listboxes, etc.
  
 
There is an extremely easy way to include the strings of the user interface into the translation system: Simply add the unit <code>LCLTranslator</code> to the main form's uses clause. When the project is compiled after this modification you'll find all strings in the po file.
 
There is an extremely easy way to include the strings of the user interface into the translation system: Simply add the unit <code>LCLTranslator</code> to the main form's uses clause. When the project is compiled after this modification you'll find all strings in the po file.
  
[[file:uses_DefaultTranslator.png]]
+
[[file:LCLTranslator.png]]
  
 
{{Note|In Lazarus versions older than 1.4 this functionality resides in unit <code>DefaultTranslator.pas</code>.}}
 
{{Note|In Lazarus versions older than 1.4 this functionality resides in unit <code>DefaultTranslator.pas</code>.}}
Line 68: Line 74:
 
== Translating ==
 
== Translating ==
 
For translation we could edit the po files directly by using a standard text editor. But it is more conventient to use a separate program optimized for this purpose. Good candidates are:  
 
For translation we could edit the po files directly by using a standard text editor. But it is more conventient to use a separate program optimized for this purpose. Good candidates are:  
* [http://sourceforge.net/projects/poedit/ poedit]
+
* [https://poedit.net/download poedit] (old link [http://sourceforge.net/projects/poedit/ poedit])
 
* [https://poeditor.com/ POEditor] or  
 
* [https://poeditor.com/ POEditor] or  
 
* [http://sourceforge.net/projects/betterpoeditor/ Better PO Editor].  
 
* [http://sourceforge.net/projects/betterpoeditor/ Better PO Editor].  
Line 82: Line 88:
 
In the same way, you can add more languages: open the .po file of the the application in <code>poedit</code>, add the translations, and save it with the corresponding language code before the po extension.
 
In the same way, you can add more languages: open the .po file of the the application in <code>poedit</code>, add the translations, and save it with the corresponding language code before the po extension.
  
Since the test application is hard-coded in English it is very easy to create an English translation file: after loading <code>imgview.po</code> into <code>poedit</code>, select each string item and press Ctrl+B which copys the raw resourcestring value into the translation memo. Save as <code>imgview.en.po</code>.
+
Since the test application is hard-coded in English it is very easy to create an English translation file: after loading <code>imgview.po</code> into <code>poedit</code>, select each string item and press {{keypress|Ctrl}}+{{keypress|B}} which copys the raw resourcestring value into the translation memo. Save as <code>imgview.en.po</code>.
  
 
In its initialization code, <code>DefaultTranslator</code> seeks for the language setting of the currently used system. Therefore, if you work on a PC set up for German language the demo project will now be in German automatically. Detection of the default language and replacing the resource strings with those from the corresponding po file has been done by the translation system of Lazarus.
 
In its initialization code, <code>DefaultTranslator</code> seeks for the language setting of the currently used system. Therefore, if you work on a PC set up for German language the demo project will now be in German automatically. Detection of the default language and replacing the resource strings with those from the corresponding po file has been done by the translation system of Lazarus.
 +
 +
== Excluding Controls ==
 +
 +
To exclude some controls from translation, you can add it in project options i18n page, full path to this control like "tmainform.edit1.text" in Execlude field
  
 
== Switching languages ==
 
== Switching languages ==
 
But what if your PC is not on German language? We can use the Lazarus translation system to switch languages at run-time.
 
But what if your PC is not on German language? We can use the Lazarus translation system to switch languages at run-time.
  
First of all, the <code>DefaultTranslator</code> unit provides commandline switches <code>--lang</code> or <code>-l</code> to override the automatic language detetion. For example,
+
First of all, the <code>LCLTranslator</code> unit gives access to commandline switches <code>--lang</code> or <code>-l</code> to override the automatic language detection. For example,
<source>
+
 
 +
<syntaxhighlight lang="pascal">
 
     imgview.exe --lang de
 
     imgview.exe --lang de
</source>
+
</syntaxhighlight>
 +
 
 
opens the German translation of the program even on an English-speaking system.
 
opens the German translation of the program even on an English-speaking system.
  
Of course, changing languages at run-time would be even more favorable. Unfortunately this is not possible with Lazarus 1.0.x out of the box. But it is no problem with Lazarus trunk or the upcoming version 1.2 where <code>DefaultTranslator</code> provides a procedure <code>SetDefaultLang</code> for switching between languages upon user request. (Of course, if you want to stick to 1.0.x you can add this procedure to the source code of <code>DefaultTranslator.pas</code> - you find all the necessary building blocks in the initialization code of the unit -, and save the modified unit in your project folder.)
+
Of course, changing languages at run-time would be even more favorable. Unfortunately this was not possible with Lazarus before version 1.2 out of the box. But it is no problem with current versions of Lazarus where <code>LCLTranslator</code> provides a procedure <code>SetDefaultLang</code> for switching between languages upon user request. (Again: "use" unit <code>DefaultTranslator</code> instead of <code>LCLTranslator</code> in case of Lazarus 1.2.x).
  
 
At first, we need some control in the user interface to switch languages. What about a new menu item "Translation" and a submenu containing the available languages? In the menu designer of the main form add a new item "Translations" and an empty submenu. Then, for each language available, add the name of the corresponding language to the submenu. Of course you can also show a flag icon for each language - free flag icons are available from [http://www.famfamfam.com/lab/icons/flags/]. In the <code>OnClick</code> event handler call <code>SetDefaultLang</code> with the language code as a parameter, e.g.  
 
At first, we need some control in the user interface to switch languages. What about a new menu item "Translation" and a submenu containing the available languages? In the menu designer of the main form add a new item "Translations" and an empty submenu. Then, for each language available, add the name of the corresponding language to the submenu. Of course you can also show a flag icon for each language - free flag icons are available from [http://www.famfamfam.com/lab/icons/flags/]. In the <code>OnClick</code> event handler call <code>SetDefaultLang</code> with the language code as a parameter, e.g.  
  
<source>
+
<syntaxhighlight lang="pascal">
 
procedure TMainForm.MEnglishlanguageClick(Sender: TObject);
 
procedure TMainForm.MEnglishlanguageClick(Sender: TObject);
 
begin
 
begin
Line 109: Line 121:
 
   SetDefaultLang('de');
 
   SetDefaultLang('de');
 
end;
 
end;
</source>
+
</syntaxhighlight>
  
 
Of course, you can use more sophisticated code which determines the language codes from the names of the po files found and creates menu items depending on the available translations.
 
Of course, you can use more sophisticated code which determines the language codes from the names of the po files found and creates menu items depending on the available translations.
Line 118: Line 130:
  
 
== Strings defined by the LCL ==
 
== Strings defined by the LCL ==
Here is one idea for refinement: The strings set up up for error messages defined by the LCL or used in standard dialogs or message boxes are not yet translated. Their translation is extremely easy: their translation files can be found in the directory <code>lcl/languages</code> of your Lazarus installation; they are named <code>lclstrconsts.*.po</code>. Pick those translations used by your application and copy them to the <code>languages</code> folder of our project.
+
Here is one idea for refinement: The strings set up up for error messages defined by the LCL or used in standard dialogs or message boxes are not yet translated. Their translation is extremely easy: their translation files can be found in the directory <code>lcl/languages</code> of your Lazarus installation; they are named <code>lclstrconsts.*.po</code>. Pick those translations used by your application and copy them to the <code>languages</code> folder of our project. (And add unit ''lclstrconsts'' to the <code>uses</code> clause if you refer to one of these strings).
  
 
This works because resourcestrings are pulled in from all language files for the active language found in the languages folder.
 
This works because resourcestrings are pulled in from all language files for the active language found in the languages folder.
  
 
== Format settings ==
 
== Format settings ==
 +
 
When creating multi-language applications translation of the strings is not the only task. Another issue is that format settings may change from country to country. Format settings - they define formatting of dates and times, the month and day names, the character to be used as decimal or thousands separator, etc. In Lazarus, the record <code>TFormatSettings</code> collects all the possible data. The <code>DefaultSettings</code> are used by the standard format conversion routines, such as StrToDate, DateToStr, StrToFloat, or FloatToStr, etc.
 
When creating multi-language applications translation of the strings is not the only task. Another issue is that format settings may change from country to country. Format settings - they define formatting of dates and times, the month and day names, the character to be used as decimal or thousands separator, etc. In Lazarus, the record <code>TFormatSettings</code> collects all the possible data. The <code>DefaultSettings</code> are used by the standard format conversion routines, such as StrToDate, DateToStr, StrToFloat, or FloatToStr, etc.
  
For Windows, there exists a procedure <code>GetLocaleFormatSettings</code> in the unit <code>sysutils</code> which returns the <code>TFormatSettings</code> for a given localization. The parameter <code>LCID</code>  specifies the language code in Windows. Unfortunately I do not know of a convenient conversion between the LCID and the language codes ('de', 'en', etc) used by Lazarus. A conversion table for lookup can be found at [http://www.science.co.il/Language/Locale-codes.asp]. In this table, you see that "German" has the LCID $407, and "English" has the LCID $409. Therefore, we modify the <code>OnClick</code> event handler for the language selection menu items as follows:
+
For Windows, there exists a procedure <code>GetLocaleFormatSettings</code> in the unit <code>sysutils</code> which returns the <code>TFormatSettings</code> for a given localization. The parameter <code>LCID</code>  specifies the language code in Windows. Unfortunately I do not know of a convenient conversion between the LCID and the language codes ('de', 'en', etc) used by Lazarus. A conversion table for lookup can be found at [[Language Codes]]. In this table, you see that "German" has the LCID $407, and "English" has the LCID $409. Therefore, we modify the <code>OnClick</code> event handler for the language selection menu items as follows:
  
<source>
+
<syntaxhighlight lang="pascal">
 
procedure TMainForm.MEnglishlanguageClick(Sender: TObject);
 
procedure TMainForm.MEnglishlanguageClick(Sender: TObject);
 
begin
 
begin
Line 139: Line 152:
 
   GetLocaleFormatSettings($407, DefaultFormatSettings);
 
   GetLocaleFormatSettings($407, DefaultFormatSettings);
 
end;
 
end;
</source>
+
</syntaxhighlight>
  
 
As a demonstration we add a status bar to our project and display the date when an image was created:
 
As a demonstration we add a status bar to our project and display the date when an image was created:
  
<source>
+
<syntaxhighlight lang="pascal">
 
procedure TMainForm.ShowPicDateTime;
 
procedure TMainForm.ShowPicDateTime;
 
var
 
var
Line 155: Line 168:
 
   end;
 
   end;
 
end;
 
end;
</source>
+
</syntaxhighlight>
  
 
This procedure is called from the <code>OnSelectionChange</code> event handler of the files listbox:
 
This procedure is called from the <code>OnSelectionChange</code> event handler of the files listbox:
  
<source>
+
<syntaxhighlight lang="pascal">
 
procedure TMainForm.LBFilesSelectionChange(Sender: TObject; User: boolean);
 
procedure TMainForm.LBFilesSelectionChange(Sender: TObject; User: boolean);
 
begin
 
begin
 
   ShowPicDateTime;
 
   ShowPicDateTime;
 
end;
 
end;
</source>
+
</syntaxhighlight>
  
 
Recompile the program and load some images. You'll see that the date format changes when the language is switched and another image is selected.
 
Recompile the program and load some images. You'll see that the date format changes when the language is switched and another image is selected.
Line 175: Line 188:
  
 
[[File:imgviewer_de.png]].
 
[[File:imgviewer_de.png]].
 +
 +
=== macOS ===
 +
 +
Refer to the [[Locale settings for macOS]] article.
 +
 +
== Operating system dialogs ==
 +
 +
The operating system dialogs (eg [[TOpenDialog]]) are not translatable by Lazarus because they use the operating system strings and not the LCL  widgetset strings which can be localised as detailed above.
 +
 +
=== macOS ===
 +
 +
{{Tip|You may need to delete the symbolic link to the executable created by Lazarus in the [[Application Bundle|application bundle]] and replace it with the actual executable before the localised language will show up in the system dialogs when using the methods described below.}}
 +
 +
For macOS, one solution is to add the language designators of the supported languages to the application's <tt>Info.plist</tt> file like this:
 +
 +
<syntaxhighlight lang=xml>
 +
        <key>CFBundleLocalizations</key>
 +
        <array>
 +
                <string>en</string>
 +
                <string>fr</string>
 +
                <string>de</string>
 +
                <string>it</string>
 +
                <string>nl</string>
 +
                <string>ru</string>
 +
                ... etc ...
 +
        </array>
 +
</syntaxhighlight>
 +
 +
and then the operating system dialogs will present users with their preferred language ('''fr''' - French example below).
 +
 +
[[Image:french_dialog_localization.png]]
 +
 +
An alternative method for localising a macOS application is to instead create an empty folder for each language in the application bundle's <tt>Resources</tt> directory. For example, for French, add an empty file named <tt>fr.lproj</tt> in <tt>MyApp.app/Contents/Resources</tt>.
 +
 +
==== Language designators ====
 +
 +
A language designator is a code that represents a language. Use the two-letter ISO 639-1 standard (preferred) or the three-letter ISO 639-2 standard. If an ISO 639-1 code is not available for a particular language, use the ISO 639-2 code instead. For example, there is no ISO 639-1 code for the Hawaiian language, so use the ISO 639-2 code. For a complete list of ISO 639-1 and ISO 639-2 codes, see [http://www.loc.gov/standards/iso639-2/php/English_list.php ISO 639.2 Codes for the Representation of Names and Languages].
  
 
== See also ==
 
== See also ==
 +
 
* [[Localization]]
 
* [[Localization]]
 +
* [[Getting_translation_strings_right|Getting translation strings right]]
 
* [[Translations / i18n / localizations for programs]]
 
* [[Translations / i18n / localizations for programs]]
 
+
* [[macOS Translation|Common macOS user interface captions]]
[[category:Tutorials]]
 
[[category:Localization]]
 
[[Category:Lazarus]]
 

Revision as of 22:11, 10 April 2022

English (en) suomi (fi)

Introduction

Preparing applications for several languages has always been a bit of a mystery to me - until I finally tried. Then I found out that is is amazingly simple once I understood the first steps. I'd like to share this information with interested users of Lazarus.

You may jump right in. But it may be a good idea to learn something about the basic ideas behind that architecture. Therefore I'd urge you to read the wiki articles Localization and/or Translations_/_i18n_/_localizations_for_programs which explain the basic fundamentals behind the scene.

Getting started

Before starting, I'd like to mention that this tutorial was initially written for Lazarus version 1.2 and now has been updated to version 1.4. There will be some notes here and here to identify the differences between the versions.

imgviewer orig.png

At first, we need an application that we want to translate. Looking through the example projects that come with Lazarus I found that examples\imgviewer may be a decent demo project. In order to keep the original, copy the entire project folder into a separate directory, e.g. imgviewer_multilanguage. Open the project file imgview.lpi, compile and run it. You'll see a typical application with a menu and some controls to show a file list and the selected image.

Let's convert this application to support translation into German.

GUI Design

During design of the GUI it shall be considered, that a string will have a different length in each language. That is why all components must have the Autosize property set to True. It is normal to expect that in some languages strings might be some 50% longer, but even 200% is not much unusual.

The GUI dimensions must consider the display of the device on which the application will be run. For a PC if a form is not scrollable, it shall not be greater that 1024x600 pixels (10" netbook display). This means that if only single-line string components are used, they shall fit properly in a 700x600 form, so when the form is enlarged to 1024x600 it shall be sufficient for other languages.

If several vertically positioned buttons are used and they are expected to equal width, besides Autosize= True they shall have .Constraints.MinWidth set to a significantly high value.

In order to prevent components from overlapping, anchoring shall be used.

Labels are often positioned to the left of the controls they belong to. In this case, precautions have to be taken to avoid that they extend into the control if they become too long. One option would be to allow multi-line labels by setting their WordWrap property to true (and AutoSize to false). Alternatively, single-line labels could be placed above the controls.

Enable translations

Only one modification of the project is required to enable translation. It is found in the project options as item "i18n". Strange word, isn't it? It is the abbreviation for "internationalization" and stands for "18 letters between the i and the n".

Put a checkmark in the checkbox Enable i18n. This activates the i18n Options underneath. Enter the name for the PO Output Directory (use locale or languages as directory, so later it would be found automatically). This is the folder where the files with translated texts will be stored. As you will see later, the translation files will have the extension .po. Let's use the folder languages. Note that this folder is relative to the folder containing the exe file. Be sure to keep this structure if you should later copy the exe to somewhere else.

enable i18n.png

Keep the checkbox Create/update .po file when saving a lfm file checked - this updates the translation "master" file whenever you save - see below.

If you compile the project again, you'll find two changes in the project folder: At first there is a new file with the extension .rst. This file type collects the resource strings declared in a unit. Resource strings are the key elements of the translation system: whenever you want a string to be translated declare it as a resourcestring, don't hard-code it, and don't declare it as const.

As an example: In order to display an error message "File does not exist." don't call the ShowMessage procedure like:

begin
  ShowMessage('File does not exist.');
end;

but declare the text as a resourcestring and use it as a parameter for the ShowMessage:

resourcestring
  SFileDoesNotExist = 'File does not exist.';
begin
  ShowMessage(SFileDoesNotExist);
end;

In the demo project there are four resourcestring declarations at the beginning of the implementation section of the main form (frmmain). In a larger project, it is conventient to collect all resourcestrings in a separate unit; you can access the strings from other forms and units and avoid cross-referencing of units this way.

The second modification of the project is a new folder "languages". It contains a file imgview.po. This file was created automatically by Lazarus and contains the resourcestrings in a form ready for translation. To create a German translation from that file create a copy and rename it imgview.de.po. "de" is the language code for "German" in the translation system, accordingly you can use "en" for "English", "ru" for "Russian" etc. See for example www.science.co.il/Language/Locale-codes.asp for a list of all language codes.

We could open imgview.de.po and add translations in a straighforward way. But there is an even simpler way as we will see later. Before doing that let's apply another modification to the demo project.

Use LCLTranslator unit (class TDefaultTranslator)

So far, the po files only contain resourcestrings that are explicitly declared as such. There are, however, a lot of other strings in our application which are not yet covered, like the entire menu and submenus, texts in labels or listboxes, etc.

There is an extremely easy way to include the strings of the user interface into the translation system: Simply add the unit LCLTranslator to the main form's uses clause. When the project is compiled after this modification you'll find all strings in the po file.

LCLTranslator.png

Light bulb  Note: In Lazarus versions older than 1.4 this functionality resides in unit DefaultTranslator.pas.

Translating

For translation we could edit the po files directly by using a standard text editor. But it is more conventient to use a separate program optimized for this purpose. Good candidates are:

I'll be using poedit here.

Install this program. If not done before, copy imgviewer.po and rename it to imgviewer.de.po. Open imgviewer.de.po in poedit.

poedit shows a list of all resourcestrings: those explicitly declared, and those extracted from lcl controls by the DefaultTranslator. Select a string and type its translation into the lower memo. Repeat with all texts. Save. Before saving open then menu item "Catalogue" / "Properties" and check if "Charset" is UTF-8 - poedit sometimes forgets about this correct setting.

poedit.png

In the same way, you can add more languages: open the .po file of the the application in poedit, add the translations, and save it with the corresponding language code before the po extension.

Since the test application is hard-coded in English it is very easy to create an English translation file: after loading imgview.po into poedit, select each string item and press Ctrl+B which copys the raw resourcestring value into the translation memo. Save as imgview.en.po.

In its initialization code, DefaultTranslator seeks for the language setting of the currently used system. Therefore, if you work on a PC set up for German language the demo project will now be in German automatically. Detection of the default language and replacing the resource strings with those from the corresponding po file has been done by the translation system of Lazarus.

Excluding Controls

To exclude some controls from translation, you can add it in project options i18n page, full path to this control like "tmainform.edit1.text" in Execlude field

Switching languages

But what if your PC is not on German language? We can use the Lazarus translation system to switch languages at run-time.

First of all, the LCLTranslator unit gives access to commandline switches --lang or -l to override the automatic language detection. For example,

    imgview.exe --lang de

opens the German translation of the program even on an English-speaking system.

Of course, changing languages at run-time would be even more favorable. Unfortunately this was not possible with Lazarus before version 1.2 out of the box. But it is no problem with current versions of Lazarus where LCLTranslator provides a procedure SetDefaultLang for switching between languages upon user request. (Again: "use" unit DefaultTranslator instead of LCLTranslator in case of Lazarus 1.2.x).

At first, we need some control in the user interface to switch languages. What about a new menu item "Translation" and a submenu containing the available languages? In the menu designer of the main form add a new item "Translations" and an empty submenu. Then, for each language available, add the name of the corresponding language to the submenu. Of course you can also show a flag icon for each language - free flag icons are available from [1]. In the OnClick event handler call SetDefaultLang with the language code as a parameter, e.g.

procedure TMainForm.MEnglishlanguageClick(Sender: TObject);
begin
  SetDefaultLang('en');
end;  

procedure TMainForm.MGermanLanguageClick(Sender: TObject);
begin
  SetDefaultLang('de');
end;

Of course, you can use more sophisticated code which determines the language codes from the names of the po files found and creates menu items depending on the available translations.

Now you can click on a language menu item, and the application language switches to the selected language automatically!

As you may notice the new menu item "Translation" and its submenu items are not translated. This is because these are new strings missing from the translated po files. Simply open the translated files in poedit and add the translations of the new strings. All the previous translations are still there. Save, and you are all set.

Strings defined by the LCL

Here is one idea for refinement: The strings set up up for error messages defined by the LCL or used in standard dialogs or message boxes are not yet translated. Their translation is extremely easy: their translation files can be found in the directory lcl/languages of your Lazarus installation; they are named lclstrconsts.*.po. Pick those translations used by your application and copy them to the languages folder of our project. (And add unit lclstrconsts to the uses clause if you refer to one of these strings).

This works because resourcestrings are pulled in from all language files for the active language found in the languages folder.

Format settings

When creating multi-language applications translation of the strings is not the only task. Another issue is that format settings may change from country to country. Format settings - they define formatting of dates and times, the month and day names, the character to be used as decimal or thousands separator, etc. In Lazarus, the record TFormatSettings collects all the possible data. The DefaultSettings are used by the standard format conversion routines, such as StrToDate, DateToStr, StrToFloat, or FloatToStr, etc.

For Windows, there exists a procedure GetLocaleFormatSettings in the unit sysutils which returns the TFormatSettings for a given localization. The parameter LCID specifies the language code in Windows. Unfortunately I do not know of a convenient conversion between the LCID and the language codes ('de', 'en', etc) used by Lazarus. A conversion table for lookup can be found at Language Codes. In this table, you see that "German" has the LCID $407, and "English" has the LCID $409. Therefore, we modify the OnClick event handler for the language selection menu items as follows:

procedure TMainForm.MEnglishlanguageClick(Sender: TObject);
begin
  SetDefaultLang('en');
  GetLocaleFormatSettings($409, DefaultFormatSettings);
end;

procedure TMainForm.MGermanLanguageClick(Sender: TObject);
begin
  SetDefaultLang('de');
  GetLocaleFormatSettings($407, DefaultFormatSettings);
end;

As a demonstration we add a status bar to our project and display the date when an image was created:

procedure TMainForm.ShowPicDateTime;
var
  dt: TDateTime;
begin
  if LBFiles.ItemIndex = -1 then
    Statusbar.SimpleText := ''
  else begin
    dt := FileDateToDateTime(FileAge(LBFiles.Items[LBFiles.ItemIndex]));
    Statusbar.SimpleText := DateToStr(dt) + ' ' + TimeToStr(dt);
  end;
end;

This procedure is called from the OnSelectionChange event handler of the files listbox:

procedure TMainForm.LBFilesSelectionChange(Sender: TObject; User: boolean);
begin
  ShowPicDateTime;
end;

Recompile the program and load some images. You'll see that the date format changes when the language is switched and another image is selected.

Unfortunately, this solution is valid only for Windows. I don't know how this issue could be handled in Linux etc. Maybe somebody does out there in the community?

Although some issues of localization (e.g. right-to-left mode, sorting order) have not been discussed I hope that this tutorial covered the most important aspects and is at least a good starting point for beginners.

In the end, here is the final screenshot of this tutorial: it shows the image viewer application translated to German.

imgviewer de.png.

macOS

Refer to the Locale settings for macOS article.

Operating system dialogs

The operating system dialogs (eg TOpenDialog) are not translatable by Lazarus because they use the operating system strings and not the LCL widgetset strings which can be localised as detailed above.

macOS

Note-icon.png

Tip: You may need to delete the symbolic link to the executable created by Lazarus in the application bundle and replace it with the actual executable before the localised language will show up in the system dialogs when using the methods described below.

For macOS, one solution is to add the language designators of the supported languages to the application's Info.plist file like this:

        <key>CFBundleLocalizations</key>
        <array>
                <string>en</string>
                <string>fr</string>
                <string>de</string>
                <string>it</string>
                <string>nl</string>
                <string>ru</string>
                ... etc ...
        </array>

and then the operating system dialogs will present users with their preferred language (fr - French example below).

french dialog localization.png

An alternative method for localising a macOS application is to instead create an empty folder for each language in the application bundle's Resources directory. For example, for French, add an empty file named fr.lproj in MyApp.app/Contents/Resources.

Language designators

A language designator is a code that represents a language. Use the two-letter ISO 639-1 standard (preferred) or the three-letter ISO 639-2 standard. If an ISO 639-1 code is not available for a particular language, use the ISO 639-2 code instead. For example, there is no ISO 639-1 code for the Hawaiian language, so use the ISO 639-2 code. For a complete list of ISO 639-1 and ISO 639-2 codes, see ISO 639.2 Codes for the Representation of Names and Languages.

See also