Use Cases (Atualizado: 02/06/2026)

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.

Implementar i18n com Claude Code: guia prático para Next.js

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

ArmadilhaO que quebraCorreção
Decidir URLs no fimRedirects e páginas indexadas ficam confusosEscolher prefixo, subdomínio ou domínio no início
Chaves vagastitle2 e text3 são difíceis de revisarNomear por tela e significado
Depender de fallbackTraduções ausentes ficam escondidasFalhar CI quando faltar chave
Concatenar data e moedaOrdem e separadores ficam erradosUsar Intl ou formatters
Esquecer metadataResultado de busca fica no idioma padrãoTraduzir 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.

#Claude Code #i18n #internacionalização #Next.js #next-intl
Grátis

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.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.