BabelizeBabelize
CLI

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 diff

Or 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 diff

What's supported

FrameworkStatus
React (Vite, Create React App and similar client-rendered apps)Supported, with react-i18next
Next.js (App Router)Supported. Set up with next-intl.
DjangoSupported, with gettext .po catalogues. See Django.
VueBeta: review the diff carefully
Static HTMLBeta: 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. --force skips 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 varProviderDefault model
ANTHROPIC_API_KEYAnthropicclaude-sonnet-5-5
OPENAI_API_KEYOpenAIgpt-6.1-sol
GEMINI_API_KEYGoogle Geminigemini-3.5-flash
GROQ_API_KEYGroqopenai/gpt-oss-120b
OPENROUTER_API_KEYOpenRouteranthropic/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. --model doesn'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 6 and tells you how to top up; an invalid key exits with 3. Rate limits are retried automatically.

Options

FlagDescription
-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-runPrint a unified diff of what would change and write nothing. This still calls your LLM provider (or uses Babelize credits).
--forceRun even if the working tree has uncommitted changes
--replaceTreat --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, --yesSkip 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-partialExit 0 even when some keys failed. They are still listed in the summary and the report, and retried next run.
--jsonPrint 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

  1. Copy. Your project is copied to a private temporary directory. node_modules, .git, .next and .turbo are left out, as are the root-level dist, build and coverage folders and anything in .gitignore. Folders with those names deeper in your source, such as a Next.js route at app/build/, are included. .babelize/ is always included, even when gitignored, because it holds the lockfile.
  2. Run. The pipeline runs on the copy: extract, generate keys, translate, inject, then finalize (add i18n dependencies and config).
  3. 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.
  4. 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 6 so 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.json

ok 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 --yes

Exit codes

CodeMeaning
0Success (or nothing to change), including a partial run with --allow-partial
2Usage 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)
3The provider rejected or couldn't be reached with your key, or BABELIZE_API_KEY was rejected
4Configuration problem: no API key set, or an unsupported project
6Some 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.
1Writing 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.
128Cancelled

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):

FieldTypeLimits
sourceLocale, targetLocaleISO code, e.g. en, fr, jaMust 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
brandVoicestring≤ 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.

StatusMeaning
400Invalid body, unsupported locale or malformed Idempotency-Key
401Missing or invalid API key
402Not enough AI credits (credits_needed, credits_remaining in the body)
422Idempotency-Key reused with a different body
429Rate limited; wait for Retry-After seconds
502The model failed; nothing was charged. Retry after Retry-After
503Hosted translation unavailable

Self-hosting the backend

Hosted translation is off unless you enable it. Set these on the backend:

Variable
HOSTED_TRANSLATION_PROVIDERRequired to enable: gemini, anthropic, openai, groq or openrouter. Unset or off disables it.
HOSTED_TRANSLATION_API_KEYThe 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_MODELOptional. Defaults to the provider's default model (gemini-3.5-flash for Gemini).
TRANSLATOR_URLOptional 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_URLRecommended. 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

How is this guide?

On this page