Локалізація вебзастосунків «по-людськи» або Чому ми розробили свій фреймворк
Привіт, мене звати Антон Пінкевич, я Engineering Team Lead у продуктовій ІТ-компанії Universe Group з екосистеми Genesis. Ми створюємо мобільні застосунки та вебплатформи для спрощення й підвищення якості життя людей.
За шість років у Universe Group побудували три успішні бізнеси: Guru Apps, FORMA, Wisey. Уже три роки я допомагаю створювати та масштабувати один з них — вебплатформу Wisey, яка слугує помічником з підвищення продуктивності для людей зі всього світу.
Ця стаття представляє новий метод локалізації React-застосунків, натхненний підходом Apple. Метод спрямований на оптимізацію процесу розробки та підвищення якості перекладів шляхом зміни традиційного процесу i18n. Я розгляну мінуси i18n, опишу новий, винайдений нами підхід, а також його переваги та недоліки. Матеріал буде корисним Front-end розробникам та розробницям рівня Middle та вище.
Вступ: проблематика традиційних методів i18n
Локалізація є критичним аспектом розробки сучасних вебзастосунків. Хоча традиційні методи i18n широко поширені, вони створюють низку незручностей як для розробників, так і для перекладачів. Отже, необхідний новий підхід до локалізації React-застосунків, що зміг би розв’язати ці проблеми й оптимізувати процес розробки та підтримки продуктів із декількома локалізаціями.
Наша гіпотеза полягала в тому, що якщо поліпшити DX (developer experience) розробників і UX (user experience) перекладачів, ми зможемо реалізувати первинну локалізацію застосунку і підтримувати її в майбутньому меншими зусиллями, ніж за традиційного підходу.
Проблеми традиційного підходу i18n
Попереднє створення файлів локалізації
i18n потребує зберігання ключів локалізації всередині json-файлів. Тобто розробник повинен спочатку створити файл, а вже потім використовувати його у коді. Це призводить до того, що він повинен придумувати назви всіх ключів для текстів без контексту їх використання у коді.
Когнітивне навантаження
Замість абстрактної проблеми краще подивитись на невеликий приклад: уявімо, що у нас є текст Select your age, тоді створюємо json із ключем:
«select_your_age»: «Select your age»
Через деякий час необхідно змінити текст на How old are you?. У підсумку ми отримуємо:
«select_your_age»: «How old are you?»
Тобто текст починає «відриватися» від ключа і доводиться постійно перечитувати файл з локалізацією, щоб знайти потрібне. Називати ключ за знаходженням в UI теж незручно. Наприклад:
«title»: «How old are you?»
Якщо title зміниться на subtitle, доведеться перейменовувати усі ключі. Загалом створити локалізацію з json-файлу можна, але підтримувати це стає нестерпно складно.
Детальний приклад використання
// JSON файл локалізації (en.json) { "greeting": "Hello", "welcome": "Welcome to our app", "user": { "profile": { "title": "Current User: {{name}}" } } }
// Використання у React-компоненті function Welcome() { const { t } = useLocale() return ( <div> <h1>{t('greeting')}</h1> {t('welcome')} <h2>{t('user.profile.title', { name: 'John' })}</h2> </div> ) }
Наш метод локалізації
Концепція
Відтак наше завдання зробити роботу та підтримувати локалізації якомога простішими. Щоб процес локалізації застосунку не відволікав програміста від роботи й не забирав додатковий час.
Уявімо, що розробники пишуть код так, ніби локалізації не існує. Розробник продовжує працювати з кодом, не торкаючись json-файлів, а необхідні етапи перекладу винесені в окремий автоматизований процес. Згенеровані json-файли мають читабельну структуру та коментарі, які допомагають із дебаггінгом у рідкісних випадках. Процес локалізації будується на файловій системі, щоб використовувати переваги інструментів розробника, такі як IDE та git.
Процес роботи
Для цього потрібно розділити процес на такі етапи:
- extraction — діставання рядків із вихідного коду;
- translation — автоматичний переклад рядків;
- verification — вичитування перекладу професійними перекладачами;
- optimization — поділ одного файлу на багато маленьких;
- editing/removing — редагування або видалення старих рядків.
Щоб локалізації були підтримуваними, необхідно додати можливість прикріплювати коментарі та метадані. А для оптимізації збірки має сенс дати можливість розділяти ці рядки по різних файлах, щоб для певних сторінок завантажувалися тільки потрібні файли.
Технічні деталі реалізації
Приклад використання:
function Welcome() { const { t } = useLocale(); return ( <div> <h1>t`Hello`</h1> t`Welcome to our app` <h2>t`Current User: ${"John"}`</h2> </div> ); }
Замість використання текстів безпосередньо, вони стають аргументами функції t.
Генерація файлу з локалізаціями (extraction)
polyglotte extract-translations ./src --outfile=./localization.json --locales=en
Скрипт автоматично витягує рядки для локалізації з вихідного коду. Його функціонування можна описати таким чином:
- Аналіз файлової структури: скрипт починає роботу із зазначеної директорії (./src) і послідовно обробляє всі файли в ній та її піддиректоріях;
- Ідентифікація елементів, що локалізуються: у процесі аналізу коду скрипт виявляє текстові рядки, що підлягають локалізації (усі аргументи функції t);
- Генерація результату: після завершення аналізу створюється файл localization.json. Цей файл містить структурований список усіх знайдених рядків для локалізації.
Отриманий json-файл слугує основною та єдиною точкою входу для подальшої роботи над локалізацією застосунку.
{ "locales": ["en"], "keys": { "Hello": { "key": "Hello", "status": "new", "managed": "automatic", "comment": "Automatically generated from file=example/index.tsx, line=5, char=20", "en": "Hello" }, "Welcome to our app": { "key": "Welcome to our app", "status": "new", "managed": "automatic", "comment": "Automatically generated from file=example/index.tsx, line=6, char=19", "en": "Welcome to our app" }, "Current User: {{1}}": { "key": "Current User: {{1}}", "status": "new", "managed": "automatic", "comment": "Automatically generated from file=example/index.tsx, line=7, char=20", "en": "Current User: {{1}}" } } }
Генерація окремих локалізацій (translation)
Після генерації основного файлу локалізації настає етап автоматизованого перекладу. Це необхідно, щоб розробник міг перевірити верстку на реальних ключах без очікування фінального перекладу. Ми використовуємо DeepL з кастомними словниками, він добре працює з markdown-розміткою і дозволяє зберегти консистентність між запитами.
polyglotte generate-locale ./localization.json --locales=en,es --provider=deepl --apiKey=*****
Скрипт проводить порівняльний аналіз файлу локалізації, виявляючи ключі, які вже мають переклад, і ті, які ще не перекладені. Для неперекладених ключів скрипт звертається до DeepL API та додає потрібні переклади прямо у файл. Згенеровані переклади маркуються для подальшого розгляду професійними перекладачами.
{ - "locales": ["en"], + "locales": ["en", "es"], "keys": { // ... "Hello": { "key": "Hello", - "status": "new", + "status": "needs_review", "managed": "automatic", "comment": "Automatically generated from file=example/index.tsx, line=5, char=20", "en": "Hello", + "es": "Hola" } } }
Вичитування та редагування (verification)
Файл локалізації може містити додаткову корисну інформацію у вигляді метаданих:
- Скриншот: до кожного рядка можна прикріпити посилання на зображення екрана, де цей текст використовується;
- Координати тексту: можна вказати, де саме на екрані розташовується кожен текст.
Використовуючи скриншот та координати, можна наочно показати контекст використання тексту для вичитки професійним перекладачем. Для цього ми побудували окремий вебзастосунок, який дає змогу завантажити файл з локалізацією та модифікувати його у зручному інтерфейсі.
{ "locales": ["en"], "keys": { // ... "Welcome to our app": { "key": "Welcome to our app", "status": "new", "managed": "automatic", "comment": "Automatically generated from file=example/index.tsx, line=6, char=19", "en": "Welcome to our app", + "metadata": { + "screenshot": "./screenshots/welcome.jpg", + "box": [0.061, 0.446, 0.875, 0.081] + } } } }
Ми завжди використовуємо Figma → Code процес, тому ще зробили невеличкий плагін до Figma, який автоматично створює локалізаційний файл з вибраних фреймів і прикріплює усі необхідні метадані до ключів.
Поділ файлу (optimization)
Завершальним етапом процесу локалізації перед релізом є підготовка оптимізованих перекладених файлів.
polyglotte prepare-translations ./localization.json
Скрипт генерує окремі json-файли для кожної мови. У перспективі планується впровадження більш гранулярного підходу з розподілом перекладів за окремими сторінками застосунку.
Файл en.json навмисно генерується порожнім. Це зумовлено тим, що для базової мови ключі та їхні значення ідентичні. Функція t має вбудований механізм обробки відсутніх ключів, якщо переклад для ключа не знайдено, функція повертає вихідний рядок, укладений у backticks. Такий підхід дає змогу знизити розмір фінального файлу та підвищити відмовостійкість у разі, коли якогось ключа не існує.
Приклад згенерованих файлів:
// en.json {}
// es.json { "Hello": "Hola", "Welcome to our app": "Bienvenido a nuestra aplicación", "User Profile": "Cliente Activo: {{1}}" }
Обробка зміни ключів (editing/removing)
У разі зміни тексту в коді:
function Welcome() { const { t } = useLocale(); return ( <div> <h1>t`Hello`</h1> - t`Welcome to our app` + t`Welcome onboard` <h2>t`Current User: ${"John"}`</h2> </div> ); }
При повторному запуску скрипта файл локалізації автоматично оновлюється:
{ "locales": [ "en", "es" ], "keys": { "Welcome to our app": { "key": "Welcome to our app", - "status": "new", + "status": "stale", "managed": "automatic", "comment": "Automatically generated from file=example/index.tsx, line=6, char=19", "en": "Welcome to our app", "es": "Bienvenido a nuestra aplicación" }, + "Welcome onboard": { + "key": "Welcome onboard", + "status": "new", + "managed": "automatic", + "comment": "Automatically generated from file=example/index.tsx, line=6, char=19", + "en": "Welcome onboard" + }, } }
Далі розробник має вручну вирішити, що робити з подібними ключами: видалити їх чи модифікувати. Завдяки git diff процес стає дуже зручним.
Нюанси
Під час впровадження системи локалізації виникає низка сценаріїв, які потребують особливої уваги.
Реалізація провайдера локалізації
Для поширення даних локалізації в React-застосунку замість singleton використовується провайдер:
// page.tsx export default function Page() { // розпізнавання мови користувача. Може відбуватися автоматично на основі даних браузера, або на основі даних у поточному pathname const userLanguage = useUserLanguage() return ( // провайдер уміє знаходити і ліниво завантажувати файли локалізацій <LocaleProvider language={userLanguage}> {/* ... */} </LocaleProvider> ) }
Це дає змогу створити та використовувати Mock-провайдери для автоматичного тестування та розробки компонентів. Наприклад, у зв’язці зі storybook.
Робота зі змінними
Інтерполяція змінних здійснюється стандартним способом:
t`Hello, ${user.name}!`
У файлі локалізації це трансформується в Hello, {{1}}!. Такий підхід дає змогу гнучко адаптувати переклади, наприклад: ¡Hola, {{1}}!. DeepL майже без помилок обробляє подібні конструкції, зберігаючи плейсхолдери. Усередині файлу з локалізацією змінні маркуються індексами, тому кількість можливих змінних необмежена.
Користувацькі ключі локалізації
Функція t підтримує розширений синтаксис з об’єктом налаштувань:
t({ key: 'username', comment: "This is user's name" })`Hello, ${user.name}!`
Це відображається у файлі локалізації наступним чином:
{ // ... "keys": { "username": { // береться з об'єкту "key": "username", // береться з об'єкту "comment": "This is user's name", "en": "Hello, {{1}}" // ... } } }
Використання різних форматів тексту
Для розширеного форматування тексту в локалізованих рядках використовується синтаксис markdown. Це дає змогу легко додавати стилізацію без необхідності впровадження HTML-тегів безпосередньо в рядки перекладу. Наприклад, **Hello, ${user.name}!**. Для цього ми використовуємо бібліотеку markdown-to-jsx.
Автоматизація в CI/CD
Для мінімізації людського фактора, процес перевірки та генерації локалізацій інтегрований у CI:
polyglotte extract-translations ./src --outfile=./localization.json --locales=en && \\ polyglotte generate-locale ./localization.json --locales=en,es --provider=deepl --apiKey=***** && \\ polyglotte prepare-translations ./localization.json
Цей скрипт автоматично запускається при кожному PR, створюючи коміт з оновленими ключами локалізації.
Статуси ключів
Система використовує чотири статуси для ключів локалізації:
- new — новий ключ, створений системою;
- stale — невикористаний ключ, що підлягає видаленню;
- needs_review — автоматично перекладений ключ, що потребує перевірки;
- translated — перевірений і готовий до використання ключ.
Ці статуси допомагають відстежувати життєвий цикл кожного ключа локалізації та керувати процесом перекладу.
Типи менеджменту ключів
Існує 2 типи менеджменту ключів:
- automatic — скрипт автоматично оновлює статуси та значення;
- manual — програміст або перекладач вручну модифікує даний ключ.
Порівняльний аналіз
Process |
i18n |
Polyglotte |
Потік даних |
JSON → code |
Code / Figma → JSON → Code |
Створення ключів |
💪 Вручну |
🤖 Автоматично |
Використання ключів |
Абстрактні ключі: user.profile.title |
Читабельні ключі: User Profile |
Переклад |
💪 Вручну |
🤖 Автоматично |
Змінні |
✅ Підтримується |
✅ Підтримується |
Множинність (pluralization) |
✅ Підтримується |
🚫 Поки не підтримується |
Інтеграція з CI |
💪 Вручну |
🤖 Автоматично |
Проєкт був розроблений для внутрішнього використання нашою компанією, тому зовнішніх тестів проведено не було. Але цей підхід дозволив команді провести локалізацію UI-частини платформи всього за три дні. Ми написали невеличкий скрипт, який пройшовся кодовою базою та додав до всіх рядків виклик функції t. Після цього стало можливим використовувати процес автоматичних перекладів, описаний вище. Тому, якщо оцінювати суб’єктивно, можна вважати, що проєкт виявився успішним.
За ці три дні ми переклали понад чотири тисячі ключів (якщо бути точним, то 4385).
Висновок
Хоч поточна реалізація не покриває всіх можливих сценаріїв використання на цей час, наприклад, немає підтримки pluralization, вона є хорошою базою для подальшого розвитку. Рішення про зміну процесу створення json-файлу в автоматичному режимі одразу ж вирішило безліч проблем, що існували раніше. Ми задоволені результатом, підтвердили гіпотезу і плануємо далі розвивати цей фреймворк.
25 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів