Локализация¶
Текст в StageFlow пишут двое, и механизма тоже два.
Собственные строки фреймворка — ошибки валидации, отказы, описания встроенных
стадий — написаны в исходниках по-английски и переводятся через каталог
gettext, который лежит внутри пакета. Ваши стадии в этом каталоге не
лежат и лежать не должны, поэтому строка спеки может быть не строкой, а
таблицей по локалям: без каталога, без извлечения, без шага сборки.
Чтобы работать по-английски, настраивать не нужно ничего: процесс, который не задал локаль, получает исходные строки.
Отвечать на языке¶
Локаль — не глобальная переменная. Она живёт в ContextVar, и именно это делает
её пригодной для сервера: обработчик ставит ту локаль, которую попросили в
запросе, два одновременно обслуживаемых запроса получают два языка, а задача,
запущенная из обработчика, наследует его локаль.
from stageflow import i18n
i18n.available_locales() # ['en', 'ru'] — на чём эта сборка умеет отвечать
i18n.get_locale() # действующая локаль, либо 'en'
with i18n.use_locale("ru"): # на время блока
pipeline.validate() # его ошибки выходят по-русски
token = i18n.set_locale("ru") # или руками, в обработчике
i18n.reset_locale(token)
set_locale(None) возвращает к исходным строкам.
Что попросил вызывающий¶
HTTP-клиент заявляет предпочтение в Accept-Language, а это не тег, а
упорядоченный список тегов. negotiate берёт его и отвечает локалью, которая у
этой сборки действительно есть:
wanted = i18n.parse_accept_language("fr;q=0.9, ru-RU;q=0.8, en;q=0.5")
locale = i18n.negotiate(wanted) # 'ru' — французского каталога нет, берём следующее
ru-RU находит каталог ru: тег откатывается к своим менее специфичным формам
(pt_BR → pt), а тег, каталога для которого нет ни у кого, откатывается к
исходному языку. Так региональному тегу не нужен отдельный каталог, а на
незнакомый язык отвечают, а не отказывают.
В бэкенде это две строки в обработчике — где именно, показано в 2. Эндпоинты:
asked = request.headers.get("accept-language", "")
with i18n.use_locale(i18n.negotiate(i18n.parse_accept_language(asked))):
...
Читать заголовок — дело бэкенда. Фреймворк к запросу не прикасается:
parse_accept_language — парсер, которому передали строку, negotiate
сопоставляет теги с каталогами на диске, use_locale ставит локаль для этого
контекста. На каком языке ответ, решается там, где есть запрос, а это не здесь.
Это про собственные сообщения фреймворка — что скажет ошибка валидации, что скажет отказ. Спеки стадий сюда не относятся: у них языка нет вообще, см. ниже.
У спек все языки, а не один¶
get_specs() кладёт в прозу все языки, на которых сборка умеет отвечать, —
таблицей {локаль: текст}:
>>> ConcatStage.get_specs()["description"]
{'en': 'Concatenate stringified parts with separator',
'ru': 'Склеивает части через разделитель, приводя их к строкам'}
Редактор забирает спеки один раз, а его читатель выбирает язык потом — и выбирает заново всякий раз, когда передумает. Бэкенд, который свёл бы прозу к одному языку, приходилось бы переспрашивать каждый раз, и выбор языка стал бы сетевым походом. Поэтому спека несёт выбор, а выбирает тот, кто её рисует; бэкенд не решает ничего.
Проза, которую никто не переводил, остаётся обычной строкой — таблица из одной записи была бы выбором, не несущим информации:
Действующая локаль этого не меняет. use_locale говорит, на каком языке этот
ответ, а спека никому не ответ:
Явно названная локаль прозу всё-таки сворачивает — для того, кто действительно отвечает одному читателю, в основном для генератора документации:
ConcatStage.get_specs(locale="ru")["description"]
# 'Склеивает части через разделитель, приводя их к строкам'
Ваши стадии — на всех языках¶
Стадия описывает себя в YAML-докстринге. Любой кусок
прозы в нём — description самой стадии и description каждого аргумента,
выхода, события и входа — может быть таблицей по локалям вместо строки:
@register_stage("TakeTicketStage")
class TakeTicketStage(BaseStage):
"""
description:
en: Takes a ticket out of the queue
ru: Берёт тикет из очереди
category: support
outputs:
- name: ticket
description:
en: The ticket taken
ru: Взятый тикет
"""
Такая таблица — это уже все языки, поэтому get_specs() отдаёт её как
написано, а редактор выбирает из неё:
TakeTicketStage.get_specs()["description"]
# {'en': 'Takes a ticket out of the queue', 'ru': 'Берёт тикет из очереди'}
TakeTicketStage.get_specs(locale="ru")["description"] # 'Берёт тикет из очереди'
TakeTicketStage.get_specs(locale="pt-BR")["description"] # английское: записи pt нет
Там, где один язык всё-таки надо выбрать — locale= выше или редактор,
выбирающий за читателя, — ключи согласуются точно так же, как теги запроса, а
исходный язык работает откатом. Таблица, написанная только на одном языке, —
это всё равно ответ; стадия с обычными строками читается одинаково на любом
языке, и для стадии, которую никто не переводил, это правильный ответ.
Свой каталог¶
Таблица на каждую строку хороша для горстки стадий и плоха для сотни. Хост, у
которого их столько, регистрирует собственный домен gettext и указывает на
него свои стадии — тогда фреймворк ищет их прозу там, а не берёт как написано:
i18n.register_domain("mystages", "/path/to/locale")
@register_stage("TakeTicketStage")
class TakeTicketStage(BaseStage):
"""
description: Takes a ticket out of the queue
"""
i18n_domain = "mystages"
Каталог раскладывается обычным для gettext образом —
<locale>/LC_MESSAGES/<domain>.mo. Отдельный домен, а не добавка к домену
stageflow: ваши строки — ваши, а искать их в каталоге фреймворка значит
искать в чужом.
Каталоги, которые поставляются¶
В stageflow/locale/ лежит исходный каталог (stageflow.pot) и по каталогу на
язык: .po, который правит переводчик, и скомпилированный .mo, который читает
рантайм. В колесо попадают оба, так что у переводчика, у которого есть пакет,
есть и исходник.
Локаль существует потому, что на диске существует каталог для неё. Ни один язык
не назван в коде нигде, кроме SOURCE_LOCALE, так что добавить язык — это
добавить файл, а не поправить список; available_locales() о нём сообщит.
Работу с каталогами делает tools/i18n.py (pip install -e '.[i18n]' — ради
babel, который нужен только при разработке и никогда для запуска StageFlow):
python tools/i18n.py extract # исходники -> locale/stageflow.pot
python tools/i18n.py update # .pot -> .po каждой локали
python tools/i18n.py update --locale de # или одной, создав её, если она новая
python tools/i18n.py compile # .po -> тот .mo, который поставляется
python tools/i18n.py stats # сколько где сделано
Извлечение читает два места, потому что переводимый текст фреймворка живёт в
двух: _("…") в исходниках и проза встроенных стадий, написанная в
YAML-докстринге, которую не разбирает ни один экстрактор — поэтому стадии
импортируются и опрашиваются, что извлекает ровно то, что публикует
get_specs().
Одно оставлено по-английски сознательно: сообщения проверок
stageflow.testing. Их читает разработчик, прогоняющий тесты, рядом с
трейсбеком Python — перевод не помог бы никому.
Редактор¶
Редактор — отдельный проект с отдельным механизмом для своего собственного текста: плоский JSON-каталог на язык, выбор в «Вид → Язык».
Спеки стадий его собственным текстом не являются, и переводить их он не
пытается: он берёт таблицы {локаль: текст}, которые прислал бэкенд, один раз
разрешает их под язык, на котором нарисован, и дальше работает со строками.
Региональный тег удовлетворяет запрос языка, отсутствующий язык откатывается к
тому, на котором спеки написаны, а таблица без того и другого отдаёт свою
единственную запись.
То есть бэкенду, обслуживающему редактор, о языке своих стадий не нужно делать
вообще ничего — он отдаёт get_specs(), а редактор разбирается сам.