Skip to content

Reference

CLI

The verbaly command lives in @verbaly/compiler. Add it to your dev dependencies so npx verbaly resolves: package managers link a command only for a package you installed yourself, and the plugins depend on the compiler without installing it for you.

Commands

14 commands, in the order the cycle uses them. Each one links to its section.

Scaffolding

verbaly init sets a project up in one command. You get a typed config (.ts if you have a tsconfig, .mjs otherwise) and your catalogs: the source one, plus one for every locale you pass in --locales. Then it tells you what to wire next. Astro, Nuxt, Next.js, SvelteKit and Vite each have their own integration; any other bundler uses @verbaly/unplugin.

npx verbaly init --locales es,pt
  1. created: verbaly.config.ts, locales/en.json, locales/es.json, locales/pt.json
  2. detected: vite
  3. next steps: 1. pnpm add verbaly @verbaly/vite
  4. 2. add verbaly() to the plugins in vite.config

Re-running is safe: an existing config or catalog is reported as “kept” and left byte-identical.

Health check

verbaly doctor is init's sibling for projects already running: it inspects the whole setup and reports every finding with the exact command that fixes it. Green means healthy; problems exit 1.

npx verbaly doctor
  1. Passed config: verbaly.config.ts found
  2. Passed catalogs: 3 locales (en, es, pt) in locales/
  3. Passed plugin: @verbaly/vite installed for vite
  4. Warning types: src/verbaly.d.ts is stale fix: run npx verbaly typegen
  5. Warning orphans: 2 catalog keys are no longer referenced (old.title, hero.cta) fix: run npx verbaly extract --prune to drop them
  6. Warning outdated: 1 translation written for an older source text (es: hero.title) fix: update them, or keep them with npx verbaly review --approve
  7. Error imports: 1 file imports t from a verbaly package, which never exports it (src/app.ts) fix: t comes from your instance (React: const t = useT()) or from virtual:verbaly
  8. Error translations: 1 broken translation (es): present but not rendering what the source renders fix: run npx verbaly check to read what each one lost

One command to answer “why isn’t this translating?” It looks at four things.

A clean report means check passes too. doctor fails on everything check fails on, plus setup faults like that import, and never on something that builds and renders fine.

Wrapping hardcoded text

verbaly wrap finds hardcoded text in your JSX and wraps it in t`…` for you. It reports first, and --write applies. Anything ambiguous is listed for a human instead of guessed, and a file where t is not available is left untouched and reported, so the project always still compiles.

npx verbaly wrap           # report what it would wrap
npx verbaly wrap --write   # apply it

Porting from another library

verbaly migrate ports the catalogs you already have. Your file keeps its shape, nested or flat, and the only thing it must rewrite is the interpolation: {{name}} becomes {name}, because a doubled brace is how Verbaly writes a literal one. It reports first, and --write applies.

npx verbaly migrate             # report what it would port
npx verbaly migrate --write     # apply it
npx verbaly migrate --plurals --write   # and merge _one/_other

--plurals is the optional half: it merges _one and _other into one message with variants, which is what buys automatic plural selection, and inside it the count prints itself already localized. It knows the older key + key_plural spelling too. Without the flag those keys keep working exactly as they are.

What it will not guess is listed instead: an i18next format inside the braces, a {{- unescaped}} value, a $t() nesting, and a merge that would overwrite a key you already use. Those messages are left exactly as they were.

Extraction

verbaly extract scans your sources (.js/.ts/.jsx/.tsx, plus .svelte, .vue and .astro), writes new messages to the source catalog, adds "" placeholders to the rest and regenerates the types. It names any file it could not parse instead of stopping on it. Every source text that followed your code gets one line with the text it had before, so an edit you made in the code reads as a change and one made in the catalog shows what it undid. Then it warns, with file and line, about what would otherwise go unnoticed: a key written with two texts, a t under another name, a message that kept a plural or format block as literal text, and a value a translator would see as {_0}. With --watch, each warning is said once, and again only if it comes back.

npx verbaly extract             # sync catalogs + types
npx verbaly extract --prune     # and drop keys nothing references
npx verbaly extract --dry-run   # say what it would change, write nothing

Type generation

verbaly typegen writes the TypeScript types of your messages and nothing else, from the text your build ships, where your framework reads them: src/verbaly.d.ts in a Vite or SvelteKit app, .verbaly/types.d.ts in Next.js, and Astro's and Nuxt's own types folder. extract, the dev servers and the build write the same file, so you only need it where none of them ran: a fresh clone, or a CI job about to run tsc.

npx verbaly typegen && tsc --noEmit   # a fresh clone, or CI

Coverage

verbaly status shows translation coverage per locale at a glance (es: 45/48 translated), how many machine translations still wait for review, how many are broken and how many were written for an older source text. It is informational only and never fails; --json gives the same numbers to badges and tooling.

npx verbaly status          # es: 45/48 translated
npx verbaly status --json   # the same numbers, for badges and tooling

Pseudo-localization

verbaly pseudo fills a QA catalog (en-XA by default) from the text your build ships in the source locale: accented letters, ⟦…⟧ markers and ~33% padding. Text that stays clean in the pseudo build is hardcoded; layouts that clip reveal themselves before a real translation does.

npx verbaly pseudo                 # locales/en-XA.json
# "Hello {name}" → "⟦Ĥéĺĺó {name} ~⟧"

Params, variant blocks and named tags survive verbatim, guaranteed by the same structural validation as translate. Re-running regenerates the whole catalog.

Machine translation

verbaly translate closes the loop: write → extract → translate → check green. The default provider uses Claude (@anthropic-ai/sdk as an optional peer + ANTHROPIC_API_KEY); batches of 20 per request, each one telling the model which source files the text lives in, so it translates with context. Any current Claude model works through translate.model or --model, and a batch the model declines goes to a fallback model instead of coming back untranslated.

