Ir para o conteúdo

Integrações

React

O pacote oficial de React 18/19 para Verbaly. Hooks sobre o mesmo core reativo, um <Trans> ciente do compilador para rich text, e chaves + params totalmente tipados.

O que você pode fazer:

Instalação

Instale o pacote com o seu gerenciador de pacotes preferido.

 pnpm add @verbaly/react

Setup e 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 com <Trans>

Escreva o texto fonte no lugar e o compilador o extrai, exatamente 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

Os Server Components renderizam já traduzidos no idioma do visitante, e os Client Components hidratam com o mesmo locale e o mesmo catálogo: sem flash de texto sem tradução nem mismatch de hidratação.

Next.js precisa da integração (@verbaly/next) junto com os bindings de React:

 pnpm add verbaly @verbaly/next @verbaly/react

Conecte tudo

Envolva o seu config com withVerbaly. Ele liga a extração ao vivo no dev e o compilador de mensagens no Turbopack e no webpack, e quando o next build termina de compilar lista o que faria o verbaly check falhar, então um comando que só carrega o seu config, como next typegen, nunca encontra esse relatório:

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'] });

No layout raiz, getVerbalyProps serializa o locale negociado e seu catálogo, e VerbalyProvider semeia o cliente a partir deles:

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 é para o cacheComponents, que o create-next-app liga: um layout que lê a requisição fora de <Suspense> faz o build falhar ali, e um idioma escolhido por cookie é exatamente isso. Sem cacheComponents, tire a linha. Para manter as páginas estáticas, coloque o idioma no endereço, mais abaixo.

Server e Client Components

Os Server Components recebem um t com escopo de request usando await getT(), resolvido uma vez por request (primeiro o endereço quando ele leva o idioma, depois o cookie, depois Accept-Language, depois seu 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>;
}

Texto rico num Server Component é o mesmo <Trans> do React, a partir de @verbaly/next/server: renderiza no idioma da requisição sem provider.

app/page.tsx
import { Trans } from '@verbaly/next/server';

<Trans>Read the <a href="/terms">terms</a> before you go on</Trans>

Fora de uma página, onde não há requisição para ler (uma imagem OG, um sitemap, um email), nomeie o idioma. Essa chamada nunca lê a requisição, então a rota continua 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') });
}

Com routing: 'no-prefix', um cartão de compartilhamento pode misturar dois idiomas: um rastreador lê a página no idioma que negocia, enquanto uma imagem que nomeia o idioma sai sempre nele. Para mantê-los juntos, dê a cada idioma sua própria url com um modo de rotas com prefixo, ou deixe a imagem ler a requisição como a página faz, com getT() sem idioma, que a gera a cada requisição.

Os Client Components usam os bindings de React, re-exportados de @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>;
}

As chaves são tipadas: getT() e useT() conferem cada chave e seus params contra o seu catálogo, e getRequestLocale() devolve um dos seus idiomas. Os tipos ficam em .verbaly/types.d.ts, junto dos módulos gerados e nunca commitados; o TypeScript pula uma pasta que começa com ponto, então o Verbaly adiciona esse arquivo ao include do seu tsconfig.json, como o Next adiciona .next/types. Num clone novo, next typegen && tsc --noEmit os escreve antes de conferi-los.

O idioma no endereço

Se as suas páginas vivem em app/[locale]/, diga isso primeiro na sua config: routing: 'prefix-all' dá a cada idioma o seu próprio segmento. É essa linha que dá ao getAlternates os outros idiomas e leva o useSwitchLocale ao novo endereço, e sem ela o setRequestLocale avisa uma vez, já que o segmento e a config se contradiriam.

verbaly.config.ts
export default {
  locales: ['en', 'es', 'pt'],
  routing: 'prefix-all',
};

Depois passe o segmento ao Verbaly. O endereço decide o idioma para todo mundo que abra o link, e a sua página volta a poder ser construída com antecedência: sem esse segmento, o Verbaly tem de ler os cabeçalhos da requisição para encontrar o idioma, e lê-los é o que faz o Next reconstruir a página em 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 entrega ao generateMetadata o endereço desta página em todos os idiomas mais o canonical, que é o que diz a um buscador que suas traduções são a mesma página e não duplicatas. É o mesmo conjunto que um site renderizado estaticamente recebe.

Trocar de idioma

useSwitchLocale segue o seu routing. Quando o endereço leva o idioma, ele vai para a mesma página no novo idioma pelo router do app; quando não, troca o cliente ao vivo, escreve o cookie que o servidor lê, e re-renderiza os Server Components via 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 navegarEnterpara abrir
Copiado para a área de transferência