DEV Community

Franco Ortiz
Franco Ortiz

Posted on • Originally published at i1n.ai

How to keep locale JSON files in sync (next-intl, i18next, or anything else)

Every multi-language project I've worked on has the same bug. Not a bug in the code, a bug in the process: someone adds a string to en.json, ships the feature, and forgets that es.json, de.json and pt.json exist. Nothing fails. The build is green. Weeks later a user screenshots your app showing English text in the middle of a Spanish UI, and now it's a support ticket instead of a diff.

This happens with next-intl, i18next, vue-i18n, react-intl, all of them. The libraries solve rendering translations. None of them solve keeping the files in sync, because that's not a runtime problem. It's a workflow problem.

Why it keeps happening

The source locale is the only file that's ever up to date, because it's the one developers touch while building. Every other locale updates on a different schedule: when a translator gets around to it, when someone notices, or never.

The failure is silent by design. Most i18n libraries fall back to the key name or to the source language when a translation is missing. That's the right behavior in production (better English than a raw common.hero.title on screen), but it means the gap never throws. Missing translations don't crash. They just quietly degrade the product for everyone who doesn't read English.

And it compounds. Two languages, you can eyeball it. Seven languages and 900 keys, and no human can tell you which locales are complete without tooling.

Level 1: diff the keys

The minimum viable fix is a script that compares key sets between your source locale and everything else. Something like:

# keys present in en.json but missing from es.json
comm -23 \
  <(jq -r 'paths(scalars) | join(".")' locales/en.json | sort) \
  <(jq -r 'paths(scalars) | join(".")' locales/es.json | sort)
Enter fullscreen mode Exit fullscreen mode

Run it before release and you at least know what's missing. Plenty of teams live here, and it's genuinely better than nothing.

The limits show up fast though. It tells you a key is missing but doesn't fill it. It doesn't catch a translation that exists but is stale because the English source changed. It doesn't validate that {name} survived into the German version. And someone has to remember to run it, which is the same failure mode we started with.

Level 2: put the check in CI

The single highest-leverage move: make locale drift fail the build. Missing key in pt.json? Red CI, PR blocked, fixed in the same change that introduced the string, by the person with the most context. The category of bug where "translations catch up eventually" just stops existing, because incomplete locales can't merge.

You can wire the diff script from level 1 into GitHub Actions in ten minutes. If you do only one thing from this post, do this. I've seen more i18n pain solved by one required CI step than by any library migration.

The catch: a blocking check is only tolerable if fixing the failure is cheap. If a missing Japanese string means "file a ticket and wait a week", people will disable the check by the second sprint. Which leads to level 3.

Level 3: close the loop with translation

The check tells you what's missing. The remaining question is who fills it in, and this is where most workflows stall, because the answer is usually "a spreadsheet, a translation agency, and a two-week round trip".

This is the part I ended up automating. I built i1n after hitting this exact wall at the fintech where I work (7 languages, locale files in the repo, translations perpetually behind). The workflow it settles into:

  • Strings stay in your locale JSON files, in your repo. No migration.
  • i1n push --translate diffs every locale against the source and AI-translates only the missing or changed keys, with product context attached so "Save" in a form doesn't come back as "Rescue".
  • i1n check runs in CI and fails on missing keys, broken placeholders, or coverage drops.
  • Native speakers review diffs instead of translating from scratch, which is the difference between a 10-minute review and a week of waiting.

The CI check has caught things nobody on the team could have: our Hindi locale drifted for a month once and no one noticed, because nobody on the team reads Hindi. The build noticed.

Whether you use i1n or build the pipeline yourself, the shape is the same: source locale as the contract, a diff against it as the check, and translation as an automated step instead of a queue. Get those three in place and locale drift stops being a recurring bug and becomes a build error you fix before merge.

The short version

  1. Your locale files are already out of sync. Run a key diff today and find out how much.
  2. Put that diff in CI as a required check. This is the 90% win.
  3. Automate the fill-in so the check is cheap to satisfy, with AI translation plus human review, or with i1n doing that pipeline for you (npx i1n init, free tier, no card).

The drift never comes back once merging depends on it.

Top comments (0)