Проверка покрытия локалей

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

В экосистеме FormatJS проверка покрытия локалей позволяет:

  • обнаруживать отсутствующие переводы;
  • находить устаревшие ключи;
  • выявлять несовпадения ICU-шаблонов;
  • предотвращать падения интерфейса;
  • автоматизировать контроль качества локализации в CI/CD.

Без системной проверки локалей со временем возникают типичные проблемы:

  • часть интерфейса отображается на базовом языке;
  • параметры ICU отличаются между переводами;
  • удалённые сообщения остаются в словарях;
  • переводчики случайно ломают форматирование;
  • новые строки не попадают в перевод.

Структура локализационных файлов

Наиболее распространённая структура в проектах с FormatJS:

src/
 ├── locales/
 │    ├── en.json
 │    ├── ru.json
 │    ├── de.json
 │    └── fr.json
 │
 ├── components/
 └── pages/

Пример базовой локали:

{
  "app.title": "Dashboard",
  "menu.profile": "Profile",
  "menu.logout": "Logout"
}

Русская локаль:

{
  "app.title": "Панель управления",
  "menu.profile": "Профиль",
  "menu.logout": "Выход"
}

Обычно одна локаль считается эталонной. Чаще всего это:

  • en;
  • en-US;
  • язык основной команды разработки.

Именно относительно неё выполняется анализ покрытия.


Проверка отсутствующих переводов

Самая базовая задача — обнаружение отсутствующих ключей.

Простейшая проверка:

import en fr om './locales/en.json';
import ru fr om './locales/ru.json';

const missingKeys = Object.keys(en).filter(
  key => !(key in ru)
);

console.log(missingKeys);

Результат:

[
  "menu.logout"
]

Подобная проверка помогает автоматически обнаруживать строки, которые забыли перевести.


Проверка лишних ключей

Не менее важно находить сообщения, которых больше нет в исходной локали.

Пример:

const obsoleteKeys = Object.keys(ru).filter(
  key => !(key in en)
);

console.log(obsoleteKeys);

Вывод:

[
  "old.settings.label"
]

Лишние ключи создают несколько проблем:

  • увеличивают размер бандла;
  • затрудняют поддержку;
  • путают переводчиков;
  • приводят к использованию устаревших текстов.

Полная проверка покрытия

На практике обе проверки объединяются.

Пример утилиты:

function validateLocale(base, target) {
  const missing = [];
  const obsolete = [];

  for (const key of Object.keys(base)) {
    if (!(key in target)) {
      missing.push(key);
    }
  }

  for (const key of Object.keys(target)) {
    if (!(key in base)) {
      obsolete.push(key);
    }
  }

  return {
    missing,
    obsolete
  };
}

Использование:

import en from './locales/en.json';
import ru from './locales/ru.json';

const result = validateLocale(en, ru);

console.log(result);

Результат:

{
  "missing": ["menu.logout"],
  "obsolete": ["old.settings.label"]
}

Проверка нескольких локалей

В крупных приложениях количество языков может измеряться десятками.

Автоматическая проверка всех файлов:

import fs from 'fs';
import path from 'path';

const localesDir = './locales';

const baseLocale = JSON.parse(
  fs.readFileSync(
    path.join(localesDir, 'en.json'),
    'utf-8'
  )
);

const localeFiles = fs.readdirSync(localesDir);

for (const file of localeFiles) {
  if (file === 'en.json') {
    continue;
  }

  const locale = JSON.parse(
    fs.readFileSync(
      path.join(localesDir, file),
      'utf-8'
    )
  );

  const missing = Object.keys(baseLocale)
    .filter(key => !(key in locale));

  if (missing.length > 0) {
    console.log(`Missing in ${file}:`);

    for (const key of missing) {
      console.log(` - ${key}`);
    }
  }
}

Проверка ICU-параметров

Одна из важнейших задач — синхронизация ICU-шаблонов.

Базовая строка:

{
  "cart.items": "{count} items"
}

Ошибка перевода:

{
  "cart.items": "{items} товаров"
}

В результате приложение попытается передать параметр count, которого нет в переводе.

Это может привести:

  • к неправильному отображению;
  • к ошибкам форматирования;
  • к исключениям в runtime.

Извлечение ICU-переменных

Проверку параметров можно реализовать через регулярные выражения.

Пример:

function extractVariables(message) {
  const matches = message.match(/{(.*?)}/g) || [];

  return matches.map(item =>
    item.replace(/[{}]/g, '')
  );
}

