Automatic Translation and a CI Linter

2 min read·Feb 19, 2026

Every time someone added a new piece of text to the app, the same manual steps followed: open the Spanish locale file, open the Catalan one, find where the new key belonged, and type out the translation by hand. Multiply that across every app, every sprint, and translation became one of those small chores nobody enjoyed and everybody occasionally forgot.

There was also a quieter failure mode hiding behind the manual one. A merge request could be approved with a new string that nobody got around to translating - and it would ship straight to production as a raw key or an empty string.


The Solution

Both problems had the same root cause: translation depended on someone remembering to do it. The fix was to remove the human from that loop entirely - scan the codebase for every key actually in use, generate the translation files automatically, and fail the build if anything is still missing.


Building It as an npm Package

Rather than writing this as a one-off script tied to one repo, I built it as a standalone, published npm package: i18extract.

It's a CLI that takes a glob pattern for your source files and a list of target languages, scans for every translation key in use, and generates a JSON file per language using the Google Translate API:

npx i18extract -l en,es,ca -i src/**/*.{js,jsx,ts,tsx} -o locales

Running this against a codebase using t('Confirm') produces:

// locales/en.json
{ "Confirm": "Confirm" }
 
// locales/es.json
{ "Confirm": "Confirmar" }

No manual lookup, no opening three files per string - the key itself is the thing that gets translated.

What About Missing Translations?

Generating translations solves new strings. It doesn't solve the case where a developer adds a key and the translation step never runs before merging. That's handled by the same tool with a --lint flag:

npx i18extract -l en,es,ca -i src/**/*.{js,jsx,ts,tsx} -o locales --lint

Run in CI, this fails the build the moment a key is used in code but missing from the locale files:

>> error: no translation found for "Confirm"

That moved the failure point from "a user sees an untranslated string in production" to "the merge request doesn't pass CI."

Packaging it as a public npm module rather than a private script meant it wasn't tied to one codebase - it could be dropped into any i18next project, versioned independently, and used by anyone hitting the same problem.


Results

  • Zero manual translations - new strings are generated automatically with a single command
  • Zero missing keys reaching production - caught by --lint in CI before merge
  • Published as a public npm package, reusable outside this one platform

What used to be three locale files and a translator's memory is now one CLI command.


This post is part of a series on scaling a frontend platform to 18 apps. Read the full overview.