Inicio
Migrar a Verbaly
Salir de otra librería de i18n es sobre todo mecánico, y no hace falta hacerlo de golpe: los dos runtimes conviven mientras vas fichero a fichero. Las tres secciones que siguen a la primera valen vengas de donde vengas; la primera es para i18next, que es de donde llega casi todo el mundo.
Vienes de i18next: qué equivale a qué
El modelo mental se traslada uno a uno, y lo único que cambia es la sintaxis:
| i18next | Verbaly |
|---|---|
i18next.init({ … }) | npx verbaly init + un plugin de build |
t('inbox.title') | t('inbox.title'): la misma llamada, con las keys ya tipadas |
{{name}} | {name} |
messages_one / messages_other | {count | one: … | other: # …}: opcional, las keys con sufijo siguen funcionando tal cual |
format: 'currency' (interpolación) | {price:currency/EUR} (Intl nativo) |
i18next-browser-languagedetector | resolveLocale() + persistLocale() |
i18next-http-backend (lazy load) | per-locale code-splitting, built into the plugin |
useTranslation() (react-i18next) | useT() (@verbaly/react) |
| Trans component | Trans component: values / components map over |
i18next.changeLanguage('es') | await setLocale('es') |
saveMissing, runtime fallbacks | npx verbaly check: CI fails instead |
Quédate con tus catálogos: entran tal como están
Tus catálogos entran sin cambiar de forma. Los grupos anidados siguen anidados, _one y _other siguen funcionando como las keys que son, y un namespace con dos puntos sigue siendo una key. Una sola cosa tiene que cambiar, y migrate la hace: {{name}} pasa a {name}, y una llave que i18next mostraba como texto se dobla, porque una llave doblada es como Verbaly escribe una llave literal.
Primero reporta, reescribe solo las llaves, y te lista lo que no va a adivinar para que eso lo decidas tú. Esto es lo que le hace al fichero:
Dos mejoras que puedes dejar para después, y ninguna hace falta para funcionar. Fusionar _one y _other en un solo mensaje con variantes ({count | one: … | other: # …}) es lo que te da la selección automática de plural, y el # pinta el número ya localizado, sin un formattedCount aparte. La segunda es menor: los puntos son una convención de nombres, no un formato. Una clave se lee igual tanto si tu archivo la escribe "inbox.title" como si la pone de title dentro de un grupo inbox.
Cambia el runtime
Las keys existentes siguen funcionando vía t('key', params), así que los componentes migrados apenas cambian. En React:
Si tus componentes llaman a un hook propio, como un useTranslate alrededor de useTranslation, consérvalo y haz que devuelva useT(): nada de lo que lo llama cambia. Un tipo de clave construido desde tu JSON, como un NestedKeyOf, también lista las rutas de los grupos, así que conviértelo en VerbalyKey de virtual:verbaly. Para esperar el idioma del visitante antes del primer pintado, como hacía un límite de Suspense, renderiza dentro de setLocale(locale).then(…): un await de nivel superior en tu entrada parte el bundle en muchos chunks.
El bundle lo nota. Una app de React con i18next y react-i18next embarca unos 23 KB de runtime; la misma app con Verbaly embarca unos 4. Sin framework son 13,6 contra 3,3. Los cuatro números son gzip y están medidos, no estimados.
Medido en Memos, una app de código abierto en 46 idiomas: 20 archivos cambiados a mano, su build y sus tests como antes, y unos 11 KB menos para cada visitante en la primera carga.
El copy nuevo se salta la ceremonia de keys: escribe el texto como tagged template y deja que el compilador lo extraiga, o conserva ids legibles con t.id. Los adapters de Vue y Svelte siguen el mismo patrón, y las páginas de HTML plano pueden bindear con data-verbaly.
t`Hola ${name}`; // key + types generated
t.id('inbox.title')`Inbox`; // readable id, i18next-style¿Vienes sin i18n? Deja que wrap haga la parte aburrida
Si tu app tiene texto hardcodeado en vez de una librería de i18n, npx verbaly wrap escanea tu JSX, encuentra las strings visibles al usuario y las envuelve en t`…` por ti. Solo reporta hasta que pases --write, y lo ambiguo (como una frase mezclada con markup) se lista para que lo envuelvas a mano en vez de adivinarse.
npx verbaly wrap- would wrap 37 texts in 12 files (report only, use
--writeto apply) - src/App.tsx:12 "Welcome back"
- src/Nav.tsx:8 aria-label → "Open menu"
Detección de idioma y carga lazy
El plugin detector y el backend HTTP desaparecen. resolveLocale lee storage → navigator.languages → fallback (SSR-safe, con narrowing BCP-47), persistLocale guarda la elección y sincroniza <html lang>, y setLocale carga el chunk de ese idioma bajo demanda:
import { resolveLocale, persistLocale } from 'verbaly';
import { setLocale, getLocale } from 'virtual:verbaly';
await setLocale(resolveLocale({ supported: ['en', 'es', 'pt'] }));
persistLocale(getLocale());Migra incremental
No hace falta un big-bang: el runtime de Verbaly pesa ~3 KB, así que ambas librerías pueden convivir mientras migras una página o namespace a la vez. Cuando los catálogos estén convertidos, dale a i18next interpolation: { prefix: '{', suffix: '}' } para que también los lea. Un orden pragmático:
Convive con i18next
npx verbaly inity el plugin de build, así el toolchain corre junto a i18next desde el día uno. Apunta el config a los catálogos que ya tienes, estén donde estén.Pasa los catálogos
npx verbaly migrate --write. Convierte{{name}}en{name}, dobla una llave que i18next mostraba como texto para que siga siendo texto, y recuerda qué idiomas convirtió, así que volver a correrlo es seguro.Cambia los hooks página a página
Sustituye
useTranslationporuseTy borra cada namespace de i18next cuando quede vacío.Compruébalo en el CI
npx verbaly checken CI, ynpx verbaly pseudopara cazar strings hardcodeados que la migración pasó por alto.