Перейти к содержанию

Миграция

Если ваш проект уже использует gettext, то вопросов, которые решают, применима ли эта библиотека, немного: обесценивает ли она имеющиеся каталоги, уживается ли с кодом, который вы менять пока не готовы, и какая часть переезда должна случиться разом. Ответы, от самого короткого:

Вопрос Ответ
Продолжат ли работать существующие .po и .mo? Да. Те же файлы, те же инструменты.
Могут ли старые и новые вызовы жить в одном файле? Да, и одно сопоставление экстрактора покрывает оба.
Меняется ли msgid? С .format() — нет. С %-формата — да.
Обязан ли весь проект переехать разом? Нет. Одна точка вызова — уже полноценное изменение.
А что с Jinja, шаблонами Django, JavaScript? Не затронуты, каталоги те же.

Остальная часть страницы — подробности за каждым из этих ответов.

С .format(): msgid не меняется

Это случай, в котором миграция не стоит почти ничего. Сообщение на str.format и сообщение на t-строке дают один и тот же ключ каталога, потому что ключ в обоих случаях — это текст, в котором {name} остался на месте:

# Before
_("Hello {name}").format(name=name)

# After — the msgid is still "Hello {name}"
tr(t"Hello {name}")

Поэтому существующий перевод остаётся привязанным. Пусть в каталоге лежит

#: app.py:6
#, python-brace-format
msgid "Hello {name}"
msgstr "こんにちは {name}"

Измените вызов, переизвлеките сообщения и обновите каталоги:

$ pybabel extract -F babel.cfg -o locales/messages.pot .
extracting messages from app.py (encoding="utf-8")
writing PO template file to locales/messages.pot
$ pybabel update -i locales/messages.pot -d locales
updating catalog locales/ja/LC_MESSAGES/messages.po based on locales/messages.pot

Вернувшаяся запись отличается двумя строками метаданных — и больше ничем: комментарием-маркером, который опознаёт её как сообщение из t-строки, и номером строки в исходнике:

#. gettext-tstrings
#: app.py:4
#, python-brace-format
msgid "Hello {name}"
msgstr "こんにちは {name}"

Ни флага fuzzy, ни повторного перевода — ни на одном языке. Сообщение выводится сразу же:

$ pybabel compile -d locales
compiling catalog locales/ja/LC_MESSAGES/messages.po to locales/ja/LC_MESSAGES/messages.mo
$ python app.py
こんにちは Ada

update --check сообщит, что каталоги устарели

Этого комментария-маркера и сдвинувшихся номеров строк достаточно, чтобы pybabel update --check заявил, что каталог нужно перегенерировать: он сравнивает запись целиком, а не только перевод. Запускайте настоящий pybabel update в том же коммите, что и изменение кода, и коммитьте каталоги вместе с ним — той же привычки уже требуют ворота CI.

С %-формата: msgid меняется, поэтому переводы становятся fuzzy

Синтаксис printf живёт внутри сообщения, поэтому его замена переписывает ключ каталога. Обойти это невозможно, и это честная цена отказа от %(name)s:

# Before
_("Hello %(name)s") % {"name": name}

# After — a different msgid
tr(t"Hello {name}")

pybabel update распознаёт новое сообщение как близкого родственника удалённого и переносит старый перевод, пометив его как fuzzy:

#. gettext-tstrings
#: app.py:4
#, fuzzy, python-brace-format, python-format
msgid "Hello {name}"
msgstr "こんにちは %(name)s"

Про это состояние надо знать три вещи:

  • Во время выполнения ничего не ломается. Записи fuzzy исключаются из скомпилированного .mo, поэтому приложение выводит исходное сообщение, пока человек не подтвердит пару, — та же деградация, через которую проходит любое переформулированное сообщение.
  • pybabel compile сообщает о каждой такой записи, потому что перенесённый %(name)s не является допустимым заполнителем в фигурных скобках, и завершается ненулевым статусом. Этот список — ваша рабочая очередь, а не ложная тревога: перечисленные записи действительно нужно править.
  • Старый флаг python-format едет следом и должен быть удалён вместе с флагом fuzzy, иначе msgfmt --check-format продолжит применять правила printf к сообщению в формате с фигурными скобками.

Для именованных printf-заполнителей правка механическая — %(name)s превращается в {name}, и больше ничего не сдвигается, — поэтому большой каталог проходится скриптом с последующей вычиткой переводчиком, а не переводится заново. Позиционный %s механическим не будет: у него нет имени, которое можно перенести, а выбор этого имени и есть смысл изменения.

Поэтому на практике %-формат мигрируют осознанно — по модулю, по релизу, по языку, — а не одним махом, от которого все каталоги разом краснеют.

Старые и новые вызовы уживаются

Экстрактор, читающий t-строки, читает и обычные вызовы gettext, поэтому одно сопоставление покрывает файл в середине миграции:

[gettext_tstrings: **.py]
encoding = utf-8
from gettext_tstrings import tr
from myapp.i18n import _

name = "Ada"
print(_("Save changes"))
print(tr(t"Hello {name}"))

Оба сообщения попадают в один шаблон, и только у сообщения из t-строки есть комментарий-маркер, включающий дополнительные проверки этой библиотеки:

#: app.py:5
msgid "Save changes"
msgstr ""

#. gettext-tstrings
#: app.py:6
#, python-brace-format
msgid "Hello {name}"
msgstr ""

Он распознаёт _(), четыре стандартных имени gettext, псевдонимы tr() / ntr() и отложенные lazy_gettext() / lazy_pgettext(). Собственный helper нужно объявить в сопоставлении.

Во время выполнения оба стиля одинаково независимы: gettext.translation() возвращает один объект переводов, и _, и точки входа этой библиотеки читают из него.

Что не переезжает

  • Языки шаблонов. {% trans %} из Jinja2, теги шаблонов Django и их экстракторы Babel продолжают работать без изменений и продолжают наполнять те же PO-каталоги. t-строки — синтаксис Python; они относятся к исходникам Python.
  • Ваши файлы каталогов. Ни смены формата, ни нового файла, ни шага конвертации.
  • Ваша платформа перевода. Обмен через .po идентичен, а флаг python-brace-format, который несёт сообщение из t-строки, — тот же самый флаг, который несёт сообщение из .format(), так что QA заполнителей продолжает работать.
  • Код не на Python. Каталог JavaScript или C в том же проекте не затрагивается.

Контрольный список миграции

  1. Добавьте extra babel там, где запускается pybabel, и переведите сопоставление python в babel.cfg на метод gettext_tstrings — одно сопоставление тогда покрывает оба стиля, а -k продолжает работать для обычных вызовов.
  2. Переводите сначала точки вызова с .format(). Переизвлеките сообщения, выполните pybabel update и закоммитьте каталоги вместе с кодом; записей fuzzy быть не должно.
  3. Переводите точки вызова с %-форматом партиями, которые реально пройдут ревью, переписывая перенесённые заполнители и снимая флаги fuzzy и python-format.
  4. Почините то, что отвергает ограничение: интерполяция должна быть простым именем, поэтому t"Hello {user.name}" сперва становится локальной переменной. Это правка в точке вызова, а не в каталоге.
  5. Когда проход завершён, включите strict = true в опциях сопоставления, чтобы неизвлекаемое сообщение роняло сборку, а не исчезало из шаблона.
  6. Добавьте проверку времени выполнения из В продакшене: рендеринг одного сообщения на каждый поставляемый язык через строгий Translator.

Шаги 2 и 3 — обычные коммиты. Ничто в этом списке не требует единого дня переключения.