Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
The content of this page was translated using an AI.
See the last version of the original content in EnglishIf you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
How to test translations without writing brittle tests
Most i18n test suites fail in one of two ways. Either they assert on literal copy, so every wording change breaks fifty tests and the team deletes them. Or they render everything in the default locale, so they prove nothing about the other seventeen. Both end in the same place, a suite nobody trusts.
Table of Contents
The patterns are library-agnostic
Every pattern below works on any i18n stack. Swap the provider for I18nextProvider, NextIntlClientProvider or IntlProvider and the tests are otherwise identical, because they assert on rendered output rather than on a library API.
The coverage tooling ports too: with the Sync JSON plugin pointed at your existing catalogues, or a compat adapter aliasing your current imports, the coverage assertion runs against the JSON you already have.
Decide what you are actually testing
Translation quality is not a test. No assertion tells you whether the German is idiomatic, and pretending otherwise is how you get a suite full of hardcoded strings.
What is worth testing is mechanical:
Open the table in a modal to view all data content clearly
| Worth testing | Not worth testing |
|---|---|
| Every required locale has a value | Whether the wording is good |
| The right locale reaches the component | Exact copy of every label |
| Plurals resolve for each category | That a translator did their job |
| RTL locales set direction and mirror | Every string in every locale |
| Formatted dates and numbers use the locale | Third-party Intl correctness |
Coverage belongs in one data-driven test, not in your component tests. That is covered in finding missing translations; this post is about the rest.
Render under a provider, assert on role
The core pattern is to mount the component inside a locale provider and query by role or test id rather than by copy.
Copy the code to the clipboard
Querying getByRole("heading") survives a copy change. getByText("Récapitulatif") does not. Use the literal only when the string itself is the thing under test, which is rare.
For attributes such as aria-label you need the raw string rather than a renderable node. In React, useIntlayer entries expose a .value field for that.
Parameterise across locales
One test body, every locale, is worth more than one test per locale.
Copy the code to the clipboard
The first assertion is the cheap generic win: if a lookup fails and your library renders the key, the DOM contains something shaped like cart.summary.title. That catches a whole class of bugs without naming a single string.
Pseudolocalisation finds what catalogues cannot
Add a fake locale that transforms every string, for example Checkout becoming [!!! Çĥéçķöũţ !!!]. Then render a page in it.
Anything still in plain English is hardcoded, and no catalogue-based check can see that, because as far as the tooling is concerned the string does not exist. The brackets do a second job: they expand the text by roughly 30 percent, which surfaces layout that breaks in German before German does.
This is worth running as a visual or end-to-end pass rather than a unit test, since the failure is something you look at.
Plurals need a test per category, not per language
Plural bugs hide because English has two forms and most developers only ever exercise those. Polish has four, Arabic six.
Copy the code to the clipboard
Pick counts that hit each CLDR category for your worst-case language rather than testing 1 and 2 everywhere. Intl.PluralRules tells you which category a number falls into, so you can derive the sample set instead of guessing. More on the categories in the ICU message format post.
The snapshot trap
Snapshots and i18n are a bad match. A snapshot of a localised component encodes every string in it, so a translator fixing a typo in Portuguese turns a green suite red, in a file no reviewer can evaluate. After the third time, someone runs -u without reading the diff, and the snapshots stop meaning anything.
If you want snapshots, take them in one locale only, and treat that as a structural check rather than a content check. Everything locale-specific belongs in explicit assertions.
Test the negotiation, not just the rendering
The most common production i18n bug is not a missing string. It is the wrong locale being selected: a URL says /fr/, the client reads navigator.language, and they disagree.
Test the resolution order directly, as a pure function, separate from any component:
Copy the code to the clipboard
This is the single highest-value i18n test most codebases are missing, and it needs no DOM.
What to run where
- Unit: locale negotiation, formatters, plural categories. Fast, no DOM.
- Component: one provider-based render per locale, asserting on roles and on the absence of raw keys.
- Coverage: one data-driven test asserting no missing required locales.
- Visual or end-to-end: pseudolocalisation pass and one RTL page, because those failures are visual.
Keep the first three in the pipeline on every commit. The last one is cheap to run nightly and expensive to run on every push.
Common mistakes
- Asserting on literal copy everywhere. Guarantees the suite is deleted within a quarter.
- Snapshotting localised components. Translators break your build, reviewers rubber-stamp the update.
- Testing only the default locale. The one locale that cannot be missing.
- Testing 1 and 2 for plurals. Misses every category English does not have.
- Mocking the i18n library away. You are then testing that your mock returns strings.
- Never testing negotiation. The most common real-world failure, and the easiest to test.
Going further
- Testing your content: CLI audit, programmatic API and UI assertions
- ESLint plugin: catching hardcoded strings and dead content
- Formatters and locale utilities, including
getHTMLTextDir - Benchmark reports across frameworks
- Drop-in react-i18next compat adapter
- How to find missing translations
- ICU message format: plurals, select and skeletons
Comments
No comments yet. Be the first to share your thoughts.
