New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

verbaly

Package Overview
Dependencies
Maintainers
1
Versions
84
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

verbaly

Effortless i18n: write natural text, ship type-safe, tree-shakeable translations.

Source
npmnpm
Version
0.73.0
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

Verbaly

Effortless i18n: write natural text, ship type-safe, tree-shakeable translations.

npm version 3.32KB gzip zero dependencies TypeScript MIT

Most i18n tools make you maintain key files by hand: keys drift from the code, no type-safety, heavy setup. Verbaly inverts the flow: write the source text in your code, a build plugin extracts stable keys, types and per-locale modules. Compiler-grade safety, a 3.32 KB runtime, zero dependencies. That is createVerbaly on its own; a site with no framework, which also pulls the DOM interpreter and the url helpers, ships 6.14 KB. Both are measured on every release by pnpm --filter verbaly size, never estimated.

// 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

// Prefer readable keys? Opt in per message:
t.id('inbox.title')`Hello ${name}`; // → t('inbox.title', { name })

Missing a translation? Every build says which ones, and verbaly check stops your CI on them, so a raw key never reaches a release. A translation that dropped your {name} or flattened a plural block fails check too: being filled in is not the same as working.

🚀 Try it in 30 seconds

pnpm add verbaly @verbaly/vite
// vite.config.ts
import verbaly from '@verbaly/vite';

export default { plugins: [verbaly({ locales: ['en', 'es', 'pt'] })] };
// anywhere in your app, extracted + typed on save
import { t, setLocale } from 'virtual:verbaly';

t`Hello ${name}`;
await setLocale('es'); // per-locale bundle loaded on demand

With the compiler, every t your project holds is typed by its catalog: the generated types fill in Register, so Translate and Locale (both exported here, types only, zero bytes) are your keys and your locales, and an instance's t checks keys and params too.

🎛️ Standalone runtime (no compiler)

verbaly also works on its own for dynamic/CMS content:

import { createVerbaly } from 'verbaly';

const v = createVerbaly({
  locale: 'en',
  messages: { en: { greeting: 'Hello {name}' } },
  loaders: { es: () => import('./locales/es.json') }, // lazy catalogs
});

v.t('greeting', { name: 'Aron' }); // "Hello Aron"
v.setLocale('es'); // auto-loads the catalog; or: await v.loadLocale('es') first

🌐 Plain HTML, no framework needed

<h1 data-verbaly="home.title"></h1>
<p data-verbaly="home.intro" data-verbaly-rich></p>
<!-- rich: 'The build <em>gate</em>' renders a real <em> (whitelist, XSS-safe) -->
<p
  data-verbaly="home.cta"
  data-verbaly-rich
  data-verbaly-links='{"repo":"https://github.com/x"}'
></p>
<!-- links: 'See the <repo>repo</repo>' renders <a href>; hrefs from you, never from messages -->
import { bindDom, createVerbaly, persistLocale, resolveLocale } from 'verbaly';

const v = createVerbaly({
  locale: resolveLocale({ supported: ['en', 'es', 'pt'] }), // storage → navigator → fallback
  /* … */
});
// { root: document } so the <title> and the meta description translate with the page
bindDom(v, {
  richLinks: { docs: { href: '/docs', target: '_blank', rel: 'noopener' } }, // named links
}); // renders + re-renders on locale change

v.setLocale('es');
persistLocale('es'); // localStorage + <html lang> + <html dir>
// locale switchers without hardcoded names or direction tables
import { localeDirection, localeName } from 'verbaly';

localeName('es'); // 'español' (endonym, via Intl.DisplayNames)
localeName('de', 'en'); // 'German'
localeDirection('ar'); // 'rtl': switchLocale/persistLocale already apply it to <html dir>
// switchLocale is the whole switch: navigation or in-place swap, then both persistence channels
// server-side (SSR): one instance per request, locale from the request itself
import { createVerbaly, resolveRequestLocale } from 'verbaly';

// the address wins when it carries the language: a url is public, a cookie is one visitor
const locale = resolveRequestLocale({
  supported: ['en', 'es', 'pt'],
  path: url.pathname,
  routing: 'prefix-except-source',
  cookie: cookies.get('verbaly-locale'),
  header: request.headers.get('accept-language'),
  fallback: 'en',
});
const v = createVerbaly({ locale, fallback: 'en', messages });
// the url helpers, all pure and safe on a server
import { alternateLinks, localePath, stripLocalePath } from 'verbaly';

localePath('pt', { supported, sourceLocale: 'en', path: '/es/docs' }); // '/pt/docs'
stripLocalePath({ supported, path: '/es/docs' }); // '/docs', the route behind the url
alternateLinks({ supported, sourceLocale: 'en', path: '/docs', baseUrl: 'https://x.dev' });
// one hreflang entry per locale plus x-default, the same set `verbaly render` writes

✨ What you get

  • Hybrid compiler + runtime: static text compiled (types + tree-shaking), dynamic content via a real runtime path.
  • Type-safe params: {name}, plurals, currency and dates inferred from the message; wrong/missing params fail to compile.
  • Intl-powered format: number/currency/date/time/relative/list/unit + CLDR plurals and select/gender, tiny surface. 'Updated {when:relative}' · '{langs:list}' · '{d:unit/kilometer}'.
  • Plain, portable JSON catalogs: no proprietary format, no lock-in.
  • A custom formatter knows which message it was called for: the fourth argument carries the param name and the message key, so its own errors can name them.
  • ICU and relative time are weight you only carry when you use them: the compiler sees them in your catalogs and wires what is needed, so an app whose messages use neither never ships those 862 bytes. A message that arrives after the build with ICU syntax shows its own source and says why, rather than rendering something that looks almost right.
  • DOM interpreter for framework-less HTML, with opt-in rich text (whitelist-based, XSS-safe) and named links (richLinks / data-verbaly-links; hrefs come from the caller, javascript: blocked).
  • Lazy catalogs: loaders + loadLocale load per-locale JSON on demand, in the runtime itself.
  • RTL and locale names built in: localeDirection keeps <html dir> right when you add Arabic or Hebrew (applied by switchLocale/persistLocale and verbaly render), and localeName gives your locale switcher real language names via Intl.DisplayNames.
  • Runtime devtools: opt-in verbaly/devtools answers "what key is this text?" in the browser: hover to see any element's key/locale/source, plus a live missing-keys panel. Tree-shaken out of production (its own chunk). Or wire the onResolve hook yourself.
  • Fast, with receipts: fully memoized hot path, benchmarked every release: 5-35× faster than i18next on lookup, interpolation and plurals.

🧩 Ecosystem

PackageDescription
verbalyCore runtime (this package)
@verbaly/viteZero-config Vite plugin
@verbaly/unpluginwebpack · Rollup · esbuild · Rspack
@verbaly/compilerExtraction + codegen + the verbaly CLI
@verbaly/react · @verbaly/vue · @verbaly/svelteFramework adapters (React also covers Preact)
@verbaly/astroAstro integration + per-locale static build
@verbaly/next · @verbaly/nuxt · @verbaly/sveltekitSSR: locale per request, hydration with no flash
@verbaly/mcpMCP server: the translation cycle as tools for an agent
@verbaly/studioLocal review server over your catalogs

All thirteen share one version number, so you never match compatible ranges.

📖 Docs: https://verbaly-web.vercel.app/docs/reference/api

🔁 Coming from i18next? Keep your keys and catalogs: the migration guide maps everything one-to-one.

⚠️ Early development (0.x): API not stable yet.

License

MIT © Aron Soto

Keywords

i18n

FAQs

Package last updated on 18 Oct 2026

Related posts