Стратегии тестирования переводов

Архитектура тестирования переводов в i18next строится вокруг проверки не только наличия строк, но и корректности всей цепочки интернационализации: загрузки ресурсов, работы интерполяции, правил плюрализации, обработки контекста и поведения при отсутствии ключей. Ошибки в i18n часто не проявляются как падение приложения — вместо этого возникают «тихие» дефекты интерфейса, что делает тестирование критически важным элементом качества.

Базовый уровень тестирования заключается в валидации переводческих файлов. Основная цель — обнаружить:

  • отсутствующие ключи в конкретной локали
  • лишние ключи, не используемые в коде
  • несоответствие структуры между языками
  • некорректные типы значений (строки вместо объектов и наоборот)

Типовой подход — построение эталонной локали (обычно en) и сравнение всех остальных языков с её структурой.

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

function getKeys(obj, prefix = '') {
  return Object.keys(obj).flatMap(key => {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    return typeof obj[key] === 'object'
      ? getKeys(obj[key], fullKey)
      : fullKey;
  });
}

test('ru locale should match en structure', () => {
  const enKeys = getKeys(en).sort();
  const ruKeys = getKeys(ru).sort();

  expect(ruKeys).toEqual(enKeys);
});

Такой подход не проверяет смысл переводов, но гарантирует структурную согласованность, что критично для i18next.

Тестирование инициализации i18n-движка

Инициализация i18next часто включает загрузчики ресурсов, fallback-локали и плагины. Ошибки здесь приводят к каскадным сбоям в приложении.

Тестирование конфигурации должно покрывать:

  • корректность fallback языка
  • наличие namespaces
  • стратегию загрузки ресурсов
  • режим debug
  • поведение при отсутствии ключей
import i18n from './i18n';

test('i18n initializes with fallback language', async () => {
  await i18n.init();

  expect(i18n.language).toBeDefined();
  expect(i18n.options.fallbackLng).toEqual(['en']);
});

Важный аспект — проверка асинхронной инициализации. При использовании backend-плагинов i18next может загружать переводы динамически, что требует ожидания завершения загрузки перед проверками.

Проверка ключей перевода и безопасное поведение при отсутствии строк

Одна из ключевых стратегий тестирования — контроль поведения при отсутствии перевода. i18next может:

  • возвращать ключ
  • использовать fallback язык
  • возвращать пустую строку (при кастомной конфигурации)

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

test('missing key returns fallback or key itself', () => {
  const result = i18n.t('non.existing.key');

  expect(
    result === 'non.existing.key' || typeof result === 'string'
  ).toBe(true);
});

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

Тестирование интерполяции

Интерполяция — один из наиболее частых источников ошибок. i18next поддерживает переменные вида {{value}}, которые должны корректно подставляться в разных контекстах.

Ключевые сценарии:

  • подстановка строк
  • числовые значения
  • null/undefined значения
  • HTML-экранирование
test('interpolation works correctly', () => {
  i18n.addResourceBundle('en', 'translation', {
    welcome: 'Hello {{name}}'
  });

  const result = i18n.t('welcome', { name: 'Alex' });

  expect(result).toBe('Hello Alex');
});

Отдельное внимание уделяется безопасности: проверяется, что опасные символы экранируются при включённой опции escapeValue.

Плюрализация и числовые формы

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

Тестируются:

  • singular / plural формы
  • языковые правила (ru, ar, ja и др.)
  • диапазоны (например, 0, 1, 2-4, 5+)
test('pluralization in russian', () => {
  i18n.addResourceBundle('ru', 'translation', {
    apple_one: '{{count}} яблоко',
    apple_few: '{{count}} яблока',
    apple_many: '{{count}} яблок'
  });

  expect(i18n.t('apple', { count: 1 })).toBe('1 яблоко');
  expect(i18n.t('apple', { count: 5 })).toBe('5 яблок');
});

Сложность возрастает при использовании ICU-подобных форматов или кастомных pluralResolvers.

Тестирование контекста (contextual translations)

Контекст позволяет изменять перевод в зависимости от состояния объекта или действия.

Примеры:

  • gender: male/female
  • status: active/inactive
  • role-based UI
i18n.addResourceBundle('en', 'translation', {
  friend_male: 'He is my friend',
  friend_female: 'She is my friend'
});

test('context-based translation', () => {
  expect(i18n.t('friend', { context: 'male' }))
    .toBe('He is my friend');
});

Контекст часто ломается при рефакторинге ключей, поэтому тесты здесь выполняют роль защитного слоя.

Мокирование i18next в unit-тестах компонентов

При тестировании UI-компонентов (React, Vue, Svelte) важно изолировать логику перевода от рендеринга.

Стратегии:

  • мок функции t
  • подмена i18n provider
  • использование тестовой локали

Пример для React Testing Library:

jest.mock('./i18n', () => ({
  t: key => key
}));

Это позволяет проверять структуру интерфейса без зависимости от переводов, ускоряя тесты и уменьшая их хрупкость.

Snapshot-тестирование переводов интерфейса

Snapshot-тесты применяются для фиксации визуального состояния переведённых компонентов. Однако их использование требует осторожности: изменения текста могут приводить к ложным падениям тестов.

Хорошая практика:

  • применять snapshot только для стабильных экранов
  • избегать динамических значений
  • использовать стабилизированные локали
test('renders translated header', () => {
  const { container } = render(<Header />);
  expect(container).toMatchSnapshot();
});

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

В i18next часто используется разделение на namespaces: auth, common, dashboard, errors.

Тестирование включает:

  • наличие всех namespaces
  • отсутствие дублирования ключей
  • корректную загрузку при lazy-loading
test('namespaces are loaded', async () => {
  await i18n.loadNamespaces(['common', 'auth']);

  expect(i18n.hasResourceBundle('en', 'common')).toBe(true);
  expect(i18n.hasResourceBundle('en', 'auth')).toBe(true);
});

Тестирование lazy-loading переводов

При динамической загрузке переводов важно проверять:

  • корректность HTTP-запросов
  • fallback при ошибке загрузки
  • кэширование ресурсов

Используется мок backend-а:

i18n.services.backendConnector.load = jest.fn((lng, ns, cb) => {
  cb(null, { hello: 'world' });
});

Псевдолокализация как метод тестирования

Псевдолокализация позволяет выявить UI-проблемы:

  • обрезание текста
  • отсутствие поддержки Unicode
  • фиксированная ширина контейнеров

Пример трансформации:

  • Hello → Hëľľõ
  • Settings → Šëţţîñğš

В тестах проверяется, что UI корректно отображает расширенные строки.

Контрактное тестирование переводов

Контрактный подход фиксирует правила:

  • каждый ключ должен иметь перевод во всех языках
  • interpolation параметры должны совпадать
  • структура JSON должна быть стабильной

Такие тесты часто интегрируются в CI как отдельный шаг, предотвращающий деградацию переводов.

Интеграция с E2E тестированием

End-to-end тесты проверяют реальное поведение приложения с включёнными локалями:

  • переключение языка
  • сохранение выбора локали
  • корректный рендер динамических страниц
cy.visit('/settings');
cy.contains('Language').click();
cy.contains('Deutsch').click();
cy.contains('Einstellungen');

Такие проверки подтверждают, что i18next корректно интегрирован в полный цикл приложения.

Проверка производительности загрузки переводов

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

  • время загрузки namespaces
  • размер JSON-файлов
  • количество запросов

Проверка обычно выполняется на уровне CI или профилировщика, фиксируя регрессии в локализации.