Translate Locally
Translate a React or Next.js repo from your terminal with your own LLM key or a Babelize API key. No GitHub App, no dashboard: the result lands in your working tree as a git diff.
babelize translate runs the Babelize pipeline on your machine. It extracts hard-coded strings, generates keys, translates them, and rewrites your components to use them. The changes are written to your working tree, where you review them with git diff.
Translate with your own LLM key (no Babelize account needed):
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY
npx @babelize/cli translate --to fr,de,ja
git diffOr with Babelize-hosted translation: create an API key at app.babelize.co/api-keys (free tier included) and set BABELIZE_API_KEY:
export BABELIZE_API_KEY=bblz_sk_...
npx @babelize/cli translate --to fr,de,ja
git diffWhat's supported
| Framework | Status |
|---|---|
| React (Vite, Create React App and similar client-rendered apps) | Supported, with react-i18next |
| Next.js (App Router) | Supported. Set up with next-intl. |
| Django | Supported, with gettext .po catalogues. See Django. |
| Vue | Beta: review the diff carefully |
| Static HTML | Beta: review the diff carefully |
The Next.js Pages Router gets translated strings and injected useTranslations() calls, but _app isn't wired up for you yet. The command prints what to add. Any other project type exits with an explanation and changes nothing.
Requirements
- A git repository. The command writes into your working tree so you can review and undo it with git.
- A clean working tree. Commit or stash your work first, so the translation shows up as its own diff.
--forceskips this check. - A translation key, set in the environment: your own LLM key, or a Babelize API key (see Babelize-hosted translation). Your own key can come from any of these providers:
| Env var | Provider | Default model |
|---|---|---|
ANTHROPIC_API_KEY | Anthropic | claude-sonnet-5-5 |
OPENAI_API_KEY | OpenAI | gpt-6.1-sol |
GEMINI_API_KEY | Google Gemini | gemini-3.5-flash |
GROQ_API_KEY | Groq | openai/gpt-oss-120b |
OPENROUTER_API_KEY | OpenRouter | anthropic/claude-sonnet-5.5 |
When several keys are set, the first one in this table wins, and any of them wins over BABELIZE_API_KEY. --provider picks a specific one (including babelize), and --model overrides the default model of your own provider. Your key is only sent to that provider and is never printed. The provider bills your account for the tokens.
Babelize-hosted translation
With BABELIZE_API_KEY set and no LLM key of your own (or with --provider babelize), strings are translated by Babelize. Create the key at app.babelize.co/api-keys; the free tier includes AI credits.
- Only the strings and their context leave your machine, never your source code. Extraction, placeholder protection, validation and code injection all run locally, exactly as with your own key.
- Babelize picks the model. The summary shows it, e.g.
Provider Babelize hosted · gemini-3.5-flash.--modeldoesn't apply. - Each translated string in each language costs the same credits as in a platform run (very long strings count as several, see credits). Strings that fail aren't charged, a retry of a rejected string isn't charged twice, and strings reused from the lockfile aren't sent at all.
- Strings longer than 2,000 characters aren't sent; they're reported as failed with a hint to use your own LLM key for them.
- When you run out of credits the command stops with exit code
6and tells you how to top up; an invalid key exits with3. Rate limits are retried automatically.
Options
| Flag | Description |
|---|---|
-t, --to <locales> | Target locales, comma-separated (fr,de,ja,pt-BR). If omitted, the command prompts for them when run interactively. |
-f, --from <locale> | Source locale (default en) |
--dry-run | Print a unified diff of what would change and write nothing. This still calls your LLM provider (or uses Babelize credits). |
--force | Run even if the working tree has uncommitted changes |
--replace | Treat --to as the complete list and remove locales from earlier runs that aren't in it |
--provider <name> | anthropic, openai, gemini, groq, openrouter, or babelize (hosted, needs BABELIZE_API_KEY) |
--model <id> | Model ID for your own provider (not available with babelize) |
-y, --yes | Skip the confirmation prompt |
--report <path> | Where to write the QA report (default .babelize/local/report.json). A --dry-run writes no report unless you pass this. |
--allow-partial | Exit 0 even when some keys failed. They are still listed in the summary and the report, and retried next run. |
--json | Print the QA report as JSON instead of the summary (for CI and scripts) |
Locales you translated earlier are kept. --to ja after --to fr,de adds Japanese and also brings French and German up to date. Their existing translations come from the lockfile, so only new strings cost tokens. To drop a locale, pass the full list you want with --replace. Removed locale files appear as deletions in the diff.
How it works
- Copy. Your project is copied to a private temporary directory.
node_modules,.git,.nextand.turboare left out, as are the root-leveldist,buildandcoveragefolders and anything in.gitignore. Folders with those names deeper in your source, such as a Next.js route atapp/build/, are included..babelize/is always included, even when gitignored, because it holds the lockfile. - Run. The pipeline runs on the copy: extract, generate keys, translate, inject, then finalize (add i18n dependencies and config).
- Check. Each target locale must have real translations. If the provider fails, or a locale comes back empty or as a copy of the source text, the run stops and your working tree isn't touched.
- Apply. New and changed files are copied into your project, and files the pipeline removed are deleted. The temporary copy is always cleaned up.
If your project lists prettier or @biomejs/biome in package.json and has it installed, the changed code files are formatted with your own formatter and config before anything is applied. If formatting fails, the run continues with unformatted files and the summary says so.
Your package manager isn't run. If dependencies were added to package.json (for example next-intl or react-i18next), the summary tells you, and you run npm install (or pnpm, yarn, bun) yourself.
Reviewing the result
The summary lists every changed file and a line per locale:
Framework Next.js
Provider Anthropic · claude-sonnet-5-5
Keys 42 extracted (42 translated this run, 0 reused from lockfile)
Files 6 modified, 7 added, 0 deleted
✓ fr 42/42 translated
✓ de 42/42 translated
unchanged: footer.brand — fine for names and codes- failed: the key couldn't be translated. Reasons include a dropped placeholder, an invalid ICU message, untranslated output and a provider error. The reason is shown next to each key. Failed keys are left out of the locale file, never filled with source text, and the next run retries them. All other translations are still written, and the command exits with code
6so CI notices. - QA warnings: the key was translated but the engine flagged something to review, such as a glossary term not kept.
- unchanged: the value equals the source text, which is usual for names and codes. It counts as translated; the engine itself rejects output that is just untranslated text.
- not wired into code: a string was extracted and translated, but the injector found nowhere to put the translation call, so your app still shows the literal text. The summary lists the file, the key and the reason. Wire it up by hand with the listed key; later runs reuse that key instead of creating a duplicate.
- left as-is, not translated: text in a module-level constant that is also used as data (an
<option>value list, an object passed to an API), so Babelize didn't give it a key. Nothing changes in your code. If it really is UI text, move it inside the component (or translate it where it's rendered). - look unused: keys in your source locale file that have no
t()reference anywhere Babelize can see. This check is only a hint, because references can hide in renamed translators, templates or other file types. These keys are still translated, kept and written like every other key. If they really are dead, remove them from the source locale file yourself. - missing: the locale has no value for the key, so your app falls back to the source locale.
Then review and commit:
git diff
npm install # if the summary says dependencies were added
git add -A && git commit -m "i18n: add fr, de"Without prettier or biome, the injected code is valid but may not match your style, so run your formatter before committing.
In CI or when output is piped, the command prints one plain line per pipeline step instead of an animated spinner.
To discard a run instead, use git checkout . && git clean -fd.
Next.js: choosing the language
Next.js projects use next-intl without changing your URLs. The locale comes from a NEXT_LOCALE cookie and falls back to the browser's Accept-Language header, then to the source locale. The generated i18n/request.ts (or src/i18n/request.ts) holds this logic and the locale list. Edit it freely: remove its first line and Babelize will leave it alone on later runs.
Components get const t = useTranslations(), which works in Server Components and in components that Client Components import. Only components that are already async get await getTranslations(). Babelize never makes a component async.
Because the locale is read per request, pages render dynamically rather than statically.
The QA report
Every run (except --dry-run, unless you pass --report) writes .babelize/local/report.json: the same data as the summary, with nothing truncated. It has one entry per key with its status in each locale (translated, warning, unchanged, failed, missing), the source text and the translation, plus full lists of failures (reason, whether a retry can help, the error message), QA warnings, unchanged values, missing keys and strings not wired into code, counts per locale, the provider and model, and start/finish timestamps.
--json prints exactly the same object to stdout, so a script can use either. The format is described by a JSON Schema shipped in @babelize/engine at schemas/translate-report.schema.json (TypeScript: import type { TranslateReport } from '@babelize/engine'):
babelize translate --to fr,de --yes --json > report.json
jq '.failures.items[] | "\(.locale) \(.key): \(.reason)"' report.json
jq '.locales[] | {locale, status, translated, total}' .babelize/local/report.jsonok is false when any key failed. A report written to a custom --report path inside the repo is recognised by its $schema field and never counts as an uncommitted change, but git status still shows it unless it's ignored, so prefer a path under .babelize/local/ or outside the repo. When the run itself fails (bad key, dirty tree, …) no report is written, and --json prints {"ok": false, "error": {"code", "message"}} instead.
The first run also adds .babelize/.gitignore containing local/report.json, so the report stays out of git while the lockfile next to it is committed. The report is safe to commit or to ignore; an untracked report never counts as an uncommitted change for the next run.
Re-running: the lockfile
Each run writes .babelize/local/babelize-lock.json to your project root. It records the source text and translation of every key. Commit it. On the next run, only new or changed strings go to your LLM provider. Everything else is reused, which saves time and tokens and keeps reviewed translations stable.
babelize init adds .babelize/ to .gitignore. Reruns on your machine still reuse the lockfile, but teammates and CI won't get it. To share it, run git add -f .babelize/local/babelize-lock.json once, or add !.babelize/local/ to .gitignore. The summary reminds you when the lockfile is ignored.
Checking translations in CI
babelize check QAs the committed locale files without translating anything (missing keys, placeholders, ICU and plural forms, glossary) and exits 1 on errors. Run it in CI after translating, or on every pull request.
Testing without an API key: pseudo-localization
--provider pseudo "translates" offline into pseudo-localized text: Save note becomes ⟦Sávé ñóté⟧, placeholders and tags are kept, and count messages become real ICU plurals for the target locale. Nothing leaves your machine and nothing is billed. Use it to check that every string is wired up (anything still in plain English after switching locale was missed) and to test the pipeline in CI. It is never picked automatically and doesn't accept --model. Don't ship the output: the lockfile marks these values as pseudo, so the next run with a real provider translates them all (nothing is reused), and until then babelize check reports every one as a pseudo_translation error.
babelize translate --to de,ja --provider pseudo --yesExit codes
| Code | Meaning |
|---|---|
0 | Success (or nothing to change), including a partial run with --allow-partial |
2 | Usage problem: not a git repo, uncommitted changes, bad locale, missing --to, or project files changed while the run was in progress (nothing is applied; rerun) |
3 | The provider rejected or couldn't be reached with your key, or BABELIZE_API_KEY was rejected |
4 | Configuration problem: no API key set, or an unsupported project |
6 | Some keys failed: everything that did translate was written, the failed keys are listed in the report (ok: false) and retried next run. Also used when translation failed as a whole and nothing was changed (including running out of Babelize credits); then there is no report and --json prints an error object. |
1 | Writing the result failed. Changes are rolled back; if a file can't be restored, the command lists it with the git checkout command to restore it. |
128 | Cancelled |
Want the full platform?
babelize translate is the local path. The Babelize platform adds GitHub PRs, translation memory shared across projects, a glossary and brand voice, and a review dashboard.
HTTP API: POST /v1/translate/batch
The endpoint behind Babelize-hosted translation, for the CLI, the MCP server and your own agents. It translates strings only, never code.
curl https://api.babelize.co/api/v1/translate/batch \
-H "Authorization: Bearer $BABELIZE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceLocale": "en",
"targetLocale": "pl",
"entries": [
{ "key": "cart.items", "text": "{count, plural, one {# item} other {# items}}" },
{ "key": "save", "text": "Save", "context": "Save your changes", "element": "button" }
]
}'Request body (unknown fields are rejected):
| Field | Type | Limits |
|---|---|---|
sourceLocale, targetLocale | ISO code, e.g. en, fr, ja | Must be supported and differ |
entries[] | { key, text, context?, element? } | 1–100 entries, unique keys, text ≤ 2,000 characters, context ≤ 1,000, element ≤ 100. context is the sentence a fragment belongs to; element is where it appears (button, label, …) |
glossary[] | { term, translation } | ≤ 50 items, each ≤ 200 characters. Terms the translation must use |
hints | { [key]: note } | ≤ 300 characters each. Per-key notes, e.g. what a previous attempt got wrong |
brandVoice | string | ≤ 1,000 characters |
Locales may carry a region or script (pt-BR, zh-TW, es-419, sr-Latn); the model is told the exact variant.
Idempotency. Send an Idempotency-Key header (8–128 letters, digits, - or _; a UUID works) and reuse it when you retry the same request. For 15 minutes a repeat with the same body returns the stored response with Idempotent-Replayed: true and charges nothing; the same key with a different body is rejected with 422 idempotency_key_reused. The server gives the model at most 90 seconds, so use a client timeout above that (the CLI uses 150 s); if you disconnect before the answer, the model call is cancelled and nothing is charged.
Placeholders, tags and brand names are safest sent as [#VAR_0#]-style tokens, which the model copies unchanged. The CLI does this for you and validates every result.
Response — one result per entry, in request order:
{
"results": [
{ "key": "cart.items", "status": "ok", "value": "{count, plural, one {# produkt} few {# produkty} many {# produktów} other {# produktu}}" },
{ "key": "save", "status": "failed", "reason": "missing_from_response", "retryable": true }
]
}The X-Babelize-Model header names the model used, and X-Credits-Charged the credits charged.
Credits. Each entry costs one translation unit per started 500 characters of text + context (at least one), and a unit costs the same as one string in one language in a platform run (5⁄6 of a credit). The batch is reserved up front; only ok results are charged and the rest is released. Fractions of a credit carry over to your next request rather than being rounded up. An entry you already paid for within the last 15 minutes with the same source and target locale, text, context, element, brand voice and applicable glossary terms is free — that's a retry of a translation you rejected (hints may differ). Duplicate texts within one request are each charged.
| Status | Meaning |
|---|---|
400 | Invalid body, unsupported locale or malformed Idempotency-Key |
401 | Missing or invalid API key |
402 | Not enough AI credits (credits_needed, credits_remaining in the body) |
422 | Idempotency-Key reused with a different body |
429 | Rate limited; wait for Retry-After seconds |
502 | The model failed; nothing was charged. Retry after Retry-After |
503 | Hosted translation unavailable |
Self-hosting the backend
Hosted translation is off unless you enable it. Set these on the backend:
| Variable | |
|---|---|
HOSTED_TRANSLATION_PROVIDER | Required to enable: gemini, anthropic, openai, groq or openrouter. Unset or off disables it. |
HOSTED_TRANSLATION_API_KEY | The platform's LLM key. Use a paid-tier key: free tiers rate-limit far below what a cloud run needs. Defaults to the provider's usual variable (GEMINI_API_KEY, ANTHROPIC_API_KEY, …). |
HOSTED_TRANSLATION_MODEL | Optional. Defaults to the provider's default model (gemini-3.5-flash for Gemini). |
TRANSLATOR_URL | Optional NLLB service. Cloud runs use it as a backup for strings the LLM can't be reached for; those are written to the locale file, flagged for review, not billed, and retried with the LLM on the next run. |
REDIS_URL | Recommended. Needed for idempotency, repair de-duplication and fractional credits; without it requests still work, rounded up to whole credits. |
While it is off, cloud runs without a customer key use the NLLB service as before, and this endpoint returns 503.
Last updated: 2026-10-13
Installation & Setup
Install the Babelize CLI (@babelize/cli), log in, verify the connection to your account, configure defaults, and log out. Step-by-step setup with examples.
Check Translations
QA your locale files from the terminal or CI: missing keys, lost placeholders, broken ICU and plural forms, glossary violations. Read-only, no network, no account.