Интернационализация и локализация
Лекция 2007 года на сайте esyr.org (читал её Андрей Черепанов, который в то время курировал сообщество русских переводчиков KDE, почему фотографии при этом такие, вопрос к фотографу ☺)
О процессе в целом
- Интернационализация (i18n) — приведение исходного кода в готовое для локализации состояние
- Локализация (l10n) — адаптация культурных и языковых предпочтений ПО
Объекты ПО
- Сообщения и тексты в составе ПО
- Размер и оформление интерфейсных элементов (RTL, например)
- Изображения: текст на них и культурная атрибуция
Управляющие клавиши (иероглифы, ага )
Параметры, чувствительные к локали (locale)
- Ссылки на внешние ресурсы (поддержка, издатель и пр.; телефоны, url и пр.)
- (в сложных случаях) Шрифты
Другие объекты продукта
- Электронная документация
- Непрограммные данные (шаблоны документов и форм и т. п.)
- Бумажная документация и др. сопутствующие предметы
Локализация: инструменты
- Общие словари (?)
- Память перевода (локальные общие словари)
- Нечёткий перевод (при лёгком изменении ресурса или при добавлении похожего)
- Множественные формы
Эти инструменты применяются реже:
- Другие производные словоформы: склонение, спряжение и прочее.
- …
- Искусственный мозг
- …
- Естественный мозг
Централизованные инструменты (в основном, перевода)
Например, Transifex
- Я нашёл как минимум пару свободных и написанных на Питоне!
Тактика перевода при локализации
- «По бедности»: на каждый язык отдельный программный продукт или библиотека
- - Программирование копипастой
- + Адаптация UI (например, RLT/LTR)
По идентификатору: print(translate(MSG_000a3f8, LANG_RU))
- Малоинформативные ID (даже если ID отражает часть сообщения, переводить надо не его)
- в т. ч. для программистов
- Требуется специальное ПО для сопоставления переводов (обычно полностью исключающее контекст)
- + Формализуемое соответствие ID паре «исходник - перевод»
- Решение об идентичности переводов принимается на уровне этого ПО
По примеру; print(translate("Did you plug your iron out?", LANG_RU))
actually, print(_("Did you plug your iron out?")), язык берётся из локали и / или устанавливается
- + Удобен для перевода программистом
- ± Случайное совпадение разных по смыслу исходников (с разным переводом)
- А так ли это хорошо для самих исходников?
- Есть возможность сменить контекст — т. н. «домен»
- - Требуется анализ по извлечению «сообщений» из разных ЯП / фреймворков
Gettext и Babel
Про gettext:
Про Babel:
Создание перевода
I18n:
Подключим в текущий namespace волшебную функцию «_()» — «перевести»
- Обмажем этой функцией все строки, нуждающиеся в переводе
Внимание! f-strings (форматные строки вида «f"...{имя}..."») по понятным причинам (строка-результат зависит от вычисления упомянутых в ней выражений) в python gettext не поддерживаются. Пользуйтесь методом .format().
…или, если очень надо, вот такой конструкцией (but why?)
Не удаляйте комментарий #, python-brace-format из po-файла, он нужен:)
l10n:
(pybabel extract) Создадим на основе i18n-ванного исходного кода шаблон перевода, файл dopmain.pot (domain — это довольно произвольное название того, что мы переводим)
(pybabel init) Создадим прототип перевода (для русского — ru.po)
(текстовый редактор или специализированный инструмент редактирования .po) Переведём все строки в ru.po
(pybabel compile) Скомпилируем перевод в файл ru.mo
Обновление перевода
Вышла новая версия программы (или сами поправили). Там «поехали» строки с сообщениями (появились новые, пропали/изменились/переместились старые).
(pybabel extract) Сгенерируем новый шаблон
(pybabel update) Обновим содержимое ru.po на основании шаблона и старого ru.po. У update много искусственного мозга:
- Не теряет старые переводы (только комментирует)
Размечает новые сообщения возможными fuzzy-переводами из старых/закомментированных
Допереводим ru.po
(pybabel compile) Компилируем новый перевод
Процесс локализации
Пример перевода начинается с этого коммита
Основную информацию см. там
Воспользуемся рецептом из методички:
Если параметр fallback установлен в False (по умолчанию), gettext.translation() бросит прерывание, если не найдёт перевода (например, на английский). С fallback=True в этом случае просто вернётся NullTranslations-объект, который тупо не станет ничего переводить, а оставит как есть.
ngettext нужен для поддержки plural, см. далее
Переключение локали и домена
Если программа должна выводить сообщения попеременно на разных языках, одним объектом типа gettext.Translation обойтись нельзя — в нём только один вариант перевода. На каждый используемый язык нужно регистрировать отдельный перевод: или из .mo-файла, или затыкать экземпляром NullTranslations. Локаль можно брать из какой-нибудь глобальной переменной, но лучше задавать с помощью locale.setlocale():
При наличии скомпилированного перевода в po/ru/LC_MESSAGES/tra.mo получим
Сообщенька The message
Переключение доменов — это просто создание нескольких корпусов перевода:
Использовать pybabel extract --no-default-keywords -k transcode.gettext
Множественные формы
Функция ngettext() в английском варианте различает форму единственного и множественного числа (эту функцию надо отдельно попросить добавить в пространство имён при gettext.install())
- Запуск программы а разных локалях:
$ LC_ALL=en_US.UTF-8 python3 DateTime.py $ LC_ALL=ru_RU.UTF-8 python3 DateTime.py
- Русский перевод имеет 3 множественные формы — единственное число, немного (2-4), и много (5 и больше), которые время от времени возникают в разных числительных.
Описание того, какие числа какой множественной форме соответствуют содержится в заголовке ru.po. Это описание компилируется в некоторый код .mo-файла, который gettext интерпретирует (замечание vslutov@, спасибо).
См. пример модификации этого описания в репозитории к лекции по Linux-разработке.
1 diff --git a/po/ru.po b/po/ru.po 2 index 0fcfd8b..cccdbac 100644 3 --- a/po/ru.po 4 +++ b/po/ru.po 5 @@ -15,7 +15,7 @@ msgstr "" 6 "MIME-Version: 1.0\n" 7 "Content-Type: text/plain; charset=UTF-8\n" 8 "Content-Transfer-Encoding: 8bit\n" 9 -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 10 +"Plural-Forms: nplurals=4; plural=(n==1? 3 : n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 11 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 12 13 #. Simple text 14 @@ -31,3 +31,4 @@ msgid_plural "Hello, %d Worlds\n" 15 msgstr[0] "Привет, %d мир\n" 16 msgstr[1] "Привет, %d мирa\n" 17 msgstr[2] "Привет, %d миров\n" 18 +msgstr[3] "Привет, мир\n" 19
Некоторые замечания
Для обвешивания нужных строк _()-ми в vim достаточно такой команды :
:%s/\("[^"]*"\)/_(\1)/gc
Domain на самом деле определяет не имя файла, а т. н. домен — один из трёх ключей, (язык, тип, домен), по которым происходит поиск перевода. Просто в Linux это превращается в путь к файлу /usr/share/locale/язык/тип/домен.mo.
Наша программа не будет ходить в /usr/share/locale, как это принято в Linux. Проблема в том, что встанавливать файлы в этот каталог можно только суперпользватель. Вместо этого переводы ожидаются в подкаталоге po/ru/LC_MESSAGES того каталога, куда установлен соответствующий модуль (то есть в …/lib/python3/site-packages/пакет/)
Редактировать .po-файл перевод можно очень разными редакторами
- многие IDE поддерживают работу с переводами
Я пользуюсь vim + vim-plugin-po-after-ftplugin
В числе прочего в переводе может нуждаться имя файла с картинкой: если на картинке была надпись или культурно обусловленный знак, в локализованной версии понадобится другой файл.
Обновление перевода
Т. н. «неточные переводы» помечаются значащим комментарием fuzzy.
При компиляции эти переводы не попадают в .mo-файл, но и не исчезают
- Неиспользованные переводы (которые были в старой версии, но отсутствуют в шаблоне), не удаляются, а комментируются
Дальнейшая работа с переводом — повторение цикла extract → update → правка ru.po → compile
Поскольку .pot-файл — это генерат, его можно не хранить в репозитории (всё равно он пересоздаётся при каждом update). При этом в шаблоне меняется время создания, даже если перевод остался прежним. Чтобы update не менял при этом перевод, ему надо добавить ключ --ignore-pot-creation-date
Про память переводов
Очень важно соблюдать единообразие при переводе одинаковых надписей. Для этого все переводы по проекту можно сложить в т. н. «compendium» (он же translation memory, она же Память переводов) — например, большой .po-файл со всеми возможными переводами, накопившимися за историю работы
Если в исходном проекте одинаковые надписи значат разное (например, «Copy» в одном месте — «Копировать», а в другом «Копия») — это плохой дизайн ,надписи надо менять
- Надписи типа «Save as…» или «Press button to continue» не придётся переводить по многу раз
- Появится больше автоматически подобранных fuzzy-переводов
- Фактически это будет ещё и словарь терминологии, который можно править при необходимости
В Babel нет поддержки compendium :(, но она есть в msgmerge из GNU Gettext
Однако есть т. н. Translator Comments (начинаются с "NOTE: "
Д/З
В семестровом проекте должен присутствовать перевод. Если вы пишете какой-то модуль/бот/библиотеку без UI, переводите документацию (см. Руководство по переводу документации Sphinx)
ВАЖНО:
скомпилированный .mo файл (при использовании gettext в качестве инструмента перевода) в git-архив не кладётся. Это генерат
.pot-файл — это тоже генерат (он будет меняться при каждой попытке обновить перевод, если даже ничего не изменилось)