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

From Lazarus wiki
Revision as of 19:51, 26 November 2013 by Swen (talk | contribs) (Enable translations)

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

At first, we need an application that we want to translate. I was looking through the example projects that come with Lazarus and found that examples\imgviewer may be 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.

imgviewer orig.png

Enable translations

There is only one modification of the project which 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.

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 to 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 resource string declarations at the beginning of the implementation section of frmmain. In a larger project, it is conventient to collect all resource strings in a separate unit; you can access the strings from other forms and units and avoid cross-referenced 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 resource strings 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 this file 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. But it is more conventient to use a separate program for this purpose. Good candidates are poedit ([1]) or Better PO Editor ([2]). 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.

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.

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.

If you use the "official" Lazarus version 1.0.x this task, however, is a bit complex because a procedure for run-time switching of language is not yet included in DefaultTranslator - it will be included in the new Lazarus 1.2 to come.

Anyway, for the users of Lazaraus 1.0.x let's borrow this procedure from the upcoming version and add it the the frmmmain unit:

procedure SetDefaultLang(Lang: string);

var
  Dot1: integer;
  LCLPath: string;
  LocalTranslator: TUpdateTranslator;
  i: integer;

begin
  LocalTranslator := nil;
  // search first po translation resources
  try
     lcfn := FindLocaleFileName('.po', Lang);
     if lcfn <> '' then
     begin
       Translations.TranslateResourceStrings(lcfn);
       LCLPath := ExtractFileName(lcfn);
       Dot1 := pos('.', LCLPath);
       if Dot1 > 1 then
       begin
         Delete(LCLPath, 1, Dot1 - 1);
         LCLPath := ExtractFilePath(lcfn) + 'lclstrconsts' + LCLPath;
         Translations.TranslateUnitResourceStrings('LCLStrConsts', LCLPath);
       end;
       LocalTranslator := TPOTranslator.Create(lcfn);
     end;
   except
     lcfn := '';
   end;

  if lcfn='' then
  begin
    // try now with MO traslation resources
    try
      lcfn := FindLocaleFileName('.mo', Lang);
      if lcfn <> '' then
      begin
        GetText.TranslateResourceStrings(UTF8ToSys(lcfn));
        LCLPath := ExtractFileName(lcfn);
        Dot1 := pos('.', LCLPath);
        if Dot1 > 1 then
        begin
          Delete(LCLPath, 1, Dot1 - 1);
          LCLPath := ExtractFilePath(lcfn) + 'lclstrconsts' + LCLPath;
          if FileExistsUTF8(LCLPath) then
            GetText.TranslateResourceStrings(UTF8ToSys(LCLPath));
        end;
        LocalTranslator := TDefaultTranslator.Create(lcfn);
      end;
    except
      lcfn := '';
    end;
  end;

  if LocalTranslator<>nil then
  begin
    if Assigned(LRSTranslator) then
      LRSTranslator.Free;
    LRSTranslator := LocalTranslator;

    // Do not update the translations when this function is called from within
    // the unit initialization.
    if (Lang<>'') then
    begin
      for i := 0 to Screen.CustomFormCount-1 do
        LocalTranslator.UpdateTranslation(Screen.CustomForms[i]);
    end;
  end;
end;

Now 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, add code which reads the "languages" folder and extracts the languages found:

<source>

to switch language to another translation.

As an example let's add a new menu item to our demo project be in German - if your computer system has German as its default language).

But what if