npx verbaly translate --dry-run        # list what's missing, write nothing
npx verbaly translate --locales es,pt  # fill only these locales
npx verbaly translate --model claude-opus-4-8  # override the default (claude-sonnet-5-5)

Every translation is checked: placeholders and tags must survive verbatim. If one doesn't, the translation is rejected and the entry stays "", so check keeps flagging it until it's fixed.

No lock-in: plug your own provider in verbaly.config.ts. In TypeScript, TranslateProvider types it for you, and origins tells you which source files each text appears in, so you can translate it for where it is used:

verbaly.config.ts
import type { TranslateProvider } from '@verbaly/compiler';

const provider: TranslateProvider = async ({ sourceLocale, targetLocale, messages, origins }) => ({
  /* key → translation */
});

export default { translate: { provider } };

Review what is waiting

Machine output is a draft, not reviewed work. translate records everything it writes in locales/.verbaly-state.json (committed with your catalogs, never edited by hand), and verbaly review closes the loop: read the drafts, then approve them. The same file remembers which source text each translation was written for, from the moment it is written, so when you change a text, its translations wait in review too, as outdated: update them, or approve them if they still hold. Each one is listed with the text that ships next to its translation, so you can judge it right there, and --approve followed by keys accepts only those.

npx verbaly review               # each one with its source text and its translation
npx verbaly review hero.title --approve   # accept only the keys you name
npx verbaly review --approve     # accept everything after reading
npx verbaly check --drafts       # CI gate: fail while drafts remain
npx verbaly check --outdated     # and while a translation is older than its text

Want merges blocked until a human signs off? Add --drafts to the check in your CI: it keeps failing while unreviewed machine translations remain, and --outdated does the same for translations written for an older source text. A translator file brought in with import counts as review on its own.

Translators & TMS

Humans in the loop? verbaly export writes translator-ready XLIFF 2.0, CSV or gettext PO files per locale and verbaly import brings them back, structure-validated with the same gate as translate. The full flow, TMS options included, lives in Work with translators.

verbaly import validates every entry and rejects and reports the broken ones. PO entries marked fuzzy count as untranslated, and imported keys stop counting as unreviewed drafts, unless you pass --draft for a file nobody has read yet. A file's language is read as one of yours, so gettext's pt_BR fills your pt-BR. A file for a new language starts its catalog under its proper code, unless your config lists its locales: then a language outside the list is skipped with a warning and exit code 1, because your app would never load it.

Export also writes native mobile resources: --format android-xml for Android's res/ layout and --format ios-strings for Xcode. Untranslated keys are left out so the app falls back to your source language (--missing applies only to the translator formats). Every format writes to verbaly-export/ unless you point --out elsewhere.

What the gate checks

check asks three questions. Is every message translated? Does every key the code writes exist? And can each translation render what the source renders? A filled-in translation is not automatically a working one, so these fail check too, wherever they came from: a translator, a machine or a hand edit. Every build lists the same problems and finishes, so run check in CI.

The translationWhy it fails
lost a {param}, or renamed itthe value never reaches the text
lost an <em>, or gained onethe emphasis or the link marker is gone
turned a plural block into plain textone wording for every count
has a plural block with no other caseevery count it does not list renders empty

Some findings are warnings instead: they print and the exit code stays 0, because the text still renders.

A build runs the same gate, and when it finds warnings it prints one line that says how many, then passes.

Whatever fails, the report ends by telling you how to fix it: scaffold and fill a message that has no translation yet, correct the key of one that lives in no catalog, repair the message itself when it is broken.

CI example

.github/workflows/ci.yml
- run: pnpm install
- run: npx verbaly check --reporter github   # each finding becomes a PR annotation
- run: pnpm build

With --reporter github, every finding shows up as an annotation on the pull request, on the file and line where the text lives. Errors arrive as errors and warnings as warnings, so a warning never turns the job red. Without it you get the plain list, with the same exit code.

Static rendering (SSG)

verbaly render is what kills the flash of untranslated text on a static site. It walks your built HTML and fills in every data-verbaly element, once per language, using the real runtime: plurals, Intl formatting, data-verbaly-args, attributes and whitelisted rich text. Inside a language, links to pages that language also has keep its prefix, so whoever arrives stays there. Getting them there is --redirect, below.

npx vite build              # or astro build, eleventy, …
npx verbaly render          # dist/index.html filled in the source locale
                            # dist/es/index.html, dist/pt/… for the rest
npx verbaly render --site out --locales es  # custom dir / locale filter

Flags

FlagDefaultDescription
--rootcwdProject root
--dirlocalesCatalogs directory
--sourceenSource locale
--localesfrom filesRun on these locales only, the source always included (init: the ones to create)
--pruneoffDrop keys no longer referenced (extract only)
--watchoffRe-extracts as source files change: live extraction for webpack, Rspack and Rollup setups (extract only)
--localeen-XAPseudo-locale id (pseudo only)
--sitedistBuilt site directory (render only)
--basesite rootSubpath the site is served under (render only)
--redirectoffSend a visitor on the home page to their language (render only)

Flags are validated per command: a flag that belongs to another command exits with an actionable error instead of being silently ignored: translate --locale es tells you it means --locales.

Config file

Flags win over verbaly.config.{js,mjs,ts,mts,json} at the project root, and the plugin takes the same options. Every one of them, with its default, lives in Configuration.

Coding agents

Your coding agent can run this same cycle without touching the shell, through the @verbaly/mcp server. That channel has a page of its own. Coding agents covers the server, its tools and resources, the installable Agent Skill and the llms.txt index this site serves.

↑↓to moveEnterto open
Copied to clipboard