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

Подводные камни

Этот сайт переведён на тридцать пять языков, и каждое издание получено тем самым циклом, которому учит эта документация. По отраслевым меркам это небольшой корпус — и его всё равно хватило, чтобы наткнуться на большинство ловушек, которые делают i18n сложнее, чем кажется.

Каждый раздел ниже — то, что здесь действительно пошло не так, как оно выглядело в тот момент и где проходит граница между тем, что библиотека проверяет за вас, и тем, что остаётся вашим решением.

Переименование переменной отправляет предложение на повторный перевод

msgid — это ключ каталога, а имя подстановки находится внутри него. Достаточно было вынести одну константу на уровень модуля и записать её заглавными, как того требует стиль Python, — author в AUTHOR, — и Copyright © 2026 {author} · MIT License превратилось в сообщение, которого не видел ни один каталог. Каждый перевод этой строки прошёл бы заново через цикл fuzzy, на всех языках, ради переименования, которое ничего не изменило для читателя.

Библиотека вас не остановит: оба написания — допустимые имена заполнителей. Что она делает, так это придаёт имени ценность, которую стоит беречь: подстановка обязана быть простым именем, поэтому в ключе каталога стоит слово, читаемое переводчиком, а не выражение.

Зеркальный случай безопасен по построению. Преобразования и спецификаторы формата не входят в msgid, поэтому ужесточение {amount:,.2f} до {amount:,.0f} не меняет ни одного ключа и нигде не обесценивает перевод.

nplurals=2 не значит две разные строки

Турецкий, венгерский, персидский и бенгальский объявляют по две формы множественного числа, и во всех четырёх две формы считаемого сообщения законно оказываются одной и той же строкой: существительное после числительного остаётся в единственном числе, так что {n} sayfa верно и для одной страницы, и для десяти. Рецензент, который «исправит» это дублирование, сломает перевод.

Обратная ошибка столь же легка. Третья форма латышского существует только для нуля; вторая словенская — двойственное число, ровно для двух; последняя форма румынского требует слова de, которого в первых двух быть не должно. Если заполнить эти ячейки единственным и множественным числом, получится каталог, неверный только для тех чисел, которые никто не проверяет.

Хуже того, порядок ячеек не семантический. Валлийский нумерует свои пять форм так, что msgstr[0] — общий случай, а msgstr[1] — единственное число. Заполнение в очевидной последовательности ставит единственное число туда, куда попадёт любое сообщение без счёта.

Библиотека ничего из этого на себя не берёт, и в этом суть: правило множественного числа целевого языка живёт в заголовке его собственного каталога, а правило объединения/пересечения позволяет переводу иметь больше форм, чем у источника, или меньше. Проверяет она единственное, что можно проверить, не зная языка: что каждая форма сохраняет нужные ей заполнители.

Две одинаковые формы могут быть такими не зря

У ирландского пять форм множественного числа, и в отчёте о сборке этого сайта несколько из них написаны одинаково. Это не промах копипасты: leathanach начинается на l, а ни одна из начальных мутаций, которые вызывают ирландские числительные, на l не записывается. Формы всё равно делают настоящую работу — основа чередуется между leathanach и leathanaigh, а числа больше десяти возвращаются к единственному числу, — но именно на существительном со значением «страница» контраст не виден.

Любая проверка, считающая одинаковые формы подозрительными, отметит корректный ирландский. Единственный рецензент здесь — человек, знающий язык.

Сообщение может согласоваться лишь с одним числом

Отчёт о сборке этого сайта сообщает, сколько страниц отрендерено и сколько это заняло. Запись вида «Rendered {n} pages in {seconds} seconds» выглядит безобидно и непереводима: gettext выбирает одну форму по одному числу, и это число — n. Слову seconds пришлось бы согласоваться с числом, которого механизм множественного числа никогда не видит.

Решение — сделать вторую величину символом единицы, а не словом; сами символы единиц тоже локализуются: каталоги этого сайта содержат s, с, ث, שנ׳ и mp, а французская, испанская и шведская типографика требует пробела перед символом там, где английская — нет. Всё это библиотеки не касается, — а вот заметить, что сообщению нужны два согласования, касается, и единственный инструмент для этого — написать сообщение иначе.

Правка английского предложения правит чужую грамматику

Главная страница когда-то говорила «all ten language editions». Удаление числа — правка в одно английское слово, сделанная потому, что число постоянно устаревало, — превратило подлежащее множественного числа в единственное. Испанскому, итальянскому, португальскому, русскому, украинскому, греческому, нидерландскому и ивриту пришлось заново согласовывать глагол; в нескольких понадобилось менять и причастие.

Правка источника, которая по-английски выглядит тривиальной, ниже по течению тривиальной не является. Пометка fuzzy, которую ставит pybabel update, — это и есть механизм, дающий каждому переводчику шанс заметить.

Невидимые различия переживают любое копирование

Руководство цитирует диагностику, содержащую (nаme), — намеренно экранированную запись, потому что названный ею символ — кириллическая а, которую ни один читатель не отличит от латинской. Переводчики этого сайта превращали эту запись в сам символ пять отдельных раз, на пяти разных языках, и каждый раз получалась страница, которая выглядела верной и была неверной.

Вот это библиотека как раз ловит, и именно поэтому диагностика устроена так, как устроена: заполнитель, в буквах которого смешаны системы письма, сообщается дважды — один раз читаемо и один раз экранированно, потому что экранированная запись — единственная, которая их различает. Неразрывный пробел внутри фигурных скобок печатается по кодовой точке по той же причине. Проверка каталога отклоняет сообщение раньше, чем оно попадёт в релиз.

Непустое — не значит переведённое

Каталог-заготовка, в котором msgid скопированы в msgstr, проходит любую наивную проверку: ничего не пусто, ничего не помечено fuzzy, множество сообщений совпадает точно. Одно издание этого сайта несколько часов существовало именно таким. Как и восемь страниц другого издания, побайтово совпадавших с английским источником, — а такое проходит проверку, сравнивающую блоки кода между ними, потому что это один и тот же файл.

Ни того ни другого библиотека переводов увидеть не может. И то и другое дёшево проверяется — но не требованием, чтобы каждая запись отличалась от источника: OK, названия продуктов, имена людей, аббревиатуры и идентификаторы из кода переводятся сами в себя, и проверка, которая это запрещает, будет вечно давать ложные срабатывания.

Измеряйте вместо этого долю — по всему каталогу или по целой странице — и отправляйте выбросы человеку. Собственный тест этого сайта делает именно так: он сравнивает строки прозы каждого издания с английским источником и падает выше 25% совпадений. Поддельное издание было на 87%; каждый настоящий перевод держится между 4% и 8% — это тот небольшой хвост строк, которые совпадают законно, вроде URL и цитируемого вывода программ. Две совокупности разнесены достаточно далеко, чтобы порог не требовалось выставлять точно.

Каталог — не единственное, что переводится

Два здешних сбоя не имели к gettext никакого отношения.

Перевод заголовка меняет якорь, который из него порождается, поэтому все ссылки на этот раздел с других страниц ломаются — молча и только в этом языке. Этот сайт закрепляет английский якорь на каждом заголовке, а тест выводит ожидаемый список из английской страницы.

А генератор сайта поставляет переводы интерфейса для шестидесяти восьми языков, среди которых нет ни суахили, ни ирландского. Без такого перевода сборка не деградирует до английского: включение шаблона падает, и издание вообще не собирается. Два собственных файла этого репозитория существуют, чтобы закрыть эту брешь.

У ваших инструментов тоже есть баги

Шаг CI, который эта документация рекомендует для отлова устаревших каталогов, — pybabel update --check — не справляется с этой задачей ни в одном проекте, использующем pgettext или npgettext. На Babel 2.18.0 он объявляет устаревшим каждый каталог с msgctxt, при каждом запуске. Сравнение идёт через Catalog.is_identical, который ищет каждое сообщение по тому ключу, под которым оно хранится, — а для контекстного сообщения этот ключ представляет собой пару (id, context), которую Catalog.get не принимает. Поиск не возвращает ничего, и каталоги никогда не оказываются равны:

>>> from babel.messages.catalog import Catalog
>>> c = Catalog(locale="ja")
>>> c.add("Guide", "ガイド", context="navigation")
<Message 'Guide' (flags: [])>
>>> c.is_identical(c)
False

Здесь его нашли, попытавшись им воспользоваться, сообщили в апстрим, а замена для этой проверки есть на странице о продакшене.

Общий урок неудобен: ворота, которые всегда красные, хуже, чем никаких, потому что команда их отключает. Убедитесь, что ваша проверка CI действительно может пройти, прежде чем доверять её падению.

Зачем нужна библиотека, в одну строку

Бо́льшая часть этой страницы — решения, которые ни один инструмент не возьмёт на себя. Что инструмент может, так это гарантировать, что перевод не изменит структуру переводимого предложения — не потеряет значение, не выдумает новое, не переформатирует и не залезет в ваши объекты, — и сказать об этом фразой, по которой тот, кому чинить, сможет действовать. Это всё, что обещает библиотека, а остальной сайт — про то, как она это обещание держит.