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
13 commands, in the order the cycle uses them. Each one links to its section.
Scaffolding
verbaly init sets up a project in one command: a typed config (.ts if you have a tsconfig, .mjs otherwise), your source catalog plus one per --locales, and the next steps for what it finds: Astro, Nuxt, Next.js, SvelteKit and Vite each have their own integration, and any other bundler uses @verbaly/unplugin.
npx verbaly init --locales es,pt- created: verbaly.config.ts, locales/en.json, locales/es.json, locales/pt.json
- detected: vite
- next steps:
pnpm add -D @verbaly/vite
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- Passed config: verbaly.config.ts found
- Passed catalogs: 3 locales (en, es, pt) in locales/
- Passed plugin: @verbaly/vite installed for vite
- Warning types: verbaly.d.ts is stale fix: run
npx verbaly extract - Warning orphans: 2 catalog keys are no longer referenced (old.title, hero.cta) fix: run
npx verbaly extract --pruneto drop them - 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 fromvirtual:verbaly - Error translations: 1 broken translation (es): present but not rendering what the source renders fix: run
npx verbaly checkto read what each one lost
One command to answer “why isn’t this translating?” It checks your config and catalogs, whether the right integration for your framework is installed, whether verbaly.d.ts is fresh, orphan keys, and the health of your translations: the missing ones, the broken ones and the warnings. It also catches two things the build gate cannot see: t imported from a verbaly package, which no bundler can resolve, and a message that kept a plural or format block as literal text. It also names any file it could not parse, as a warning: its texts are not extracted, but the file may still build in your project. A healthy report means a build that passes: doctor fails on everything check fails on, plus setup faults like those two, and never on something that builds and renders.
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.
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.
--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 verbaly.d.ts. It warns when a message kept a plural or format block as literal text, and names any file it could not parse instead of stopping on it.
Coverage
verbaly status shows translation coverage per locale at a glance (es: 45/48 translated), how many machine translations still wait for review and how many are broken. It is informational only and never fails; --json gives the same numbers to badges and tooling.
Pseudo-localization
verbaly pseudo fills a QA catalog (en-XA by default) from 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.
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.
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:
import type { TranslateProvider } from '@verbaly/compiler';
const provider: TranslateProvider = async ({ sourceLocale, targetLocale, messages, origins }) => ({
/* key → translation */
});
export default { translate: { provider } };Review the drafts
Machine output is a draft, not reviewed work. translate records everything it writes in locales/.verbaly-drafts.json (committed with your catalogs, never edited by hand), and verbaly review closes the loop: read the drafts, then approve them.
Want merges blocked until a human signs off? Add --drafts to the check in your CI: it keeps failing while unreviewed machine translations remain. 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.
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 two questions, not one. Is every message translated? And can each translation render what the source renders? A filled-in translation is not automatically a working one, so these stop the build too, wherever they came from: a translator, a machine or a hand edit.
| The translation | Why it fails |
|---|---|
lost a {param}, or renamed it | the value never reaches the text |
lost an <em>, or gained one | the emphasis or the link marker is gone |
| turned a plural block into plain text | one wording for every count |
has a plural block with no other case | every count it does not list renders empty |
Two findings are warnings instead: they print, the exit code stays 0, because the text still renders. One is a plural set missing forms the language needs, since Polish and Arabic ask for more than English. The other is a specific case like =0 that the source had and the translation dropped, so that count now falls back to other.
Whatever fails, the report closes with the step that repairs that failure: scaffold and fill for a message with no translation yet, correct the key for one that lives in no catalog, and repair the message itself for a broken one.
CI example
- run: pnpm install
- run: npx verbaly check --reporter github # each finding becomes a PR annotation
- run: pnpm buildWith --reporter github every finding shows up as an annotation on the pull request, pointing at the file and line where the text lives: errors as errors, warnings as warnings, so a warning never turns the job red. Without it you get the plain text list, same exit code.
Static rendering (SSG)
verbaly render kills the flash of untranslated content on static sites: it walks your built HTML and pre-fills every data-verbaly element per locale using the real runtime: plurals, Intl formatting, data-verbaly-args, attribute translation and whitelisted rich text. Inside a language, links to pages that language also has keep its prefix, so a visitor who arrives there stays there. Getting them there is --redirect, below.
- Each language gets
<html lang>and<html dir>set (right-to-left locales mirror correctly); the source locale is filled in place, the rest mirror todist/<locale>/…. - The
data-verbalyattributes stay in the output, so client-side locale switching keeps working on top of the pre-rendered page. - Messages are HTML-escaped (never injected), missing keys are reported and left untouched, re-runs are idempotent, and translated attributes pass the same guards as the runtime: unsafe URLs blocked,
style/srcdocnever written. - Named links: set
render.linksinverbaly.config.*(ordata-verbaly-linksper element) and rich messages pre-render real<a href>elements, attribute-escaped, withjavascript:blocked. - Multi-locale SEO: set
render.baseUrl(or pass--base-url) and every page gets reciprocalhreflangalternates;--sitemapwrites an i18n sitemap,--cleandrops stale locale pages. - The sitemap lists only pages a search engine should index: one that redirects, or carries
noindex, is left out. Every page is still translated and published, so a visitor who lands on one gets it in their language. Leave more out by name withrender.exclude, a list of globs against the path inside your build directory. - Using a different data attribute?
--attributepoints the renderer at it, the same way the runtime takes one, so both sides stay in step. - Self-canonical mirrors:
rel="canonical"andog:urlare rewritten to each locale's own URL, because a cross-locale canonical would make search engines ignore the hreflang set. - Send a visitor to their language:
--redirect(orrender.redirect) puts a tiny script at the top of your home page that moves them before anything is drawn. It only fires there, on purpose: a search engine asking for a deep page has to get that page and not a detour. A saved choice always wins, the query and the hash come along, and it can never loop because it does nothing once you are already in that language. Use{ on: 'all' }if you want every page of the source language to route, andstorageKeyto point it at the key you save the choice under. - Site served from a subfolder?
--base /app(orrender.base) tells the renderer where the site starts, so links inside each language read/app/es/docsinstead of pointing nowhere. The same option exists onlocaleFromPathandlocalePathso the runtime agrees with the build.
Flags
| Flag | Default | Description |
|---|---|---|
| --root | cwd | Project root |
| --dir | locales | Catalogs directory |
| --source | en | Source locale |
| --locales | from files | Extra locales, comma-separated |
| --prune | off | Drop keys no longer referenced (extract only) |
| --watch | off | Re-extracts as source files change: live extraction for webpack, Rspack and Rollup setups (extract only) |
| --locale | en-XA | Pseudo-locale id (pseudo only) |
| --site | dist | Built site directory (render only) |
| --base | site root | Subpath the site is served under (render only) |
| --redirect | off | Send 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.