Blogue 18ways
Como adicionar i18n e localização multilingue a uma app Next.js existente sem prejudicar a SEO ou reconstruir tudo de raiz.
I18n (internacionalização), l10n (localização), suporte multilingue… como lhe quiseres chamar, mais cedo ou mais tarde precisas de atualizar a tua aplicação Next.js para suportar mais do que um idioma.
A dificuldade não está em traduzir uma frase. Está em adicionar vários idiomas a uma aplicação Next.js real sem quebrar o SEO, embaraçar o teu codebase ou criar para ti um problema de manutenção.
Se quiser ir diretamente para o código, consulte os exemplos GitHub do 18ways-next.
Antes de mais, precisamos de instalar os nossos pacotes. Vamos configurar as bibliotecas do 18ways, mas ferramentas como o i18next funcionam bem se o teu projeto for simples e tiver apenas conteúdo estático.
npm install @18ways/next @18ways/reactCrie o seu ficheiro de configuração:
// 18ways.config.js
module.exports = {
apiKey: 'pk_dummy_demo_token',
baseLocale: 'en-GB',
router: 'app', // 'app', or 'path' depending on which Next.js router you are using
};Envolva a sua configuração do Next.js:
// next.config.js
const { withWays } = require('@18ways/next/config');
const nextConfig = {
/*
* your normal Next.js config here
*/
};
module.exports = withWays(nextConfig);Adicione um proxy na raiz para que / possa redirecionar para o locale certo:
// proxy.js
export { default, config } from '@18ways/next/proxy';Depois adiciona um layout localizado:
// app/layout.jsx
import './styles.css';
import { WaysRoot } from '@18ways/next/server';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className="next-demo-body">
<WaysRoot>{children}</WaysRoot>
</body>
</html>
);
}E pronto! Agora tem:
Agora já podes traduzir a tua primeira página. Ferramentas como o i18next vão exigir que separem todo o teu texto em chaves de tradução e depois as referenciem no teu código. O aspeto exato disto vai depender da biblioteca que tiveres escolhido.
Se estiveres a usar 18ways, podes simplesmente envolver o texto que queres traduzir num componente <T>:
// src/components/MyExampleComponent.jsx
import { useT, T } from '@18ways/react';
export default function MyExampleComponent() {
const t = useT();
return (
<div>
<T>Hello world!</T>
<img
src="https://example.com/image.png"
alt={t('Example image')}
/>
</div>
);
}Provavelmente vai querer permitir que os utilizadores mudem a sua escolha de idioma:
// src/components/Footer.jsx
import { T, LanguageSwitcher } from '@18ways/react';
export default function Footer() {
return (
<footer>
<T>My footer content</T>
<LanguageSwitcher />
</footer>
);
}Assim que a infraestrutura estiver montada, há várias formas de estes projetos correrem mal.
A renderização do lado do servidor (SSR) é o que permite à sua aplicação Next.js servir HTML pré-renderizado. Isto é vital tanto para SEO como para o utilizador não ver flashes de conteúdo errado ou de idioma incorreto.
Se estiveres a usar uma biblioteca como o i18next, tens de ter muito cuidado para que as tuas traduções estejam a ser carregadas e inseridas durante o render lado do servidor. Podes testar isto verificando a view-source: da tua página, por exemplo view-source:http://localhost:3000/. Deves verificar isto também em produção, para garantir que funciona igualmente na tua build de produção.
Se estiveres a usar 18ways, não te preocupes com isto. Está tudo tratado por ti.
Se estiveres a usar 18ways, não precisas de te preocupar com isto de todo. O 18ways não precisa de chaves de tradução; podes deixar o teu texto no sítio, como normal.
Muitos sistemas de i18n obrigam-no a dividir o seu código em chaves de tradução. É importante ter cuidado com a forma como nomeia estas chaves.
Chaves más são vagas:
// bad-keys.en-GB.js
module.exports = {
title: 'Continue',
button: 'Pay now',
label: 'Home',
};Essas chaves não dizem quase nada a tradutores e programadores sobre onde o texto aparece.
Melhores chaves incluem contexto:
// better-keys.en-GB.js
module.exports = {
'checkout.payment.primaryButton': 'Pay now',
'checkout.payment.stepTitle': 'Complete your payment',
'account.sidebar.homeLink': 'Home',
};Evite também gerar chaves dinamicamente:
// bad-dynamic-keys.js
const key = `checkout.${status}.${buttonType}`;
const translatedText = t(key);Isto vai fazer falhar ferramentas de IDE que tentam tornar as chaves de tradução menos insuportáveis. Também vai tornar extremamente difícil procurar e limpar chaves de tradução antigas.
// better-dynamic-keys.js
const keyMap = {
success: {
primary: t('checkout.success.primary'),
default: t('checkout.success.default'),
},
error: {
primary: t('checkout.error.primary'),
default: t('checkout.error.default'),
},
};
const translatedText = keyMap[status][buttonType];Isto é menos DRY, mas é a melhor forma de evitar que as chaves de tradução fiquem descontroladas.
Ainda melhor é usar uma ferramenta como o 18ways; assim, nunca precisas de chaves de tradução de todo:
// app/[lang]/checkout/page.jsx
<T>Pay now</T>A deteção de locale significa decidir que idioma um utilizador deve ver antes de o mudar explicitamente.
Normalmente, isso envolve uma combinação de:
Accept-Language do navegadorNa middleware simples do Next.js, isso costuma ser assim:
// middleware.js
/**
* You don't need any of this if you're using 18ways
*/
import { NextResponse } from 'next/server';
const acceptedLocales = ['en-GB', 'fr-FR'];
export function middleware(request) {
const savedLocale =
request.cookies.get('preferred-locale')?.value;
const browserLocale =
request.headers
.get('accept-language')
?.split(',')[0] || 'en-GB';
const locale = acceptedLocales.includes(savedLocale)
? savedLocale
: acceptedLocales.includes(browserLocale)
? browserLocale
: 'en-GB';
if (request.nextUrl.pathname === '/') {
return NextResponse.redirect(
new URL(`/${locale}`, request.url)
);
}
return NextResponse.next();
}Algumas bibliotecas têm auxiliares para facilitar isto, em graus variáveis. Com o 18ways, a deteção inicial e a camada de redirecionamento são tratadas por si.
Línguas diferentes formatam datas, números e dinheiro de forma diferente.
Por exemplo:
04/05/2026 pode significar 4 de maio ou 5 de abril€1.999,00 e 1.999,00 € são ambos válidos, dependendo do localeEm JavaScript puro, trata disso por si com Intl:
// formatting-dates-and-currency.js
const myLocale = getCurrentLocale(); // depends on your lib
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const dateLabel = new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'long',
}).format(someTimestamp);
const someMoney = {
amount: 1999,
currency: 'EUR',
};
const moneyLabel = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: someMoney.currency,
}).format(someMoney.amount);
const translatedText = t(
'my.translation.key',
{ dateLabel, moneyLabel }
);Se estiveres a usar 18ways, isto é tratado por ti:
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const someMoney = {
amount: 1999,
currency: 'EUR',
};
<T>My text with {{ someTimestamp }} and {{ someMoney }}</T>Não construa UI traduzida juntando fragmentos:
// bad-string-joining.jsx
const clickHereText = t('click.here');
const toGetStartedText = t('to.get.started')
<p><a href="#">{clickHereText}</a> {toGetStartedText}.</p>Isto vai falhar em várias línguas.
Podes dizer “👉👉Clique aqui👈👈 para começar” em inglês, mas em francês é mais natural dizer “Pour commencer, 👉👉cliquez ici👈👈”. Algumas línguas, como o japonês, precisam até de palavras antes e depois, como “始めるには👉👉こちら👈👈をクリックしてください”.
A estrutura da frase pode mudar, por isso dividi-la em partes torna uma boa tradução muito mais difícil.
Frases completas traduzem melhor porque os tradutores podem reorganizar as palavras naturalmente e ver o significado como uma unidade completa.
Se estiver a traduzir JSX assim:
// rich-text-message.jsx
<p>
Something <strong>that we want to be bold</strong>
</p>terás de consultar a tua biblioteca de i18n sobre como gerir isto, porque é tratado de forma muito diferente por bibliotecas diferentes.
Se estiver a usar o 18ways, pode simplesmente traduzir o bloco JSX inteiro como faria normalmente.
// rich-text-message.jsx
<p>
<T>Something <strong>that we want to be bold</strong></T>
</p>As variáveis permitem-lhe manter a frase inteira enquanto insere valores em tempo de execução.
A maioria das bibliotecas de i18n suporta isto de alguma forma. Com o 18ways:
// app/[lang]/page.jsx
<T>Hello {{ name: 'Ada' }}</T>Esse padrão funciona bem para:
A frase mantém-se legível, e o valor mantém-se explícito.
Se precisares de traduzir também a própria variável, certifica-te de a envolver num t(...):
const animal = t('dog');
<T>Favourite animal: {{ animal }}</T>As regras de plural diferem consoante o idioma. O inglês tem uma forma plural (1 year, 2 years, etc.). O polaco tem várias formas plurais (1 rok, 2 lata, 5 lat). O japonês não tem qualquer forma plural (1 年, 2 年, 3 年).
A maior parte das bibliotecas de i18n permite especificar sintaxe semelhante a ICU para tratar plurais:
// app/[lang]/page.jsx
<T>
{{
unreadCount,
format:
'plural, =0{No unread messages} =1{One unread message} other{{unreadCount} unread messages}',
}}
</T>O acima funcionará em 18ways, mas na maioria dos casos também podes fazer simplesmente isto:
// app/[lang]/page.jsx
<T>{{ unreadCount }} unread messages</T>O 18ways trata dos plurais por si!
Adicionar vários idiomas a uma aplicação Next.js não tem de se tornar num projeto de reescrita.
Se não estiver a usar uma solução como o 18ways — certifique-se de acertar no encaminhamento e no SSR e, depois, avance para mover o seu texto para chaves de tradução, tendo o cuidado de evitar as armadilhas comuns da i18n.
Se estiveres a usar 18ways, então isto é tratado por ti, e só precisas de começar a envolver o teu texto em blocos <T>!