Curso de Internacionalização (i18n)
Por que fazer, como fazer e como manter — com demos ao vivo rodando direto no navegador.
A tese deste curso: internacionalização (i18n) não é "traduzir o app". É a arquitetura que permite que o mesmo código sirva qualquer idioma, formato de data, número e moeda — sem reescrever nada. Quem faz i18n só quando precisa, paga caro depois. Quem faz desde o começo, escala com custo marginal.
Este curso é interativo: há demos ao vivo (mude o idioma da própria página, veja plurais e datas em várias línguas) e checklists com progresso salvo. Conteúdo em PT-BR, baseado em documentação oficial (Unicode CLDR, ICU, FormatJS) e guias de 2026.
Por que internacionalizar
Antes do "como", o "porquê". Internacionalizar é decisão de negócio, não só de engenharia.
i18n ≠ l10n ≠ tradução
| Termo | Significado | Quem faz |
|---|---|---|
| Internacionalização (i18n) | Preparar o código para suportar qualquer idioma/região (strings externalizadas, datas, RTL) | Desenvolvedores |
| Localização (l10n) | Adaptar o produto a um mercado específico (tradução + moeda + cultura) | PM, UX writers, tradutores |
| Tradução | Converter texto de um idioma para outro | Tradutores / máquinas |
Regra: você não pode pular o i18n e esperar que o l10n dê certo. "i18n é a fundação; l10n são os cômodos." Strings hardcoded e frases montadas por concatenação tornam qualquer tradução impossível sem reescrita.
Motivos práticos
- Mercado: bilhões de usuários não falam inglês. Localizar é a diferença entre "serve para mim" e "feito para mim".
- Confiança e conversão: o usuário compra no próprio idioma. Checkout, preço e moeda errados quebram confiança na hora.
- Requisito legal/regulatório: vários mercados (UE, Canadá, Brasil em certos contratos) exigem idioma e formatos locais.
- SEO: conteúdo no idioma local é indexado e buscado no idioma local (hreflang).
- Acessibilidade:
langcorreto afeta leitores de tela, hifenização e pronúncia. - Custo futuro: adicionar o 2º idioma em código i18n-ready custa frações do custo de retrofit em app legado.
Quando começar
Regra de ouro: comece a i18n no primeiro dia — ou, no mínimo, antes do segundo idioma. O maior erro das equipes é lançar 5 idiomas de uma vez. Comece com 1 idioma estrangeiro: ele força a resolver extração, handoff de tradução, QA e release — e os próximos idiomas ficam baratos.
Fig. M1-1 — Custo cumulativo: fazer i18n desde o início tem custo pequeno e estável; retrofit depois do lançamento custa uma ordem de grandeza a mais (e trava o produto durante a migração).
Exercícios M1
- Ex.1.1 Explique a diferença entre i18n, l10n e tradução em 3 frases.
- Ex.1.2 Liste 3 motivos para internacionalizar um app seu (ou hipotético).
- Ex.1.3 Por que começar com um único idioma estrangeiro em vez de cinco?
Conceitos fundamentais
Os tijolos de qualquer sistema i18n: locale, Unicode, CLDR, RTL e a API Intl do navegador.
Locale: idioma + região
Um locale identifica idioma e variante regional. O código pt-BR = português do Brasil; pt-PT = Portugal; en-US vs en-GB. Formato: lang-REGION (BCP 47). Usar apenas pt pode gerar formatos genéricos errados (ex.: moeda com símbolo regional incorreto).
| Locale | Data | Número | Moeda |
|---|---|---|---|
| pt-BR | 25/08/2026 | 1.234,56 | R$ 1.234,56 |
| en-US | 08/25/2026 | 1,234.56 | $1,234.56 |
| de-DE | 25.08.2026 | 1.234,56 | 1.234,56 € |
| ar-EG | ٢٥/٠٨/٢٠٢٦ | ١٬٢٣٤٫٥٦ | ١٬٢٣٤٫٥٦ ج.م |
Unicode e UTF-8
Todo o texto deve viver em UTF-8 (padrão de fato). Sem isso: mojibake (caracteres quebrados), problemas com emoji, caracteres acentuados e scripts complexos. Use UTF-8 em banco, API, HTML e arquivos de tradução.
CLDR: as regras do mundo em dados
O CLDR (Unicode Common Locale Data Repository) é a fonte canônica de regras por idioma: formatos de data/número/moeda, plurais, ordem de classificação, nomes de meses. Todo sistema i18n sério usa CLDR (por baixo do Intl, do ICU, do i18next). Não invente regra: use os dados.
RTL: idiomas da direita para a esquerda
Árabe, hebraico, persa e urdu lêem da direita para a esquerda. O layout precisa espelhar (menu à direita, progresso invertido, ícones de "voltar" virados). Adicionar RTL depois do lançamento é caro — desenhe o layout com dir="rtl" em mente desde o início.
Demo ao vivo — formatação com Intl
Exercícios M2
- Ex.2.1 O que diferencia
en-USdeen-GB? Dê 2 exemplos práticos. - Ex.2.2 Por que usar CLDR em vez de "regras caseiras" de plural/data?
- Ex.2.3 O que muda num layout quando o idioma é RTL?
Arquitetura de strings
A regra número um: nenhum texto visível hardcoded. Toda string visível sai do código e entra num arquivo de recursos.
Externalizar: o antes e o depois
// ❌ Antes (hardcoded + concatenação — o pesadelo)
<button>Você tem " + count + " mensagens novas</button>
// ✅ Depois (chave + interpolação)
<button>{t('inbox.newMessages', { count })}</button>
Abordagens de mensagens
| Abordagem | Como funciona | Exemplos |
|---|---|---|
| Key-based | O código usa uma chave (inbox.newMessages); o texto fica nos arquivos de tradução | i18next, react-i18next |
| Source-based | O código usa o texto em inglês como chave; extração automática | react-intl (defaultMessage), gettext |
| ICU MessageFormat | Formato padrão que embute plurais/selects na própria mensagem | react-intl, FormatJS, Lingui |
| MessageFormat 2 (MF2) | Padrão novo da Unicode (candidato final 2025) — plurais por .match explícito | adoção ainda inicial |
Regras de ouro para strings
- Nunca concatene ("Você tem " + n + " itens") — cada idioma tem ordem de palavras diferente. Use interpolação
{n}. - Não traduza código (variáveis, nomes de arquivos, mensagens de log) — só o que o usuário vê.
- Dê contexto à chave (ex.:
menu.savevsdialog.save) para o tradutor acertar o sentido. - Mantenha frases inteiras juntas — quebrar em pedacinhos impede boa tradução (gênero, ordem, concordância).
- Idioma de fallback sempre — se faltar tradução, mostra o idioma base em vez de quebrar.
- Interpolação tipada — as libs validadas em build (react-intl/FormatJS, Lingui) pegam erro de chave/variável na hora.
Plurais: o erro que todo mundo comete
Nunca use count === 1 ? singular : plural. O inglês tem 2 formas; o árabe tem 6 (zero, one, two, few, many, other); o russo 3; o japonês 1. Uma checagem binária produz texto gramaticalmente errado para 5 das 6 categorias do árabe. Use Intl.PluralRules ou uma lib CLDR-aware.
// en.json (react-intl / ICU)
{ "inbox.items": "{count, plural, one {# mensagem nova} other {# mensagens novas}}" }
// ar.json — precisa cobrir 6 categorias
{ "inbox.items": "{count, plural, zero {# mensajes} one {# mensaje} two {# mensajen} few {# mensajes} many {# mensaje} other {# mensaje}}" }
Demo ao vivo — plurais por idioma
Exercícios M3
- Ex.3.1 Reescreva "Você tem 3 mensagens" e "Você tem 1 mensagem" como uma única chave ICU com plural.
- Ex.3.2 Por que concatenar frases quebra a tradução? Dê um exemplo de ordem de palavras que muda.
- Ex.3.3 Teste a demo de plurais em ar e ru. Quantas formas diferentes aparecem?
Datas, números e plurais na prática
Formatar é tão i18n quanto traduzir — e é onde mais se erra. A regra: armazene sempre em UTC/ISO; formate só na hora de exibir, com a API Intl.
Datas, horas e fuso
- Armazene em UTC (ISO 8601:
2026-08-25T20:30:00Z) em banco/API. - Converta para o fuso do usuário apenas na exibição — o servidor nunca deve "adivinhar" o fuso.
- Nunca formate data com strings fixas (
"DD/MM/YYYY") — useIntl.DateTimeFormat.
new Intl.DateTimeFormat('de-DE', { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Berlin' })
.format(new Date())
// → "25. August 2026, 22:30"
Números e moeda
Intl.NumberFormat e Intl.NumberFormat(..., { style: 'currency', currency }) cuidam de separadores, símbolo e posição da moeda. Nunca hardcode o símbolo nem a posição. Guarde o valor numérico puro (não use "R$ 1.234,56" como string de dados).
Text expansion
Fig. M4-1 — Expansão de texto: alemão/finlandês/russo podem ficar até ~30% mais longos que o inglês. Layout com largura fixa corta o texto. Nunca use largura fixa — deixe ~20–30% de folga.
Bibliotecas (escolha consciente)
| Lib | Formato | Pontos fortes | Quando usar |
|---|---|---|---|
| react-intl (FormatJS) | ICU MessageFormat | Padrão, tipado, RSC-ready (v10), plurais no texto | React/Next; quer padrão estrito |
| i18next / react-i18next | Key-based JSON (sufixos _one/_other) | 6,3M+ downloads/semana, plugins (detector, backend), <Trans> | React; ecossistema + flexibilidade |
| Lingui | ICU (macros) | Compilação e validação em build | Time que quer checagem estática forte |
| gettext / Fluent | .po / Fluent | Maduro (gettext), Fluent legível p/ tradutores | Apps desktop/CLI; Mozilla-style |
Para quem usa TS puro (como a landing da issoai.com), uma tipagem de dicionário (interface de chaves por idioma) já dá segurança em tempo de compilação — ver M7.
Exercícios M4
- Ex.4.1 Onde você armazena timestamps? Onde converte para o fuso do usuário?
- Ex.4.2 Teste a demo do M2: o mesmo número em ar-EG (dígitos arábicos) vs en-US. O que muda além do separador?
- Ex.4.3 Escolha uma lib para o seu stack e justifique (ICU vs key-based).
Roteamento, SEO e RTL
Na web, o idioma também vive na URL, no HTML e nos metadados.
Roteamento por idioma
Três padrões comuns:
- Prefixo de caminho:
/pt,/en,/es(claro, padrão em Next.js multi-idioma). - Subdomínio:
pt.issoai.com(separa mercados, útil para geo). - Negociação por cabeçalho
Accept-Language: sem prefixo (mais simples, pior para SEO/manter link).
Metadados essenciais
<html lang="pt-BR" dir="ltr"> <!-- idioma + direção do documento -->
<link rel="alternate" hreflang="en" href="https://site.com/en/" />
<link rel="alternate" hreflang="pt-BR" href="https://site.com/pt/" />
<link rel="alternate" hreflang="x-default" href="https://site.com/" />
lang: correto para leitores de tela, hifenização e seletores CSS:lang().dir="rtl": para árabe/hebraico; o CSS lógico (margin-inline-start,inset-inline) espelha sozinho.hreflang: diz ao Google qual URL serve cada idioma/região (SEO).
Detecção e fallback
Não confie só no Accept-Language — usuários mudam de idioma o tempo todo. Dê sempre um seletor visível, respeite a escolha (cookie/localStorage) e use o header só como padrão inicial. Sempre tenha um fallback (idioma base) para chaves ausentes.
Exercícios M5
- Ex.5.1 Para um site seu, qual roteamento escolheria (prefixo/subdomínio/header)? Por quê?
- Ex.5.2 Monte os metadados
lang/dir/hreflangpara 2 idiomas. - Ex.5.3 Como você faz fallback de tradução? (chave ausente → idioma base).
Workflow e qualidade
Tradução boa é processo, não acaso. O pipeline se repete a cada release.
O pipeline contínuo
Fig. M6-1 — O ciclo: extrair → traduzir → revisar → integrar → publicar → medir. Automatize extração e integração; a revisão humana fica só onde importa.
Ferramentas e camadas
| Camada | Ferramentas | Papel |
|---|---|---|
| Extração | FormatJS CLI, i18next-parser, Lingui extract, babel-gettext | Varrer o código e gerar o arquivo-base de strings |
| Gerenciamento | Crowdin, Lokalise, POEditor, SimpleLocalize, Locize, Transifex | TMS: tradutores, glossário, memória de tradução, contexto |
| Tradução | Humana + IA (DeepL/Google) com revisão | UI/onboarding: IA + revisão; jurídico/marketing: humano |
| Integração/QA | CI (pull automático), pseudo-localização, checagem de chaves | Nunca lançar release com chave ausente/quebrada |
Pseudo-localização (a técnica que salva)
A pseudo-localização substitui cada caractere por uma versão acentuada/alongada (Çǿńtìńùé) e testa em runtime: revela texto hardcoded que não foi externalizado e layouts que estouram com texto longo. Rode-a em QA antes de cada release.
Checklist de qualidade
- Completude: nenhuma chave ausente em nenhum idioma (fallback cobre, mas reporte).
- Formato: datas/números/moeda via Intl; nada hardcoded.
- RTL: testar árabe/hebraico em todas as telas.
- Expansão: telas com texto longo (30%+) não quebram.
- Contexto: tradutor vê screenshot/contexto da chave.
- CI: build falha se chave inválida ou tradução incompleta.
Exercícios M6
- Ex.6.1 Desenhe o seu pipeline (extrair→…→publicar) e marque o que automatizar.
- Ex.6.2 Rode pseudo-localização no seu app e anote 3 problemas que apareceram.
- Ex.6.3 Escolha um TMS (ou fluxo manual) e justifique.
Caso real: issoai.com (lib/i18n.ts)
A landing da issoai.com é trilíngue (pt/en/es) e roda exatamente os conceitos deste curso — sem lib: um dicionário tipado em TypeScript. Vamos ver como e por quê.
A arquitetura
- Uma única fonte de verdade:
lib/i18n.tscom uma interfaceDict(o "contrato" de chaves) e um objeto por idioma (pt,en,es). ProjectCopy: o tipo de cada card de produto —name, tag, status, desc, f1, f2, f3, a1. Se uma chave faltar num idioma, o compilador quebra (segurança em tempo de build, sem runtime).- Roteamento: o componente recebe
lange o seletor trocat(o dicionário) — re-render, sem recarregar.
// lib/i18n.ts (simplificado)
export interface ProjectCopy {
name: string; tag: string; status: string;
desc: string; f1: string; f2: string; f3: string; a1: string;
}
export interface Dict {
projects: { juice: ProjectCopy; sentinela: ProjectCopy; estrategias: ProjectCopy; /* ... */ };
how: { eyebrow: string; title: string; steps: HowStep[] };
/* ... */
}
export const pt: Dict = { projects: { juice: { name: "Juice", /* ... */ }, /* ... */ } };
Demo ao vivo — a landing você mesmo
Lições do caso real
- Tipar o dicionário evita a classe de bugs mais comum (chave errada/tradução faltando) sem pagar runtime de lib.
- Escopo certo: trilíngue para a home (marketing, precisa de portabilidade); os cursos são PT-only por decisão de produto. Nem tudo precisa de N idiomas.
- Conteúdo de marca fica fora do dicionário quando escrito igual nos idiomas (ex.: nomes de produtos no rodapé).
- Armadilha real: texto embutido em componentes (aria-label, placeholder) também é string — precisa entrar no dicionário.
Armadilhas finais (resumo)
| Armadilha | Como evitar |
|---|---|
| Hardcode de texto/aria-label/placeholder | Extração automática + pseudo-localização |
| Concatenação e plural binário | Interpolação + Intl.PluralRules/ICU |
| Data/número/moeda com formato fixo | Intl.* sempre; UTC no storage |
| Largura fixa / layout en-US-only | Layout fluido (+20–30%) e CSS lógico |
| RTL depois do lançamento | Planejar dir/rtl desde o início |
| Tradução sem contexto | TMS com screenshot/contexto + glossário |
| Confiar só em máquina em texto sensível | IA + revisão; humano em jurídico/marketing |
Exercícios M7
- Ex.7.1 No seu projeto, liste 3 strings que estão hardcoded hoje e onde deveriam ir.
- Ex.7.2 Vale a pena tipar o dicionário (TS) no seu caso? Por quê?
- Ex.7.3 Decida quantos idiomas o seu produto PRECISA (não "poderia ter") e justifique.
Fontes e links
- Unicode CLDR — plural rules: cldr.unicode.org
- MDN — Intl: Intl · Intl.PluralRules · DateTimeFormat
- FormatJS / ICU MessageFormat: formatjs.io · react-intl: formatjs.github.io
- i18next: i18next.com · react-i18next: react.i18next.com
- MessageFormat 2 (padrão Unicode): formatjs.io/docs/message-format
- Guia técnico completo de i18n: simplelocalize.io
- Guia de localização web (2026): lingohub.com
- i18n Best Practices 2026: better-i18n.com
- Front-End Checklist — regras de i18n: frontendchecklist.io
- Caso real: a própria issoai.com (dicionário tipado em
VPS/landing/lib/i18n.ts)
⚠️ Ecossistema muda rápido — verifique a documentação oficial da lib/framework escolhida (estado deste curso: ago/2026).