React i18next Testing: Unit Tests, Mocking, and RTL Integration
Testing internationalization in React applications requires a different mindset than standard component testing. With react-i18next, you're not just testing rendered text — you're testing that the correct keys are resolved, interpolation works, pluralization rules apply, and language switching triggers the right re-renders. This guide covers practical strategies for all of these.
Setting Up the i18n Mock
The most common mistake when testing i18next is initializing the full i18n instance in every test. This is slow and introduces flakiness from async resource loading. Instead, mock it.
Create a test utility file:
// src/test-utils/i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
ns: ['common', 'errors'],
defaultNS: 'common',
resources: {
en: {
common: {
greeting: 'Hello, {{name}}!',
itemCount: '{{count}} item',
itemCount_other: '{{count}} items',
logout: 'Log out',
},
errors: {
notFound: 'Page not found',
serverError: 'Something went wrong',
},
},
fr: {
common: {
greeting: 'Bonjour, {{name}} !',
itemCount: '{{count}} article',
itemCount_other: '{{count}} articles',
logout: 'Se déconnecter',
},
},
},
interpolation: { escapeValue: false },
});
export default i18n;Then wrap your RTL renderer:
// src/test-utils/render.js
import React from 'react';
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n';
export function renderWithi18n(ui, options = {}) {
const { lng = 'en', ...renderOptions } = options;
i18n.changeLanguage(lng);
return render(
<I18nextProvider i18n={i18n}>{ui}</I18nextProvider>,
renderOptions
);
}Testing Basic Translation
import { screen } from '@testing-library/react';
import { renderWithi18n } from '../test-utils/render';
import WelcomeBanner from './WelcomeBanner';
test('renders greeting with interpolated name', () => {
renderWithi18n(<WelcomeBanner name="Alice" />);
expect(screen.getByText('Hello, Alice!')).toBeInTheDocument();
});
test('renders French greeting when locale is fr', () => {
renderWithi18n(<WelcomeBanner name="Alice" />, { lng: 'fr' });
expect(screen.getByText('Bonjour, Alice !')).toBeInTheDocument();
});The component under test:
import { useTranslation } from 'react-i18next';
export default function WelcomeBanner({ name }) {
const { t } = useTranslation('common');
return <h1>{t('greeting', { name })}</h1>;
}Testing Pluralization
Pluralization is one of the most bug-prone areas of i18n. Test each plural form explicitly:
import CartSummary from './CartSummary';
test('shows singular item count', () => {
renderWithi18n(<CartSummary count={1} />);
expect(screen.getByText('1 item')).toBeInTheDocument();
});
test('shows plural item count', () => {
renderWithi18n(<CartSummary count={5} />);
expect(screen.getByText('5 items')).toBeInTheDocument();
});
test('shows zero as plural', () => {
renderWithi18n(<CartSummary count={0} />);
expect(screen.getByText('0 items')).toBeInTheDocument();
});If you support languages like Russian or Arabic with complex plural forms, add them explicitly to your test resources and write a case for each form (one, few, many, other).
Testing Language Switching
Language switching involves async state updates. Use act and waitFor:
import { act, screen, fireEvent } from '@testing-library/react';
import LanguageSwitcher from './LanguageSwitcher';
test('switches from English to French on button click', async () => {
renderWithi18n(<LanguageSwitcher />, { lng: 'en' });
expect(screen.getByText('Log out')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /français/i }));
await screen.findByText('Se déconnecter');
});If language switching is triggered outside the component (e.g., from a Redux action or context), test the translation output rather than the i18n call itself. Don't assert on i18n.changeLanguage being called — assert on what the user sees.
Namespace Isolation
When components use multiple namespaces, verify the correct namespace is loading the key:
test('error messages come from errors namespace', () => {
renderWithi18n(<ErrorPage code={404} />);
expect(screen.getByText('Page not found')).toBeInTheDocument();
});
test('nav labels come from common namespace', () => {
renderWithi18n(<NavBar />);
expect(screen.getByText('Log out')).toBeInTheDocument();
});A common bug is a component using t('notFound') without specifying a namespace, falling back to common where the key doesn't exist. The test catches this as a missing translation fallback key instead of the expected string.
Testing Missing Translation Keys
You want to catch missing keys before they reach production. Add a missingKeyHandler to your test i18n instance:
const missingKeys = [];
i18n.on('missingKey', (lngs, namespace, key) => {
missingKeys.push({ lngs, namespace, key });
});
afterEach(() => {
if (missingKeys.length > 0) {
throw new Error(
`Missing i18n keys: ${missingKeys.map(k => `${k.namespace}:${k.key}`).join(', ')}`
);
}
missingKeys.length = 0;
});This turns any untranslated key into a test failure automatically.
Testing with useTranslation Hook Directly
For testing logic that depends on t without rendering a component:
import { renderHook } from '@testing-library/react';
import { useTranslation } from 'react-i18next';
import { I18nextProvider } from 'react-i18next';
import i18n from '../test-utils/i18n';
test('t function resolves interpolation', () => {
const wrapper = ({ children }) => (
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
);
const { result } = renderHook(() => useTranslation('common'), { wrapper });
expect(result.current.t('greeting', { name: 'Bob' })).toBe('Hello, Bob!');
});Mocking i18next in Unit Tests (No Provider)
For pure unit tests of utility functions that call t directly, mock the module:
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key) => key,
i18n: { changeLanguage: jest.fn() },
}),
}));This returns the key as-is, making assertions simple: expect(screen.getByText('common:greeting')).toBeInTheDocument(). Use this pattern only when you're testing component structure, not translation correctness.
Integration Test Checklist
Before shipping a new locale:
- All keys present in target locale (use
missingKeyHandlerpattern above) - Pluralization tested for counts 0, 1, 2, 5
- Interpolated values render correctly (no raw
{{name}}leaking) - Language switch re-renders all visible strings
- Namespace boundaries respected — no key bleed between
commonand feature namespaces - Fallback language renders when target locale key is missing
Testing i18next thoroughly is largely about discipline: explicit test resources, explicit language assertions, and a missing-key handler that fails loudly. The mock provider pattern keeps tests fast while still exercising the real resolution logic.