Использование:

extractVariables('{count} items');

Результат:

['count']

Сравнение ICU-переменных

Полная проверка:

function validateVariables(base, target) {
  const issues = [];

  for (const key of Object.keys(base)) {
    if (!(key in target)) {
      continue;
    }

    const baseVars = extractVariables(base[key]);
    const targetVars = extractVariables(target[key]);

    const same =
      JSON.stringify(baseVars.sort()) ===
      JSON.stringify(targetVars.sort());

    if (!same) {
      issues.push({
        key,
        base: baseVars,
        target: targetVars
      });
    }
  }

  return issues;
}

Проверка plural-форм

ICU plural-выражения особенно чувствительны к ошибкам.

Исходная строка:

{
  "notifications":
    "{count, plural, one {# notification} other {# notifications}}"
}

Некорректный перевод:

{
  "notifications":
    "{count, plural, one {# уведомление}}"
}

Отсутствие категории other нарушает работу plural-механизма.


Использование парсера ICU

Для надёжной проверки рекомендуется использовать официальный парсер FormatJS.

Установка:

npm install @formatjs/icu-messageformat-parser

Проверка:

import { parse } from '@formatjs/icu-messageformat-parser';

function validateMessage(message) {
  try {
    parse(message);

    return true;
  } catch (error) {
    return false;
  }
}

Использование:

validateMessage(
  '{count, plural, one {# item}}'
);

Проверка синтаксических ошибок

Типичные ошибки ICU:

Незакрытые скобки

{
  "title": "{count items"
}

Неверный plural

{
  "title":
    "{count, plural one {Item}}"
}

Ошибочный select

{
  "gender":
    "{gender, select, male {He}}"
}

Автоматическая валидация предотвращает подобные ошибки ещё до сборки приложения.


Проверка пустых переводов

Иногда перевод существует формально, но содержит пустую строку.

Пример:

{
  "menu.logout": ""
}

Проверка:

function findEmptyMessages(locale) {
  return Object.entries(locale)
    .filter(([_, value]) => {
      return value.trim() === '';
    })
    .map(([key]) => key);
}

Проверка одинаковых переводов

В некоторых проектах требуется искать сообщения, которые остались непереведёнными.

Пример:

{
  "menu.profile": "Profile"
}

Для русской локали это может означать, что перевод забыли выполнить.

Проверка:

function findUntranslated(base, target) {
  return Object.keys(base).filter(
    key => base[key] === target[key]
  );
}

Использование CLI-инструментов FormatJS

Экосистема FormatJS содержит CLI для автоматизации проверки локалей.

Установка:

npm install --save-dev @formatjs/cli

Проверка сообщений через CLI

Пример проверки:

formatjs extract "src/**/*.{js,jsx,ts,tsx}" \
  --out-file lang/en.json

Команда извлекает все сообщения из исходного кода.


Сравнение локалей

После извлечения сообщений можно проверять актуальность переводов.

Пример workflow:

formatjs extract \
  "src/**/*.{ts,tsx}" \
  --out-file temp/messages.json

Затем:

node scripts/check-locales.js

Проверка в CI/CD

Контроль покрытия локалей особенно важен в непрерывной интеграции.

Пример GitHub Actions:

name: Locale Validation

on:
  pull_request:

jobs:
  locales:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm install

      - run: npm run validate-locales

Завершение сборки при ошибках

Хорошая практика — аварийно завершать pipeline при проблемах локализации.

Пример:

if (
  missing.length > 0 ||
  invalidMessages.length > 0
) {
  process.exit(1);
}

Это предотвращает попадание дефектных переводов в production.


Проверка coverage в процентах

Иногда требуется вычислять процент заполнения локали.

Пример:

function calculateCoverage(base, locale) {
  const total = Object.keys(base).length;

  const translated = Object.keys(base)
    .filter(key => {
      return key in locale &&
        locale[key].trim() !== '';
    })
    .length;

  return (translated / total) * 100;
}

Использование:

const coverage = calculateCoverage(en, ru);

console.log(`${coverage.toFixed(2)}%`);

Формирование отчётов

Часто результаты проверки экспортируются в отдельные отчёты.

Пример JSON-отчёта:

{
  "locale": "ru",
  "coverage": 94.5,
  "missing": [
    "settings.title"
  ],
  "obsolete": [
    "legacy.menu"
  ]
}

HTML-отчёты

Для больших команд удобно генерировать визуальные отчёты.

Пример структуры:

