Translation Modes
Every translate result carries the mode that ran: mode is provider, agent
or dry-run.
Provider Mode
A provider is configured: the kit calls it, validates what comes back and writes it.
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. For the MCP
server, set I18N_PROVIDER and I18N_MODEL on the server process instead;
partial configuration warns on stderr and falls back to agent mode at startup,
and discover reports the active mode without translating anything.
Custom Endpoints
A gateway, a self-hosted model server or a corporate proxy speaking the same protocol:
the-i18n-cli translate --layer root --provider openai --model llama3 \
--baseUrl http://localhost:11434/v1 --apiKey unused
--baseUrl beats I18N_BASE_URL, which beats providerBaseUrl in your
kit config. Blank counts as unset, so an
exported-but-empty variable cannot disable an endpoint set further down.
Give the base only; the provider's own request path is appended.
Agent Mode
No provider is configured. This is the default in an MCP host, and the two surfaces do different things with it.
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 with write_translations.
In the CLI, nothing is translated: every key comes back skipped with reason
no-provider, and the result says how to enable provider mode.
0 having translated
nothing, because skipping is not failing. A CI job that forgets --provider goes
green while doing no work — read summary.totalTranslated before trusting it.The Result Contract
Every missing key lands in 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 and the two closed reason sets are on the agent contract.
An authentication error aborts the run with one message rather than failing key
by key, a rate limit is retried with backoff, and a response cut off at the token
limit comes back as truncated — lower --batchSize.
Translation Memory
translate fills gaps: a key is translated when its target is missing, so
rewording a source string leaves the existing translations in place. What each
translation was made from is recorded by default: every target value written
records a hash of its source text in .i18n-kit.lock.json at the project root,
which the first run creates. Commit it; it is sorted, so its diff is readable.
Turn it off to keep the file out of the project altogether — an existing one is then left where it is, unread:
import { defineI18nKitConfig } from '@the-i18n-kit/cli/config'
export default defineI18nKitConfig({
translationMemory: false,
})
translate reports keys whose source has changed since as stale and counts
them in summary.staleCount, still without touching the values. Pass
--overwriteStale (overwriteStale over MCP) to re-translate them instead;
with translationMemory: false nothing is known to be stale and the flag
changes nothing. translate-key --no-overwrite answers the same question per
locale, on its skipped entries. A key with no record is not stale.
status counts the same keys where a lockfile exists: stale per locale and
per layer, summary.staleCount overall. Coverage alone calls an outdated
translation done, since a value is there either way.
Nothing Is Written Unchecked
Three things are checked per value before it reaches a locale file:
- Placeholder parity, per plural variant —
{placeholders},@:linked.refs, and:paramsfor PHP. 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.
- ICU MessageFormat structure — argument names and types,
selectarms,=Nplural arms and#must survive, anotherarm is required, and unbalanced braces are rejected. CLDR categories may differ: Englishone/otherbecoming Polishone/few/many/otheris not a loss.
A value failing any of them 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
--targetLocales overrides the protection, with a warning. An entry that
resolves to nothing protects nothing — see
locale references.