- Type-safe keys & params
- Inferred from your text
- Runtime size (gzip)
- ~3 KB, zero deps
- Key maintenance
- None, auto-extracted
- Dynamic / CMS content
- Real runtime path
- Plain HTML (no framework)
- DOM interpreter
- Missing translations
- Build fails
- Catalog format
- Plain JSON
Writenaturaltext.Shiptype-safetranslations.
The compiler extracts your messages, generates the types and splits bundles per locale. A missing translation fails the build, so it never reaches your users.
Features
Why Verbaly
Zero config, actually
One Vite plugin. No giant init(), no Babel macros, no proprietary formats. Catalogs are plain JSON you can take anywhere.
import verbaly from '@verbaly/vite';
export default { plugins: [verbaly()] };Types from your messages
The text itself declares its params, so if you forget one TypeScript stops you.
t('inbox', { count: 3 }); ✓
t('inbox'); ✗ { count: number }The build gate
Missing translations abort vite build, so raw keys never reach your users.
✗ [es] x7Ka9q2f: "Welcome back"
✓ all locales completePlain HTML, first-class
No framework? The DOM interpreter hydrates data-verbaly nodes, re-renders on locale switch and watches new elements.
<h1 data-verbaly="home.title"></h1>
<p data-verbaly="inbox" data-verbaly-args='{"count":3}'></p>Compare
Same job, less machinery
An honest look against the tools you'd otherwise reach for. It shows where Verbaly wins, and where the others are fine too.
| Feature | i18next | Lingui | Paraglide | |
|---|---|---|---|---|
| Type-safe keys & params | Inferred from your text | Plugin / manual | Partial | Yes |
| Runtime size (gzip) | ~3 KB, zero deps | ~14 KB (+9 KB react) | ~5 KB | ~0 (compiled) |
| Key maintenance | None, auto-extracted | By hand | Extract → compile step | By hand |
| Dynamic / CMS content | Real runtime path | Yes | Yes | Weak |
| Plain HTML (no framework) | DOM interpreter | — | — | — |
| Missing translations | Build fails | Runtime warning | CI step | Build fails |
| Catalog format | Plain JSON | JSON | PO / JSON | inlang format |
- Type-safe keys & params
- Plugin / manual
- Runtime size (gzip)
- ~14 KB (+9 KB react)
- Key maintenance
- By hand
- Dynamic / CMS content
- Yes
- Plain HTML (no framework)
- —
- Missing translations
- Runtime warning
- Catalog format
- JSON
- Type-safe keys & params
- Partial
- Runtime size (gzip)
- ~5 KB
- Key maintenance
- Extract → compile step
- Dynamic / CMS content
- Yes
- Plain HTML (no framework)
- —
- Missing translations
- CI step
- Catalog format
- PO / JSON
- Type-safe keys & params
- Yes
- Runtime size (gzip)
- ~0 (compiled)
- Key maintenance
- By hand
- Dynamic / CMS content
- Weak
- Plain HTML (no framework)
- —
- Missing translations
- Build fails
- Catalog format
- inlang format
Sizes are approximate gzip runtime footprints; every project weighs trade-offs differently.
Start here
From words to shipped locales.
Three steps and a guardrail, or let verbaly init scaffold it in one command. No key files written by hand: the compiler does the boring parts while you keep writing product copy.
Install
One command, two packages.
pnpm add verbaly @verbaly/viteAdd the plugin
Point it at the locale you write in.
vite.config.tsimport verbaly from '@verbaly/vite'; export default { plugins: [verbaly({ sourceLocale: 'es' })] };Write text, not keys
Keys, types and catalogs: generated.
src/app.tsimport { t, setLocale } from 'virtual:verbaly'; t`Hola ${name}`; // keys, types & catalogs: generated await setLocale('pt'); // lazy-loads its chunkLet CI guard it
Exits 1 while any locale is incomplete, so untranslated text never ships.
.github/workflows/ci.yml- run: npx verbaly check # the build gate, in your pipeline
Rather break it yourself?
Edit the catalogs, switch locales and empty a translation, all on the real runtime, live.