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

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

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


Конфигурация i18next в тестовой среде

В тестах обычно создаётся отдельный экземпляр i18next, изолированный от основной конфигурации приложения. Это предотвращает влияние глобального состояния и упрощает контроль переводов.

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

i18n
  .use(initReactI18next)
  .init({
    lng: 'en',
    fallbackLng: 'en',
    resources: {
      en: {
        translation: {
          title: 'Hello',
          button_save: 'Save',
        },
      },
    },
    interpolation: {
      escapeValue: false,
    },
  });

export default i18n;

В тестовой конфигурации часто отключается загрузка ресурсов из сети и фиксируется язык, чтобы исключить асинхронность и флаки-тесты.


Интеграция с React Testing Library

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

import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from './testI18n';
import Button from './Button';

const renderWithI18n = (ui) =>
  render(
    <I18nextProvider i18n={i18n}>
      {ui}
    </I18nextProvider>
  );

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


Проверка текстов vs проверка ключей

Одним из ключевых решений в стратегии тестирования становится выбор между проверкой переведённого текста и проверкой ключей.

Проверка текста

expect(screen.getByText('Save')).toBeInTheDocument();

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

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

Более стабильный подход — использование data-testid или ролей:

expect(screen.getByTestId('save-button')).toBeInTheDocument();

или:

expect(screen.getByRole('button')).toBeInTheDocument();

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


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

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

resources: {
  en: {
    translation: {
      welcome: 'Hello, {{name}}',
    },
  },
}

Тестирование:

i18n.changeLanguage('en');

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

Особенность заключается в том, что проверяется не UI, а сама функция перевода, что позволяет изолировать логику интерполяции.


Множественное число и правила pluralization

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

resources: {
  en: {
    translation: {
      item: '{{count}} item',
      item_plural: '{{count}} items',
    },
  },
}
expect(i18n.t('item', { count: 1 })).toBe('1 item');
expect(i18n.t('item', { count: 5 })).toBe('5 items');

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


Тестирование смены языка

Компоненты, реагирующие на изменение языка, требуют проверки обновления UI после смены состояния i18next.

i18n.changeLanguage('en');
renderWithI18n(<Header />);

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

i18n.changeLanguage('de');

expect(screen.getByText('Hallo')).toBeInTheDocument();

Важным аспектом является ожидание асинхронного обновления интерфейса:

await waitFor(() => {
  expect(screen.getByText('Hallo')).toBeInTheDocument();
});

Асинхронная загрузка переводов

При использовании backend-плагинов (например, HTTP backend) переводы загружаются асинхронно, что делает тестирование более сложным.

Типичный подход — мокирование backend:

i18n.init({
  lng: 'en',
  backend: {
    loadPath: '/locales/{{lng}}/{{ns}}.json',
  },
  initImmediate: false,
});

Отключение initImmediate переводит инициализацию в синхронный режим, устраняя гонки.


Моки i18next

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

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

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

Недостатком становится потеря проверки корректности ключей и ресурсов.


Snapshot-тестирование и i18n

Snapshot-тесты в интернационализированных приложениях часто становятся нестабильными. Любое изменение текста приводит к изменению снапшота, даже если поведение компонента не изменилось.

const { container } = renderWithI18n(<Card />);
expect(container).toMatchSnapshot();

Практика показывает, что использование snapshot-тестов оправдано только при фиксированном языке и стабильных переводах, либо при мокировании t.


Тестирование fallbackLng

Механизм fallbackLng используется при отсутствии перевода. Его корректность также требует проверки.

i18n.init({
  lng: 'ru',
  fallbackLng: 'en',
  resources: {
    en: {
      translation: {
        submit: 'Submit',
      },
    },
  },
});
expect(i18n.t('submit')).toBe('Submit');

Такой тест фиксирует поведение системы при отсутствии локализации.


Псевдолокали и детектирование проблем

Для выявления жёстко закодированных строк используется псевдолокаль, например:

lng: 'pseudo',

или кастомная конфигурация:

interpolation: {
  prefix: '[[',
  suffix: ']]',
}

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


Изоляция пространства имён (namespaces)

i18next активно использует namespaces, что влияет на структуру тестов.

i18n.init({
  ns: ['common', 'auth'],
  defaultNS: 'common',
  resources: {
    common: {
      translation: {
        ok: 'OK',
      },
    },
    auth: {
      translation: {
        login: 'Login',
      },
    },
  },
});

Тестирование компонентов, зависящих от разных namespaces, требует явного контроля активного пространства имён или его фиксации в тестовой среде.


Проверка вызовов t-функции

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

const tMock = jest.fn((key) => key);

jest.mock('react-i18next', () => ({
  useTranslation: () => ({
    t: tMock,
  }),
}));
expect(tMock).toHaveBeenCalledWith('button_save');

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


Комбинированная стратегия тестирования

Практика показывает, что наиболее устойчивые тестовые наборы строятся на сочетании нескольких подходов:

  • проверка семантики через роли и тестовые идентификаторы
  • точечная проверка интерполяции через i18n.t
  • контроль множественного числа и fallback-логики
  • изолированное мокирование переводов для UI-тестов
  • ограниченное использование snapshot-тестов при фиксированном языке

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