Reference
Runtime API
The verbaly core is ~3 KB gzipped with zero dependencies, and everything builds on native Intl.
createVerbaly(options)
| Option | Type | Description |
|---|---|---|
| locale | string | Initial locale; defaults to navigator.language in the browser |
| fallback | string | string[] | Fallback chain after BCP-47 narrowing (es-MX → es) |
| messages | object | Locale → tree; nested keys flatten to dots (home.title). An empty string counts as untranslated and keeps falling back, the same rule as check |
| loaders | object | Lazy 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 |
| formatters | object | Custom {v:name} formatters |
| partial | string[] | 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 |
| onMissing | function | (key, locale): return a string to substitute; silences the default warn-once |
| onResolve | function | (info) on every t(): key, locale, value and status (hit / fallback / miss). Powers devtools |
Instance
| Member | Description |
|---|---|
| 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) |
| locale | Current locale (readonly) |
| locales | Loaded + loadable locales (readonly): feed a language switcher from one source of truth |
| version | Change counter that powers framework adapters |
Locale helpers
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
v.t('home.title'); ✓
v.t('home.title', { x: 1 }); ✗ no params declared
v.t('nope'); ✗ unknown keyTypeScript 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.