Sunday, 31 March 2013

Vala #6: Localization

Basics

Software applications are usually developed in a single language. So even though a programmer in Russia would be writing code using English keywords, the user interface and the messages in the application would probably be in Russian. Localizing an application involves writing the application in such a way that it can be easily translated to other languages.

The most common method is to keep all strings in an external resource file. Separate files are created for each language, and depending on the language selected by the user, the strings are loaded from the corresponding file on application startup. Separating the strings from the source code is important. It allows separate translation teams to work on translating the resource files while the developers can focus on application development.

Localization and GNU gettext

GNU gettext is a translation framework that provides a set of utilities for generating resource files and loading translated strings. It supports most common languages like C, C++, C#, Java and Perl and is extremely powerful and simple to use.

GNU gettext and Vala

Adding translation support to a Vala application is pretty simple. We will use the following program to demonstrate the steps involved.

hello.vala

void main() 
{ 
    stdout.printf("This is a sentence in English"); 
    stdout.printf("\n"); 
}

Localizing a Vala application involves the following steps:

1. Mark the strings to translate.

Source code files usually contain a large number of strings, and not all strings need to be translated. Mark the strings to be translated by enclosing the string in brackets with a leading underscore.

Original line:

stdout.printf("This is a sentence in English"); 

After marking the string:

stdout.printf(_("This is a sentence in English")); 

2. Setting the locale

Add the following lines of code at the start of the application.

Intl.setlocale(LocaleCategory.MESSAGES, "");
Intl.textdomain(GETTEXT_PACKAGE); 
Intl.bind_textdomain_codeset(GETTEXT_PACKAGE, "utf-8"); 
Intl.bindtextdomain(GETTEXT_PACKAGE, "./locale");

GETTEXT_PACKAGE is a string constant that is defined at the top of the source file. You need to define this only once in any one of the source files. Since our app is named hello.vala we will set it to hello.

Now we have the code:

const string GETTEXT_PACKAGE = "hello"; 

void main() 
{ 
    Intl.setlocale(LocaleCategory.MESSAGES, "");
    Intl.textdomain(GETTEXT_PACKAGE); 
    Intl.bind_textdomain_codeset(GETTEXT_PACKAGE, "utf-8"); 
    Intl.bindtextdomain(GETTEXT_PACKAGE, "./locale"); 

    stdout.printf(_("This is a sentence in English")); 
    stdout.printf("\n"); 
}

Save it as hello.vala and compile it with the command:

valac -X -DGETTEXT_PACKAGE="hello" hello.vala

Note the additional parameters that we are passing to the vala compiler. The -X and -D options need to be specified before the source file name.

Extracting the strings

Extract the marked strings from the source file with the following command:

xgettext --language=C --keyword=_ --escape --sort-output -o hello.pot hello.vala

The xgettext utility parses the hello.vala source file and writes all marked strings to a file named hello.pot. A POT file (Portable Object Template) is a template file that contains a pair of strings for each string that is extracted from the source file. Since our program contains a single string, the POT file will have the following:

msgid "This is a sentence in English"
msgstr ""

msgid is the original string. It is used as an identifier.
msgstr is the translated string.

Creating a translation

Let's create a French translation for our app.

Run the following command to create a folder for storing translation files:

mkdir -p locale/fr_FR/LC_MESSAGES

The folders are named according to the following naming convention:
<2-letter-language-code>_<2-letter-region-code>/LC_MESSAGES

Run the following command to create a translation file (PO) from the template file (POT):

msginit -l fr_FR -o locale/fr_FR/LC_MESSAGES/hello.po -i hello.pot

Open the generated file hello.po in a text editor. Update the msgstr value and save the file.

hello.po

msgid "This is a sentence in English"
msgstr "Cette phrase est en fran├žais"

The translation file needs to be compiled before it can be used by the app. Run the following command to compile the PO file.

cd locale/fr_FR/LC_MESSAGES
msgfmt --check --verbose -o hello.mo hello.po

This generates a file hello.mo which is in machine-readable (binary) format.

That's it. We're all done.

Testing the translation

First install the support packages for the French language on your system.
Run the following command in terminal window:

sudo aptitude install -y language-pack-fr

Wait while the language packs are downloaded and installed.

Run the following command for changing the current locale:

export LANGUAGE=fr_FR.UTF-8

Run the program in the same terminal window:

./hello

Notes

const string GETTEXT_PACKAGE = "hello"; 

void main() 
{ 
    Intl.setlocale(LocaleCategory.MESSAGES, "");
    Intl.textdomain(GETTEXT_PACKAGE); 
    Intl.bind_textdomain_codeset(GETTEXT_PACKAGE, "utf-8"); 
    Intl.bindtextdomain(GETTEXT_PACKAGE, "./locale"); 

    stdout.printf(_("This is a sentence in English")); 
    stdout.printf("\n"); 
}

The bindtextdomain() function sets the search directory for the translation files. Since we created the directory locale for keeping translation files, we are binding the text domain to the path ./locale. If we install the MO files to the default system path /usr/share/locale, then there is no need to call the bindtextdomain() function.

4 comments:

  1. Here is a very good online tool that works perfectly with gettext: https://poeditor.com/. You just need to create an account and upload your po, pot or other type of file you might have and the translation interface will help you through your work.

    ReplyDelete
  2. Thank you for this fantastic blog.

    ReplyDelete
  3. I have bookmarked your web site for future articles similar to this one. Maintain posting for much more.
    conversion help

    ReplyDelete

If you are reporting an issue and commenting as an anonymous user, please leave your email address so that I can get in touch with you.