Локалізація вебзастосунків «по-людськи» або Чому ми розробили свій фреймворк

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Привіт, мене звати Антон Пінкевич, я 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

Скрипт автоматично витягує рядки для локалізації з вихідного коду. Його функціонування можна описати таким чином:

  1. Аналіз файлової структури: скрипт починає роботу із зазначеної директорії (./src) і послідовно обробляє всі файли в ній та її піддиректоріях;
  2. Ідентифікація елементів, що локалізуються: у процесі аналізу коду скрипт виявляє текстові рядки, що підлягають локалізації (усі аргументи функції t);
  3. Генерація результату: після завершення аналізу створюється файл 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)

Файл локалізації може містити додаткову корисну інформацію у вигляді метаданих:

  1. Скриншот: до кожного рядка можна прикріпити посилання на зображення екрана, де цей текст використовується;
  2. Координати тексту: можна вказати, де саме на екрані розташовується кожен текст.

Використовуючи скриншот та координати, можна наочно показати контекст використання тексту для вичитки професійним перекладачем. Для цього ми побудували окремий вебзастосунок, який дає змогу завантажити файл з локалізацією та модифікувати його у зручному інтерфейсі.

{
  "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, створюючи коміт з оновленими ключами локалізації.

Статуси ключів

Система використовує чотири статуси для ключів локалізації:

  1. new — новий ключ, створений системою;
  2. stale — невикористаний ключ, що підлягає видаленню;
  3. needs_review — автоматично перекладений ключ, що потребує перевірки;
  4. translated — перевірений і готовий до використання ключ.

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

Типи менеджменту ключів

Існує 2 типи менеджменту ключів:

  1. automatic — скрипт автоматично оновлює статуси та значення;
  2. manual — програміст або перекладач вручну модифікує даний ключ.

Порівняльний аналіз

Process

i18n

Polyglotte

Потік даних

JSON → code

Code / Figma → JSON → Code

Створення ключів

💪 Вручну

🤖 Автоматично

Використання ключів

Абстрактні ключі: user.profile.title

Читабельні ключі: User Profile

Переклад

💪 Вручну

🤖 Автоматично

Змінні

✅ Підтримується

✅ Підтримується

Множинність (pluralization)

✅ Підтримується

🚫 Поки не підтримується

Інтеграція з CI

💪 Вручну

🤖 Автоматично

Проєкт був розроблений для внутрішнього використання нашою компанією, тому зовнішніх тестів проведено не було. Але цей підхід дозволив команді провести локалізацію UI-частини платформи всього за три дні. Ми написали невеличкий скрипт, який пройшовся кодовою базою та додав до всіх рядків виклик функції t. Після цього стало можливим використовувати процес автоматичних перекладів, описаний вище. Тому, якщо оцінювати суб’єктивно, можна вважати, що проєкт виявився успішним.

За ці три дні ми переклали понад чотири тисячі ключів (якщо бути точним, то 4385).

Висновок

Хоч поточна реалізація не покриває всіх можливих сценаріїв використання на цей час, наприклад, немає підтримки pluralization, вона є хорошою базою для подальшого розвитку. Рішення про зміну процесу створення json-файлу в автоматичному режимі одразу ж вирішило безліч проблем, що існували раніше. Ми задоволені результатом, підтвердили гіпотезу і плануємо далі розвивати цей фреймворк.

Корисні посилання

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Цікавий підхід — дякую, що поділилися!

Колись думав над чимось схожим. Якщо це вже працює в продакшені, перекладено 4000+ ключів, і є покращення DevEx — це супер! Автоматизація змін у JSON теж суперкорисна функція.

Декілька запитань:
1. З точки зору перекладача: «metadata/screenshot» — це зручно, але чи не виникають труднощі з навігацією при зростанні кількості скрінів у проєкті? Припускаю, що зрозуміти контекст лише зі скріншоту може бути складно.
2. Чи з додаванням pluralization усе проходить без проблем?

1. з поточним розміром додатку ні, але в майбутньому можна зробити якийсь bird eye view з плиткою усіх скріншотів;
2. так, все окей 🙂

Дякую за статтю. Цікаво. Найбільше мені сподобалось, що ви винайшли велосипед, але цей велосипед зроблений для вас і з повним контролем.
Не зовсім зрозумів про те, як відбувається заміна тексту та ключа. Ви старий заміняєте чи завжди додаєте новий? І що якщо ключі повторюються?
І що до pluralization — дуже хочеться побачити ваше рішення. Будь-ласка, як буде — напишіть.

1,2. в нас працює такий алгоритм:
— якщо ключ є в коде, але немає у файлі — додаємо новий ключ у кінець списку;
— якщо ключа немає в коді, але є у файлі — помічаємо статусом `stale`, на такі ключі вже вручну звертає увагу розробник (змінює код, щоб використовувати ключ, або видаляє ключ з файлу);
— якщо ключ є у код і є у файлі — ми просто використовуємо цей ключ. Тобто дублікати неможливі.

3. добре, як тільки буде апдейт — відпишу у коментарях. Ми pluralization доки пропустили, тому що нам таккий функціонал не потрібен для нагальних потреб.

Дякую за статтю. Підкажіть, ваша бібліотека з відкритим кодом? Щось не знайшов посилання на репозиторій.

Добрий день, нажаль поки с закритим. Як кажуть у Нетфлікс: «код готовий для продакшену, але ще не готовий до опенсорс».

дякую за статтю, в такому підході є низка питань
1. незрозуміло що є source of truth — фігма чи код і як вони співставлені?
2. чи сортуються якимось чином ключі у результуючому файлі? наприклад я змінив «user profile» на «customer profile» і відповідно перейменував tsx-компонент. якщо місце перекладу в файлі залежить від дерева компонент то переклад переїхав далеко в diff, для сортування за алфавітом треба робити додатковий прохід
3. як ф-я t знає яку змінну підставляти в плейсхолдер Hello {{1}}?
4. на чому ви написали polyglotte?
мені тут сподобалося можливість тримати всі переклади в одному файлі, я би не генерував окремі файли для перекладу на інші мови

1. завжди код. Плагін у фігми лише допомагає генерувати верстку та тексти для локалізації на початку роботи над новими екранами;
2. ключі не сортуються, вони працюють як append-only, тобто нові ключі завжди у кінці. Нам доволі комфортно з таким працювати, але сортування виглядає як гарна ідея, дякую;
3. Вона бере змінні за порядком з template literal’у. Тобто якщо у перекладі замінити місцями цифри (наприклад Hello {{2}} {{1}}!, то при використанні у коді <p>t`Hello ${"John"} ${"Dou"}!`</p> вийде Hello Dou John!;
4. писали як швидкий прототип на typescript. Але як тільки побачимо, що технологія буде сталою, то перепишемо на rust (ми у трендах, хєхє);

Для програмістів та перекладачів усі ключі зберігаються в одному файлі. Росформування файлів за мовами потрібно, щоб знизити кількість трафіку.

1. так ви ж писали що можете додати box і скрін з фігми. тоді фігма плагін має вишукати коректно ключ створений з коду і проапдейтити його? чи це взагалі було додано як майбутня мрія? ) адже ще потрібен інструмент щоб приймав переклади і відмальовував квадратик на скріні
2. оскільки ви втратили можливість групу ключі довільно, то можливо сортувати краще
4. цікаво що у вас ще на расті, у вас же ніби не блокчейн що раст-програмісти сидять?

1. ключі це звичайний текст, якщо managed = automatic, то плагін і робить пошук по цьому тексту. MVP інструменту для редагування текстів вже є, зібрали за пару днів за допомогою shadcn.
Він отримує zip файл (у якому є json та усі необхідні скріншоти), рендерить це і дозволяє редагувати json. Після редагування зберігає назад у zip файл, який програміст забирає у репозиторій. Ми використовуємо такий процес для вичитки текстів професійними перекладачами. Можливо у майбутньому автоматизуємо через CI/CD, щоб створювався PR на зміни після перекладачів;

4. у нас 2 основні мови на бекенді: typescript (nodejs/bun) та go. Наразі rust це як хобі/мрія до розробки, але ми вже готуємо до релізу мікросервіс для зберігання та обробки великої кількості аналітичних івентів (rust + questdb).

Проблема локалізації програм виникла ще з першими програмами у минулому столітті. Там само, як, наприклад, проблема з календарями і розрахунком дат, проблема з часовими поясами і розрахунком часу. Здавалося б: за стільки часу рішення таких задач не тільки має бути знайдено — воно має настільки устоятися що може бути захаркоджено на апаратному рівні, як, наприклад, математичний со-процесор чи алгоритми стиску чи шифрування.
Насправді більшість комп’ютерів та операційних систем і так мають якщо не апаратні — то програмні рішення для цих проблем. Програмам не треба рахувати дні чи години — бо це працює на рівні операційної системи.
Але з появою «фронтенду» усе ніби відкотилося у минуле. Виявилось що у Жабаскрипті усі ці проблеми не тільки не вирішені — вони навіть ускладнені неповноцінною реалізацією (бо браузер не може повноцінно використовувати готові можливості операційної системи).
Як виявилося — фронтенд девелопери і досі вигадують свої нові фреймвоки для локалізації!

Ну жабаскрiпт девелопери роблять фреймворки щодня, така наша доля.
Або це ще один метод навчання.

I have not failed. I’ve just found 10,000 ways that won’t work. © Thomas Edison

Не розумію, чому це названо

Новий метод

, бо я думав, що вже всюди так. Angular з коробки майже все має, для React є Lingui та аналоги, і так далі. Отримані файли .xliff, .po тощо можна перекласти в Crowdin або подібних сервісах.

В оригінальному тексті такого не було :)
Гадаю, що це клікбейт від редакторів

