Key parity passes. Placeholder parity is missing.
Most projects that check translations in CI compare keys: every key in en.json exists in de.json. That check stays green while a translation drops or renames a {placeholder}, which is the bug users actually see.
A real example
Plane runs a key-parity script on its locales. Its Czech template toasts still lost {templateName} and {templateType}, so Czech users saw a toast without the template's name. Every key was present, so the existing check passed. The fix was three strings, merged the same day.
The pattern repeats across projects:
- Solidus runs i18n-tasks'
inconsistent_interpolations, but only on the gems whereen.ymllives; its 39-locale translation gem had no base locale, so the check passed trivially. - 24pullrequests had a spec comparing key sets; after two rounds of dropped-interpolation fixes it added an interpolation check to that spec.
- nocodb had 182 plural strings in 13 languages where machine translation turned vue-i18n's
|separator into<unk>; every key was present.
The three-line fix
Add a placeholder check next to the key check. In any CI:
npx @shipi18n/cli check ./locales -s en \ --severity 'missing-key=off,untranslated=off,orphan-key=off'
The --severity flags leave key coverage to your existing check and report only what breaks at runtime: dropped or renamed placeholders, broken plurals, invalid ICU. In GitHub Actions, the Action does the same with PR annotations.
Or extend the check you already have
If you'd rather keep everything in your own test suite, the rule is simple: for every key, the translation must contain every placeholder the source string has (extra ones can be legitimate, such as a word form some languages need). Rails apps with i18n-tasks already have it: i18n-tasks check-consistent-interpolations.
Details on each finding: placeholder-missing, plural-forms, icu-invalid.