Internationalizer
AI-native internationalization pipeline for software projects. Translate, validate, and manage i18n files using LLMs.

العربية · বাংলা · Čeština · Dansk · Deutsch · Ελληνικά · Español · Suomi · Français · עברית · हिन्दी · Indonesia · Italiano · 日本語 · 한국어 · Bahasa Melayu
Nederlands · ਪੰਜਾਬੀ · Polski · Português · Română · Русский · Svenska · తెలుగు · ไทย · Türkçe · Українська · Tiếng Việt · 粵語 · 简体中文 · 繁體中文
Why Internationalizer?
Most i18n tools are either runtime libraries (i18next, react-intl) or key-management SaaS platforms (Crowdin, Lokalise). None of them solve the actual translation problem well:
- Manual translation doesn't scale past a few languages
- Machine translation APIs (Google Translate, DeepL) ignore your terminology, tone, and UI conventions
- Generic LLM translation works better, but without glossaries and style guides, you get inconsistent results
Internationalizer is different. It's a CLI pipeline that combines LLM translation with:
- Per-language glossaries — enforce consistent terminology across your app
- Per-language style guides — control tone, formality, pluralization, and typography
- Translation memory — skip unchanged strings, save money on API calls
- Deterministic validation — catch missing or extra keys, protected-structure drift, glossary issues, and plural or ICU errors before they ship
- Explicit approval — keep provider or adopted provenance separate from human review
- Fluent and pseudolocales — preserve translator context and exercise accented or bidirectional layouts without an API call
Installation
Install from npm:
npm install -g internationalizer
Or run without a global install:
npx internationalizer --help
The npm package installs the matching prebuilt binary from npm via platform-specific optional dependencies.
Install with Go:
go install github.com/Tom-R-Main/Internationalizer/cmd/internationalizer@latest
Or build from source:
git clone https://github.com/Tom-R-Main/Internationalizer.git
cd Internationalizer
go build -o internationalizer ./cmd/internationalizer
npm Packages
- Git tags and npm package versions must match, for example
v0.1.0 and 0.1.0
- The root
internationalizer package depends on platform packages such as internationalizer-darwin-arm64
- Supported npm targets: macOS arm64/x64, Linux arm64/x64, Windows x64
- CI publishing requires a GitHub secret named
NPM_TOKEN
Quick Start
- Create a config file in your project root:
source_locale: en
target_locales: [fr, de, es, ja]
bundles:
- id: app
source: locales/en.json
target: locales/{locale}.json
format: json
llm:
provider: gemini
model: gemini-3.8-flash
api_key_env: GOOGLE_AI_STUDIO_API_KEY
export GOOGLE_AI_STUDIO_API_KEY=your-ai-studio-key
- Preview what will be translated:
internationalizer translate --dry-run
internationalizer translate
- Validate and approve the exact generated artifacts:
internationalizer validate
internationalizer review list --status needs_review
internationalizer review approve --locale fr --all
internationalizer validate --require-approved
Commands
translate
Find missing or stale keys and translate them via an LLM.
internationalizer translate
internationalizer translate -l fr
internationalizer translate --dry-run
internationalizer translate --adopt-existing
internationalizer translate --refresh-policy
internationalizer translate --batch-size 20
internationalizer translate --concurrency 2
Translation state independently reports missing, source-stale, policy-stale,
current, and manually edited conditions, so a manual edit cannot conceal a
source or policy change. Policy-stale values are reported but only retranslated
with --refresh-policy. Manually edited values are never overwritten
automatically. Use --adopt-existing when introducing the manifest to existing
translations or when explicitly accepting a manual edit as the new provenance
baseline. Adoption does not imply human approval; use review approve for that
separate decision.
pseudo
Generate deterministic test locales without a provider or translation-memory
lookup. Accented output defaults to en-XA; bidirectional output defaults to
ar-XB. ICU and Fluent runtime syntax, code, links, and markup are preserved.
internationalizer pseudo
internationalizer pseudo --strategy bidi
internationalizer pseudo --dry-run
internationalizer pseudo --locale qps-ploc
The generator refreshes only artifacts it previously recorded as pseudo
output. Use --force to replace any other existing target deliberately.
review
Inspect and approve the exact target content currently bound to its source and
translation policy. Generated, cached, and adopted values all begin in
needs_review; approval is invalidated by later source, policy, or target
changes. Pseudolocales are tracked separately as test artifacts.
internationalizer review list --status needs_review
internationalizer review approve --locale fr --bundle app --key common.save
internationalizer review approve --locale fr --all
validate
Check all locale files against their source bundles. Default validation checks
structural coverage (the percentage of required target keys present), reports
extra keys as warnings, and fails for missing keys, interpolation mismatches,
or invalid ICU MessageFormat structure.
internationalizer validate
internationalizer validate --json
internationalizer validate -q
internationalizer validate --strict
internationalizer validate --require-state
internationalizer validate --require-approved
--strict also reports translated coverage. A linguistic value identical to
its source is untranslated unless the glossary explicitly contains an exact
same-source, same-target entry for the complete value; ignore_case is honored,
but a glossary term embedded in a longer value is not an exemption. Strict mode
fails on extra keys, source-identical values, changed interpolation/HTML/code/
Markdown-link structure, glossary violations, and configured plural forms.
--require-state verifies each target against .internationalizer.lock. It
fails when a key is untracked, or when its recorded source, translation policy,
or target hash is stale. --require-approved implies --require-state and also
fails if the exact current artifact has not been approved. Both can be combined
with --strict.
Human and JSON reports use stable finding codes:
missing_key / extra_key | Source and target key sets differ |
blank_translation | A non-empty source has an empty strict-mode target |
source_identical | A strict-mode linguistic value remains untranslated |
protected_structure_mismatch | Interpolation, HTML, code, or link structure changed |
glossary_violation | No approved target term or variant was found |
plural_form_missing | A configured locale plural form is absent |
icu_message_syntax | A source or target ICU message is malformed |
icu_argument_mismatch | ICU argument names, types, or formatter styles differ |
icu_selector_mismatch | Selectors differ or a plural category is invalid for the target locale |
untracked | No manifest record exists for the target |
source_stale | Source content changed after the recorded translation |
policy_stale | The generated prompt or model settings changed |
target_modified | Target content differs from the manifest record |
needs_review | Current provenance exists, but the exact target is not approved |
detect
Inspect existing configuration and discover catalogs, including nested apps.
internationalizer detect
internationalizer detect --json
internationalizer config check --json
Discovery reports runtime evidence, uncovered catalogs, and unresolved choices.
i18next dependencies suggest message_syntax: i18next; ICU integration evidence
requires a runtime decision. JSON is a storage format, not a message grammar.
config plan and config apply
Create a proposal, review its diff, then explicitly apply the saved plan:
internationalizer config plan --json \
--add-bundle web=exf-app/web/src/i18n/locales/en.json \
--syntax web=i18next --syntax default=i18next \
--confirm-source tmp/english-keys.json --out config-plan.json
internationalizer config apply --plan config-plan.json --no-input --json
internationalizer translate --dry-run --json
This example assumes an existing source_path: tmp/english-keys.json marketing
config. Select paths and syntax for your own runtime; discovery does not decide
which artifacts ship. Plan/apply preserves existing provider settings, locale
overrides, glossary paths, and bundle IDs. It rejects stale plans and recognizes
an already-applied configuration. --no-input disables prompts; it does not
authorize additional actions.
See the onboarding and JSON contract for initial setup,
filters, error codes, and retry behavior. internationalizer commands --json
describes installed workflow entry points and their effects.
JSON compatibility: validate --json now emits a schema_version: 1 envelope.
Consumers of its previous array output must read data.reports instead.
glossary
Manage per-language glossary terms that are enforced during translation.
internationalizer glossary list --locale fr
internationalizer glossary add --locale fr --source "Dashboard" --target "Tableau de bord"
internationalizer glossary remove --locale fr --source "Dashboard"
tm
Manage translation memory (JSONL cache of previously translated strings).
internationalizer tm stats
internationalizer tm export
internationalizer tm clear --force
Configuration Reference
source_locale: en
target_locales: [fr, de, es, ja, yue, zh-CN, zh-TW, ar]
message_syntax: auto
bundles:
- id: app
source: locales/en.json
target: locales/{locale}.json
format: json
message_syntax: i18next
- id: docs
source: README.md
target: docs/i18n/{locale}.md
format: markdown
message_syntax: plain
- id: browser
source: browser/locales/en-US/browser.ftl
target: browser/locales/{locale}/browser.ftl
format: fluent
llm:
provider: gemini
model: gemini-3.8-flash
api_key_env: GOOGLE_AI_STUDIO_API_KEY
reasoning_effort: max
locale_overrides:
yue:
provider: openrouter
model: deepseek/deepseek-v4-flash-0731
api_key_env: OPENROUTER_API_KEY
zh-CN:
provider: openrouter
model: deepseek/deepseek-v4-flash-0731
api_key_env: OPENROUTER_API_KEY
zh-TW:
provider: openrouter
model: deepseek/deepseek-v4-flash-0731
api_key_env: OPENROUTER_API_KEY
batch_size: 40
concurrency: 4
style_guides_dir: style-guides
glossary_dir: glossary
tm_path: .internationalizer/tm.jsonl
manifest_path: .internationalizer.lock
validation:
plural_style: i18next-v4
Locale identifiers must be well-formed BCP 47 tags such as fr, pt-BR, or
sr-Latn-RS. Canonical-equivalent target locales are rejected as duplicates,
and locale-specific provider overrides match canonical-equivalent spelling.
In the example above, locales without an override—including Japanese—inherit
the global Gemini configuration.
File format and message syntax are separate settings. message_syntax accepts
auto (the default), i18next, icu, or plain, globally and per bundle:
i18next protects {{name}}, nested paths such as {{user.name}}, escaping
modifiers such as {{- name}}, formatting modifiers, and repeated placeholders.
It enables i18next v4 locale-specific plural keys. Other braces are literal;
{.sift,.claude,.codex,.agents} is not parsed as ICU. Custom interpolation
delimiters and nesting expressions are not part of this profile.
icu always parses the message as ICU, including malformed input that cannot
be recognized by automatic detection. Parsing errors never fall back to text.
plain treats braces as text and does not impose an interpolation grammar.
auto infers each message's grammar from its source. Select an explicit mode
for catalogs mixing prose and code. Fluent resources require auto and use
their own grammar; Markdown documents cannot select icu.
Validation, provider and TM output checks, adoption, pseudolocalization, and
review approval share the selected syntax. HTML <code> contents and Markdown
code spans are preserved exactly. Explicit syntax profiles enforce protected
content even without --strict; --strict additionally checks translation
quality and glossary rules. Source errors are reported once per bundle with
source_path, blocked_by_source, and blocked_locales in JSON reports.
The syntax setting is part of the policy hash. This prompt-contract update
makes previously recorded policies stale, including those using auto.
Use translate --dry-run to inspect them, translate --refresh-policy to
regenerate, or translate --adopt-existing to validate and record existing
values under the new policy. Adoption leaves entries needing explicit review.
Dry-run reports “Would translate” with planned keys and blocked job counts.
ICU MessageFormat values are parsed structurally. Simple arguments, select,
plural, selectordinal, number, date, and time are supported, including
nested messages, plural offsets, exact-number selectors, and #. Validation
checks syntax, argument types and formatter styles, plural offsets, select
branch identity, and target-locale CLDR plural categories. Provider output that
breaks these invariants is rejected before a locale file or translation-memory
record is written.
Fluent (.ftl) resources are handled as semantic source documents rather than
flattened maps. Message values, terms, and attributes become independent units;
comments are passed to the provider as developer context and included in source
provenance. Serialization preserves resource comments and ordering. Validation
protects variables, references, functions, selector defaults and branches, and
data-l10n-name markup slots while allowing target-locale selector variants and
natural reordering of named rich-text elements.
With i18next-v4, recognized source plural families are expanded during
translation to the target locale's CLDR categories. A target-only category uses
the source family's _other value as its translation template. Strict
validation requires those target categories; source-only categories are
optional for target locales that do not use them.
Style Guides
Style guides are Markdown files that get injected into the LLM translation prompt. They control tone, formality, typography, and other language-specific conventions.
style-guides/
_conventions.md # shared rules for all languages
fr.md # French-specific rules
ja.md # Japanese-specific rules
ar.md # Arabic-specific rules
Shared conventions (_conventions.md)
Define rules that apply to all languages: interpolation syntax, HTML preservation, string type conventions (buttons vs. labels vs. errors), etc.
Per-language guides ({locale}.md)
Define language-specific rules: formality register (tu vs. vous), punctuation (guillemets, inverted question marks), plural forms, date/number formatting, and a terminology glossary.
Style guides are durable policy inputs, not generated output. Internationalizer
reads them but never rewrites them. Their content is hashed separately from the
glossary and prompt contract, so an application code change does not make a
translation stale. Editing a guide intentionally marks that locale for policy
review; changing internal prompt wording does not, unless the prompt contract
version also changes.
See examples/react-app/style-guides/ for a working example.
Glossary Format
Glossary files are JSON arrays stored in {glossary_dir}/{locale}.json:
[
{
"source": "Dashboard",
"target": "Tableau de bord",
"variants": ["Panneau de contrôle"],
"enforcement": "error",
"ignore_case": false,
"whole_word": true
}
]
variants lists other approved target forms. enforcement may be error,
warning, or omitted for the default error behavior. Terms are injected into
the LLM prompt as a terminology table, ensuring consistent translation across
your application. An exact entry such as {"source":"API","target":"API"}
also exempts that complete source-identical value from strict untranslated-value
findings; it does not exempt a longer value merely containing API.
Translation Memory
Translation memory is stored as a JSONL file (one JSON record per line). Each record contains:
- The bundle, key, source value, translated value, and canonical target locale
- Source, style-guide, glossary, prompt-contract, and combined policy hashes
- The provider and model that produced the translation
- A timestamp
On subsequent runs, strings with the same source and policy hashes are served
from the cache without calling the LLM. The default path is under the ignored
.internationalizer/ directory, so it remains a local cache. Set tm_path to a
tracked location if your project intentionally shares translation memory. The
reviewable .internationalizer.lock manifest is versioned separately.
Manifest schema v2 records provenance origin and review status independently,
so “generated successfully” and “approved by a person” cannot be conflated.
Supported Formats
| JSON | .json | Key-value (nested, dot-notation flattened) |
| YAML | .yml, .yaml | Key-value (preserves comments and ordering) |
| Markdown | .md, .mdx | Preamble and H2-level sections |
| Fluent | .ftl | Semantic messages, terms, attributes, comments, and selectors |
Markdown targets contain invisible internationalizer:unit comments before
H2 sections. These stable markers let Internationalizer add, move, or edit one
source section without retranslating unrelated sections. Existing unmarked
documents receive markers on their next successful update.
Project Type Detection
internationalizer detect identifies your i18n setup by checking:
- Existing configured bundles and source-locale filenames/directories
- Nested
package.json dependencies and localization-module ICU references
- Runtime evidence separately from file format, with uncertainty made explicit
Scanning is bounded and excludes dependency, build, hidden, and data directories.
Dynamic plugin registration and unconventional catalog paths need explicit
configuration; static detection cannot prove that ICU integration is absent.
Architecture
cmd/internationalizer/ CLI entry point and command definitions
internal/
config/ YAML config loading with defaults
detect/ Project type auto-detection
fluentpattern/ Fluent pattern validation and safe text transforms
formats/ Format adapters (JSON, YAML, Markdown, Fluent)
glossary/ Per-locale glossary management
llm/ LLM provider interface + implementations
anthropic.go Anthropic Claude backend
openai.go OpenAI / compatible backend
gemini.go Google Gemini via AI Studio backend
OpenRouter uses openai.go with custom base_url
locale/ BCP 47 identity and CLDR plural categories
message/ ICU MessageFormat parser and structural comparison
policy/ Stable translation-policy hashing
pseudo/ Provider-free accented and bidi test locales
review/ Explicit artifact approval workflow
state/ Versioned translation manifest
styleguide/ Style guide loader
tm/ JSONL translation memory
translate/ Translation orchestrator
validate/ Locale validation and diffing
Comparison to Alternatives
| LLM-powered translation | Yes | No | Partial | Yes |
| Per-language style guides | Yes | No | No | No |
| Glossary enforcement | Yes | No | Yes | No |
| Translation memory | Yes | No | Yes | No |
| CLI / local execution | Yes | N/A | No | Manual |
| Git-friendly files | Yes | Yes | Partial | Manual |
| No SaaS dependency | Yes | Yes | No | Varies |
| Open source (AGPL-3.0) | Yes | Yes | No | Varies |
License
AGPL-3.0
See THIRD_PARTY_NOTICES.md for dependency notices.
Contributing
See CONTRIBUTING.md for development setup and guidelines. All contributions require DCO sign-off.