Skip to content

Reference

Runtime API

The verbaly core is ~3 KB gzipped with zero dependencies, and everything builds on native Intl.

createVerbaly(options)

import { createVerbaly } from 'verbaly';

const v = createVerbaly({
  locale: 'es',
  fallback: ['en'],
  messages: { es: { home: { title: 'Inicio' } } },
});
OptionTypeDescription
localestringInitial locale; defaults to navigator.language in the browser
fallbackstring | string[]Fallback chain after BCP-47 narrowing (es-MX → es)
messagesobjectLocale → tree; nested keys flatten to dots (home.title). An empty string counts as untranslated and keeps falling back, the same rule as check
loadersobjectLazy catalogs: locale → () => import('./es.json'), loaded on demand. The one for your starting locale is fetched right away, and the UI updates when it lands
formattersobjectCustom {v:name} formatters
partialstring[]Languages whose messages came with the page: nothing is fetched for them, and a message the page never showed loads the rest once instead of falling back in silence
onMissingfunction(key, locale): return a string to substitute; silences the default warn-once
onResolvefunction(info) on every t(): key, locale, value and status (hit / fallback / miss). Powers devtools

Instance

MemberDescription
t(key, params?)Translate. Keys and params are type-checked from your messages. Bad data never crashes a render, and never slips through unnoticed: a missing param, a plural block with no catch-all case, a placeholder with no argument, a value that is not text, a language code the browser cannot read. Each one warns once in the console, naming the message, the path or the code it came from
t`...`Tagged template: interpolates source text; the compiler upgrades it
t.id(key)`...`Readable-key opt-in: formats the inline source; the compiler rewrites it to a keyed call
setLocale(locale)Switch locale, notify subscribers, auto-loads a pending lazy catalog
loadLocale(locale)Load a lazy catalog (BCP-47 narrowing, deduped): await it before setLocale for a flash-free switch
addMessages(locale, tree, options?)Merge messages at runtime: the lazy-loading primitive. Pass { partial: true } when the messages are one page's worth, so the language stays incomplete and the next missing message still fetches the rest
subscribe(fn)Change listener; returns unsubscribe
has(key)Key exists in the current chain
inspect(key)Origin locale (from) + source text for a key (devtools/tooling)
localeCurrent locale (readonly)
localesLoaded + loadable locales (readonly): feed a language switcher from one source of truth
versionChange counter that powers framework adapters

Locale helpers

import { alternateLinks, inlineMessages, localeDirection, localeFromPath, localeName, localePath, negotiateLocale, persistLocale, resolveLocale, resolveRequestLocale, stripLocalePath, switchLocale } from 'verbaly';

// which language is this page? a fact of the url, undefined when it carries no prefix
localeFromPath({ supported: ['en', 'es', 'pt'] }) ?? 'en';

// which language does this visitor want? url prefix → storage → navigator → fallback
resolveLocale({ supported: ['en', 'es', 'pt'] });
persistLocale('es');                             // localStorage + <html lang> + <html dir>

// server-side: match an Accept-Language header
negotiateLocale('es-PE,en;q=0.8', ['en', 'es']);   // → 'es'

// per-request: url (when it carries the language) → cookie → header → fallback
resolveRequestLocale({ supported: ['en', 'es'], path, routing, cookie, header });

// client switch for SSR setups: catalog → locale → cookie + <html lang> + <html dir>
await switchLocale(instance, 'es');

// language switchers: names and direction from Intl, no tables
localeName('es');        // → 'español'
localeName('de', 'en');  // → 'German'
localeDirection('ar');   // → 'rtl'

// pre-rendered sites: the same page in another language, for a switcher that navigates
localePath('pt', { supported: ['en', 'es', 'pt'], sourceLocale: 'en' });  // /es/docs?q=1#faq → /pt/docs?q=1#faq

// the route behind a localized url, for a router that keeps one tree
stripLocalePath({ supported: ['en', 'es'], path: '/es/docs' });   // → '/docs'

// this page's address in every language, for the head of a server-rendered page
alternateLinks({ supported, sourceLocale: 'en', path: '/docs', baseUrl });

