i18n no Next.js
Orientação estrutural para colocar um aplicativo Next.js em vários idiomas: qual deve ser o layout da pasta, o que quebra quando está errado e o que evitar.
Escolha a biblioteca primeiro
Padrão: next-intl para qualquer coisa nova no App Router. Não precisa de plugin de build, etapa de macro ou gerenciamento de instâncias por localidade; as mensagens são JSON simples tipadas a partir da sua localidade de origem.
| Biblioteca | Use quando | Custo |
|---|---|---|
| next-intl (padrão) | Novo trabalho no App Router, qualquer tamanho de equipe | Nenhum — JSON simples, sem geração de código |
| @lingui/react | Você quer extração em tempo de compilação e um runtime pequeno, ou uma camada de i18n entre React/Vue/Astro | Plugin de macro SWC ou Babel, fixado exatamente |
| next-i18next | A equipe já conhece i18next, ou você migra código do Pages Router | Instância por requisição, divisão getT/useT |
i18n embutido do Next.js | Apenas Pages Router. Nunca App Router | Não aplicável |
A configuração i18n: { locales, defaultLocale } do Next.js é apenas para Pages Router. No App Router ela não faz nada — roteamento de localidade lá significa um segmento [locale] mais um proxy. Não deixe o agente "ativar" o i18n embutido para um projeto App Router.
Comparação completa, incluindo custo de bundle e trade-offs por biblioteca:
references/library-comparison.md.
Arquitetura canônica
Mantenha dicionários estáticos fora de src/ para que o empacotamento e os caminhos de importação permaneçam óbvios, e mantenha toda a maquinaria de localidade em uma pasta.
messages/ # dicionários estáticos puros — mantenha fora de /src
├── en.json # fonte da verdade; todas as outras localidades espelham
├── de.json
└── ja.json
src/
├── i18n/
│ ├── routing.ts # defineRouting: localidades + defaultLocale
│ ├── request.ts # getRequestConfig: resolve localidade, carrega mensagens
│ └── navigation.ts # createNavigation: Link/redirect com ciência de localidade
├── proxy.ts # detecção de localidade + redirecionamento (Next 16; era middleware.ts)
└── app/
├── [locale]/ # envolve todo o app localizado
│ ├── layout.tsx # valida localidade, <html lang>, generateStaticParams
│ └── page.tsx
└── not-found.tsx # 404 global fica FORA de [locale]
Regras que fazem essa forma funcionar:
[locale]é um segmento dinâmico raiz. Tudo localizado vive sob ele. Rotas de nível raiz (not-found.tsx,error.tsx,api/) ficam fora e não devem chamar hooks de i18n incondicionalmente, ou quebram em tempo de execução.- Uma localidade de origem.
messages/en.jsoné o mestre. Todos os outros arquivos devem espelhar exatamente sua forma. - Todo conhecimento de localidade em
src/i18n/. Componentes importamLinkde@/i18n/navigation, nunca denext/link. Um lugar para adicionar um idioma.
Configuração (greenfield)
Execute nesta ordem. A ordem importa — proxy.ts depende de routing.ts, e o layout depende de request.ts.
- Instale
next-intl, envolvanext.config.tscomcreateNextIntlPlugin(). - Crie
src/i18n/routing.tscomdefineRouting. - Crie
src/proxy.tsexportandocreateMiddleware(routing)mais o matcher padrão. - Crie
src/i18n/request.tscomgetRequestConfigresolvendo a localidade. - Mova
app/**paraapp/[locale]/**; adicionegenerateStaticParamse valide o parâmetro. - Crie
src/i18n/navigation.tscomcreateNavigation(routing). - Crie
messages/en.jsone substitua strings codificadas poruseTranslations. - Defina
<html lang>e adicione metadados localizados + hreflang.
Conteúdo exato dos arquivos para cada etapa: references/next-intl-setup.md.
Armadilhas (Gotchas)
Leia esta seção antes de escrever qualquer código de i18n. Estas são as falhas que custam horas.
setRequestLocaleé legado. No Next.js 16.3+, leia a localidade comnext/root-paramsdentro degetRequestConfig. A maioria dos guias, tutoriais e dados de treinamento de LLM ainda ensinamsetRequestLocale(params.locale). Ainda funciona e está documentado como retrocompatível — mas é a API antiga. Use apenas no Next < 16.3.generateStaticParamsainda é necessário de qualquer forma.paramsé uma Promise no Next 15+.const {locale} = await params. Trateparams.localecomo um erro de tipo, não de runtime.- Valide a localidade ou valores desconhecidos renderizam. Use
hasLocale(routing.locales, locale)enotFound(). Sem isso,/xx/aboutrenderiza sua página em inglês em uma URL falsa. proxy.ts, nãomiddleware.ts, no Next 16.middleware.tsestá obsoleto. O proxy roda apenas em Node.js — o runtime Edge não é suportado. Lingui e next-i18next anunciam proxies seguros para Edge; se você precisa de Edge, você tem um trade-off real, não uma vitória gratuita.- Nunca concatene strings traduzidas.
"Você tem " + contagem + " itens"é agramatical em japonês e árabe. Use ICU:"{count, plural, =1 {Um item} other {# itens}}". - Nunca interpole com
+ou template literals dentro de uma mensagem. Use placeholders nomeados —{nome}— para que a ordem das palavras possa mudar. - Datas e números causam diferenças de hidratação.
new Date()no servidor e no navegador diferem por fuso horário e relógio. DefinatimeZoneemgetRequestConfig, ou renderize apenas no cliente para tempos relativos. Sintoma: "Text content does not match server-rendered HTML". - Formatação depende da localidade.
toLocaleString()sem uma localidade explícita formata pela localidade do runtime, não pela localidade solicitada. UseuseFormatter()/getFormatter(). getRequestConfigexecuta por requisição e é cacheado pelo React. Não coloque estado que varia com a requisição (carrinho, preferências do usuário) lá. Apenas configuração que varia com a localidade.- Server Actions e Route Handlers não têm parâmetros raiz.
await locale()funciona apenas em Server Components. Passe a localidade explicitamente como argumento em actions e handlers.
Retrofitting de um aplicativo existente
Trate isso como uma migração com impacto de URL, não uma mudança de configuração. Toda URL existente muda de forma quando você adiciona um prefixo [locale].
Planeje nesta ordem:
- Inventário de strings — execute
scripts/find-untranslated.shpara encontrar texto JSX codificado. - Escolha a estratégia de roteamento antes de codificar. Prefixo de localidade sempre (
/en/about,/de/about) é o padrão; é inequívoco e limpo para hreflang.localePrefix: 'as-needed'serve a localidade padrão no caminho base — URLs melhores, sinal SEO mais fraco. Decida agora; mudar depois é uma remigração completa. - Implemente o roteamento antes de tocar em qualquer componente. Faça
proxy.ts+[locale]funcionar com mensagens apenas em inglês. - Mova as rotas para
[locale]/— isso é um grandegit mvmecânico. Faça em um commit para quegit log --followpermaneça útil. - Substitua strings por namespace, não por arquivo. Traduza um namespace de ponta a ponta para que a forma da chave seja comprovada antes de se multiplicar.
- Adicione redirecionamentos para cada URL que mudou.
/about→/en/about(ou 307 para a localidade detectada). Sem isso, você perde todos os rankings existentes. Não negociável. - Adicione hreflang + metadados localizados apenas depois que cada localidade tenha a rota, ou você emite alternates apontando para 404s.
Sequência completa de migração com notas de redirecionamento e rollback: references/migration.md.
O que evitar
- Não coloque
messages/dentro desrc/. Dicionários somente servidor na árvore de origem borram o que é enviado ao cliente. - Não importe
Linkdenext/linkem rotas localizadas. Isso remove o prefixo de localidade e linka silenciosamente para o idioma errado. - Não escreva manualmente prefixos de localidade em hrefs (
href={/${locale}/about}). UseLinkde@/i18n/navigation, que lida com roteamento prefixado, as-needed e de domínio. - Não use um namespace de chave único e plano. Agrupe por funcionalidade/página. Chaves planas colidem e não são pesquisáveis.
- Não traduza automaticamente em tempo de build como fonte da verdade. A saída da máquina é um rascunho; cometê-la sem revisão torna os erros permanentes e não rastreáveis.
- Não adicione uma localidade a
routing.localese considere pronto. Sem ummessages/xx.jsontraduzido, cada página renderiza chaves ou cai de volta silenciosamente. - Não misture estratégias de roteamento.
as-neededmais umhreflangapontando para URLs prefixadas é um bug SEO autoinfligido.
Referências
Leia sob demanda, não antecipadamente:
references/library-comparison.md— escolha entre next-intl, Lingui, next-i18next; custo de bundle, gatilhos de migração, formato de configuração por biblioteca.references/next-intl-setup.md— todo arquivo na configuração canônica, prontos para copiar e colar.references/messages-and-keys.md— estrutura de namespace, nomeação de chaves, sintaxe ICU, formatação de data/número/lista, mensagens tipadas.references/seo.md— hreflang,<html lang>,generateMetadatalocalizado, canonical, alternates de sitemap, regras de SEO para trocador de localidade.references/migration.md— retrofit de aplicativo existente, redirecionamentos, rollback.
Scripts
scripts/find-untranslated.sh— sinaliza strings provavelmente codificadas voltadas ao usuário em um projeto para que você possa dimensionar o trabalho antes de começar. Execute como etapa 1 de um retrofit.scripts/run-trigger-eval.sh— mede se adescriptiondesta skill dispara nos prompts certos. Use ao alterar a descrição.
Testando a descrição
A description é a única coisa que o agente vê antes de decidir carregar esta skill. Se ela falhar em disparar, nada abaixo importa.
bash scripts/run-trigger-eval.sh --dry-run # mostra o plano, sem chamadas de API
bash scripts/run-trigger-eval.sh --split train # 12 consultas x 3 execuções
bash scripts/run-trigger-eval.sh --split validation --out report.json
Conjunto de consultas: evals/trigger-queries.json — 12 devem disparar, 8 não devem disparar, divididos 60/40 em um conjunto fixo de treino e validação.
Ao editar a descrição:
- Corrija falhas usando apenas o conjunto de treino. Toque no conjunto de validação apenas uma vez no final para verificar se a mudança generaliza — se o treino melhora e a validação cai, você superajustou.
- Falhas de "deve disparar" significam que a descrição é muito estreita: adicione o conceito que a consulta representa, nunca as palavras-chave da consulta.
- Falhas de "não deve disparar" significam que é muito ampla: diga o que a skill não cobre.
- Execute novamente antes de editar. O disparo é não determinístico, e uma execução não prova nada.
Não adicione palavras-chave de uma consulta com falha — é assim que descrições acabam parecendo uma lista de palavras-chave e param de corresponder a prompts reais.