Multilingual Programming Guide

Concepts

Internationalization (also called i18n) is performed by using strings in a specific language in the code (typically English) and then translating them to the target language.

Using actual strings rather than resource identifiers makes code much more convenient and readable. For instance, a simple multilingual button can be programmed as follows:

Evas_Object *button = elm_button_add(parent);
elm_object_translatable_text_set(button, "Click Here");

Messages requiring translation are typically automatically extracted from the sources and placed into files with the extension .po, one for each language. For instance, the file "fr.po" file for the example code above could contain:

#: some_file.c:43 another_file.c:41
msgid "Click Here"
msgstr "Cliquez ici"

In this code example, the program which extracts strings has found two occurrences of the same string : one in some_file.c on line 43 and another in another_file.c on line 41. It returns the original string after msgid. The translation comes after msgstr.

Strings with no translation are stored as an empty string in the .po file. If this happens, the program will just use the original string.

The extractor program may add the keyword fuzzy to the line immediately above msgid. This means the original string has changed and needs to be reviewed.

Don't be surprised if the translation is correct even though you didn't change it. The extractor program is sometimes able to "guess" the updated translation.

Internationalization in EFL

Marking text parts as translatable

Translation commonly involves the following APIs:

elm_object_translatable_text_set(Evas_Object *obj, const char *text)
elm_object_item_translatable_text_set(Elm_Object_Item *it, const char *text)

These set the untranslated string for the default part of the given Evas_Object or Elm_Object_Item and mark the string as translatable.

There are similar functions if you wish to set the text for a part that is not default:

elm_object_translatable_part_text_set(Evas_Object *obj, const char *part, const char *text)
elm_object_item_translatable_part_text_set(Elm_Object_Item *it, const char *part, const char *text)

Make sure to provide the untranslated string to these functions, as EFL will trigger the translation and re-translate the strings automatically should the system language change.

You can set the text and the translatable property separately if you wish. Set text in the usual way. The translatable property is set through elm_object_part_text_translatable_set().

There are also get() counterparts to the set() functions listed above.

Translating texts directly

The approach described in the previous section is not always applicable. For instance it won't work if you are populating a genlist, if you need to use plurals in your translation or if you want to do something with the translation besides placing it in elementary widgets.

You can however retrieve the translation for a given text using gettext from ''<libintl.h>'' :

char * gettext(const char * msgid);

The input of this function is a string which will be copied to an msgid field in the .po files. The function thenreturns the translation (the corresponding msgstr field).

In order to use gettext, you must first set the locale:

setlocale(LC_ALL,"");

LC_ALL is a catch-all Locale Category (LC). Set this to alter all LC categories such as LC_MESSAGES which handles message translations and LC_TYPES which manages supported character sets.

By setting the locale to ''""'', you are implicitly assigning it to the user's defined locale, which is determined by the user's LC or LANG environment variables. If there is no user-defined locale, the default locale C is used.

bindtextdomain("hello","/usr/share/locale/");

This above command binds the name hello to the message files root directory. The program will search for ''hello.mo'' in ''/usr/share/locale//LC_MESSAGES/'', where might be fr_FR as set in the user's defined locale. Use this to specify where you want your locale files stored. You can use hello when setting the gettext domain through textdomain(). This corresponds to the name of the file to be searched in the appropriate locale directory.

The bindtextdomain() call is not mandatory. If you choose to install your file in the system's default locale directory it can be omitted. The default location can change from system to system however, so you may prefer to do this regardless.

textdomain("hello");

The above command sets the application name to hello. This makes gettext calls search for the file ''hello.mo'' in the appropriate directory. You can switch between different domains if you wish through binding various domains and setting the textdomain or by using dcgettext() (see below) at runtime.

Consider using the below example as a basis for setting the text for a genlist item:

#include<libintl.h>
#include<locale.h>
 
#define _(str) gettext(str)
 
static char *
_genlist_text_get(void *data, Evas_Object *obj, const char *part)
{
   return strdup(gettext("Some Text"));
   /* or usual way
    * return strdup(_("Some Text"));
    */
}
 
EAPI_MAIN int
elm_main(int argc, char **argv)
{
   setlocale(LC_ALL,"");
   bindtextdomain("hello","/usr/share/locale");
   textdomain("hello");
 
   /* ... */
 
   elm_run();
   elm_shutdown();
   return 0;
}
ELM_MAIN()

Plurals

Plurals are handled via the ngettext() function:

char * ngettext (const char * msgid, const char * msgid_plural, unsigned long int n);

Note that msgid is the same as before, i.e. the untranslated string. Use msgid_plural to define the plural form of msgid. You can also define the quanity. Any value above 1 is considered plural.

A corresponding "fr.po" file would contain the following lines:

msgid "%d Comment"
msgid_plural "%d Comments"
msgstr[0] "%d commentaire"
msgstr[1] "%d commentaires"

Multiple plurals

You can choose to several plural forms. For instance, the ".po" file for Polish could contain:

The index values after msgstr are defined in system-wide settings. The ones for Polish are given below:

"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"

