Блог ·
Шістнадцять тисяч ключів, девʼять мов
Переклад панелі керування не є текстовою задачею. Найважливіші відмови є тихими: втрачений аргумент, сплющена множина, запобіжник, що не перевіряє нічого.
Автор: Provibr
Наш англійський каталог повідомлень наразі містить трохи більше за шістнадцять тисяч рядків. Каталогів девʼять, по одному на мову, і від них вимагається мати рівно ті самі ключі. Це десь понад сто сорок тисяч шматків тексту, які мають лишатися правдивими один щодо одного, поки дюжина людей змінює продукт під ними.
Цікавим у цій задачі є не обсяг. Цікаве те, що майже жодна з відмов не оголошує про себе. Про переклад з одруківкою клієнт повідомить протягом тижня. Переклад, який тихо загубив аргумент, вічно рендерить речення з діркою всередині, і ніхто не заводить тікет про речення, що читається нормально.
Ось набір запобіжників, до якого ми дійшли, і міркування за кожним з них. Вони не привʼязані до жодного фреймворка; ці режими відмови є властивістю того, що мов більш ніж одна.
Клас вад, що має значення
Почнімо з тієї, яка змусила нас поставитися до цього серйозно.
Англійський рядок каже Welcome back, {name}. Хтось його перекладає, і перекладом є Welcome back. Він граматичний, це розумне речення, і воно рендериться без помилки в будь-якій системі шаблонів, що існує. Параметр просто не зʼявляється. Немає ні винятку, ні рядка в журналі, ні попередження про відсутній ключ.
Ця вада невидима для кожного тесту, який перевіряє, чи ключі існують, а саме такий тест майже кожен пише першим. Єдине, що її знаходить, це порівняння: витягти множину імен плейсхолдерів з еталонного рядка, витягти їх з перекладу і вимагати, щоб вони збігалися.
Щойно ви побудували це порівняння, потрібними виявляються три уточнення, і кожне з них походить від справжнього рядка, який прослизнув.
Порівнюйте типи аргументів, а не лише імена. Голий {count} не форматується. Він друкує сире число, без роздільника тисяч, будь-якою мовою. Напишіть {count, number}, і воно форматується. Якщо тип є в англійській і відсутній у перекладі, імʼя збігається, структура гілок збігається, а одна мова тихо рендерить сире ціле число там, де решта рендерять відформатоване. Те саме стосується дат: аргумент дати, що втратив свій тип, рендерить читачеві позначку часу в ISO.
Не порівнюйте стилі аргументів. Валютний стиль проти простого числового є вибором подання, який законно різниться від мови до мови, а запобіжник, що на це свариться, привчає людей його ігнорувати. Тип є структурним, стиль є смаком, і лише одне з цього належить тестові.
Порівнюйте імена гілок, а не їхній текст. Множина, яку перекладач сплющив, зберігає свій аргумент, тож порівняння плейсхолдерів радо її пропускає, а на екрані врешті стоїть голе число без іменника після нього. Саме це було в нашому нідерландському каталозі, і саме так це знайшли.
Множина не є розвʼязаною задачею, яку можна скопіювати
Інстинкт каже вимагати, щоб переклад мав ті самі гілки множини, що й англійська. В англійській їх дві: one і other. Ця вимога правильна для мов, які теж мають дві, і рівно неправильна для тих, які не мають, бо вона радо приймає каталог, у якому бракує категорій, потрібних його власній мові.
Кілька вимірювань, усі зі стандартних правил, що постачаються з будь-яким сучасним середовищем виконання:
| Мова | Що дивує людей |
|---|---|
| Французька | Нуль бере однинну гілку. «0 machine», а не «0 machines». |
| Українська | Чотири категорії. Нуль потрапляє в many, разом з 5 до 20; 21 це one, а 22 це few. |
| Румунська | Від 2 до 19 це few, 20 це other, а 101 знову few. |
| Арабська | Шість категорій, з поділом на 0, 1, 2, від 3 до 10 і від 11 до 99. |
Румунський рядок має наслідок, що проявляється як чистий обсяг. Румунська ставить прийменник перед іменником вище девʼятнадцяти, тож фраза, якій в англійській множина взагалі не потрібна, потребує її в румунській. Підрахунок повідомлень у наших каталогах, що містять множину: в англійській їх 319, у румунській 498. 179 повідомлень потребують конструкції множини в румунській, якої англійська не потребує зовсім. Українська додає 13 своїх, французька 4.
Ось одне з них, і це цілком звичайна англійська:
en: Start {days}-day trial
uk: Почати пробний період на {days, plural, one {# день} few {# дні} many {# днів} other {# дня}}Ніщо в англійському реченні не натякає, що це задача множини. Запобіжник, який виводить свої очікування з англійської, ніяк не може цього побачити.
Тож другий запобіжник працює для кожної мови проти її власного каталогу: чи для кожної множини в ньому оброблено кожну категорію, яку ця мова справді може породити, чи то за іменем, чи то гілкою з точним значенням?
Цьому запобіжникові потрібне одне свідоме обмеження, інакше він стає непотрібним. Французька та іспанська мають категорію many, яка стосується лише точних кратних мільйона. Вимагати її всюди означало б додати сотні гілок, яких жодна людина ніколи не побачить, у продукті, що рахує сервери, адреси, учасників команди і дні. Тож перевірка керується вибіркою: вона питає, які категорії досяжні для тих чисел, які ключ реалістично бачить, і є невеликий явний перелік ключів, що рахують вище, тож механізм буде на місці того дня, коли щось таке зʼявиться.
Цей перелік наразі порожній, і це вимірювання, а не недогляд. Два кандидати, які мають вигляд таких, що можуть виростати великими, рахують байти, а французьке правило хоче точного кратного мільйона; файл рівно на 1 000 000 байтів є збігом, а не випадком.
Запобіжник, який мовчки нічого не робив
Тепер незручний випадок, бо це саме та відмова, про яку вся ця стаття, тільки сталася вона з самим інструментарієм.
У нас є перевірка, що кожен виклик перекладу в коді посилається на ключ, який існує. Вона розбирає джерело, стежить, який обʼєкт перекладача належить якому простору імен, і шукає кожен ключ. Вона звітувала, що покриває кілька тисяч ключів. Вона була зеленою.
Вона також пропускала кожен виклик, що походив зі шаблону деструктуризації, бо код, який привʼязував перекладача до простору імен, обробляв лише простий ідентифікатор і мовчки ігнорував усе інше. Немає привʼязки, отже немає ключа для перевірки, отже немає й відмови. Шаблон, який вона пропускала, виявився саме тим, що вживається в більшій частині нашого застосунку.
Ми знайшли це тому, що хтось угадав імʼя ключа, написав його, а тест лишився зеленим.
З цього вийшли два правила, і ми тепер застосовуємо обидва до кожного статичного аналізу, який пишемо.
Аналізатор, що зустрічає форму, якої не розуміє, мусить падати гучно, а не пропускати тихо. Невпізнане є знахідкою, а не порожньою дією. Уся цінність інструмента в тому, що він подивився на все.
Доведіть запобіжник червоним, перш ніж довіряти йому зеленому. Зламайте щось навмисно і подивіться, як тест падає. Перевірка, яка ніколи не падала, не була протестована; її лише запускали.
Близький родич цього уроку: назву бренду в наших каталогах писали чотирма різними способами, а єдине місце в усій кодовій базі, яке знало правильне написання, знало його як виняток у білому списку. Єдиний компонент, що мав правильну відповідь, було налаштовано дивитися повз неї. Виняток не є запобіжником, і виправленням зазвичай є вивернути річ навпаки: сформулювати правило, а винятки нехай будуть даними, що на ньому висять.
Розбирати власний формат повідомлень важче, ніж здається
Витягання плейсхолдерів регулярним виразом працює, поки не перестає, і те, як воно відмовляє, повчальне, бо воно відмовляє на ваших найкращих рядках.
Гілка select, текст якої складається з одного слова, для зіставлювача взірців має вигляд рівно як аргумент. Наш старий сканер здіймав хибну тривогу на найретельніше написаних повідомленнях у каталозі і водночас проґавлював справжні аргументи всередині гілок, бо регулярний вираз не вміє рахувати дужки. Він помилявся в обидва боки одночасно.
Заміна ходить дужками як належить і рекурсивно заходить у тіла гілок. Це тридцять рядків замість одного, і це різниця між запобіжником, якому люди довіряють, і запобіжником, який люди обходять анотаціями.
Типографіка є запобіжником на кожну мову, а не спільним
Останню категорію взагалі не можна перевірити порівнянням, бо вона є рівно тим, що між мовами різниться. Французька хоче нерозривний пробіл перед двокрапкою, крапкою з комою, знаком оклику і знаком питання. Українська хоче конкретний символ апострофа, схожий на два інші апострофи, які, ймовірно, є у вас на клавіатурі. Іспанська відкриває питання перевернутим знаком, у тій точці, де питання починається, а не на початку речення. Румунська вживає власні лапки і діакритики з комою знизу.
Ніщо з цього не порівнюване між мовами, тож кожна мова, що має правила, отримує власну перевірку. Це звучить як дублювання і ним не є: альтернативою є єдиний загальний запобіжник, який може стверджувати лише перетин девʼяти наборів правил, а це на практиці ніщо.
Одне застереження з досвіду роботи у великому репозиторії: нерозривний пробіл невидимий, і кілька інструментів послужливо перетворять його на звичайний пробіл дорогою. Саме тому ми вставляємо їх скриптом і потім перевіряємо, що вони досі на місці.
Що ми збудували б першим і в якому порядку
Якщо ви починаєте з однієї мови й очікуєте додати ще, ось порядок, який зберіг би нам найбільше.
- Ті самі ключі в кожному каталозі, з перевіркою в CI. Дешево, і це фундамент, на якому стоїть решта.
- Порівняння плейсхолдерів і типів аргументів з еталонною мовою. Саме воно ловить тихі вади.
- Порівняння імен гілок для множини і select, щоб сплющена множина була відмовою.
- Покриття множини на кожну мову, кероване власними правилами мови, ще до появи першої мови, якій потрібно більше категорій, ніж вашій еталонній.
- Типографіка на кожну мову, одна маленька перевірка на кожну мову, що має свої домовленості.
- Статична перевірка, що кожен ключ, на який посилається код, існує, і, що важливіше, що вона чесна щодо викликів, яких не змогла зрозуміти.
Тема, що проходить крізь усі шість, полягає в тому, що вади перекладу не кидають винятків. Вони рендеряться. Кожен запобіжник тут існує тому, що щось відрендерилося бездоганно, мовою, якої ніхто в кімнаті не читає, і сказало щось трохи інше, ніж ми мали на увазі.