i18n para Web
Internacionalização é arquitetura, não uma passada de tradução no final. O objetivo é uma interface que renderize corretamente para qualquer localidade sem casos especiais: sem sentenças concatenadas, sem formatos fixos, sem layout que quebre quando o alemão é 35% mais longo ou o árabe inverte a direção.
Quando Usar Esta Habilidade
Use quando o usuário pedir para:
- Adicionar um idioma, seletor de idioma ou roteamento de locale
- Traduzir um aplicativo, extrair strings ou configurar um fluxo de tradução
- Corrigir bugs de pluralização, gênero ou "1 item / 2 items"
- Formatar datas, números, moeda, tempo relativo ou listas por localidade
- Suportar RTL (árabe, hebraico, persa, urdu)
- Adicionar hreflang, metadados localizados ou sitemaps multilíngues
- Revisar uma configuração de i18n existente, chaves ausentes ou desvio de tradução
- Escolher entre next-intl, react-i18next, FormatJS, Lingui ou um TMS
Referências relacionadas estão em references/ e scripts em scripts/. Leia apenas o que a tarefa precisa. Os caminhos são relativos ao diretório desta habilidade (o agente imprime o caminho quando a habilidade é carregada).
Inegociáveis
- Nunca construa uma frase concatenando fragmentos traduzidos. Traduza a mensagem inteira com placeholders:
"{count} novas mensagens", nãot('novas') + ' ' + count + ' ' + t('mensagens'). - Nunca use texto em inglês como chave. Chaves são identificadores estáveis (
inbox.unreadCount), para que alterações de texto não quebrem as buscas nem invalidem todas as traduções. - Toda string voltada ao usuário é externalizada, incluindo
aria-label,alt,title,placeholder, erros de validação, estados vazios, toasts, e-mails, notificações push e saída de PDF. - Nunca codifique formatos. Sem
toFixed(2) + '$', semMM/DD/YYYYmanual, sem nomes de meses em inglês, sem suposições de24h. UseIntl. - Um locale é uma tag BCP 47, não um idioma.
pt-BRept-PT,zh-Hansezh-Hant,es-MXees-ESsão produtos diferentes. Armazene a tag completa. - O estado do locale deve ter escopo de requisição no servidor. Nunca coloque o locale atual em uma variável mutável no nível do módulo durante SSR; requisições concorrentes vazarão idiomas entre si.
- Traduções são entrada não confiável se vierem de um TMS, comunidade ou CMS. Nunca as passe para
innerHTML/dangerouslySetInnerHTMLsem sanitizar.
Fluxo de Trabalho
1. Audite antes de alterar o código
- Detecte a stack, o roteador e a biblioteca de i18n existente antes de propor qualquer coisa.
- Encontre strings hardcoded:
Trate a saída como candidatos, não como verdade absoluta: pule fixtures de teste, constantes, URLs e logs voltados ao desenvolvedor.node scripts/scan-hardcoded.mjs src - Faça um inventário de todas as localidades atualmente enviadas e onde os catálogos residem.
- Identifique as áreas de maior risco primeiro: autenticação, checkout, erros, e-mails e textos legais.
2. Decida a arquitetura (escreva-a)
Responda a estas perguntas antes de escrever código:
| Decisão | Padrão | Notas |
|---|---|---|
| Estratégia de locale | Prefixo de caminho /es/ | Melhor SEO e capacidade de cache. Subdomínios/ccTLDs apenas com um motivo. |
| Locale padrão | Idioma de origem | Não redirecione automaticamente visitantes de primeira viagem para um locale adivinhado sem uma opção de substituição. |
| Detecção de locale | URL → cookie → Accept-Language → padrão | A URL vence para que links compartilhados nunca mudem de idioma. |
| Formato do catálogo | JSON por localidade por namespace | Carregue namespaces sob demanda; nunca envie todas as localidades para o cliente. |
| Sintaxe de mensagem | ICU MessageFormat | Portátil entre bibliotecas e TMSs; suporta plural/select/ordinal. |
| Estilo da chave | feature.component.element | Estável, pesquisável, sem texto em inglês. |
| Fonte da verdade da tradução | TMS ou repositório, não ambos | Escolha um; sincronize em uma direção no CI. |
| Fallback | Cadeia de locale es-MX → es → en | Registre fallbacks em desenvolvimento para detectar lacunas. |
3. Externalize e estruture as mensagens
- Substitua strings por chaves semânticas e mova o texto para o catálogo de locale base.
- Use uma chave por frase completa. Se uma frase contiver um link ou palavra em negrito, use interpolação rich-text em vez de dividi-la.
- Mantenha os placeholders nomeados (
{nomeUsuario}), nunca posicionais ({0}). - Não coloque HTML em catálogos, a menos que a biblioteca suporte explicitamente tags tipadas.
- Agrupe por superfície do produto, não por página:
checkout.json,auth.json,errors.json.
Consulte references/icu.md para regras de plural, select, ordinal e rich-text.
4. Configure o roteamento e a renderização de locale
- Derive o locale da rota, valide-o contra a lista de suportados e retorne 404 para locales desconhecidos em vez de cair silenciosamente no fallback.
- Forneça um seletor de idioma que preserve o caminho atual e a query string.
- Defina
<html lang="...">e<html dir="...">no documento renderizado no servidor. - Carregue apenas o catálogo do locale ativo mais o fallback padrão.
- Torne o mesmo catálogo disponível para renderização no servidor e no cliente; evite lógica de tradução duplicada ou divergente.
Configuração e armadilhas específicas de framework: references/frameworks.md.
Roteamento, detecção, hreflang e sitemaps: references/routing-seo.md.
5. Lidere com valores sensíveis ao locale
Use Intl (ou o wrapper do framework), nunca formatação manual:
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone }).format(date)
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(-3, 'day')
new Intl.ListFormat(locale, { type: 'conjunction' }).format(items)
new Intl.PluralRules(locale).select(count)
new Intl.Collator(locale).compare(a, b)
- Armazene timestamps em UTC; renderize no fuso horário do usuário, não no do servidor.
- Moeda não é o mesmo que locale: um usuário francês pode pagar em USD.
- Ordene/pesquise com
Intl.Collator, não com<oulocaleCompare()sem um locale. - Conversão de maiúsculas/minúsculas:
toLocaleUpperCase(locale), porque oiturco se tornaİ.
6. Projete para expansão e RTL
- Considere +30–40% de comprimento de texto (alemão, finlandês, russo) e evite botões de largura fixa ou rótulos de linha única.
- Deixe os contêineres crescerem, permita quebra de linha e teste com pseudo-localização:
node scripts/pseudo-localize.mjs locales/en.json --out locales/en-XA.json - Use propriedades CSS lógicas (
margin-inline-start,padding-inline,inset-inline) em vez deleft/right. - Espelhe ícones direcionais (setas, chevrons, progresso), não logotipos, relógios, controles de mídia ou marcas de verificação.
- Envolva conteúdo inline gerado pelo usuário em
<bdi>oudir="auto".
Lista de verificação completa de RTL: references/rtl.md.
7. Configure a garantia de qualidade da tradução no CI
No mínimo, falhe a construção quando os catálogos forem estruturalmente inválidos:
node scripts/check-locales.mjs locales --base en
Ele verifica chaves ausentes/extras, valores vazios, incompatibilidades de placeholder e sintaxe ICU quebrada. Adicione a compilação ICU via CLI da sua biblioteca quando disponível:
npx formatjs compile-folder --ast locales/en locales/compiled/en
Adicione testes para categorias de plural, saída de data/número com um fuso horário fixo e renderização RTL. Consulte references/testing.md.
Decisões e Armadilhas Principais
Seleção de biblioteca
| Necessidade | Escolha |
|---|---|
| Next.js App Router, componentes de servidor | next-intl |
| React SPA, ecossistema maduro, muitos plugins | react-i18next |
| ICU-first, agnóstico de framework, extração CLI | FormatJS (react-intl) |
| Catálogos em tempo de compilação, runtime pequeno, segurança de tipos | Lingui |
| Vue | vue-i18n |
| Svelte | svelte-i18n ou paraglide |
| Astro | roteamento i18n integrado + sua biblioteca de mensagens |
| Vanilla / mínimo | Intl + um carregador de catálogo pequeno |
Não introduza uma segunda biblioteca de i18n em um aplicativo que já tem uma. Estenda a configuração existente.
Pluralização
As categorias de plural são específicas do idioma. O inglês tem one/other; o árabe tem zero/one/two/few/many/other; o japonês tem apenas other. Nunca escreva count === 1 ? ... : ....
"inbox.unread": "{count, plural, =0 {Nenhuma mensagem não lida} one {# mensagem não lida} other {# mensagens não lidas}}"
Gênero e concordância gramatical
Use select em uma variável de gênero e mantenha o valor da variável neutro em relação ao idioma (female, male, neutral, unknown), não palavras localizadas:
"profile.greeting": "{gender, select, female {Bem-vinda, {name}} male {Bem-vindo, {name}} other {Bem-vindo/a, {name}}}"
Se um idioma precisar de concordância em adjetivos, substantivos ou artigos, exponha o gênero/caso gramatical como um argumento em vez de hackear a string.
Rich text
Não divida "Leia nossos " + link + " termos" em três chaves. Use interpolação de tags:
"signup.terms": "Eu concordo com os <link>Termos de Serviço</link>"
t.rich('signup.terms', { link: (chunks) => <a href="/terms">{chunks}</a> })
SEO
- Uma URL indexável por localidade; nunca sirva conteúdo traduzido por máquina por trás da mesma URL.
hreflangauto-referente maisx-default; URLs absolutas; inclua todas as localidades.- Localize
<title>, meta description, tags OG e dados estruturados. - Não combine
hreflangcomnoindexem páginas localizadas.
Detalhes em references/routing-seo.md.
Cache
- O locale no caminho torna o cache do CDN direto. Se você negociar por cabeçalho, envie
Vary: Accept-Languagee espere fragmentação de cache. - Não armazene em cache a preferência de idioma personalizada em uma resposta de CDN compartilhada.
- Mantenha
Content-Languageconsistente com o locale renderizado.
Lista de verificação de revisão
Antes de declarar o i18n concluído:
- Nenhuma string voltada ao usuário hardcoded em componentes, e-mails ou erros
- Nenhuma concatenação de sentenças; cada mensagem é uma unidade traduzível completa
- Chaves são semânticas e estáveis; nenhum texto em inglês como chave
- Todo placeholder existe em todas as localidades; a sintaxe ICU compila
- Plurais usam
Intl.PluralRules/ICU, nãocount === 1 - Gênero/concordância tratados via
select, não sufixos"/a" - Datas, números, moeda, tempo relativo usam
Intlcom locale/fuso horário explícitos -
<html lang>edirestão corretos na primeira pintura (SSR), não definidos em um efeito - Seletor de locale preserva caminho/query e funciona com links profundos
- Locale desconhecido retorna 404, não um fallback silencioso
- Layout sobrevive a +35% de expansão e RTL
-
hreflang, canônico, sitemap e metadados localizados estão corretos - Validação de catálogo é executada no CI
- Comportamento de fallback é intencional e observável
Anti-padrões
t('ola') + ', ' + nome— sentenças fragmentadaschave={textoEmIngles}— texto traduzido como chavecount === 1 ? 'item' : 'itens'— lógica de plural apenas em inglêsnew Date().toLocaleDateString()sem locale ou fuso horáriovalue.toFixed(2)para dinheirolocale === 'ar' && { direction: 'rtl' }espalhado pelos componentes em vez de umdirno nível do documento- Definir
<html lang>dentro deuseEffect— causa um flash e quebra crawlers - Carregar todas as localidades no pacote inicial
- Redirecionar automaticamente por IP sem forma de substituir
- Traduzir nomes próprios, nomes de produtos, identificadores de código ou URLs
- Strings traduzidas por máquina e enviadas sem revisão
- Assumir que um arquivo de catálogo escala além de algumas centenas de chaves