// the messages `verbaly render` wrote into this page, when you asked it to
inlineMessages();   // → { 'home.title': 'El gate del build' } or undefined

localeFromPath and resolveLocale answer two different questions, and picking the wrong one is the classic bug. localeFromPath(options) asks which language this page is in. It reads the /{locale}/ prefix and returns undefined when the url carries none, so on a site with a tree per language you write localeFromPath(...) ?? sourceLocale and nothing can contradict the url. resolveLocale(options) asks which language this visitor wants: the url prefix first, then the stored choice, then navigator.languages with BCP-47 narrowing, then your fallback (path: false switches the url off). Use it to decide where to send someone, never to decide what a page already says. persistLocale remembers a switch. All of them are SSR-safe. The first two take a base for a site served from a subfolder, and the storage ones accept a custom storageKey (false turns storage off). See Plain HTML → Locale bootstrap for the full pattern.

negotiateLocale(header, supported, fallback?) is the server-side counterpart: give it an Accept-Language header and your locales and it returns the best match. It respects quality values and ignores case. Each language finds the closest locale you support: es-PE matches es, zh-HK matches zh-Hant, and pt or pt-PT matches pt-BR, but never one in another script (zh-HK never gets zh-Hans). It works with any server; the SvelteKit, Nuxt and Next.js integrations use it under the hood.

resolveRequestLocale(options) makes the whole per-request decision in one call. Give it the path and your routing, and the address decides: a prefixed page is that language for everyone, and a page with no prefix is your source language, so a saved choice can never translate it by accident. Leave routing out and the order is the stored cookie first, then the Accept-Language header, then your fallback. LOCALE_STORAGE_KEY exports the shared name (verbaly-locale) that the browser storage and the SSR cookie both use.

inlineMessages(root?) reads the messages verbaly render wrote into the page when render.inlineCatalog is on, so a pre-translated page needs no catalog over the network. You rarely call it: the generated module already does, and passes the result as partial so a message the page never showed still loads on demand instead of falling back in silence.

stripLocalePath(options) is the inverse of localePath: it gives the route behind a localized address, so a router can match one tree instead of one per language. alternateLinks(options) returns this page's address in every language plus an x-default, ready for link rel="alternate" tags in a server-rendered head. It is the same set verbaly render writes into a static site, and it is empty when your addresses carry no language, because one address cannot honestly claim to be every language at once.

switchLocale(instance, locale, options?) is the client-side switch the SSR integrations share: it loads the catalog first, then switches the locale, writes the verbaly-locale cookie and updates the lang and dir attributes. Safe to call on the server (it does nothing there); @verbaly/sveltekit re-exports it.

localeName(locale, displayIn?) and localeDirection(locale) feed a language switcher without hardcoded tables: real language names via Intl.DisplayNames (the name in its own language by default) and the writing direction of any locale. Right-to-left languages need no extra work: switchLocale, persistLocale, the SSR integrations and verbaly render already keep dir right on their own.

Type-level safety

editor
v.t('home.title');            ✓
v.t('home.title', { x: 1 });  ✗ no params declared
v.t('nope');                  ✗ unknown key

TypeScript parses your messages: FlatKeys flattens nested trees, ParamNames reads {param} occurrences, and TArgs makes params required exactly when the message declares them.

With the compiler, the generated types fill in Register, so Translate is your project's t and Locale the union of your locales: every t an adapter hands out (useT(), getT()) and every instance of the generated module checks keys and params against your catalog. An instance you build from a dictionary keeps that dictionary's types. All three are types only, so they cost nothing at runtime.

Low-level exports

Building tooling on top of Verbaly? The package exports the same pieces the compiler uses. parse(message) gives you the message AST, and parseTags(text) with RICH_TAGS is the rich-text tokenizer and its tag whitelist, which is what a custom renderer needs. flatten(tree) turns a nested tree into dot-keys. safeHref(href) and safeAttribute(name, value) are the URL and attribute guards, and normalizeLink(link) is the one link normalizer every adapter shares, along with the RichLink type.

↑↓to moveEnterto open
Copied to clipboard