Implementar i18n com Claude Code: guia prático para Next.js
Fluxo prático para i18n em Next.js com Claude Code, next-intl, checagem de traduções, casos de uso e armadilhas.
i18n começa como decisão de arquitetura
i18n é a abreviação de internacionalização: preparar um aplicativo para funcionar em vários idiomas e regiões. Em produção, isso vai muito além de traduzir textos para JSON. É preciso definir URLs, detecção de idioma, metadata de SEO, formato de datas e moedas, pluralização, links internos e uma regra clara para falhar quando uma tradução estiver ausente. Claude Code ajuda porque consegue ler o repositório, editar vários arquivos, executar comandos e resumir riscos. Mesmo assim, ele precisa de um alvo bem definido.
O erro que Masa viu em sites de conteúdo e páginas SaaS é pedir “traduza tudo” logo no início. A primeira versão parece boa, mas depois aparecem CTAs sem revisão, preços mal formatados, description no idioma errado e links internos quebrados. A abordagem mais segura é pedir a Claude Code o fluxo completo: base de roteamento, arquivos de mensagens, migração de páginas, script de verificação e checklist de revisão.
Este guia usa Next.js App Router com next-intl. Durante a revisão, use as referências oficiais: documentação do Claude Code, roteamento do next-intl, guia de internacionalização do Next.js e referência Intl da MDN. Elas ajudam a evitar exemplos antigos, especialmente na diferença entre proxy.ts e middleware.ts.
flowchart LR
A[Requisitos] --> B[Roteamento]
B --> C[Mensagens JSON]
C --> D[Páginas e metadata]
D --> E[Checagens]
E --> F[SEO e publicação]
Estrutura mínima com Next.js e next-intl
O exemplo usa ja como idioma padrão e adiciona en e de. Em Next.js 16, o arquivo esperado para interceptar rotas é proxy.ts. Em projetos antigos, o papel pode estar em middleware.ts; peça para Claude Code verificar a versão instalada.
src/
app/
[locale]/
layout.tsx
page.tsx
i18n/
navigation.ts
request.ts
routing.ts
messages/
ja.json
en.json
de.json
proxy.ts
routing.ts concentra idiomas e caminhos localizados. Isso evita substituições manuais de strings em vários componentes.
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['ja', 'en', 'de'],
defaultLocale: 'ja',
pathnames: {
'/': '/',
'/pricing': {
ja: '/pricing',
en: '/pricing',
de: '/preise',
},
'/docs': {
ja: '/docs',
en: '/docs',
de: '/dokumentation',
},
},
});
export type Locale = (typeof routing.locales)[number];
// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);
request.ts lê o locale da requisição e carrega as mensagens corretas. Se o idioma não for suportado, volta ao padrão.
// src/i18n/request.ts
import { hasLocale } from 'next-intl';
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});
// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
};
Colocar as chaves em páginas reais
Não jogue todos os textos em common. Esse arquivo deve guardar apenas navegação, seletor de idioma e botões realmente compartilhados. Textos específicos de tela devem ficar em namespaces como HomePage, PricingPage ou DocsPage. Namespace é o nome de um grupo de chaves relacionadas.
{
"common": {
"language": {
"label": "Idioma de exibição",
"ja": "日本語",
"en": "English",
"de": "Deutsch"
},
"nav": {
"docs": "Documentação",
"pricing": "Preços"
}
},
"HomePage": {
"title": "Entregue conhecimento do time em vários idiomas",
"lead": "Mostre {count} artigos no idioma do leitor.",
"cta": "Agendar uma conversa"
}
}
Em uma página do App Router, getTranslations funciona bem em Server Components e prepara o caminho para traduzir metadata.
// src/app/[locale]/page.tsx
import { getTranslations, setRequestLocale } from 'next-intl/server';
type Props = {
params: Promise<{ locale: string }>;
};
export default async function HomePage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'HomePage' });
return (
<main>
<h1>{t('title')}</h1>
<p>{t('lead', { count: 42 })}</p>
<a href={`/${locale}/pricing`}>{t('cta')}</a>
</main>
);
}
O seletor de idioma deve manter a rota atual e trocar só o locale. Substituições manuais de string podem quebrar caminhos como /enquiry.
Bloquear lacunas de tradução no CI
Quando Claude Code gera arquivos de mensagens, a revisão humana costuma pegar frases estranhas, mas nem sempre percebe uma chave ausente. O script abaixo compara o idioma base com os demais.
// scripts/check-translations.mjs
import { readdir, readFile } from 'node:fs/promises';
const messagesDir = new URL('../src/messages/', import.meta.url);
const baseLocale = 'ja';
function flattenKeys(value, prefix = '') {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return [prefix];
}
return Object.entries(value).flatMap(([key, child]) => {
const nextPrefix = prefix ? `${prefix}.${key}` : key;
return flattenKeys(child, nextPrefix);
});
}
async function readMessages(locale) {
const file = new URL(`${locale}.json`, messagesDir);
return JSON.parse(await readFile(file, 'utf8'));
}
const files = await readdir(messagesDir);
const locales = files.filter((file) => file.endsWith('.json')).map((file) => file.replace(/\.json$/, ''));
const baseKeys = new Set(flattenKeys(await readMessages(baseLocale)));
let hasError = false;
for (const locale of locales.filter((item) => item !== baseLocale)) {
const targetKeys = new Set(flattenKeys(await readMessages(locale)));
const missing = [...baseKeys].filter((key) => !targetKeys.has(key));
const extra = [...targetKeys].filter((key) => !baseKeys.has(key));
if (missing.length || extra.length) {
hasError = true;
console.error(`\n${locale}.json has translation key drift`);
if (missing.length) console.error('Missing:', missing.join(', '));
if (extra.length) console.error('Extra:', extra.join(', '));
}
}
if (hasError) process.exit(1);
console.log(`Translation keys are aligned for ${locales.length} locales.`);
Esse script não avalia naturalidade. Ele garante que a estrutura de chaves está completa, o que já evita botões invisíveis e metadata incompleta em produção.
Três casos de uso reais
O primeiro caso é uma página de preços SaaS. Preço envolve moeda, imposto, período de cobrança e nome do plano. Peça para Claude Code usar Intl.NumberFormat ou formatters do next-intl, não concatenação manual. Depois, revise se a frase soa natural para o mercado local.
O segundo caso é uma documentação técnica. Termos como middleware, proxy, locale e namespace nem sempre devem ser traduzidos. Explique o termo na primeira ocorrência em português claro e mantenha o original depois, para facilitar busca na documentação oficial.
O terceiro caso é um painel administrativo. O risco principal é erro de operação. “Excluir”, “desativar” e “revogar convite” precisam ser precisos. Botões, modais de confirmação, notificações e logs de auditoria devem seguir o mesmo sistema de chaves.
Para blogs e mídia, o corpo do artigo não é suficiente. Title, description, canonical, alternate links, imagem OGP e links internos também precisam ser verificados. Coloque essa regra em CLAUDE.md best practices para que Claude Code mantenha o padrão.
Armadilhas comuns e resultado testado
| Armadilha | O que quebra | Correção |
|---|---|---|
| Decidir URLs no fim | Redirects e páginas indexadas ficam confusos | Escolher prefixo, subdomínio ou domínio no início |
| Chaves vagas | title2 e text3 são difíceis de revisar | Nomear por tela e significado |
| Depender de fallback | Traduções ausentes ficam escondidas | Falhar CI quando faltar chave |
| Concatenar data e moeda | Ordem e separadores ficam errados | Usar Intl ou formatters |
| Esquecer metadata | Resultado de busca fica no idioma padrão | Traduzir generateMetadata |
Testei o script com um conjunto pequeno de mensagens e removi HomePage.cta do arquivo inglês. A checagem falhou e mostrou a chave ausente. Ela não consegue decidir se “Agendar uma conversa” é o CTA ideal para todos os mercados. Essa é a divisão correta: Claude Code e CI cuidam das lacunas mecânicas; pessoas revisam tom, preço, jurídico e adaptação cultural. Para fechar o processo, use também o checklist de revisão Claude Code ou a consultoria ClaudeCodeLab.
Artigos relacionados
Autenticação segura com Claude Code: sessões Next.js, JWT e OAuth
Implemente autenticação segura com Claude Code: sessões Next.js, JWT, OAuth, CSRF, RBAC, auditoria e testes.
Como implementar paginação com Claude Code, React e Next.js
Guia prático de paginação com Claude Code: estado na URL, API, acessibilidade e testes de borda.
Claude Code e Next.js full-stack: guia prático de App Router
Guia prático para usar Claude Code com Next.js App Router: limites servidor/cliente, Server Actions, APIs, validação, auth e revisão.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.