---
title: "Machine translation"
description: "Translate missing messages with any LLM or translation service, check every result, and approve them in batches."
canonical_url: "https://x18n.dev/guide/machine-translation"
---
# Machine translation

> Translate missing messages with any LLM or translation service, check every result, and approve them in batches.

`x18n translate` fills in missing messages through **translators**: plugins that send messages to a model or a translation service. x18n does everything around the call. It picks the messages, adds the context a translator needs, checks every result, and writes the results through the same plan as every other edit.

Every value a translator writes is marked as machine-translated until someone approves it or changes it. To review those values in bulk, run [`x18n review approve --machine`](#approve-in-batches).

## Set up a translator

`@x18n/translator-ai` is the translator for LLMs. It works with any [AI SDK](https://ai-sdk.dev) provider.

```sh
pnpm add -D @x18n/translator-ai @ai-sdk/anthropic
```

```ts [x18n.config.ts]
import { anthropic } from '@ai-sdk/anthropic'
import { defineConfig } from '@x18n/core'
import translatorAi from '@x18n/translator-ai'

export default defineConfig({
  plugins: [
    translatorAi({
      model: anthropic('claude-sonnet-5-5')
    }),
  ],
  translate: {
    subject: 'the UI of Acme, a project management app',
    rules: {
      de: ['Address the user as "du".']
    },
  },
})
```

The provider reads its API key from the environment, here `ANTHROPIC_API_KEY`. Keep keys out of config files.

YAML and JSON configs pass the options in `[specifier, options]` form. A model id string goes to the AI SDK default provider, the Vercel AI Gateway, which reads `AI_GATEWAY_API_KEY`:

```yaml [x18n.config.yml]
plugins:
  - ['@x18n/translator-ai', {model: anthropic/claude-sonnet-5-5}]
```

Run `x18n translators` to see the translators and the chain each locale uses.

## Translate

```sh
x18n translate --to de --estimate   # what would be sent, and to which translator
x18n translate --to de,fr           # translate and write
x18n translate --to all --dry-run   # translate, then print the plan without writing
```

On a terminal, a paid run asks before it starts. Pass `--yes` to skip the question. The run selects the same messages as [`x18n prompt translate`](https://x18n.dev/agents/prompts): missing and empty messages, plus stale translations with `--stale` and values that checks report with `--check`.

The translator gets the context of each message: its description, code around its usages, values in related locales, the glossary terms the batch uses, and existing messages as a style reference. Stale translations get the old translation and a word diff, so only the changed part is translated again.

Every result passes model-free checks before x18n writes it:

- **Placeholders.** Every placeholder of the source is in the result, and the result adds none.
- **Syntax.** The message syntax is valid, as the [`invalid-syntax`](https://x18n.dev/checks/invalid-syntax) check says.
- **Message checks.** Checks such as [`identical-to-source`](https://x18n.dev/checks/identical-to-source) also judge each result, at the severity they have in your config. A result with an error is not written.

A result that fails goes back to the same translator once, with the problems listed. If it fails again, the next translator in the chain gets it. If no translator gets it right, the message is rejected. A rejected message is listed in the output and remembered, so later runs skip it until its source changes. Pass `--retry-rejected` to try again. When a run rejects any message, `x18n translate` exits with code 1.

### What a run writes

| Message                                     | Default                                               | With `--overwrite` |
| ------------------------------------------- | ----------------------------------------------------- | ------------------ |
| Missing or empty                            | The value, marked as machine-translated               | The value          |
| Has a value (stale, or reported by a check) | A [review suggestion](https://x18n.dev/guide/reviews) | The value          |

Pass `--write suggestion` to leave only suggestions, or `--write none` to see the results without writing anything.

### Limits and cost

`translate.limit` (default 200) caps the messages per run. With `--budget-usd 2` (or `translate.budget`), x18n stops before a request once the run has cost that much. The output shows the cost when the provider reports it. Answers are cached, so a run that x18n repeats sends only what changed.

## Machine-translated messages

A value that a translator wrote is recorded in the [review file](https://x18n.dev/guide/reviews#the-review-file), under `machine`:

```yaml [.x18n/reviews.yml]
machine:
  nav.about:
    de:
      translator: ai
      model: claude-sonnet-5-5
      prompt: '1'
      time: 2026-11-05T10:00:00.000Z
      value: 4be1c2
```

`value` is a hash of the value that was written. The message counts as machine-translated while it still has that value and no open approval approved it. If someone edits the message, or approves it, the mark is gone.

## Approve in batches

`x18n review approve` approves one message. With a glob or a filter, it approves many in one plan:

```sh
x18n review approve --machine --locale de --dry-run   # what it would approve
x18n review approve --machine --locale de             # approve everything a translator wrote in de
x18n review approve 'checkout.*' --locale de,fr       # approve a part of the keys
x18n review approve --from reviewed.json              # approve exactly the values in this file
```

A batch skips messages and gives the reason:

- the value is empty, or already approved;
- the value changed since the reviewer saw it, with `--from`;
- the value fails its placeholder or syntax checks;
- the thread has an open change request (pass `--override-change-requests` to approve anyway);
- the translation is stale (pass `--stale approve`, or `--stale mark-fresh` to also mark it fresh).

A batch MUST be narrowed by a glob, `--namespace`, `--machine`, `--state`, `--thread` or `--from`. Pass `'**'` to approve every key on purpose. On a terminal, x18n asks before it writes more than 20 approvals.

## Write a translator

A translator is a plugin capability. It turns a batch of messages into translations, and core does the rest:

```ts
import { definePlugin, defineTranslator } from '@x18n/core'

const echo = defineTranslator({
  id: 'echo',
  features: {
    placeholders: 'xml', // send placeholders as <x id="0"/>; or 'token', or 'raw' with a placeholder list
    context: false, // the service reads text only
    feedback: false, // it cannot use the problems of an earlier try
    update: false,
    glossary: 'none',
    batch: { maxItems: 50, maxChars: 30_000 },
    paid: true,
  },
  supports: ({ to }) => to !== 'tlh',
  async translate(batch, ctx) {
    if (!ctx.secret({ env: 'ECHO_API_KEY' }))
      throw new Error('ECHO_API_KEY is not set')
    return {
      items: batch.items.map(item => ({ id: item.id, text: item.text }))
    }
  },
})

export default definePlugin({
  name: 'echo-translator',
  translators: [echo]
})
```

- Return one entry per item, with the item's `id`. An item that is missing from the reply counts as a failure.
- `batch.prompt` holds the request that x18n renders for LLMs. An LLM translator SHOULD send it as it is.
- Throw on a failed request. x18n logs the error and hands the items to the next translator.


## Sitemap

See the full [sitemap](https://x18n.dev/sitemap.md) for all pages.
