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:
Extract messages to translate from sources to
.pot
file (extract
)Create or update
.po
files from.pot
files (update
)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.
<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>
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.