Framework
HTML plano
¿Sin framework? Verbaly trata el HTML plano como ciudadano de primera, algo que ninguna otra herramienta i18n con compilador hace. Marca elementos, llama bindDom, listo.
Uso
<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-rendersAtributos
| Atributo | Propósito |
|---|---|
| data-verbaly | Clave cuya traducción se vuelve el texto del elemento |
| data-verbaly-args | Params JSON compartidos por las traducciones de texto y atributos |
| data-verbaly-attr | Mapa JSON de atributo → clave, p. ej. placeholder, title, aria-label. Desde 0.17.0: los atributos URL (href, src…) se sanitizan contra esquemas inseguros; on*, style y srcdoc se bloquean, así que un catálogo no puede inyectar scripts |
| data-verbaly-rich | Opt-in: renderiza los tags del mensaje como elementos reales (ver Rich text) |
| data-verbaly-links | Mapa JSON de nombre de link → href para links nombrados (ver Links nombrados) |
Rich text
Por defecto las traducciones renderizan vía textContent, así que los tags quedan literales. Marca el elemento con data-verbaly-rich y los mensajes con tags se vuelven elementos reales:
{ "gate.title": "The build <em>gate</em>" }<h3 data-verbaly="gate.title" data-verbaly-rich></h3>
<!-- renders: The build <em>gate</em> -->Una clave por frase, así que el traductor ve el mensaje completo y cada idioma mantiene su propio orden de palabras. El modelo de seguridad:
- Solo renderiza una whitelist de tags de frase:
em,strong,code,b,i,u,s,small,mark,sub,sup,span,kbd,abbr,br,wbr. - Los elementos se construyen programáticamente, nunca
innerHTML, y los atributos jamás vienen del mensaje. - Los tags fuera de la whitelist se desenvuelven a texto inerte: un
<script>en un catálogo renderiza como texto plano, sin elemento. - Sobrescribe la whitelist con
bindDom(v, { richTags: ['q'] }).
Links nombrados
Los links quedan fuera de la whitelist a propósito: un <a> sin atributos no sirve, y los atributos nunca salen de los mensajes. Desde 0.11.0 nombras el link en el mensaje y provees el href desde el código:
{ "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>- Un link es un href
stringo{ href, target, rel }; el mapa por elementodata-verbaly-linksse mergea sobre elrichLinksglobal. - Los hrefs vienen de tu código, nunca del catálogo, y los esquemas inseguros (
javascript:,data:) los bloqueasafeHref: el<a>se renderiza sinhref. - Los nombres que no están en el mapa se desenvuelven a texto inerte, como cualquier tag desconocido, así que las traducciones nunca rompen la página.
Bootstrap de idioma
Dos helpers cubren el boilerplate que toda app repite, elegir el idioma inicial y recordar la elección del usuario:
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: elección guardada →navigator.languages(con narrowing BCP-47,es-PE→es) →fallback(por defecto, el primer idioma soportado). SSR-safe.- Ambos aceptan un
storageKeycustom (defaultverbaly-locale);falsedesactiva el storage. - Catálogos lazy (
0.8.0): solo la fuente va en el bundle, y losloaderstraen el resto bajo demanda, deduplicados y con narrowing BCP-47.
Comportamiento
- Re-renderiza cada elemento bindeado en
setLocale/addMessages. - Un
MutationObserverdetecta nodos agregados después y ediciones de atributos, así que el contenido dinámico simplemente funciona. bindDomdevuelve ununbind()que desconecta todo.- Opciones:
bindDom(v, { root, attribute, richTags, richLinks })para acotar el escaneo, renombrar el atributo, sobrescribir la whitelist rich o proveer links nombrados.