13.2.5. 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

13.2.5.1. Server side

13.2.5.1.1. 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.lib.i18n.trstr.TrStr 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 13.1. Sample directory structure
πŸ— bar
β”œβ”€β”€ πŸ— template
β”‚   └── πŸ—Ž section.mako
β”œβ”€β”€ πŸ—Ž __init__.py
└── πŸ—Ž util.py
Listing 13.2. File util.py
from nextgisweb.lib.i18n import trstr_factory
_ = trstr_factory('bar')

Function nextgisweb.lib.i18n.trstr.trstr_factory() allows you to simplify creation of strings TrStr 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 13.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 13.4. File __init__.py #2
@view_config(renderer='string')
def view(request):
    return request.localizer.translate(something())

Note

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.

13.2.5.1.2. 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 13.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 13.6. File template/section.mako #2
<% from nextgisweb_foo.bar.util import _ %>
<div>${tr(_("Another message for translation"))}</div>

Note

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.

13.2.5.2. 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.

13.2.5.2.1. Modern JavaScript

Consider the following directory structure of bar component:

Listing 13.7. Directory structure
πŸ— bar
└── πŸ— nodepkg
    └── πŸ— bar
        β”œβ”€β”€ πŸ—Ž some-module.js
        └── πŸ—Ž package.json

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

Listing 13.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);

13.2.5.2.2. Old-style JavaScript

Listing 13.9. Directory structure
πŸ— bar
└── πŸ— amd
    └── πŸ— ngw-bar
        β”œβ”€β”€ πŸ—Ž mod-a.js
        β”œβ”€β”€ πŸ—Ž mod-b.js
        └── πŸ— template
            └── πŸ—Ž html.hbs
Listing 13.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.

13.2.5.2.3. 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 13.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 13.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 13.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 13.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)
    });
});

Warning

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.

Note

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

13.2.5.3. 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