Integraciones
React
El paquete oficial de React 18/19 para Verbaly. Hooks sobre el mismo core reactivo, un <Trans> consciente del compilador para rich text, y claves + params totalmente tipados.
Qué puedes lograr:
- Traduce con hooks (useT, useLocale) que re-renderizan exactamente cuando cambia el idioma
- Escribe rich text en el lugar: el compilador extrae los children de
<Trans>como cualquiert`…` - Mantén claves y params totalmente tipados, con autocompletado desde tus propios mensajes
- Concurrent-safe por construcción: las suscripciones van sobre
useSyncExternalStore - Úsalo en Preact a través de
preact/compat, sin paquete extra
Instalación
Instala el paquete con tu gestor de paquetes preferido.
pnpm add @verbaly/reactSetup y hooks
import { VerbalyProvider, useT, useLocale } from '@verbaly/react';
import { verbaly } from 'virtual:verbaly';
<VerbalyProvider instance={verbaly}>
<App />
</VerbalyProvider>
function Inbox() {
const t = useT();
const [locale, setLocale] = useLocale();
return <p>{t('inbox', { count: 3 })}</p>;
}- Re-renderiza vía
useSyncExternalStore: concurrent-safe, amigable con SSR. useVerbaly()expone la instancia completa cuando la necesitas.- ¿Sin provider encima de un componente en el navegador?
useT()yuseLocale()leen la instancia que creavirtual:verbaly, así que una app de una sola página o un test de componente no necesitan envoltorio. En el servidor el provider es obligatorio, porque cada petición tiene su propia instancia. useT()entrega untnuevo cuando cambia el idioma, así que unuseMemoo un efecto que dependa de él lo sigue; mientras nada cambia, sigue siendo la misma función.
Rich text con <Trans>
Escribe el texto fuente en el lugar y el compilador lo extrae, exactamente como t`…`:
import { Trans } from '@verbaly/react';
// you write:
<Trans>Read the <a href='/terms'>terms</a> before continuing</Trans>
// the compiler extracts "Read the <a>terms</a> before continuing" and rewrites to:
<Trans id='x7Ka9q2f' components={{ a: <a href='/terms' /> }} />- Los atributos de los hijos se preservan; los params (
{count}) se vuelvenvalues; el anidado funciona; los tags desconocidos caen a su texto interno. - Los tags de la whitelist se renderizan de verdad:
<em>,<code>y compañía en un mensaje se vuelven elementos reales, la misma whitelist segura del intérprete de DOM.richTagsreemplaza la lista; una lista vacía aplana los tags a texto plano. - Claves legibles: escribe
<Trans id="inbox.title">…</Trans>con children y se extrae bajo ese id en vez de un hash. - Runtime-first sigue funcionando: pasa
idsin children (o con tus propiosvalues/components) y el compilador no toca el elemento. - Aplican las reglas de whitespace de JSX: un salto de línea entre un elemento y texto no renderiza espacio; usa el idiom
{' '}, la extracción es fiel a lo que React renderiza. - Links nombrados: el prop
linksmapea nombres de tag a hrefs:<docs>…</docs>se renderiza como un<a>real sin componente custom.componentsgana sobrelinkscon el mismo nombre; los hrefsjavascript:se bloquean. - ¿Sin provider? Pasa el prop
instancey<Trans>renderiza directo desde ahí, re-renderizando en cada cambio de idioma. - Las dos formas pasan el typecheck: hijos en tu código,
iden runtime. Un<Trans>que el compilador nunca vio, sin plugin o en un fichero fuera deinclude, pinta sus hijos tal como están escritos, como unt`…`sin compilar. - En un React Server Component, importa
Transde@verbaly/react/server: pinta desde lainstanceque le pasas, sin provider y sin hooks. En Next.js,@verbaly/next/serverexporta uno que encuentra solo la instancia de la petición.
Next.js
Los Server Components se renderizan ya traducidos al idioma del visitante, y los Client Components hidratan con el mismo locale y el mismo catálogo: sin flash de texto sin traducir ni mismatch de hidratación.
- Cada página se renderiza en el idioma del visitante, tomado de la dirección cuando la lleva, y si no de su elección guardada o del header
Accept-Language - Sin flash de texto sin traducir: el HTML que envía el servidor y la página ya hidratada son idénticos, catálogo incluido
- Cada request tiene su propia instancia, así que dos visitantes simultáneos nunca ven el idioma del otro
- Turbopack y webpack, ambos de primera: el compilador de
tcorre como loader en cualquiera de los dos bundlers, sobre los mismos archivos que leeverbaly extract
Next.js necesita la integración (@verbaly/next) junto a los bindings de React:
pnpm add verbaly @verbaly/next @verbaly/reactConéctalo
Envuelve tu config con withVerbaly. Conecta la extracción en vivo en dev y el compilador de mensajes en Turbopack y webpack, y cuando next build ha compilado lista lo que haría fallar a verbaly check, así que un comando que solo carga tu config, como next typegen, nunca se encuentra ese reporte:
import { withVerbaly } from '@verbaly/next';
export default withVerbaly({ /* your Next config */ });
// locales live in your verbaly.config (npx verbaly init), or inline:
export default withVerbaly({}, { locales: ['en', 'es', 'pt'] });En el layout raíz, getVerbalyProps serializa el locale negociado y su catálogo, y VerbalyProvider siembra el cliente desde ellos:
import { getRequestLocale, getVerbalyProps } from '@verbaly/next/server';
import { VerbalyProvider } from '@verbaly/next/client';
import { localeDirection } from 'verbaly';
// the language comes from the cookie and Accept-Language, so the shell renders per request
export const instant = false;
export default async function RootLayout({ children }) {
const locale = await getRequestLocale();
const props = await getVerbalyProps();
return (
<html lang={locale} dir={localeDirection(locale)}>
<body>
<VerbalyProvider {...props}>{children}</VerbalyProvider>
</body>
</html>
);
}export const instant = false es para cacheComponents, que create-next-app enciende: un layout que lee la petición fuera de <Suspense> hace fallar el build ahí, y un idioma elegido por cookie es justo eso. Sin cacheComponents, quita la línea. Para que las páginas sigan siendo estáticas, pon el idioma en la dirección, más abajo.
Server y Client Components
Los Server Components reciben una t con alcance de request usando await getT(), resuelta una vez por request (primero la dirección cuando lleva el idioma, luego la cookie, luego Accept-Language, luego tu fallback):
import { getT } from '@verbaly/next/server';
export default async function Page() {
const t = await getT();
return <h1>{t`Welcome back`}</h1>;
}El texto enriquecido en un Server Component es el mismo <Trans> que en React, desde @verbaly/next/server: se pinta en el idioma de la petición sin provider.
import { Trans } from '@verbaly/next/server';
<Trans>Read the <a href="/terms">terms</a> before you go on</Trans>Fuera de una página, donde no hay petición que leer (una imagen OG, un sitemap, un email), nombra el idioma. Esa llamada nunca lee la petición, así que la ruta sigue siendo estática:
import { getT } from '@verbaly/next/server';
export async function GET() {
const t = await getT({ locale: 'es' });
return Response.json({ title: t('home.lead') });
}Con routing: 'no-prefix', una tarjeta para compartir puede mezclar dos idiomas: un rastreador lee la página en el idioma que negocia, mientras que una imagen que nombra su idioma siempre sale en ese. Para que vayan juntos, da a cada idioma su propia url con un modo de rutas con prefijo, o deja que la imagen lea la petición como lo hace la página, con getT() sin idioma, que la genera en cada petición.
Los Client Components usan los bindings de React, re-exportados desde @verbaly/next/client:
'use client';
import { useT } from '@verbaly/next/client';
export function Counter() {
const t = useT();
return <p>{t`You have new messages`}</p>;
}Las claves están tipadas: getT() y useT() comprueban cada clave y sus params contra tu catálogo, y getRequestLocale() devuelve uno de tus idiomas. Los tipos viven en .verbaly/types.d.ts, junto a los módulos generados y nunca se commitean; TypeScript se salta una carpeta que empieza por punto, así que Verbaly añade ese fichero al include de tu tsconfig.json, como Next añade .next/types. En un clon fresco, next typegen && tsc --noEmit los escribe antes de comprobarlos.
El idioma en la dirección
Si tus páginas viven bajo app/[locale]/, primero dilo en tu config: routing: 'prefix-all' le da a cada idioma su propio segmento. Esa línea es lo que le da a getAlternates los demás idiomas y lleva a useSwitchLocale a la dirección nueva, y sin ella setRequestLocale avisa una vez, porque el segmento y la config se contradirían.
export default {
locales: ['en', 'es', 'pt'],
routing: 'prefix-all',
};Después pásale el segmento a Verbaly. La dirección decide el idioma para todo el que abra el enlace, y tu página vuelve a poder construirse por adelantado: sin ese segmento, Verbaly tiene que leer las cabeceras de la petición para encontrar el idioma, y leerlas es lo que hace que Next reconstruya la página en cada visita.
import { setRequestLocale, getVerbalyProps, getAlternates } from '@verbaly/next/server';
import { locales } from 'virtual:verbaly';
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }) {
const { locale } = await params;
return { alternates: getAlternates({ path: `/${locale}`, baseUrl: 'https://example.com' }) };
}
export default async function LocaleLayout({ children, params }) {
const { locale } = await params;
setRequestLocale(locale); // before anything calls getT()
return <VerbalyProvider {...(await getVerbalyProps())}>{children}</VerbalyProvider>;
}getAlternates le da a generateMetadata la dirección de esta página en todos los idiomas más el canonical, que es lo que le dice a un buscador que tus traducciones son la misma página y no duplicados. Es el mismo conjunto que recibe un sitio renderizado estáticamente.
Cambiar de idioma
useSwitchLocale sigue a tu routing. Cuando la dirección lleva el idioma, va a la misma página en el idioma nuevo con el router de la app; cuando no, cambia el cliente en vivo, escribe la cookie que el servidor lee, y re-renderiza los Server Components vía router.refresh():
'use client';
import { useSwitchLocale } from '@verbaly/next/client';
export function LangPicker() {
const switchLocale = useSwitchLocale();
return <button onClick={() => switchLocale('es')}>Español</button>;
}- La cookie por defecto es
verbaly-locale(la misma identidad que la storage key del core);cookie: falseen las opciones la desactiva, paraAccept-Languagepuro. fallbackpor defecto es tu locale fuente; la negociación entiende q-values y estrecha regiones (es-PEmatcheaes).next buildlista las claves que faltan, las rotas y las desconocidas una vez que ha compilado, con las claves exactas a completar, y termina, también con Yarn PnP.failOnMissing: truelo para ahí en su lugar, con el reporte una sola vez encima de la línea de error de Next.withVerbalyconserva un hookcompiler.runAfterProductionCompiletuyo y lo corre primero. Un wrapper aplicado por fuera dewithVerbalyque reemplace ese hook en vez de llamarlo apagaría el informe del build, así que dejawithVerbalycomo el más externo.- Dev y build muestran las mismas palabras: un texto escrito en tu código gana a una edición a mano del catálogo en los dos. En dev, la terminal dice lo que si no pasaría en silencio: esa edición reemplazada, una clave escrita con dos textos, una traducción que ya es más vieja que su texto fuente. Cada cosa una vez, y otra si vuelve; una lista larga se queda en pocas líneas, y un catálogo guardado a medio escribir pausa la extracción en vivo con una línea que dice por qué, en vez de una traza de error.
next dev,next buildynext typegendicen una vez, al arrancar, si un fichero fuera de tuincludeescribet`…`, que ningún build traduce: en un proyecto sinsrc/, añade ahícomponents/.- Leer los headers de la petición es lo que hace que una página se renderice en cada visita, así que pasa el segmento
[locale]y deja de pasar. Para un sitio sin servidor ninguno, prefiere salida pre-traducida por idioma con verbaly render.