i18n-engine
v0.1.0
Published
Claude-powered translation sync for next-intl-shaped message catalogs: diffs source-locale JSON against a per-locale baseline, translates only new/changed keys, and validates ICU placeholders and rich-text tags survive.
Downloads
22
Maintainers
Readme
i18n-engine
Claude-powered translation sync for next-intl-shaped message catalogs
(nested JSON, ICU MessageFormat placeholders, rich-text tags). Point it at a messages/ directory
and a source-locale JSON file, and it:
- Diffs the source locale against a per-locale baseline (
messages/.baselines/<locale>.json) to find only new or changed keys — never re-translates content that hasn't moved. - Sends just those keys to the Claude API with a domain-specific system prompt you configure (what the app is, who it's for, terms to leave untranslated, a glossary for cross-app consistency).
- Validates that ICU placeholders (
{name},{count, plural, ...}) and rich-text tags (<strong>...</strong>) survived translation before writing anything. - Rewrites the locale file in source-key order (so diffs stay reviewable) and updates the baseline.
It's a maintainer-time CLI, not a runtime dependency — the JSON it writes is read by next-intl at runtime; this tool itself is never imported by your app.
Install
npm install --save-dev i18n-engineRequires an ANTHROPIC_API_KEY in the environment (or ant auth login) to actually translate. The
CLI runs fine without one as long as every requested locale is already in sync — it only touches
the API for locales with new or changed keys.
Quickstart
Create i18n.config.mjs next to your messages/ directory:
// i18n.config.mjs
export default {
sourceLocale: "es",
curatedLocales: ["es", "en", "pt"], // human-reviewed; flagged post-write for a native-speaker pass
protectedTerms: ["MyAppName", "OSM"], // identifiers to leave untranslated
domainIntro:
"You are translating MyApp, a [one-line description of the app and its audience].",
languageNames: {
es: "Spanish (neutral Latin American)",
en: "English",
pt: "Portuguese (Brazilian)",
de: "German",
},
// optional: cross-app terminology consistency
glossary: [
{ term: "shelter", translations: { es: "refugio", pt: "abrigo", de: "Notunterkunft" } },
],
};Then:
npx i18n-engine # sync every locale in languageNames
npx i18n-engine de fr # sync only these locales
npx i18n-engine --config path/to/i18n.config.mjs demessages/<sourceLocale>.json is your single source of truth; add a language by adding it to
languageNames (and to curatedLocales only once a human has reviewed it) — no new dictionary
file to hand-write, the CLI creates messages/<locale>.json on first run.
Commit the baseline files (messages/.baselines/) — they're the sync state. Skipping them
makes the next run re-translate (and re-bill) everything from scratch.
See examples/demo-app/ for a minimal worked config + already-in-sync messages (running the CLI
against it does a zero-network exit since nothing is stale).
Config reference (SiteConfig)
| field | required | description |
|---|---|---|
| sourceLocale | yes | locale code the source file is written in, e.g. "es" |
| languageNames | yes | { [code]: descriptive name } for every locale to translate into, including the source |
| domainIntro | yes | 1–3 sentences: what the app is, who it's for — goes in the system prompt |
| curatedLocales | no | locales that are human-reviewed; flagged post-write for manual review |
| protectedTerms | no | identifiers to keep untranslated (org/product names, acronyms) |
| glossary | no | [{ term, translations: { [code]: text } }] for fixed cross-app terminology |
| messagesDir | no | defaults to messages/ next to the config file |
| baselinesDir | no | defaults to messagesDir/.baselines |
| model | no | defaults to claude-opus-4-8 (or $ANTHROPIC_MODEL) |
| keysPerRequest | no | defaults to 80 |
Library usage
For a custom wrapper script (e.g. to compose with your own build tooling) instead of the CLI convention:
import { syncAllLocales } from "i18n-engine";
const config = {
messagesDir: new URL("./messages", import.meta.url).pathname,
sourceLocale: "es",
languageNames: { es: "Spanish", en: "English" },
domainIntro: "...",
};
process.exit(await syncAllLocales(config, process.argv.slice(2)));flatten, unflatten, buildSystemPrompt, icuArgs, and richTags are also exported if you need
the building blocks directly.
Porting a legacy flat-dict format
If you're migrating from a hand-written Record<key, string> module format (e.g.
src/i18n/<locale>.ts) to next-intl's nested messages/<locale>.json:
npx i18n-port-dict --i18n-dir src/i18n --messages-dir messages <sourceLocale> <code1> [code2 ...]This seeds the baseline with the current source-locale content, so the sync CLI treats the ported files as already in sync rather than re-translating them. Only pass codes you've confirmed are real, reviewed content — it ports whatever the module exports as-is and does not detect stub translations (e.g. source text copy-pasted under another locale's code).
License
Apache-2.0
