Автоматизация тестов локализации

Автоматизация тестов локализации в JavaScript-проектах с использованием i18next направлена на выявление ошибок перевода, несоответствий ключей, пропущенных строк и регрессионных проблем, возникающих при изменении словарей или логики переключения языков. При росте числа поддерживаемых языков ручная проверка становится неприемлемой по стоимости и ненадёжности, поэтому тесты становятся частью конвейера сборки.

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

Базовая структура тестируемой локализационной системы

В типичном проекте на i18next присутствуют следующие элементы:

  • конфигурация i18next (инициализация, плагины, backend)
  • ресурсы переводов (JSON-файлы по языкам и неймспейсам)
  • функции доступа к переводу (t, i18n.t)
  • UI-слой, зависящий от текущего языка
  • механизм переключения языка

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

Тестирование инициализации i18next

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

Пример теста на Jest:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

beforeAll(async () => {
  await i18n
    .use(initReactI18next)
    .init({
      lng: 'en',
      fallbackLng: 'en',
      resources: {
        en: {
          translation: {
            hello: 'Hello'
          }
        }
      }
    });
});

test('i18next initialized correctly', () => {
  expect(i18n.isInitialized).toBe(true);
});

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

Проверка наличия ключей перевода

Одна из наиболее частых проблем — отсутствие ключей в одном из языков. Автоматизация решает эту задачу через сравнение ресурсов.

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

test('translation keys match between locales', () => {
  const enKeys = Object.keys(en);
  const ruKeys = Object.keys(ru);

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

Этот подход обеспечивает базовую синхронизацию словарей, предотвращая ситуацию, когда интерфейс частично отображается на fallback-языке.

Для вложенных структур используется рекурсивное извлечение ключей:

function extractKeys(obj, prefix = '') {
  return Object.keys(obj).reduce((acc, key) => {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    if (typeof obj[key] === 'object') {
      return acc.concat(extractKeys(obj[key], fullKey));
    }
    return acc.concat(fullKey);
  }, []);
}

Тестирование функции t()

Функция перевода является ядром i18next. Её тестирование позволяет гарантировать корректность интерполяции и fallback-логики.

test('translation function returns correct value', () => {
  i18n.changeLanguage('en');
  expect(i18n.t('hello')).toBe('Hello');
});

Особое внимание уделяется параметрам интерполяции:

test('interpolation works correctly', () => {
  i18n.init({
    lng: 'en',
    resources: {
      en: {
        translation: {
          welcome: 'Hello {{name}}'
        }
      }
    }
  });

  expect(i18n.t('welcome', { name: 'Alex' })).toBe('Hello Alex');
});

Ошибки интерполяции часто возникают при изменении шаблонов переводов, поэтому такие тесты предотвращают скрытые регрессии.

Проверка fallback-языков

Fallback-механизм критически важен для стабильности интерфейса. Тестирование заключается в эмуляции отсутствующего ключа.

test('fallback language is used when key is missing', () => {
  i18n.init({
    lng: 'ru',
    fallbackLng: 'en',
    resources: {
      en: {
        translation: {
          hello: 'Hello'
        }
      },
      ru: {
        translation: {}
      }
    }
  });

  expect(i18n.t('hello')).toBe('Hello');
});

Такой тест фиксирует корректность поведения цепочки поиска переводов.

Тестирование React-компонентов с react-i18next

При использовании React интеграция с react-i18next требует проверки рендеринга компонентов в разных языковых состояниях.

import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';

test('component renders translated text', () => {
  const { getByText } = render(
    <I18nextProvider i18n={i18n}>
      <MyComponent />
    </I18nextProvider>
  );

  expect(getByText('Hello')).toBeInTheDocument();
});

Для изоляции тестов часто используется мок i18next:

jest.mock('react-i18next', () => ({
  useTranslation: () => ({
    t: (key) => key
  })
}));

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

Snapshot-тестирование локализованных интерфейсов

Snapshot-тесты позволяют фиксировать визуальное состояние компонентов при разных языках.

test('matches snapshot in English', () => {
  i18n.changeLanguage('en');

  const tree = render(<MyComponent />);
  expect(tree).toMatchSnapshot();
});
test('matches snapshot in Russian', () => {
  i18n.changeLanguage('ru');

  const tree = render(<MyComponent />);
  expect(tree).toMatchSnapshot();
});

Сравнение снапшотов выявляет не только ошибки перевода, но и проблемы верстки, возникающие из-за разной длины строк.

Тестирование переключения языков

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

test('language switching updates translations', async () => {
  await i18n.changeLanguage('ru');
  expect(i18n.language).toBe('ru');

  expect(i18n.t('hello')).toBeDefined();
});

В UI-тестах проверяется реактивность интерфейса после смены языка.

End-to-End тестирование локализации

E2E-тесты с использованием Cypress или Playwright позволяют проверить поведение системы в реальном браузере.

describe('Localization flow', () => {
  it('changes language on UI interaction', () => {
    cy.visit('/');

    cy.get('[data-testid="lang-switch"]').click();
    cy.contains('Привет');
  });
});

E2E-уровень особенно важен для проверки динамически загружаемых переводов и асинхронных backend-плагинов i18next.

Тестирование загрузчиков ресурсов

При использовании i18next-http-backend или кастомных загрузчиков важно тестировать корректность получения переводов.

import Backend from 'i18next-http-backend';

test('loads translations from backend', async () => {
  await i18n.use(Backend).init({
    lng: 'en',
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json'
    }
  });

  const value = i18n.t('hello');
  expect(value).toBeDefined();
});

Мокирование HTTP-запросов через msw позволяет избежать нестабильности тестов.

Регрессионный контроль переводов

Регрессии в локализации часто возникают при удалении или переименовании ключей. Для их контроля применяется статический анализ ресурсов.

function findMissingKeys(base, target) {
  return base.filter(key => !target.includes(key));
}

test('no missing translation keys', () => {
  const missing = findMissingKeys(
    Object.keys(en),
    Object.keys(ru)
  );

  expect(missing).toEqual([]);
});

Дополнительно используется линтинг JSON-файлов переводов, проверяющий структуру и запрещающий пустые строки.

Интеграция тестов в CI/CD

Автоматизация тестирования локализации приобретает практический смысл только при включении в конвейер сборки. Типовой пайплайн включает:

  • проверку целостности ключей
  • unit-тесты i18next
  • snapshot-тестирование UI
  • E2E-тесты критических сценариев

Каждый этап выполняется при изменении переводов или логики интернационализации. Это позволяет выявлять ошибки до попадания в продакшен.

Статический анализ и дополнительные проверки

Помимо runtime-тестов используются статические инструменты:

  • ESLint-плагины для проверки ключей t()
  • скрипты валидации JSON-структуры переводов
  • анализ неиспользуемых ключей

Пример простого анализа неиспользуемых ключей:

function findUnusedKeys(translations, usedKeys) {
  return Object.keys(translations).filter(
    key => !usedKeys.includes(key)
  );
}

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