Framework
Plain HTML
No framework? Verbaly treats plain HTML as a first-class citizen, something no other compiler-based i18n tool does. Mark elements, call bindDom, done.
Usage
<h1 data-verbaly="home.title"></h1>
<p data-verbaly="inbox" data-verbaly-args='{"count":3}'></p>
<input data-verbaly-attr='{"placeholder":"search.hint"}' />import { bindDom, createVerbaly } from 'verbaly';
const v = createVerbaly({ locale: 'es', messages });
const unbind = bindDom(v); // render + observe
v.setLocale('en'); // every bound node re-rendersAttributes
| Attribute | Purpose |
|---|---|
| data-verbaly | Key whose translation becomes the element's text |
| data-verbaly-args | JSON params shared by text and attribute translations |
| data-verbaly-attr | JSON map of attribute → key, e.g. placeholder, title, aria-label. Since 0.17.0: URL attributes (href, src…) are sanitized against unsafe schemes; on*, style and srcdoc are blocked, so a catalog can't inject scripts |
| data-verbaly-rich | Opt-in: render tags in the message as real elements (see Rich text) |
| data-verbaly-links | JSON map of link name → href for named links (see Named links) |
Rich text
By default translations render via textContent, so tags stay literal. Mark the element with data-verbaly-rich and tagged messages become real elements:
{ "gate.title": "The build <em>gate</em>" }<h3 data-verbaly="gate.title" data-verbaly-rich></h3>
<!-- renders: The build <em>gate</em> -->One key per sentence, so translators see the whole message and each locale keeps its own word order. The safety model:
- Only a whitelist of phrasing tags renders:
em,strong,code,b,i,u,s,small,mark,sub,sup,span,kbd,abbr,br,wbr. - Elements are built programmatically, never
innerHTML, and attributes never come from the message. - Non-whitelisted tags are unwrapped to inert text:
<script>in a catalog renders as plain text, no element. - Override the whitelist with
bindDom(v, { richTags: ['q'] }).
Named links
Links are deliberately excluded from the whitelist: an <a> without attributes is useless, and attributes never come from messages. Since 0.11.0 you name the link in the message and provide the href from code:
{ "cta": "Read the <docs>guide</docs> now" }bindDom(v, {
richLinks: { docs: { href: '/docs', target: '_blank', rel: 'noopener' } },
});
// renders: Read the <a href="/docs" target="_blank" rel="noopener">guide</a> now<p data-verbaly="cta" data-verbaly-rich data-verbaly-links='{"docs":"/es/docs"}'></p>- A link is a
stringhref or{ href, target, rel }; the per-elementdata-verbaly-linksmap merges over the globalrichLinks. - Hrefs come from your code, never from the catalog, and unsafe schemes (
javascript:,data:) are blocked bysafeHref: the<a>renders withouthref. - Names missing from the map unwrap to inert text, like any unknown tag, so translations never break the page.
Locale bootstrap
Two helpers cover the boilerplate every app repeats, picking the initial locale and remembering the user's choice:
import { bindDom, createVerbaly, persistLocale, resolveLocale } from 'verbaly';
const v = createVerbaly({
locale: resolveLocale({ supported: ['en', 'es', 'pt'] }), // storage → navigator → fallback
messages: { en }, // bundle only the source
loaders: { es: () => import('./locales/es.json'), pt: () => import('./locales/pt.json') },
});
bindDom(v);
// on user switch:
await v.loadLocale('es'); // flash-free (setLocale alone also auto-loads)
v.setLocale('es');
persistLocale('es'); // localStorage + <html lang>resolveLocale: stored choice →navigator.languages(with BCP-47 narrowing,es-PE→es) →fallback(defaults to the first supported locale). SSR-safe.- Both accept a custom
storageKey(defaultverbaly-locale);falsedisables storage. - Lazy catalogs (
0.8.0): only the source ships in the bundle, andloadersfetch the rest on demand, deduped, with BCP-47 narrowing.
Behavior
- Re-renders every bound element on
setLocale/addMessages. - A
MutationObserverpicks up nodes added later and attribute edits, so dynamic content just works. bindDomreturns anunbind()that disconnects everything.- Options:
bindDom(v, { root, attribute, richTags, richLinks })to scope the scan, rename the attribute, override the rich whitelist or provide named links.