Here there are 3 forms including singular. The index is 0 (singular) if the given integer n is 1. If ''(n % 10 >= 2 && % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)'', the index is 1, otherwise it's 2.

Handling language changes at runtime

Users can change the system language settings at any time. When this happens Ecore Events notifies the application, which can then change the language used in Elementary. The widgets then receive a "language,changed" signal and can set their text again.

The first step is to handle the ecore event:

static Eina_Bool
_app_language_changed(void *data, int type, void *event)
{
   // Set the language in elementary
   elm_language_set(setlocale(LC_ALL,NULL));
}
 
int
main(int argc, char *argv[])
{
    ...
 
    // Retrieve the current system language
    ecore_event_handler_add(ECORE_EVENT_LOCALE_CHANGED , _app_language_changed, NULL);
 
    ...
}

The call to elm_language_set() above will triggerlanguage,changed, which can then be handled like any other smart event.

Extracting messages for translation

The xgettext tool can extract strings to translate to a .pot file (po template) while msgmerge can maintain existing .po files. The typical workflow is as follows:

  • Run xgettext once; it will generate a Portable Object Template (.pot) file
  • When adding a new translation, copy the .pot file to .po and translate that file
  • New runs of xgettext will update the existing ".pot" file and msgmerge will update ".po" files

A typical call to xgettext looks like:

 xgettext --directory=src --output-dir=res/po --keyword=_ --keyword=N_ --keyword=elm_object_translatable_text_set:2 --keyword=elm_object_item_translatable_text_set:2 --add-comments= --from-code=utf-8 --foreign-user

This will extract all strings used inside the _() function (usual optional short-hand for gettext()), select UTF-8 encoding and add the comments immediately before the strings to the output files.

A typical call to msgmerge is as follows:

msgmerge --width=120 --update res/po/fr.po res/po/ref.pot

POT files contain a series of paired lines starting with the keywords msgid and msgstr respectively. In the above example there is only one such pair : msgid is shown first followed by a string in the source language. This is followed by msgstr in the next line which is immediately followed by a blank string.

In order to translate the application, POT files are copied as PO (.po) files into their respective language folders translated. This means every string adjacent to msgid will have corresponding translated string (in local script), adjacent to msgstr. For the above example of a button, the code for the French language might resemble:

msgid "Click Here\n"
msgstr "Cliquez ici\n"

Compiling and running a Localized Application

Create an MO (.mo) file using the following command:

msgfmt helloworld.po -o helloworld.mo

Using root, copy the MO file to /usr/share/locale//LC_MESSAGES. For instance, if you want to support French use:

cp helloworld.mo /usr/share/locale/fr_FR/LC_MESSAGES/

Don't forget to export your chosen language too:

export LANG=fr_FR.utf8

Finally, compile and execute your program.

Internationalization tips

Don't make assumptions about languages

The grammar of respective language can vary wildly. For instance, in English typography no character must appear before colons and semicolons (':' and ';'). However in French there should be an "espace fine insécable", i.e. a non-breakable space (HTML's  ). This is narrower than regular spaces.

You may think this distinction to be unimportant but the above example would prevent proper translation of this code:

snprintf(buf, some_size, "%s: %s", gettext(error), gettext(reason));

The proper way to do this is to use a single string and let the translators manage the punctuation. This means, translating the format string instead:

snprintf(buf, some_size, gettext("%s: %s"), gettext(error), gettext(reason));

Translations may have different lengths

Depending on the language, translations will have a different length on screen. Some languages have shorter constructs than other in some cases while the reverse may be true in others. Some languages can also have a word for a concept while others won't, which will require you to use multiple words in place of one.

For source control, don't commit .po if only line indicators have changed

For the example above, a translation block looks like:

#: some_file.c:43 another_file.c:41
msgid "Click Here"
msgstr "Cliquez ici"

If you insert a new line at the top of "some_file.c", the line indicator will change to:

#: some_file.c:44 another_file.c:41

Obviously, on non-trivial projects, such changes will happen often. If you use source control and commit such changes, even though no actual translation change has happened, each and every commit will probably contain a change to the ".po" files. This will hamper readability of the change history. If several people are collaborating on a project and need to merge their changes, this could also create huge merge conflicts each time.

Only commit changes to ".po" files when you make changes to translations, not merely because line comments have changed.

Use _() as a shortcut for gettext() function

Since calling gettext() might happen very often, it is often abbreviated to _(), for instance:

#define _(str) gettext(str)

Proper sorting: strcoll()

You can sort data for display usin the string comparison strcoll(). It works in the same way as strcmp() but sorts according to the current locale settings:

int strcmp(const char *s1, const char *s2);
int strcoll(const char *s1, const char *s2);

The function prototype is a standard one and indicates how to order strings. A detailed explanation is outside the scope of this guide however you should be able to use the strcoll() function as the comparison function for sorting your data sets:

Working with translators

The system described above is a common one and will likely be known to translators. This means that simply citing "gettext" might be enough to receive help via the internet.

Don't hesitate to put comments in your code right above the strings to translate,as these can be extracted along with the strings and put in the .po files for the translator to see them.