Тестирование кода, использующего i18next, почти всегда упирается в необходимость изолировать слой переводов от бизнес-логики. В реальных приложениях i18n добавляет асинхронную инициализацию, загрузку ресурсов, работу с интерполяцией и множественными языками, что усложняет предсказуемость тестов. Моки позволяют зафиксировать поведение переводов и убрать внешние зависимости, сохраняя проверку логики компонентов и функций.
Основная проблема тестирования с реальным i18next — нестабильность окружения:
Изоляция переводческого слоя позволяет тестировать только поведение приложения, а не корректность i18n-конфигурации.
Самый распространённый подход — подмена функции перевода
t.
Пример для Jest:
jest.mock('i18next', () => ({
t: (key) => key
}));
Такой вариант полезен, когда важно лишь проверить, что ключи используются корректно.
Более расширенный вариант:
const translations = {
hello: 'Привет',
goodbye: 'Пока'
};
jest.mock('i18next', () => ({
t: (key) => translations[key] || key
}));
Преимущество подхода — отсутствие инициализации i18n и стабильный результат.
В приложениях на React чаще используется react-i18next. В тестах компоненты не должны зависеть от реального контекста провайдера.
Базовый мок useTranslation:
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key) => key,
i18n: {
changeLanguage: jest.fn()
}
}),
initReactI18next: {
type: '3rdParty',
init: jest.fn()
}
}));
Такой подход полностью убирает необходимость подключения i18n provider в тестах.
Когда важно проверять конкретные строки, а не ключи, используется словарь:
const resources = {
ru: {
translation: {
submit: 'Отправить',
cancel: 'Отмена'
}
},
en: {
translation: {
submit: 'Submit',
cancel: 'Cancel'
}
}
};
let currentLang = 'ru';
jest.mock('i18next', () => ({
changeLanguage: (lng) => {
currentLang = lng;
return Promise.resolve();
},
language: () => currentLang,
t: (key) => resources[currentLang]?.translation?.[key] || key
}));
Такой мок позволяет тестировать переключение языков без реального i18n-движка.
Более гибкий подход — создание фабрики мока:
export const createI18nMock = (lang = 'ru') => {
const state = { lang };
return {
language: state.lang,
changeLanguage: (lng) => {
state.lang = lng;
return Promise.resolve();
},
t: (key) => key
};
};
Использование в Vitest:
vi.mock('i18next', () => ({
default: createI18nMock()
}));
Такой подход удобен при масштабных тестовых наборах, где требуется контролируемое состояние.
i18next активно использует интерполяцию:
t('welcome', { name: 'Alex' })
Простой мок часто это игнорирует, но в тестах UI может быть важно проверять итоговую строку.
Расширенный мок:
const t = (key, options = {}) => {
if (key === 'welcome') {
return `Привет, ${options.name}`;
}
return key;
};
Для более универсального решения можно добавить шаблонизацию:
const t = (key, options = {}) => {
let result = key;
Object.entries(options).forEach(([k, v]) => {
result = result.replace(`{{${k}}}`, v);
});
return result;
};
i18next поддерживает сложную систему plural rules. В тестах часто достаточно упрощённой модели:
const t = (key, options = {}) => {
if (key === 'items') {
return options.count === 1 ? '1 элемент' : `${options.count} элементов`;
}
return key;
};
Такой мок позволяет проверять поведение UI без реализации CLDR-правил.
В реальных проектах переводы разделены по namespace:
t('auth:login.button')
Упрощённый мок может игнорировать namespace:
const t = (key) => key.split(':').pop();
Более контролируемый вариант:
const dictionaries = {
auth: {
login: {
button: 'Войти'
}
}
};
const t = (key) => {
const [ns, path] = key.split(':');
return dictionaries[ns]?.login?.[path] || key;
};
Некоторые тесты падают из-за init():
i18next.init()
Упрощённый мок:
jest.mock('i18next', () => ({
init: () => Promise.resolve(),
t: (key) => key
}));
Это убирает необходимость ожидания готовности i18n в тестах.
В тестах компонентов важно избегать реального провайдера:
render(<App />)
Если используется реальный i18n, тест становится нестабильным. Поэтому мок должен подменять:
ti18n.languagechangeLanguageuseTranslationТиповой минимальный набор:
useTranslation: () => ({
t: (key) => key,
i18n: {
language: 'ru',
changeLanguage: jest.fn()
}
})
1. Потеря реактивности языка
Если language не реактивен, тесты не проверяют реальное
переключение UI.
2. Слишком примитивный t()
Возврат только ключа скрывает ошибки в интерполяции и форматировании строк.
3. Игнорирование namespace
Код может работать в моках, но ломаться в реальной конфигурации.
4. Несовместимость с async init
Тесты проходят, но приложение падает в рантайме.
t(key) → быстрые unit-тесты логикиБаланс между уровнем абстракции и реалистичностью напрямую влияет на стабильность тестового набора и стоимость поддержки.