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:
- Traduza com hooks (useT, useLocale) que re-renderizam exatamente quando o idioma muda
- Escreva rich text no lugar: o compilador extrai os children de
<Trans>como qualquert`…` - Mantenha chaves e params totalmente tipados, com autocompletar das suas próprias mensagens
- Concurrent-safe por construção: as assinaturas usam
useSyncExternalStore - Use no Preact através de
preact/compat, sem pacote extra
Instalação
Instale o pacote com o seu gerenciador de pacotes preferido.
pnpm add @verbaly/reactSetup e 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 via
useSyncExternalStore: concurrent-safe, amigável com SSR. useVerbaly()expõe a instância completa quando você precisa.- Sem provider acima de um componente no navegador?
useT()euseLocale()leem a instância que ovirtual:verbalycria, então um app de página única ou um teste de componente não precisa de wrapper. No servidor o provider é obrigatório, porque cada requisição tem a sua própria instância. useT()entrega umtnovo quando o idioma muda, então umuseMemoou um efeito que dependa dele acompanha; enquanto nada muda, continua sendo a mesma função.
Rich text com <Trans>
Escreva o texto fonte no lugar e o compilador o extrai, exatamente 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' /> }} />- Os atributos dos filhos são preservados; os params (
{count}) viramvalues; o aninhamento funciona; os tags desconhecidos caem para o texto interno. - As tags da whitelist renderizam de verdade:
<em>,<code>e companhia numa mensagem viram elementos reais, a mesma whitelist segura do interpretador de DOM.richTagssubstitui a lista; uma lista vazia achata as tags para texto puro. - Chaves legíveis: escreva
<Trans id="inbox.title">…</Trans>com children e ele extrai sob esse id em vez de um hash. - Runtime-first ainda funciona: passe
idsem children (ou com seus própriosvalues/components) e o compilador não toca no elemento. - Valem as regras de whitespace do JSX: uma quebra de linha entre um elemento e texto não renderiza espaço; use o idiom
{' '}, a extração é fiel ao que o React renderiza. - Links nomeados: a prop
linksmapeia nomes de tag para hrefs:<docs>…</docs>renderiza como um<a>real sem componente custom.componentsganha delinksno mesmo nome; hrefsjavascript:são bloqueados. - Sem provider? Passe a prop
instancee<Trans>renderiza direto dali, re-renderizando a cada troca de idioma. - As duas formas passam no typecheck: filhos no seu código,
idem runtime. Um<Trans>que o compilador nunca viu, sem plugin ou num arquivo fora doinclude, renderiza os filhos como estão escritos, como umt`…`não compilado. - Num React Server Component, importe
Transde@verbaly/react/server: ele renderiza a partir dainstanceque você passa, sem provider e sem hooks. No Next.js,@verbaly/next/serverexporta um que encontra sozinho a instância da requisição.
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.
- Cada página renderiza no idioma do visitante, tirado do endereço quando ele o leva, e se não da escolha salva ou do header
Accept-Language - Sem flash de texto sem tradução: o HTML que o servidor envia e a página já hidratada são idênticos, catálogo incluído
- Cada request tem sua própria instância, então dois visitantes simultâneos nunca veem o idioma um do outro
- Turbopack e webpack, ambos de primeira: o compilador de
troda como loader em qualquer um dos dois bundlers, nos mesmos arquivos que overbaly extractlê
Next.js precisa da integração (@verbaly/next) junto com os bindings de React:
pnpm add verbaly @verbaly/next @verbaly/reactConecte 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:
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:
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):
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.
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:
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:
'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.
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.
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():
'use client';
import { useSwitchLocale } from '@verbaly/next/client';
export function LangPicker() {
const switchLocale = useSwitchLocale();
return <button onClick={() => switchLocale('es')}>Español</button>;
}- O cookie padrão é
verbaly-locale(a mesma identidade da storage key do core);cookie: falsenas opções o desativa, paraAccept-Languagepuro. fallbacké por padrão seu locale fonte; a negociação entende q-values e estreita regiões (es-PEcasa comes).next buildlista as chaves que faltam, as quebradas e as desconhecidas depois de compilar, com as chaves exatas a preencher, e termina, também com Yarn PnP.failOnMissing: trueo para ali em vez disso, com o relatório uma única vez acima da linha de erro do Next.withVerbalymantém um hookcompiler.runAfterProductionCompileseu e o roda primeiro. Um wrapper aplicado por fora dowithVerbalyque substitua esse hook em vez de chamá-lo desligaria o relatório do build, então deixe owithVerbalycomo o mais externo.- Dev e build mostram as mesmas palavras: um texto escrito no seu código vence uma edição à mão no catálogo nos dois. Em dev, o terminal diz o que de outra forma aconteceria em silêncio: essa edição substituída, uma chave escrita com dois textos, uma tradução que já é mais velha que o seu texto fonte. Cada coisa uma vez, e de novo se voltar; uma lista longa fica em poucas linhas, e um catálogo salvo pela metade pausa a extração ao vivo com uma linha que diz por quê, em vez de um rastro de erro.
next dev,next buildenext typegendizem uma vez, ao começar, se um arquivo fora do seuincludeescrevet`…`, que nenhum build traduz: num projeto semsrc/, adicionecomponents/ali.- Ler os headers da requisição é o que faz uma página renderizar a cada visita, então passe o segmento
[locale]e isso deixa de acontecer. Para um site sem servidor nenhum, prefira saída pré-traduzida por idioma com verbaly render.