Step-by-step instructions for creating multi-language applications

From Free Pascal wiki
Jump to navigationJump to search

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 is maybe a good idea to learn something about the basic ideas behind that architecture. Therefore I'd urge you to read Translations_/_i18n_/_localizations_for_programs which explains the basic fundamentals behind the scene.

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.

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 German translation.

Enable translations

Only one modification of the project is required to enables translation. It is found in the project options as item "i18n". Strange word, isn't it? It is the abbrevation 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. 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 DefaultTranslator

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.

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

uses DefaultTranslator.png

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 poedit or Better PO Editor. 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, type its translation in 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

If you work with 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 the same way, you can add more languages: open the .po file of the the appliation in poedit, add the translations, and save it with the corresponding language 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.

Changing 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.

The DefaultTranslator of Lazarus trunk or 1.2 contains a procedure SetDefaultLang which can be employed to switch between languages upon user interaction. Unfortunately, this is only a recent addition to Lazarus, it is not yet available in the current version 1.0.x. (Of course, if you want to stick to 1.0.x you add this procedure to the source code of DefaultTranslator.pas - it is built from the code running at initialization of the unit -, and save the modified unit in your project folder.)

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!

Strings defined by the LCL

Here is one idea for refinement: The strings setup 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; in our case, we copy lclstrconsts.de.po and lclstrconsts.en.po.

This works because, when translating, resourcestrings are look for in all language files 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.

For Windows, there exists a procedure GetLocaleFormatSettings in the unit sysutils which returns a TFormatSettings which is used when calling string conversion functions such as StrToDate, StrToFloat etc. The function result is determined by the parameter LCID. This is number specifying 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 can be found at [2]. 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;

To demonstrate this effect 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;

When you recompile the program, load some images you 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 out there in the community?

Anyway, here is the final screenshot of this tutorial: it shows the imgviewer application translated to German.

imgviewer de.png.