Estágio 02 · 02-19
LockedSoftware global. Logística pode crescer pra Brasil + México + EU em poucos anos. Mas i18n quase sempre é after-thought: dev escreve "You have ${n} orders" hardcoded em English; PR aprova; meses depois, time inteiro reescreve telas pra suportar pt-BR/es-MX/de-DE/he-IL/ar-SA. Custa 2-5x mais que ter feito desde o início.
i18n não é "tradução do front-end". Inclui: Unicode correctness (UTF-8/16, normalization, combining chars, emoji), pluralization (10+ regras em russo, polonês, árabe), gender-aware messages, number/currency/date format por locale, timezone math, RTL layouts, fonts com glyphs corretos, sorting/search com collation, input methods, certificados e regulamentação por país (LGPD, GDPR, CCPA), tax calc por região (já em 02-18), nomes pessoais cross-cultural (não há "first name + last name" universal), endereços com formatos divergentes.
Este módulo é o ofício de produzir software que não quebra no segundo locale: Unicode profundo, ICU MessageFormat, biblioteca i18n moderna (FormatJS, i18next, Lingui), Intl APIs do browser, RTL com CSS logical properties, locale-aware sorting, timezone correto (sempre UTC + tz id), e l10n process (translation memory, glossary, contractor workflow).
Charset universal (1.1M code points, 150k assigned). Encodings:
Code point ≠ grapheme cluster. "é" pode ser 1 code point (precomposed) ou 2 (e + combining acute). Família emoji 👨👩👧 é 1 grapheme com 5 code points.
str.length em JS conta UTF-16 code units. Errado pra emoji + accents. Use Intl.Segmenter (modern) ou grapheme-splitter lib.
Same string aparente, bytes diferentes. Forms:
é precomposto).e + combining).Comparação requer normalização. str.normalize('NFC'). Bug clássico: usuário cadastra "café" (NFD), login compara com "café" (NFC), mismatch silencioso.
Sort cultural-aware. "ç" em Português < "z"; em Castellano antigo, "ñ" tinha posição própria.
String.localeCompare(b, locale, opts) ou Intl.Collator(locale, opts). Opts:
sensitivity: 'base'/'accent'/'case'/'variant'.numeric: true: "10" > "2" como números.Postgres collation: schemas, índices baseados. Trocar collation pode invalidar índice, caso real causando incidents.
English: 0/1 vs N+. Russian: 1, 2-4, 5+. Polish: 1, 2-4, 5+ except 22-24, 25-29 different. Árabe: 6 categorias (zero, one, two, few, many, other).
ICU CLDR define regras. Use:
new Intl.PluralRules('pl').select(5); // → 'many'
Frameworks: ICU MessageFormat, FormatJS, i18next, Lingui.
{n, plural,
=0 {Sem pedidos}
one {Um pedido}
other {# pedidos}
}
ICU select:
{gender, select,
female {Ela viu seu perfil}
male {Ele viu seu perfil}
other {Visualizaram seu perfil}
}
Cross-language: em Português, gender afeta artigo/adjetivo. Em English, raramente. Regras divergem.
Intl.NumberFormat:
1234.5 → en-US: 1,234.5; pt-BR: 1.234,5; de-DE: 1.234,5; fr-FR: 1 234,5; ar-EG: ١٬٢٣٤٫٥ (não-Latin digits).Currency:
$1,234.50 (en-US) vs R$ 1.234,50 (pt-BR) vs 1 234,50 € (fr-FR) vs €1.234,50 (de-DE).Intl.NumberFormat('pt-BR', {style: 'currency', currency: 'BRL'}).Dates:
new Intl.DateTimeFormat('ja-JP', {dateStyle: 'long'}).format(d) → "2026年4月28日".UTC sempre. Persist UTC. Display in user tz.
Tz id (IANA): America/Sao_Paulo. NÃO offset (-03:00); offset muda com DST.
Intl.DateTimeFormat('pt-BR', {timeZone: 'America/Sao_Paulo'}).
Libs: date-fns-tz, dayjs-timezone, Luxon. JS native Temporal API (chegando, parcial).
DST: Brasil não tem desde 2019. Ainda assim, world has. Calculation com horário "futuro" envolve atualizações IANA tzdata. Mantenha tzdata atualizado (geralmente 2-4 releases/ano).
Common bugs:
Anti-pattern: "You have " + n + " orders". Concatena gramática.
Pattern: chave + ICU template.
t('orders.count', { count: n });
Locales:
// en
{"orders.count": "You have {count, plural, =0 {no orders} one {one order} other {# orders}}"}
// pt-BR
{"orders.count": "Você {count, plural, =0 {não tem pedidos} one {tem um pedido} other {tem # pedidos}}"}
Árabe, hebraico, persa, urdu são RTL. Layout inverte.
CSS logical properties:
margin-inline-start em vez de margin-left.text-align: start em vez de left.padding-inline-end.border-inline-start.dir="rtl" em html. Browser flips layout. Imagens direcionais (setas, ícones de "voltar") precisam mirror.
Não-Latin scripts: Cyrillic (russo), Greek, CJK (Chinese/Japanese/Korean), Arabic, Hebrew, Devanagari (hindi), Thai, etc.
System fonts: cobrem só subset. Custom font precisa coverage. Google Fonts, Noto family ("no tofu"; cobertura ampla), Source Sans/Han.
CJK = renders pesados. Use subsetting + variable fonts.
Emoji: cada plataforma renders diferente. Twemoji (Twitter), Noto Color Emoji.
Chinese/Japanese/Korean usam IME (Input Method Editor): user digita romaji/pinyin, sugere candidates, confirma. UI deve não processar keydown durante composition (use compositionstart/compositionend).
Bug clássico: search-as-you-type dispara em cada keydown durante IME composition, query incompleta vai pro backend.
Postgres: CREATE COLLATION pt_BR (locale = 'pt_BR.UTF-8'). Diferença em sorting/comparison.
pg_trgm para fuzzy. Unaccent extension pra search ignorando acentos: unaccent('café') = 'cafe'.
Elasticsearch / Meilisearch (02-15): tokenizer + stemmer language-aware. RSLP (português) ≠ Snowball English.
Já em 02-18, recall:
Edge: zero-decimal currencies (JPY, KRW). 100 yen é 100, não 1.00.
Stripe / PSPs lidam minor units: 100 USD = 10000 cents; 100 JPY = 100 (no decimals).
US: street, city, state, zip. Brasil: street, número, complemento, bairro, cidade, estado, CEP. Japão: zip, prefecture, city, street, building (orderem inversa). UK: street, town, postcode (state ausente).
Backend: persist como structured JSON ou separated fields. Front-end: render com template por país. Don't impose US shape.
Lib: i18n-postal-address, libphonenumber pra phone.
"First name + last name" não é universal. Spanish: dois sobrenomes (paterno + materno). Indonesian: muitos só 1 nome. Hungarian: family name first. Chinese: family name first.
Persist full_name (string) + opcional display_name. Don't force first/last separation se não preciso.
Falabamos como 2025+ pattern: Patrick McKenzie's "Falsehoods Programmers Believe About Names" é leitura obrigatória.
Engenharia: dependendo do mercado, multi-region storage, audit logs, data residency, right-to-be-forgotten endpoints.
orders.list.empty).if (locale === 'en') ... else ... em código.localeCompare.lang attribute (a11y + tools).Você precisa, sem consultar:
Intl.PluralRules pra polonês.Internationalize a Logística para 3 locales: pt-BR (default), en-US, es-MX.
Intl.timezone em profile.pt_BR.UTF-8 em campos relevantes.en-XA que expande strings 30% e adiciona acentos. Use pra detect overflow.TRANSLATION-PROCESS.md definindo workflow.<html> por locale.Intl namespace.