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

Рассмотрим локализацию на русский язык на примере несуществующего компонента bar из несуществующего пакета foo. В этом случае структура каталогов будет выглядеть следующим образом:

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

Извлекаем строки из файлов исходного кода с настройками по-умолчанию:

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

В результате будет создан pot-файл foo/foo/locale/bar.pot. Поскольку этот файл в любой момент можно сгенерировать, не нужно помещать его внутрь системы контроля версий. На основании pot-файла создаем po-файл для русского языка:

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

В результате будет создан po-файл foo/foo/locale/ru/LC_MESSAGES/bar.pot (это стандартная структура для gettext, возможно мы от нее откажемся в пользу более простой). Этот файл нужно заполнить в соответствии с переводом указанных в нем строк при помощи текстового редактора или специализированного редактора po-файлов. После того как все готово, компилируем po-файлы в mo-файлы, которые так же не нужно помещать в систему контроля версий:

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

Ниже приведена итоговая структура каталогов. Файлы bar.jed - это аналог mo-файла для javascript-библиотеки jed, которая используется для локализации на стороне клиента. Эти файлы так же создаются на этапе компиляции и представляют из себя json-файлы.

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

На этом первичная локализация завершена, через какое-то время в пакете могли добавится новые строки, в этом случае нужно заново извлечь строки, автоматически обновить po-файл, отредактировать его и снова скомпилировать:

(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

6.2.4.1. Cервер

6.2.4.1.1. Python

Для обеспечения возможности извлечение строк для перевода их необходимо разметить соответствующим образом. Ниже описано как это сделать, а так же как обеспечить вывод уже переведенных строк пользователю.

Поскольку код на python выполняется на сервере, один и тот же экземпляр приложения должен иметь возможность обслуживать пользователей с разными локалями, то необходимо использовать двухступенчатую работу со строками: вначале строка отмечается как требующая перевода, затем непосредственно перед выводом пользователю переводится с учетом предпочтений пользователя. Решают эту задачу класс nextgisweb.i18n.trstring.TrString, который в целом аналогичен классу translationstring.TranslationString, но с некоторыми дополнительными удобствами в отношении интерполяции строк. Рассмотрим на примере несуществующего компонента bar из несуществующего пакета foo.

Listing 6.1. Структура директории
bar
├── __init__.py
├── util.py
└── template
    └── section.mako
Listing 6.2. util.py
from nextgisweb.i18n import trstring_factory
_ = trstring_factory('bar')

Функция nextgisweb.i18n.trstring.trstring_factory() позволяет упростить создание строк TrString с предопределенным доменом, который указывается в параметрах функции. Для удобства и функция и класс так же доступны для импортирования из модуля nextgisweb.i18n, что и показано в примерах.

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

Использование символа подчеркивания необходимо для корректного извлечения строк для перевода, то есть нельзя импортировать его с другим именем from .util import _ as blah это не позволит корректно извлечь строки для перевода.

Для перевода в соответствии с предпочтениями пользователя (один пользователь может хотеть английский язык, другой русский) необходимо перевести строку при помощи метода request.localizer.translate(trstring):

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

Поскольку request имеет смысл только в веб-сервисе, это значит что на данном этапе не получится использовать локализацию в утилитах командной строки nextgisweb.

6.2.4.1.2. Mako

Часть требующих перевода строк так же содержится в mako-шаблонах обрабатываемых на сервере. По сути работа mako-шаблонов мало чем отличается от python кода, так что и схема работы такая-же: вначале отмечаем строку для перевода специальной функцией, потом переводим через request с учетом предпочтений пользователя.

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

Чтобы немного сократить эту длинную запись в контекст mako-шаблона добавлена функция tr(), которая делает то же самое. Таким образом пример приведенный ниже полностью равноценен предыдущему:

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

Note

К сожалению, по не очень понятным причинам, не получится использовать эту функцию как модификатор ${expression | tr}. Почему-то в этом случае в функцию попадает результат работы стандартного модификатора n, то есть markupsafe.Markup.

Для того, чтобы отследить, что все строки требующие перевода были переведены при выводе в шаблоне в режиме отладки (настройка debug компонента core) к стандартному модификатору n добавляется специальный модификатор, который проверяет был ли выполнен перевод при помощи request.localizer и если нет, то в лог выводится соответствующее сообщение.

6.2.4.2. Kлиент

6.2.4.2.1. Javascript

При выполнении javascript-кода на клиенте, предпочтения пользователя известны сразу и необходимость в двухступенчатой обработка отсутствует. Это значит, что перевод и отметку строк для перевода можно совместить в одной функции. Для работы с gettext на стороне клиента используется библиотека jed исходные json-файлы для которой готовятся на сервере при компиляции po-файлов.

Listing 6.7. Структура директории
bar
└── amd
    └── ngw-bar
        ├── mod-a.js
        ├── mod-b.js
        └── template
            └── html.hbs
Listing 6.8. amd/ngw-bar/mod-a.js
define([
    'ngw-pyramid/i18n!bar'
], function (i18n) {
    var translated = i18n.gettext('Some message for translation');
    alert(translated);
});

В результате загрузки этого модуля будет выведено сообщение, переведенное точно так же как и на сервере, в этом случае используется общий с сервером набор сообщений, то есть то что доступно на сервере доступно и на клиенте.

6.2.4.2.2. Handlebars

В dijit-виджетах часто используется построение виджетов на базе шаблона, который тоже может требовать интернационализации. Для этого предусмотрена возможность предварительно пропустить шаблон через шаблонизатор handlebars где при помощи специальных модификаторов обеспечивается как извлечение строк, так и их перевод.

Listing 6.9. 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 6.10. amd/ngw-bar/html.hbs
<strong>{{gettext "Another message for translation"}}</strong>

Note

Для извлечения строк из шаблонов handlebars необходимо установить NodeJS. Это позволяет использовать оригинальный парсер handlebars на javascript для обработки шаблонов.

В случае виджета на базе шаблона, использование handlebars для интернационализации будет выглядеть следующим образом, по сравнению с исходным примером в документации dijit:

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)
    });
});

Note

Согласно используемым настройкам, указанным в файле babel.cfg, шаблоны виджетов должны иметь расширение .hbs и располагаться внутри директории template.

6.2.4.3. Настройки

Язык используемый определяется настройкой locale.default компонента core. Как было сказано выше, по-умолчанию используется английский язык. Таким образом для того, чтобы все сообщения выводились на русском языке в config.ini нужно указать (значение этой настройки передается и в настройку pyramid pyramid.default_locale_name и dojoConfig.locale):

[core]
locale.default = ru

Поскольку mo-файлы не хранятся внутри системы контроля версий, перед запуском необходимо скомпилировать po-файлы для каждого пакета:

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

В веб-интерфейсе пока нет возможности переключать язык, но если это необходимо для тестирования, то к любому запросу можно передать параметр __LOCALE__, который работает точно так же как параметр core:locale.default. Так же можно использовать cookie имененем __LOCALE__, чтобы не передать параметр в каждом запросе вручную.