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

О процессе в целом

Объекты ПО

Другие объекты продукта

Локализация: инструменты

Эти инструменты применяются реже:

TODO Нейросети!

Централизованные инструменты (в основном, перевода)

Тактика перевода при локализации

Gettext и Babel

Про gettext:

Про Babel:

<!> msgmerge в PyBabel

Создание перевода

  1. I18n:

    • Подключим в текущий namespace волшебную функцию «_()» — «перевести»

    • Обмажем этой функцией все строки, нуждающиеся в переводе
    • Внимание! f-strings (форматные строки вида «f"...{имя}..."») по понятным причинам (строка-результат зависит от вычисления упомянутых в ней выражений) в python gettext не поддерживаются. Пользуйтесь методом .format().

    • Не удаляйте комментарий #, python-brace-format из po-файла, он нужен:)

  2. l10n:

    1. (pybabel extract) Создадим на основе i18n-ванного исходного кода шаблон перевода, файл dopmain.pot (domain — это довольно произвольное название того, что мы переводим)

    2. (pybabel init) Создадим прототип перевода (для русского — ru.po)

    3. (текстовый редактор или специализированный инструмент редактирования .po) Переведём все строки в ru.po

    4. (pybabel compile) Скомпилируем перевод в файл ru.mo

Обновление перевода

Вышла новая версия программы (или сами поправили). Там «поехали» строки с сообщениями (появились новые, пропали/изменились/переместились старые).

  1. (pybabel extract) Сгенерируем новый шаблон

  2. (pybabel update) Обновим содержимое ru.po на основании шаблона и старого ru.po. У update много искусственного мозга:

    • Не теряет старые переводы (только комментирует)
    • Размечает новые сообщения возможными fuzzy-переводами из старых/закомментированных

  3. Допереводим ru.po

  4. (pybabel compile) Компилируем новый перевод

Процесс локализации

Пример перевода начинается с этого коммита

Основную информацию см. там

Воспользуемся рецептом из методички:

   1 import gettext
   2 translation = gettext.translation(домен, каталог_с_переводами, fallback=True)
   3 _, ngettext = translation.gettext, translation.ngettext

Переключение локали и домена

Если программа должна выводить сообщения попеременно на разных языках, одним объектом типа gettext.Translation обойтись нельзя — в нём только один вариант перевода. На каждый используемый язык нужно регистрировать отдельный перевод: или из .mo-файла, или затыкать экземпляром NullTranslations. Локаль можно брать из какой-нибудь глобальной переменной, но лучше задавать с помощью locale.setlocale():

   1 LOCALES = {
   2     ("ru_RU", "UTF-8"): gettext.translation("tra", "po", ["ru"]),
   3     ("en_US", "UTF-8"): gettext.NullTranslations(),
   4 }
   5 
   6 def _(text):
   7     return LOCALES[locale.getlocale()].gettext(text)
   8 
   9 
  10 for loc in LOCALES:
  11     locale.setlocale(locale.LC_ALL, loc)
  12     print(_("The message"))

При наличии скомпилированного перевода в po/ru/LC_MESSAGES/tra.mo получим

Сообщенька
The message

Переключение доменов — это просто создание нескольких корпусов перевода:

   1 transcode = gettext.translation(f"{domain}", _podir, fallback=True)
   2 transtext = gettext.translation(f"{domain}_story", _podir, fallback=True)
   3 
   4 message(transcode.gettext("This is a message!"))
   5 
   6 speech(transtext.gettext("Curiouser and curiouser!"))

Множественные формы

См. пример.

Модификация множественных форм

Русский перевод имеет 3 множественные формы — единственное число, немного (2-4), и много (5 и больше), которые время от времени возникают в разных числительных. Описание того, какие числа какой множественной форме соответствуют содержится в заголовке ru.po. Это описание компилируется в некоторый код .mo-файла, который gettext интерпретирует (замечание vslutov@, спасибо).

Пример: программа и .po-файл к ней:

   1 import gettext
   2 translation = gettext.translation("plurals", "po", fallback=True)
   3 _, ngettext = translation.gettext, translation.ngettext
   4 
   5 
   6 for i in (0, 1, 2, 3, 5, 6, 11, 21, 26, 131):
   7     print(_("Hello,"), ngettext("{} world!", "{} worlds!", i).format(i))

Po-файл;

   1 # Russian (Russia) translations for PROJECT.
   2 # Copyright (C) 2025 ORGANIZATION
   3 # This file is distributed under the same license as the PROJECT project.
   4 # FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
   5 #
   6 msgid ""
   7 msgstr ""
   8 "Project-Id-Version: PROJECT VERSION\n"
   9 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
  10 "POT-Creation-Date: 2025-04-09 00:34+0300\n"
  11 "PO-Revision-Date: 2025-04-09 00:33+0300\n"
  12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
  13 "Language: ru_RU\n"
  14 "Language-Team: ru_RU <LL@li.org>\n"
  15 "Plural-Forms: nplurals=4; plural=(n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : "
  16 "n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
  17 "MIME-Version: 1.0\n"
  18 "Content-Type: text/plain; charset=UTF-8\n"
  19 "Content-Transfer-Encoding: 8bit\n"
  20 "Generated-By: Babel 2.15.0\n"
  21 
  22 #: plurals.py:12
  23 msgid "Hello,"
  24 msgstr ""
  25 
  26 #: plurals.py:12
  27 msgid "{} world!"
  28 msgid_plural "{} worlds!"
  29 msgstr[0] "{} мир!"
  30 msgstr[1] "{} мира!"
  31 msgstr[2] "{} миров!"
  32 msgstr[3] "мир!"

Как работает:

$ python3 plurals.py                      
Hello, 0 миров!
Hello, мир!
Hello, 2 мира!
Hello, 3 мира!
Hello, 5 миров!
Hello, 6 миров!
Hello, 11 миров!
Hello, 21 мир!
Hello, 26 миров!
Hello, 131 мир!

Некоторые замечания

Обновление перевода

Поскольку .pot-файл — это генерат, его можно не хранить в репозитории (всё равно он пересоздаётся при каждом update). При этом в шаблоне меняется время создания, даже если перевод остался прежним. Чтобы update не менял при этом перевод, ему надо добавить ключ --ignore-pot-creation-date

Про память переводов

Очень важно соблюдать единообразие при переводе одинаковых надписей. Для этого все переводы по проекту можно сложить в т. н. «compendium» (он же translation memory, она же Память переводов) — например, большой .po-файл со всеми возможными переводами, накопившимися за историю работы

В Babel нет поддержки compendium :(, но она есть в msgmerge из GNU Gettext

Однако есть т. н. Translator Comments (начинаются с "NOTE: "

Д/З

В семестровом проекте должен присутствовать перевод. Если вы пишете какой-то модуль/бот/библиотеку без UI, переводите документацию (см. Руководство по переводу документации Sphinx)

LecturesCMC/PythonDevelopment2025/09_I18n (последним исправлял пользователь FrBrGeorge 2025-04-09 01:21:33)