Перейти до змісту

Міграція

Якщо ваш проєкт уже користується 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, тож застосунок рендерить початкове повідомлення, доки людина не підтвердить пару, — та сама деградація, яку проходить будь-яке переформульоване повідомлення.
  • CI лишається зеленим, поки вони fuzzy. Чекер заповнювачів пропускає записи fuzzy — так само, як це робить msgfmt --check-format, — бо запис, який не здатен дістатися середовища виконання, не має валити збірку. Щойно перекладач знімає прапорець, запис перевіряється, як усі інші, — тож %(name)s, залишений у підтвердженому перекладі, буде впійманий саме тоді, коли він інакше почав би рендеритися.
  • Старий прапорець python-format їде разом і має бути видалений разом із прапорцем fuzzy, інакше msgfmt --check-format і далі застосовуватиме правила printf до повідомлення у brace-форматі.

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

Тому міграція може йти в тому темпі, який дозволяє рев'ю: неконвертований запис fuzzy — це видима одиниця роботи в каталозі, а не зламана збірка.

Старі й нові виклики співіснують

Видобувач, що читає 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(). Ваш власний помічник треба назвати в зіставленні.

Під час виконання обидва стилі однаково незалежні: 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 — звичайні коміти. Ніщо в цьому списку не потребує дня великого перемикання.