<table>
  <tr>
    <th>Locale</th>
    <th>Coverage</th>
  </tr>

  <tr>
    <td>ru</td>
    <td>98%</td>
  </tr>
</table>

Проверка fallback-локалей

Некоторые приложения используют fallback-механизм:

<IntlProvider
  locale="ru"
  defaultLocale="en"
  messages={messages}
/>

Важно понимать, что fallback скрывает проблемы перевода:

  • отсутствующие строки могут долго оставаться незамеченными;
  • интерфейс становится частично смешанным;
  • QA сложнее выявлять ошибки локализации.

Поэтому fallback не заменяет полноценную проверку покрытия.


Проверка namespace-структуры

Крупные приложения часто группируют сообщения по namespace.

Пример:

{
  "auth.login.title": "Login",
  "auth.login.button": "Sign in",
  "dashboard.header.title": "Dashboard"
}

Проверка namespace помогает:

  • контролировать структуру;
  • избегать конфликтов ключей;
  • обнаруживать случайные переименования.

Проверка дубликатов

Иногда разные ключи содержат одинаковые тексты.

Пример:

{
  "button.save": "Save",
  "form.submit": "Save"
}

Это не всегда ошибка, но может указывать:

  • на дублирование логики;
  • на неправильную организацию сообщений;
  • на возможность переиспользования ключей.

Проверка:

function findDuplicates(locale) {
  const map = new Map();

  for (const [key, value] of Object.entries(locale)) {
    if (!map.has(value)) {
      map.set(value, []);
    }

    map.get(value).push(key);
  }

  return [...map.entries()]
    .filter(([_, keys]) => keys.length > 1);
}

Проверка длины переводов

В некоторых интерфейсах критична длина текста.

Например:

  • мобильные приложения;
  • кнопки;
  • таблицы;
  • навигационные панели.

Проверка:

function findLongTranslations(locale, lim it = 80) {
  return Object.entries(locale)
    .filter(([_, value]) => {
      return value.length > lim it;
    });
}

Проверка RTL-локалей

Для арабского и иврита дополнительно проверяются:

  • корректность символов;
  • отсутствие смешения направлений;
  • поддержка bidi;
  • корректность ICU-шаблонов.

Пример RTL-локалей:

  • ar;
  • he;
  • fa.

Интеграция с переводческими платформами

Проверка покрытия часто объединяется с:

  • Crowdin;
  • Lokalise;
  • Phrase;
  • Transifex.

Типичный pipeline:

  1. Извлечение сообщений.
  2. Отправка переводчикам.
  3. Получение локалей.
  4. Автоматическая проверка.
  5. Публикация сборки.

Автоматическая генерация missing-файлов

Иногда удобно автоматически создавать шаблоны недостающих переводов.

Пример:

function generateMissing(base, locale) {
  const result = {};

  for (const key of Object.keys(base)) {
    if (!(key in locale)) {
      result[key] = base[key];
    }
  }

  return result;
}

Проверка во время разработки

В development-режиме полезно отображать отсутствующие переводы визуально.

Пример:

function missingTranslation(id) {
  return `@@${id}@@`;
}

Результат в интерфейсе:

@@menu.logout@@

Такой подход помогает быстро замечать проблемы локализации.


Runtime-проверки

Некоторые проекты выполняют дополнительную валидацию прямо в браузере.

Например:

if (!messages[id]) {
  console.warn(
    `Missing translation: ${id}`
  );
}

Однако runtime-проверки считаются вспомогательными. Основной контроль должен происходить до сборки.


Проверка типизации локалей в TypeScript

При использовании TypeScript можно типизировать ключи переводов.

Пример:

type MessageKey =
  keyof typeof messages;

Использование:

function t(id: MessageKey) {
  return intl.formatMessage({ id });
}

Это позволяет обнаруживать ошибки ключей ещё на этапе компиляции.


Генерация union-типов

Автоматическая генерация:

export type TranslationKey =
  | 'menu.profile'
  | 'menu.logout'
  | 'settings.title';

Подобный подход существенно уменьшает вероятность ошибок.


Масштабирование проверки локалей

В больших системах проверка локалей обычно включает:

  • CLI-проверки;
  • AST-анализ ICU;
  • coverage-метрики;
  • CI/CD-валидацию;
  • типизацию ключей;
  • linting;
  • автоматическую генерацию отчётов.

Полноценная система контроля локализации становится отдельной частью инфраструктуры приложения и напрямую влияет на стабильность интернационализированного интерфейса.