Tutorial

react-i18next: find missing translation keys before your users do

2026-06-10 · 5 min read

react-i18next has a default behavior that quietly ships bugs: when a key is missing from the active language, t() returns the key itself. Your French user sees cart.checkout.cta on a button. No exception, no console error in production, no failing test. The app is technically working and visibly broken.

The fallbackLng option hides the problem rather than fixing it: the French user now sees English, which reads as "this company does not care about French" instead of "this company has a bug". Either way, nothing told you.

Why tests never catch it

Component tests run with one locale, almost always the source language, because that is what the fixtures use. A key missing from de.json is invisible to a test suite that renders with en. Snapshot tests make this worse: they snapshot English and pass forever. The bug class lives entirely in the data, so the place to catch it is the data.

The quick audit

For a one-off check, flatten both files and diff the key sets. This catches the missing keys, today:

const flat = (o, p = '') => Object.entries(o).flatMap(([k, v]) =>
  typeof v === 'object' && v !== null ? flat(v, p + k + '.') : [p + k]);
const en = new Set(flat(require('./locales/en.json')));
const de = new Set(flat(require('./locales/de.json')));
console.log([...en].filter(k => !de.has(k)));

This script has the same blind spots every key-diff script has. It says nothing about the {{count}} a translator dropped, the i18next plural suffix (key_one, key_other) that only exists in one language, the English sentence pasted into de.json to "fix" the build, or the translation that went stale when you reworded the source. Those are the bugs that actually reach users, because they survive the key diff.

The standing gate

The durable fix is the same one you use for test coverage: compute translation health on every push and refuse the merge when a language regresses. Polylens scans i18next JSON files for 13 issue classes including missing keys, placeholder and plural-suffix mismatches, untranslated and stale strings, scores each language 0 to 100, and returns HTTP 422 from one endpoint when the gate fails:

curl -fsSO https://polylens.sh/polylens-ci.mjs
POLYLENS_TOKEN=plk_... node polylens-ci.mjs ./public/locales

curl -f turns the 422 into a non-zero exit, so the build goes red exactly like a failing test. The dashboard then lists every broken string per language, which is the part the diff script never gave you: a list you can hand to whoever fixes the French.

You can try the engine on two of your real locale files in the playground on the landing page, no signup. If the French file is as complete as you think, it takes thirty seconds to confirm.

Get a health score for your locale files in under a minute. Free for one project.

More from the blog: all posts