Concepts

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.

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 is not reachable this way.

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.

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 goes green while doing no work — read summary.totalTranslated before trusting it.

The Result Contract

Every missing key lands in 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 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:

i18n-kit.config.ts
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 :params for 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, select arms, =N plural arms and # must survive, an other arm is required, and unbalanced braces are rejected. CLDR categories may differ: English one/other becoming Polish one/few/many/other is 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.

Copyright © 2026