Concepts

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:

SourceScope
--baseUrlOne invocation
I18N_BASE_URLThe environment, including an MCP server process
providerBaseUrl in your kit configThe 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.

An API key is still required even when the endpoint ignores it. Pass any placeholder for a local server.This overrides the endpoint only. A provider that also changes the request shape or the auth header — Azure OpenAI among them — needs its own client and is not reachable this way.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.

A CLI translate run with no provider succeeds. It exits 0 having translated nothing, because skipping is not failing. A CI job that forgets --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:

BucketMeans
translatedWritten
wouldTranslateDry runs only: what would be written
failedAttempted and rejected, with a reason
skippedNot 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:

ReasonWhat happened
provider-errorThe provider call failed
omitted-by-modelThe model returned a response that did not include this key
truncatedThe response hit the token limit — lower batchSize
placeholder-mismatchThe translation's placeholders do not match the source
plural-mismatchThe translation has a different number of plural variants
write-errorTranslated, but the locale file could not be written

Why Something Was Skipped

ReasonWhat happened
no-providerAgent mode in the CLI — nothing was attempted
already-translatedThe target already had a value
protected-localeExcluded 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 :params for 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.

A 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.
Copyright © 2026