Translation Modes
translate and translate-key, and their MCP counterparts, run in one of two
modes. Every result names the one that ran, so a caller never has to infer it:
{ "mode": "provider" }
mode is provider, agent, or dry-run.
Provider Mode
A provider is configured, so the kit calls it and writes the results itself.
the-i18n-cli translate --layer root --provider google --model gemini-2.5-flash
Three providers ship: openai, anthropic and google. The key comes from
--apiKey or the provider's environment variable — OPENAI_API_KEY,
ANTHROPIC_API_KEY, GEMINI_API_KEY.
For the MCP server, configure it on the server process instead, with
I18N_PROVIDER and I18N_MODEL. Partial configuration — a provider with no
model, or no key — logs a warning and falls back to agent mode, so a
misconfigured server degrades at startup rather than surprising a caller
mid-request. discover reports the active mode without translating anything.
Custom Endpoints
Anything that speaks the same protocol works: a gateway, a self-hosted model server, a corporate proxy.
the-i18n-cli translate --layer root --provider openai --model llama3 \
--baseUrl http://localhost:11434/v1 --apiKey unused
Three sources, highest precedence first:
| Source | Scope |
|---|---|
--baseUrl | One invocation |
I18N_BASE_URL | The environment, including an MCP server process |
providerBaseUrl in your kit config | The whole project, shared through the repository |
A blank flag or environment variable counts as unset, so an exported-but-empty
variable cannot silently disable an endpoint configured further down the chain.
A blank providerBaseUrl in a config file is rejected at load time instead —
unlike a shell variable, it cannot get there by accident.
google has no endpoint override at all. Passing a base URL with it is rejected
as a configuration error rather than ignored, so a misconfiguration cannot look
like it worked.Agent Mode
No provider is configured. This is the default in an MCP host, and the two surfaces do different things with it — deliberately.
In the MCP server, the translate tools return per-locale fallbackContexts:
the source values plus your glossary, style notes and locale notes. The calling
agent translates them with the model it is already running on and persists the
result through write_translations. Nothing is lost; the work moves.
In the CLI, nothing is translated. There is no model on the other end of a
terminal, so every key is reported as skipped with reason no-provider and the
result explains how to enable provider mode.
--provider
therefore goes green while doing no work — add --failOnFailed, or check the
counts, if that should block.The Result Contract
Every missing key is accounted for by exactly one bucket:
| Bucket | Means |
|---|---|
translated | Written |
wouldTranslate | Dry runs only: what would be written |
failed | Attempted and rejected, with a reason |
skipped | Not attempted, with a reason |
The invariant, per locale:
missing = translated + wouldTranslate + failed + skipped
A run that quietly loses keys cannot balance, so a caller verifies completeness by arithmetic rather than by re-scanning the project.
Why Something Failed
A closed set, so an agent branches on the value instead of pattern-matching English:
| Reason | What happened |
|---|---|
provider-error | The provider call failed |
omitted-by-model | The model returned a response that did not include this key |
truncated | The response hit the token limit — lower batchSize |
placeholder-mismatch | The translation's placeholders do not match the source |
plural-mismatch | The translation has a different number of plural variants |
write-error | Translated, but the locale file could not be written |
Why Something Was Skipped
| Reason | What happened |
|---|---|
no-provider | Agent mode in the CLI — nothing was attempted |
already-translated | The target already had a value |
protected-locale | Excluded by protectedLocales |
Provider failures are triaged before they cost a whole run: an authentication error aborts immediately with one clear message rather than failing key by key, a rate limit is retried with backoff, and a response cut off at the token limit is detected from the provider's finish reason rather than surfacing as malformed output.
Nothing Is Written Unchecked
Two things are checked per value before it reaches a locale file:
- Placeholder parity, per plural variant —
{placeholders},@:linked.refs, and:paramsfor PHP. Checked per variant rather than per string, so a placeholder that survives in the singular and vanishes in the plural is still caught. - Plural variant count — the number of pipe-separated variants must match the source.
A value failing either is rejected into failed rather than written. The key
stays missing, which is recoverable; a locale file with a broken interpolation is
not.
Protected Locales
Locales listed in protectedLocales are excluded from the default target set and
reported as skipped with reason protected-locale. Naming one explicitly in
--targets overrides the protection, with a warning.
protectedLocales entry that resolves to nothing protects nothing, and the run
that machine-translates the locale you meant to protect looks exactly like a
successful one. See referring to locales.