14.2.5. Internationalization and localization

Internationalization and localization of NextGIS Web is built on top of gettext and babel libraries. The workflow for working with messages is standard for projects based on gettext:

  1. Extract messages 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)

Each NextGIS Web component becomes an independent domain in terms of the gettext library. As a result, there is no way to internationalize messages that do not belong to any component.

To be able to extract messages 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 messages.

All on messages are performed using nextgisweb-i18n command line utility. To update translation in nextgisweb_foo you can do the following:

$ nextgisweb-i18n --package nextgisweb_foo update --extract --locale ru
$ nano package/nextgisweb_foo/nextgisweb_foo/locale/ru.po
$ nextgisweb-i18n --package nextgisweb_foo compile

The following standard gettext-like functions are supported for backend and frontend:

  • gettext(message)

  • ngettext(singular, plural, n)

  • pgettext(context, message)

  • npgettext(context, singular, plural, n)

In addition, each of these functions has a formatter variant with the f suffix: gettextf, ngettextf, pgettextf and npgettextf. They return callables that perform string interpolation using Python sting format placeholders. For example:

>>> gettextf("Hello, {}!")("Arthur")
Hello, Arthur!

Template stings may contain one of the following:

  • One anonymous placeholder: Hello, {}!

  • Named placeholders: Hello, {first} {last}!

  • Index based placeholders: ``Hello, {0} {1}!`

14.2.5.1. Server side

14.2.5.1.1. Python

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 messages: first, a message is marked as requiring translation, then before displaying it to the user, it’s translated according to the user’s preferences.

from nextgisweb.env import gettext

@view_config(renderer='json')
def view(request):
    tr = request.localizer.translate
    return tr(gettext("Some message for translation"))

Note

Formatting with gettext("Hello, {}!").format("Arthur") and gettext("Hello, %s!") % "Arthur" works but deprecated.

Note

Python formatting modifiers are not supported. To format numbers, convert arguments to a string first.

Some formatting examples:

from nextgisweb.env import gettextf

def user_info_message(tr, user):
    return tr(gettextf("Your login is {kn} and full name is {dn}.")(
        kn=user.keyname, dn=user.display_name
    ))

def percent_left_message(tr, percent):
    return tr(gettextf("{} left.")("{:.2f}%".format(percent)))

14.2.5.1.2. Mako

Some of the strings that require translation are also contained in Mako templates. In fact, the work of mako templates is not much different from Python. You don’t need import anything as it’s imported behind the scene. Consider the following example:

<div>
    ${tr(gettext("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.

14.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 are combined into one function.

14.2.5.2.1. Modern JavaScript and TypeScript

Simple messages with gettext:

import { gettext } from "@nextgisweb/pyramid/i18n";

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

Some formatting examples:

import { gettextf } from "@nextgisweb/pyramid/i18n";

const msgHelloFmt = gettextf("Hello, dear {}!");
const msgFromToFmt = gettext("A message from {from} to {to}.")

function sayHello(name) {
    const msgTranslated = msgHelloFmt(name);
    console.log("Localized message: " + msgTranslated);
};

function noteTitle(sender, receiver) {
    return msgFromToFmt({from: sender, to: receiver});
};

Formatted messages with plural can be translated with ngettextf:

import { ngettextf } from "@nextgisweb/pyramid/i18n";

function countSheepAndWolves(sheep, wolves) {
    const msgSheep = ngettextf("{} sheep.", "{} sheep", sheep)(sheep);
    const msgWolves = ngettextf("{} wolf", "{} wolves", wolves)(wolves);

    console.log(msgSheep);
    console.log(msgWolves);
}

You can translate React elements with Translated:

import { gettextf } from "@nextgisweb/pyramid/i18n";
import { Translated } from "@nextgisweb/pyramid/i18n/translated";

import { OpenInNewIcon } from "@nextgisweb/gui/icon";

const msgCommandTipFmt = gettextf("For {command} click {icon} icon.");

function CommandTip({ command }) {
    return (
        <Translated
            msgf={msgCommandTipFmt}
            args={{ command, icon: <OpenInNewIcon /> }}
        />
    );
}

14.2.5.2.2. Legacy JavaScript

define(["@nextgisweb/pyramid/i18n!"], function ({ gettext }) {
    const msgTranslated = gettext("Some message for translation");
    console.log("Localized message: " + msgTranslated);
});

As a result of loading this module, a message will be displayed, translated in exactly the same way as on the server.

14.2.5.2.3. Handlebars

Dijit-widgets 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 14.1. 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 14.2. SomeWidget.js
define([
    "dojo/_base/declare",
    "dijit/_WidgetBase",
    "dijit/_TemplatedMixin",
    "@nextgisweb/pyramid/i18n!",
    "dojo/text!./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.