16.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. Extract strings to translate from sources to .pot file (extract)

  2. Create or update .po files from .pot files (update)

  3. Compile .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 do not belong to any component.

Translations of each component exist within its own domain in terms of the gettext library. For a component a certain 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 nextgisweb_foo package. In this case, the directory structure will look like this:

🗁 nextgisweb_foo
├── 🗁 nextgisweb_foo
│   └── 🗁 bar
│       └── 🗀 locale
└── 🗎 setup.py

Extract strings from source code files with default settings:

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

This will create a .pot file nextgisweb_foo/bar/locale/.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 nextgisweb_foo update bar --locale ru

As a result, the .po file nextgisweb_foo/bar/locale/ru.po will be created. 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 nextgisweb_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.

🗁 nextgisweb_foo
├── 🗁 nextgisweb_foo
│   └── 🗁 bar
│       └── 🗁 locale
│           ├── 🗎 .pot
│           ├── 🗎 ru.mo
│           └── 🗎 ru.po
└── 🗎 setup.py

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 nextgisweb_foo extract bar
(env) $ nextgisweb-i18n --package nextgisweb_foo update bar
(env) $ nano nextgisweb_foo/bar/locale/ru.po
(env) $ nextgisweb-i18n --package nextgisweb_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 nextgisweb_foo package.

Listing 16.1. Sample directory structure
🗁 bar
├── 🗁 template
│   └── 🗎 section.mako
├── 🗎 __init__.py
└── 🗎 util.py
Listing 16.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 16.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 16.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 16.5. File template/section.mako #1
<% from nextgisweb_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 16.6. File template/section.mako #2
<% from nextgisweb_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

When executing client-side code, user preferences are already known and there is no need for two-step processing. Translation and marking strings for translation can be combined into one function. The jed library is used as gettext library implementation with .jed files precompiled from .po files on the server side.

Modern JavaScript

Consider the following directory structure of bar component:

Listing 16.7. Directory structure
🗁 bar
└── 🗁 nodepkg
    └── 🗁 bar
        ├── 🗎 some-module.js
        └── 🗎 package.json

And here is the simple example, where string extraction and translation work:

Listing 16.8. File bar/nodepkg/bar/some-module.js
import i18n from "@nextgisweb/pyramid/i18n!";

const translated = i18n.gettext("Some message for translation");
console.log("Localized message: " + translated);

Old-style JavaScript

Listing 16.9. Directory structure
🗁 bar
└── 🗁 amd
    └── 🗁 ngw-bar
        ├── 🗎 mod-a.js
        ├── 🗎 mod-b.js
        └── 🗁 template
            └── 🗎 html.hbs
Listing 16.10. File amd/ngw-bar/mod-a.js
define([
    "@nextgisweb/pyramid/i18n!"
], 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 16.11. File amd/ngw-bar/mod-b.js
define([
    "@nextgisweb/pyramid/i18n!",
    "dojo/text!.template/html.hbs"
], function (i18n, template) {
    var translated = i18n.renderTemplate(template);
    alert(translated);
});
Listing 16.12. amd/ngw-bar/html.hbs
<strong>{{gettext "Another message for translation"}}</strong>

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

Listing 16.13. File amd/ngw-bar/template/SomeWidget.hbs
<div data-dojo-type="${baseClass}">
    <input data-dojo-type="dijit/form/TextBox"
        data-dojo-props="placeHolder: {{gettextString 'Placeholder'}}"/>
    <button data-dojo-type="dijit/form/Button">{{gettext 'Button'}}</button>
</div>
Listing 16.14. File amd/ngw-bar/template/SomeWidget.js
define([
    "dojo/_base/declare",
    "dijit/_WidgetBase",
    "dijit/_TemplatedMixin",
    "@nextgisweb/pyramid/i18n!",
    "dojo/text!./template/SomeWidget.hbs"
], function(declare, _WidgetBase, _TemplatedMixin, i18n, template) {
    return declare([_WidgetBase, _TemplatedMixin], {
        templateString: i18n.renderTemplate(template)
    });
});

Предупреждение

Pay attention to quotes escaping inside attribute values such as data-dojo-props and use gettextString there instead of gettext. It’ll escape quotes keeping javascript code valid.

Примечание

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