Internationalizing your Project

Introduction

You have a project that uses Obviel for its user interface, and now you want it to work in multiple languages. For this you need to adjust your application to support multiple languages, a process known as “internationalization”, or “i18n” for short (as “internationalisation” has 18 letters between the ‘i’ and the ‘n’).

How do you go about this?

The idea is that you mark up all translatable texts in your project’s code (.js and .obvt files) in a special way. Then when your application runs for the marked up pieces of text a translations registry is consulted. If a translation is found for a marked up piece of text, it is used instead of the original text.

A special extraction tool can be used to extract the marked up pieces of text from .js and .obvt files, so that they can be given to human translators for translation.

Now let’s go into some more detail.

JavaScript i18n with jsgettext

Imagine we have this piece of JavaScript code we want to i18n:

alert("Hello world!");

This application has been written with one language in mind, in this case English. But if the application runs with another language, for instance French, we’d like to see the popup in that language (for instance “Bonjour monde!”) instead of in English.

You can make this happen using jsgettext. This uses the gettext approach, which is very often used for i18n for a variety of projects and programming languages. Include jsgettext like this:

<script type="text/javascript" src="/path/to/Gettext.js"></script>

We now need to make a translation available for our Hello world! message. This is what that looks like:

var json_locale_data = {
  'myproject': {
    "Hello world!": [null, "Bonjour monde!"]
  }
};

The exact structure of json_locale_data object is not something to worry about, as later in this document we’ll introduce a tool to generate this automatically for you. We just wanted to provide for the purposes of a demo.

We are now ready to create a Gettext object for your project that knows about the translations available:

var gt = new Gettext({
  domain: 'myproject',
  locale_data: json_locale_data
});

We will also create a little convenience function _ that can be used to mark up text to make it translatable:

var _ = function(msgid) { return gt.gettext(msgid); };

This is a bit of code to put together, but you’d typically do all this only once per project and use it everywhere.

So now let’s return to our JavaScript code to i18n:

alert("Hello world!");

we adjust it to use our _ function:

alert(_("Hello world!"));

When you run your application now, you’ll see “Bonjour monde!”.

Here is all this put together as a working demo.

Extracting translatable content

We know how to mark up our JavaScript code so it can be translated now. We now need a tool that can extract all pieces of marked up text so that we can give it all to a translator.

We recommend the use of the Babel i18n tool with code that uses Obviel. While Babel primarily written to support i18n of Python applications, it also supports JavaScript applications.

If you’re on Linux, Babel may be available in your Linux distribution. You can also install it manually using one of the Python tools used for this (pip, easy_install, or buildout). After you do this a pybabel commandline tool will be available.

To make Babel work with .js files we first need to configure it. Create a .cfg file with the following content:

[javascript: **.js]
extract_messages = _

Now you can use it on your project’s directory that contains all the .js files:

$ pybabel extract -F myconfig.cfg project_directory > myproject.pot

You will now have a .pot (PO template) file that will be the source of all your actual translation files for your application, the .po files. For example, if you had applied this to the demo described above, you would get a .pot file like this (skipping some metadata boilerplate):

#: src/demo/i18n-js-demo.js:15
msgid "Hello world!"
msgstr ""

In the comment line it points out where in the code the text to translate was found; this is sometimes useful context for translators.

Creating .po files

For each language our project has translations for, we now need to create a .po file from our .pot file. You can do this with the GNU gettext tool suite, using the msginit tool (details about msginit):

$ msginit -l fr_FR -i myproject.pot -o myproject-fr_FR.po

This command says we want to create a specific .po` file for the French language locale (``fr_FR). This is what the generated .po file looks like:

#: src/demo/i18n-js-demo.js:15
msgid "Hello world!"
msgstr ""

This is in fact very similar to our .pot file!

We can now edit the file to add the French translations:

#: src/demo/i18n-js-demo.js:15
msgid "Hello world!"
msgstr "Bonjour monde!"

In practice, you would likely give this file to someone else: the person translating the application to French. They can then edit it directly to add or update the translations, or use some GUI tool that can work with .po files. When they are done, they would give the file back to you.

Updating .po files

When your project evolves, you will likely add new texts to translate, or change or remove existing ones. You can use the babel extract tool as usual to extract a new .pot file containing all the current translation texts. You can then use the GNU gettext msgmerge tool (details) to update your existing .po files so that the new translatable texts become available to your translators:

$ msgmerge -U myproject-fr_FR.po myproject.pot

Creating .js files from the .po files

We now have a .po file with the right translations. While jsgettext offers a way to load up such .po files directly, it is recommended you use an optimized JavaScript file for this instead. We can do this using the pojson tool (installable using one of the Python methods such as pip, easy_install or buildout).

Using pojson you can convert the .po file to an optimized .js file:

bin/pojson convert -j myproject myproject-fr_FR.po > myproject-fr_FR.js

You now have a file with the following contents (skipping some metadata again, and pretty printed):

var json_locale_data = {
  'myproject': {
    "Hello world!": [null, "Bonjour monde!"]
  }
};

This is the structure we created by hand before, so once that’s passed into the Gettext constructor, we’re done.

Translating .obvt files

We’ve now demonstrated the procedure for a .js file, but what about Obviel Template .obvt files? We provide a plugin to Babel called babel-obviel that knows how to extract translatable text from .obvt files. You install it using the standard Python installation tools (make sure it is installed in the same Python as Babel is, so that Babel can pick up on it).

Now you could have a template like this, marked up for translation:

<p data-trans="">Hello world!</p>

We need to teach pybabel extract about .obvt files in the .cfg file we created before:

[javascript: **.js]
extract_messages = _

[obvt: **.obvt]

When you now run pybabel extract on your project, it will extract translatable texts not only from .js files but from .obvt files as well. You can translate these in the same way.

Read Obviel Template i18n for much more detail on how to mark up your .obvt files for i18n.

Variables

Obviel Template supports the use of variables within data-trans sections, like this:

<p data-trans="">Hello {who}!</p>

or like this for variables within elements:

<p data-trans="">Hello <em data-tvar="who">{who}</em>!</p>

pybabel extract will extract both of these to this text in the .po file:

Hello {who}!

The translator now needs to take care to include this variable in the translation as well:

Bonjour {who}!

A translator could decide it makes more sense to move the variable to another place as well:

{who}, bonjour!

Obviel Template’s i18n system will use these variables automatically. To use such variables in a .js file, you can use a special variables() formatting function that Obviel Template makes available:

variables(_("Hello {who}!"), { who: "Bob" });