Saltar al contenido

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:

Instalación

Instala el paquete con tu gestor de paquetes preferido.

 pnpm add @verbaly/react

Setup y hooks

App.tsx
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>;
}

Rich text con <Trans>

Escribe el texto fuente en el lugar y el compilador lo extrae, exactamente como t`…`:

Agree.tsx
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' /> }} />

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.

Next.js necesita la integración (@verbaly/next) junto a los bindings de React:

 pnpm add verbaly @verbaly/next @verbaly/react

Coné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:

next.config.ts
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:

app/layout.tsx
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):

app/page.tsx
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.

app/page.tsx
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:

app/og/route.ts
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:

Counter.tsx
'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.

verbaly.config.ts
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.

app/[locale]/layout.tsx
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():

LangPicker.tsx
'use client';
import { useSwitchLocale } from '@verbaly/next/client';

export function LangPicker() {
  const switchLocale = useSwitchLocale();
  return <button onClick={() => switchLocale('es')}>Español</button>;
}
↑↓para moverteEnterpara abrir
Copiado en el portapapeles