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.
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.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.