What is Verbaly?
Most i18n tools make you maintain key files by hand: keys drift from the code, there's no type-safety, and setup is heavy.
Verbaly inverts the flow. You write the source text right in your code. A build plugin extracts the messages, generates stable keys, types, and one module per locale. So you get compiler-grade safety with a runtime of 3.35 KB gzip and zero dependencies. That number is measured on every release by a gate in the repo, not estimated: pnpm --filter verbaly size.
// You write this:
t`Hello ${name}, you have ${count} messages`;
// The compiler generates: a stable key + inferred types + per-locale entries.
t('usNGUet6', { name, count }); // ← fully typed, tree-shakeable
Missing a translation? Every build says which ones, and verbaly check stops your CI on them, so a raw key never reaches a release.
Quick start
pnpm add verbaly @verbaly/vite
pnpm add -D @verbaly/compiler # the verbaly command: init, extract, check
npx verbaly init
// vite.config.ts
import verbaly from '@verbaly/vite';
export default {
plugins: [verbaly()],
};
// verbaly.config.ts, created by npx verbaly init: list your languages, the build and `verbaly check` read them
export default { sourceLocale: 'en', locales: ['en', 'es', 'pt'] };
// anywhere in your app
import { t, setLocale } from 'virtual:verbaly';
t`Hello ${name}`; // extracted + typed on save
await setLocale('es'); // per-locale bundle loaded on demand
The same works inside .svelte and .vue components, script and markup:
<h1>{$t`Hello ${name}`}</h1>
Plain HTML, no framework? Bind by attribute:
<h1 data-verbaly="home.title"></h1>
👉 Full docs: https://verbaly-web.vercel.app/docs
Packages
Why it's different
- Hybrid compiler + runtime: static text is compiled (types + tree-shaking); dynamic/CMS content has a real runtime path. No forced trade-off.
- Type-safe params:
{name}, plurals, currency and dates are inferred from the message itself. Wrong or missing params fail to compile. Intlall the way: number, currency, date/time, relative time ({when:relative}), lists ({langs:list}) and units ({d:unit/kilometer}). Zero dependencies, the platform does the heavy lifting.- Tiny & tree-shakeable: ~3KB gzip core, zero dependencies, per-locale code-splitting.
- Fast, with receipts: fully memoized hot path: 5-35× faster than i18next on lookup/interpolation/plurals (
pnpm bench, benchmarked every release). - No proprietary format: plain, portable JSON catalogs. No lock-in. Most TMS platforms ingest them natively, and
verbaly export/importround-trips XLIFF 2.0, CSV or gettext PO with human translators; exports carry the source-file location of every message so translators see context, and imports are structure-validated, so a typo in a{param}or tag never reaches your UI. The same catalogs also export as native mobile resources (--format android-xml/ios-strings) for a companion app. - Works with plain HTML: a
data-verbalyDOM interpreter for the framework-less case, with opt-in rich text (data-verbaly-rich, whitelist-based, XSS-safe), named links (richLinks; hrefs from your code, never from messages) and locale bootstrap helpers (resolveLocale/persistLocale). - Catches missing and broken translations before a release: the #1 i18n pain, gone.
verbaly checkreads every translation against its source and rejects the ones that cannot render it, whether they came from a translator, a machine or a hand edit. A dropped{param}, a lost<em>, a flattened plural block, a plural set with no catch-all case (that one renders an empty string) all fail it with the reason in plain words, annotated on the source line in CI. Every build lists them too, broken ones first, and goes on: a text never stops a deploy unless you ask for it withfailOnMissing: true. It also warns, without failing, when a language needs plural forms your catalog does not carry yet. And when a catalog reaches your app without passing through the build (a lazy loader, a CMS), the runtime reports the same problems in the console instead of degrading quietly: it never crashes on bad data, and it never hides it either. - RTL and language names built in: adding Arabic or Hebrew just works:
switchLocale, the SSR integrations andverbaly renderkeep<html dir>right on their own, andlocaleName('es')gives your locale switcher real names ('español') viaIntl.DisplayNames, no hardcoded tables. - Static sites ship translated:
verbaly renderpre-fills your built HTML per locale (dist/es/…,<html lang>and<html dir>set). No flash of untranslated content on SSG. - i18n QA built in:
verbaly pseudogenerates a pseudo-locale (⟦Ĥéĺĺó ~⟧) that exposes hardcoded strings and clipped layouts;verbaly translatefills real locales via Claude or your own provider;verbaly doctordiagnoses the whole setup with the exact fix for each finding.
How it compares
Coding agents
Verbaly ships first-class support for AI coding agents:
-
MCP server: give your agent the whole cycle as tools (
verbaly_init,verbaly_doctor,verbaly_wrap,verbaly_extract,verbaly_status,verbaly_missing,verbaly_translate,verbaly_write_drafts,verbaly_drafts), each answering with structured output as well as text, plus resources that let it read your config and your catalogs without spending a call:claude mcp add verbaly -- npx -y @verbaly/mcpMachine translations stay drafts until a human approves them (
verbaly review --approve, or the local review server below), so an agent can fill gaps without silently shipping unreviewed text. An agent that translates on its own saves its work withverbaly_write_drafts, checked and marked as a draft the same way. Approving is deliberately not a tool. -
Agent Skill:
skills/verbalyteaches an agent the write → extract → check → translate cycle and the rules that keep it safe. Install it into a project:npx degit AronSoto/verbaly/skills/verbaly .claude/skills/verbaly -
llms.txt: the docs site publishes verbaly-web.vercel.app/llms.txt for agents that read documentation.
Review translations locally
@verbaly/studio serves your whole project over localhost: every catalog, what is missing, what is
broken, what a machine wrote, and where each message is used in your code.
npx verbaly-studio
Your JSON catalogs are the database and git is the history, so it cannot fall out of sync with your
code: it does not own anything. It never edits your source text, because that lives in your code
and the key derives from it, and a translation you save runs the same two validations
verbaly check runs in CI.
The panel opens on a list of your messages: you edit in the row itself, you mark what a machine
wrote as read, you read what verbaly doctor found, and you run the two commands that change your
catalogs. Translating shows the bill first, because it is the one that spends money. --json
prints the same object the panel reads, so the whole thing is scriptable too.
Editor: translations on hover
Verbaly keys are hashes, so reading t('a1B2c3D4') in a diff tells you nothing. The i18n-ally extension can show the real text inline and on hover once you point it at your catalogs. Two files, and no Verbaly config changes:
.vscode/settings.json
{
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.keystyle": "flat",
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "en"
}
.vscode/i18n-ally-custom-framework.yml
languageIds:
- javascript
- typescript
- javascriptreact
- typescriptreact
- vue
- svelte
- astro
usageMatchRegex:
- "[^\\w\\d]t\\(\\s*['\"`]({key})['\"`]"
monopoly: true
keystyle has to match the shape of your own catalogs: flat if they hold dotted keys, nested if they hold groups. Verbaly reads both and writes back the shape your file already has, so the extension is the one that needs telling. Use the extension for reading, not for writing: its extract and rename actions assume you own the keys, and in Verbaly the compiler does. New keys come from verbaly extract, translations from your catalogs, verbaly export/import or verbaly translate.
Development
pnpm install
pnpm build # tsdown → ESM + CJS + .d.ts
pnpm test # Vitest
pnpm coverage # full suite + lcov report
pnpm typecheck
pnpm --filter verbaly bench # hot-path benchmarks vs i18next
⚠️ Early development.
0.xpublished: API not stable yet.
Feedback
Using Verbaly? That already helps. What helps even more:
- Something broke or surprised you → open a bug. A minimal snippet is enough.
- Something felt like friction → tell us what. The roadmap is driven by real use, not by feature lists.
- It just works → a star or a mention helps others find it.
Contributions welcome: see CONTRIBUTING.md.