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 · 粵語 · 简体中文 · 繁體中文
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
Install from npm:
npm install -g internationalizerOr run without a global install:
npx internationalizer --helpThe 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@latestOr build from source:
git clone https://github.com/Tom-R-Main/Internationalizer.git
cd Internationalizer
go build -o internationalizer ./cmd/internationalizer- Git tags and npm package versions must match, for example
v0.1.0and0.1.0 - The root
internationalizerpackage depends on platform packages such asinternationalizer-darwin-arm64 - Supported npm targets: macOS arm64/x64, Linux arm64/x64, Windows x64
- CI publishing requires a GitHub secret named
NPM_TOKEN
- Create a config file in your project root:
# .internationalizer.yml
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- Set your API key:
export GOOGLE_AI_STUDIO_API_KEY=your-ai-studio-key- Preview what will be translated:
internationalizer translate --dry-run- Run the translation:
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-approvedFind missing or stale keys and translate them via an LLM.
internationalizer translate # translate all locales
internationalizer translate -l fr # translate French only
internationalizer translate --dry-run # preview without API calls
internationalizer translate --adopt-existing # baseline existing translations without API calls
internationalizer translate --refresh-policy # refresh prompt/style/model-stale entries
internationalizer translate --batch-size 20 # smaller batches
internationalizer translate --concurrency 2 # fewer parallel callsTranslation 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.
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 # accented en-XA
internationalizer pseudo --strategy bidi # bidi ar-XB
internationalizer pseudo --dry-run # show planned artifacts only
internationalizer pseudo --locale qps-ploc # choose another valid locale tagThe generator refreshes only artifacts it previously recorded as pseudo
output. Use --force to replace any other existing target deliberately.
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 --allCheck 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 # human-readable output
internationalizer validate --json # machine-readable JSON
internationalizer validate -q # exit code only
internationalizer validate --strict # enforce translation quality rules
internationalizer validate --require-state # require current manifest provenance
internationalizer validate --require-approved # also require explicit approval--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:
| Code | Meaning |
|---|---|
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 |
Inspect existing configuration and discover catalogs, including nested apps.
internationalizer detect
internationalizer detect --json
internationalizer config check --jsonDiscovery 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.
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 --jsonThis 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.
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"Manage translation memory (JSONL cache of previously translated strings).
internationalizer tm stats # show record counts
internationalizer tm export # dump as JSON
internationalizer tm clear --force # delete all records# .internationalizer.yml
# Source language (default: en)
source_locale: en
# Languages to translate into (required)
target_locales: [fr, de, es, ja, yue, zh-CN, zh-TW, ar]
# One or more source-to-target mappings (required).
# {locale} is replaced with each configured target locale.
message_syntax: auto # default; overridden per bundle below
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
# Backward compatibility: source_path still maps targets to sibling files
# such as locales/fr.json. Prefer bundles for new projects.
# source_path: locales/en.json
# LLM provider settings
llm:
# Provider: "anthropic", "openai", "gemini", or "openrouter" (default: gemini)
provider: gemini
# Model name defaults by provider:
# anthropic: claude-opus-5
# openai: gpt-5.6-luna (reasoning effort defaults to max)
# gemini: gemini-3.8-flash
# openrouter: deepseek/deepseek-v4-pro-0813
model: gemini-3.8-flash
# Environment variable containing the API key
api_key_env: GOOGLE_AI_STUDIO_API_KEY
# Base URL for OpenAI-compatible endpoints (optional)
# base_url: https://api.openai.com
# OpenAI GPT-5-series Responses API reasoning effort
# (default: max for the OpenAI provider)
reasoning_effort: max
# Optional LLM settings for individual target locales. An override using the
# global provider inherits unspecified global settings. A different provider
# uses that provider's defaults for unspecified settings.
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
# Keys per LLM call (default: 40)
batch_size: 40
# Parallel LLM calls (default: 4)
concurrency: 4
# Directory containing per-locale style guide Markdown files (default: style-guides)
style_guides_dir: style-guides
# Directory containing per-locale glossary JSON files (default: glossary)
glossary_dir: glossary
# Path to translation memory file (default: .internationalizer/tm.jsonl)
tm_path: .internationalizer/tm.jsonl
# Versioned source, policy, target, and provenance state
# (default: .internationalizer.lock; commit this file)
manifest_path: .internationalizer.lock
# Optional translation and strict-validation rules
validation:
plural_style: i18next-v4 # generate and validate target-locale plural formsLocale 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:
i18nextprotects{{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.icualways parses the message as ICU, including malformed input that cannot be recognized by automatic detection. Parsing errors never fall back to text.plaintreats braces as text and does not impose an interpolation grammar.autoinfers each message's grammar from its source. Select an explicit mode for catalogs mixing prose and code. Fluent resources requireautoand use their own grammar; Markdown documents cannot selecticu.
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 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
Define rules that apply to all languages: interpolation syntax, HTML preservation, string type conventions (buttons vs. labels vs. errors), etc.
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 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 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.
| Format | Extensions | Mode |
|---|---|---|
| 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.
internationalizer detect identifies your i18n setup by checking:
- Existing configured bundles and source-locale filenames/directories
- Nested
package.jsondependencies 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.
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
| Feature | Internationalizer | i18next | Crowdin | Generic LLM |
|---|---|---|---|---|
| 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 |
See THIRD_PARTY_NOTICES.md for dependency notices.
See CONTRIBUTING.md for development setup and guidelines. All contributions require DCO sign-off.