12.2.4. Internationalization and localization

Internationalization and localization of NextGIS Web is powered by gettext and babel libraries. The procedure for working with strings is standard for projects based on gettext:

  1. Extracting strings to translate from sources to pot-file (extract)

  2. Creating or updating po-files based on pot-files (init and update)

  3. Compiling po-files into mo-files (compile)

All lines that are shown to the user must be written in sources in English and only then translated into other languages.

Translation is performed for each component of NextGIS Web separately. As a result, there is no way to internationalize strings that are not belongs to any component.

Translations of each component exists within its own domain in terms of the gettext library. In one component one line can be translated in one way, and in another - in another, even without the use of contexts.

All actions on translations are performed using nextgisweb-i18n command line utility from the nextgisweb package. This utility is an add-on to the pybabel utility from babel package with default settings preset.

Let’s consider localization to Russian on the example of bar component from foo package. In this case, the directory structure will look like this:

foo
├── setup.py
├── foo
│   └── bar
└── locale

Extract strings from source code files with default settings:

(env) $ nextgisweb-i18n --package foo extract bar

This will create a pot-file foo/foo/locale/bar.pot. Since this file can be generated at any time, there is no need to put it inside the source control system.

Create a po-file for the Russian language based on the pot-file:

(env) $ nextgisweb-i18n --package foo init bar ru

As a result, a po-file foo/foo/locale/ru/LC_MESSAGES/bar.pot will be created (this is a standard structure for gettext, perhaps sometimes we will replace it in favor of a simpler one). This file must be filled in accordance with the translation of the strings in it using a text editor or a specialized po-file editor.

After this, we compile po-files into mo-files, which also do not need to be placed in the version control system:

(env) $ nextgisweb-i18n --package foo compile bar

The resulting directory structure is given bellow. The files bar.jed are analogous to the mo file for the javascript library jed, which is used for client-side internationalization. These files are also created at the compilation stage.

foo
├── setup.py
└── foo
    ├── bar
    └── locale
        ├── bar.pot
        └── ru
            └── LC_MESSAGES
                ├── bar.jed
                ├── bar.mo
                └── bar.po

This completes the initial internationalization, after some time new lines may have be added to the package, in this case you need to re-extract the lines, automatically update the po-file, edit it and compile again:

(env) $ nextgisweb-i18n --package foo extract bar
(env) $ nextgisweb-i18n --package foo update bar
(env) $ nano foo/foo/locale/ru/LC_MESSAGES/bar.po
(env) $ nextgisweb-i18n --package foo compile bar

Server side

Python code

To be able to extract strings for translation, they must be marked up appropriately. Below is described how to do this, as well as how to ensure the display of already translated strings to the user.

Since python code is executed on a server, the same application instance must be able to serve users with different locales, it is necessary to use a two-step work with strings: first, the string is marked as requiring translation, then before displaying it to the user, it’s translated according to the user’s preferences.

The class nextgisweb.i18n.trstring.TrString solves this problem, which is similar to the class translationstring.TranslationString, but with some additional convenience in terms of string interpolation. Let’s look at the example of the bar component from the foo package``.

Listing 12.1. Sample directory structure
bar
├── __init__.py
├── util.py
└── template
    └── section.mako
Listing 12.2. File util.py
from nextgisweb.i18n import trstring_factory
_ = trstring_factory('bar')

Function nextgisweb.i18n.trstring.trstring_factory() allows you to simplify creation of strings TrString with a predefined domain, which is specified in the function parameters. For convenience, both the function and the class are also available for import from the module nextgisweb.i18n, as shown in the examples.

Listing 12.3. File __init__.py #1
from .util import _
def something():
    return _('Some message for translation')

Usage of the underscore character is necessary for extraction of translation strings, so you can’t import it with a different name from .util import _ as blah, it will break extraction process.

For string output in accordance with the user’s preferences (one user may want English, the other Russian), you need to translate the string using the request.localizer.translate(trstring) method:

Listing 12.4. File __init__.py #2
@view_config(renderer='string')
def view(request):
    return request.localizer.translate(something())

Примечание

Since request only makes sense in the web application, this means that currently it isn’t possible to use localization in the nextgisweb command line utilities.

Mako templates

Some of the strings that require translation are also contained in the mako-templates. In fact, the work of mako templates is not much different from the python code: first, we mark the string for translation with a special function, then we need to translate through request, taking into account the user’s preferences.

Listing 12.5. File template/section.mako #1
<% from foo.bar.util import _ %>
<div>${request.localizer.translate(_("Another message for translation"))}</div>

To shorten this long notation a bit, a tr() function has been added to the mako-template’s context, which does the same. The example below is completely equivalent to the previous one:

Listing 12.6. File template/section.mako #2
<% from foo.bar.util import _ %>
<div>${tr(_("Another message for translation"))}</div>

Примечание

Unfortunately, it isn’t possible use this function as a modifier ${expression | tr}. In this case, the result of the standard modifier n, that is markupsafe.Markup gets into the function.

In order to track that all strings requiring translation were translated when outputting in the template in debug mode (setting debug of the component core) a special modifier is added to the standard modifier n, which checks whether the translation was performed using request.localizer and if not, then the corresponding warning is displayed in the log.

Client side

Javascript

When executing javascript code on the client side, user preferences are already known and there is no need for two-step processing. Translation and marking strings for translation can be combined in one function. To work with gettext on the client side, the jed library is used. Source jed-files for which are prepared on the server during compilation of po-files.

Listing 12.7. Directory structure
bar
└── amd
    └── ngw-bar
        ├── mod-a.js
        ├── mod-b.js
        └── template
            └── html.hbs
Listing 12.8. File amd/ngw-bar/mod-a.js
define([
    'ngw-pyramid/i18n!bar'
], function (i18n) {
    var translated = i18n.gettext('Some message for translation');
    alert(translated);
});

As a result of loading this module, a message will be displayed, translated in exactly the same way as on the server. In this case client and server use the same set of strings.

Handlebars

Dijit-widgets often use template-based construction, which may also require internationalization. To do this, it is possible to first pass the template through the template engine handlebars.

Listing 12.9. File amd/ngw-bar/mod-b.js
define([
    "ngw-pyramid/hbs-i18n",
    "dojo/text!.template/html.hbs",
    "ngw-pyramid/i18n!bar"
], function (hbsI18n, template, i18n) {
    var translated = hbsI18n(template, i18n);
    alert(translated);
});
Listing 12.10. amd/ngw-bar/html.hbs
<strong>{{gettext "Another message for translation"}}</strong>

Примечание

To extract strings from handlebars templates, you need to have nodejs installed. This allows you to use the original handlebars javascript parser to handle templates.

In case of a template-based widget, using handlebars for internationalization would look like the original example in the dijit documentation:

define([
    "dojo/_base/declare",
    "dijit/_WidgetBase",
    "dijit/_TemplatedMixin",
    "ngw-pyramid/hbs-i18n",
    "dojo/text!./template/SomeWidget.hbs",
    "ngw-pyramid/i18n!comp"
], function(declare, _WidgetBase, _TemplatedMixin, hbsI18n, template, i18n) {
    return declare([_WidgetBase, _TemplatedMixin], {
        templateString: hbsI18n(template, i18n)
    });
});

Примечание

According to the settings, specified in the babel.cfg file, widget templates should have the .hbs extension and be located inside template directory.

Configuration options

The default language is determined by the locale.default setting of the core component. English is used by default. Thus, in order for all messages to be displayed in Russian in the config.ini, you need to specify:

[core]
locale.default = ru