Интернационализация в JavaScript-приложениях добавляет дополнительный слой сложности в тестирование пользовательских интерфейсов. Основная проблема заключается в том, что текст в интерфейсе перестаёт быть статическим: он зависит от активного языка, пространства имён, параметров интерполяции, правил множественного числа и асинхронной загрузки ресурсов.
При использовании i18next и его интеграций (например, react-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 используется
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>
);
Такая обёртка стандартизирует тестовую среду и позволяет не дублировать конфигурацию в каждом тесте.
Одним из ключевых решений в стратегии тестирования становится выбор между проверкой переведённого текста и проверкой ключей.
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, а сама функция перевода, что позволяет изолировать логику интерполяции.
Система множественного числа в 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 переводит инициализацию в
синхронный режим, устраняя гонки.
В некоторых сценариях используется полное мокирование системы переводов.
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key) => key,
i18n: {
changeLanguage: jest.fn(),
},
}),
}));
Такой подход превращает переводы в идентификаторы, упрощая тестирование логики компонентов и устраняя зависимость от текстов.
Недостатком становится потеря проверки корректности ключей и ресурсов.
Snapshot-тесты в интернационализированных приложениях часто становятся нестабильными. Любое изменение текста приводит к изменению снапшота, даже если поведение компонента не изменилось.
const { container } = renderWithI18n(<Card />);
expect(container).toMatchSnapshot();
Практика показывает, что использование snapshot-тестов оправдано
только при фиксированном языке и стабильных переводах, либо при
мокировании t.
Механизм fallbackLng используется при отсутствии перевода. Его корректность также требует проверки.
i18n.init({
lng: 'ru',
fallbackLng: 'en',
resources: {
en: {
translation: {
submit: 'Submit',
},
},
},
});
expect(i18n.t('submit')).toBe('Submit');
Такой тест фиксирует поведение системы при отсутствии локализации.
Для выявления жёстко закодированных строк используется псевдолокаль, например:
lng: 'pseudo',
или кастомная конфигурация:
interpolation: {
prefix: '[[',
suffix: ']]',
}
Это позволяет визуально обнаруживать строки, не проходящие через систему перевода.
i18next активно использует namespaces, что влияет на структуру тестов.
i18n.init({
ns: ['common', 'auth'],
defaultNS: 'common',
resources: {
common: {
translation: {
ok: 'OK',
},
},
auth: {
translation: {
login: 'Login',
},
},
},
});
Тестирование компонентов, зависящих от разных namespaces, требует явного контроля активного пространства имён или его фиксации в тестовой среде.
В некоторых случаях проверяется не результат, а факт использования переводов.
const tMock = jest.fn((key) => key);
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: tMock,
}),
}));
expect(tMock).toHaveBeenCalledWith('button_save');
Такой подход фиксирует контракт между компонентом и системой локализации, но не затрагивает визуальный слой.
Практика показывает, что наиболее устойчивые тестовые наборы строятся на сочетании нескольких подходов:
Такой подход снижает зависимость тестов от текстового содержимого и сохраняет контроль над логикой интернационализации.