UPD: було, але виправив 😁

Підтверджую, в оригінальному тексті було саме про новий метод :)

Тепер знаємо, що можно робити провокативні заяви про «новий метод» на старі технології 😁

Все нове — це добре забуте старе... 😁

Може, щось не уважно прочитав, але виглядає як велосипед. lingui працює схоже і все необхідне вже має. Нічого в ручну не створюється теж, екстракт запустив і все, джіес файли ci/cd робить

Так-так. Є таке :)
Коли ми вивчали існуючі інструменти то теж звернули на lingui увагу. Він дуже прикольний, але в нас було 2 причини, які зупинили від використання:

1. немає можливості розширювати переклади за допомогою «багатих» метаданих.
Якщо подивитесь на той файл, що генерує наш інструмент, то можете звернути увагу на статуси перекладів та візуальну інформацію для перекладачів. Це дуже необхідна фіча, яку було б не дуже комфортно робити у форматі PO файлів (робити свою специфікацію, або stringified json);

2. не сподобалось те, що lingui змінює код у білд таймі, що додає додаткову причину для дебагінгу у майбутньому.

Ну і там вийшло ~1000 строк коду з повним контролем з нашої сторони ¯\_(ツ)_/¯

виглядає так, що можна було зробити PR в їх репозиторій з таким функціоналом, на крайняк форк, і далі використовувати лібу в яку комітять 200+ розробників з усього світу, і де є потужна документація :)

Це для UI-related рядків. Тобто у CMS, нажаль, не перенести

Гарний підхід щодо поширеного завдання, дякую. Цікаво чи автоматизуєте тестування перекладу чи завжди це мусить бути людина?

Ми робимо це у 2 кроки:
1. автоматична генерація текстів і перекладів, щоб інженер одразу бачив «живий» інтерфейс і дані. Тут ми повністю довіряємо 3rd party (deepl);
2. професійна людина вичитує і править текст за необхідності у візуальному редакторі. Після цього статус рядка змінюється на `translated` і зміна текстів може буди зроблена тільки людиною (щоб не було регресій).

Підписатись на коментарі