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

Підводні камені

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

Переклад заголовка змінює якір, згенерований з нього, тож кожне міжсторінкове посилання в цей розділ ламається — мовчки й лише в тій мові. Цей сайт закріплює англійський якір на кожному заголовку, а тест виводить очікуваний список з англійської сторінки.

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

Ваші інструменти теж мають вади

Крок 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 взагалі здатна пройти, перш ніж довіряти їй падіння.

Для чого ця бібліотека, в одному рядку

Більша частина цієї сторінки — судження, якого не перебере жоден інструмент. Що інструмент може, так це гарантувати, що переклад не здатен змінити структуру речення, яке він перекладає: не може прибрати значення, вигадати його, переформатувати чи дотягнутися до ваших об'єктів, — і сказати про це реченням, за яким той, кому це виправляти, здатен діяти. Це і є все, що обіцяє ця бібліотека, а решта цього сайту — про те, як вона це тримає.