10.2.3. Интернационализация и локализация

Интернационализация NextGIS Web работает на базе библиотек gettext и babel. Порядок работы со строками стандартный для проектов на базе gettext:

  1. Извлечение строк для перевода из исходных текстов в pot-файл (extract)
  2. Создание или обновление po-файлов локализации на базе pot-файлов (init и update)
  3. Компияция po-файлов в mo-файлы (compile)

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

Интернационализация выполняется для каждого компонента NextGIS Web отдельно. Как следствие, на данный момент нет способа интернационализации строк не относящихся ни к одному компоненту. Локализация каджого компонента существует в рамках своего, одноименного с компонентом, домена (domain) в терминах библиотеки gettext. То есть в одном компоненте одна строка может быть переведена одним образом, а в другом - другим, даже без использования контекстов.

Все действия со строками выполняются при помощи утилиты командной строки nextgisweb-i18n из пакета nextgisweb. Эта утилита является надстройкой над утилитой pybabel из пакета babel с предустановленными по-умолчанию настройками.

Рассмотрим локализацию на русский язык на примере несуществующего компонента 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

Cервер

Python

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

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

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

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

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

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

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

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

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

Mako

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

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

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

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

Примечание

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

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

Kлиент

Javascript

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

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

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

Handlebars

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

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

Примечание

Для извлечения строк из шаблонов 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)
    });
});

Примечание

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

Настройки

Язык используемый определяется настройкой 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__, чтобы не передать параметр в каждом запросе вручную.