Pular para o conteúdo
~/.primo-academy.sh
☰ Aulas · i18n / l10n: Intl APIs, ICU MessageFormat, RTL, translation workflow e pseudo-localization · 0/7
Recomendado: essencial

Intl APIs: NumberFormat, DateTimeFormat, Collator, ListFormat

4 min de leitura

fonte

Voce internacionalizou o codigo: extraiu strings, definiu locales. Agora vem a parte sutil: formatar numeros, datas, moedas, listas - tudo de forma locale-aware, sem reinventar a roda. Esse no cobre o Intl.* do JS: namespace nativo do browser/Node que faz formatacao correta pra qualquer locale baseado nos dados do CLDR (Unicode Common Locale Data Repository).

O Intl.* e' o cavalo de batalha da i18n. Sem ele, voce faria tabelas gigantescas de formatacao por locale ("em pt-BR usa virgula, em en-US usa ponto, em de-DE usa ponto, em fr-FR usa espaco..."). Com Intl, uma chamada, resultado correto pro locale. Zero dependencies, zero dados, suportado em 100% dos browsers modernos (ES2018+).

Voce sai de "template literal com virgula fixa" pra "formatacao que respeita o idioma do usuario automaticamente".

O essencial 🟢

Intl.NumberFormat - numeros, moedas, porcentagens. O mais usado da familia Intl:

// Numero basico
new Intl.NumberFormat("pt-BR").format(1234.5);
// "1.234,5" (pt-BR usa . pra milhar, , pra decimal)

new Intl.NumberFormat("en-US").format(1234.5);
// "1,234.5" (en-US inverte)

new Intl.NumberFormat("de-DE").format(1234.5);
// "1.234,5" (alemão igual pt-BR mas sem 2 casas)

// Moeda
new Intl.NumberFormat("pt-BR", {
  style: "currency",
  currency: "BRL",
}).format(99.9);
// "R$ 99,90"

new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
}).format(99.9);
// "$99.90"

new Intl.NumberFormat("ja-JP", {
  style: "currency",
  currency: "JPY",
}).format(99.9);
// "¥100" (JPY nao tem centavos!)

// Porcentagem
new Intl.NumberFormat("pt-BR", {
  style: "percent",
}).format(0.25);
// "25%"

new Intl.NumberFormat("tr-TR", {
  style: "percent",
}).format(0.25);
// "%25" (turco coloca % antes do numero!)

// Casas decimais
new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
}).format(99.9);
// "$100"

// Notacao cientifica
new Intl.NumberFormat("en-US", { notation: "scientific" }).format(123456);
// "1.235E5"

Intl.DateTimeFormat - datas locale-aware. Datas tem infinitos formatos (MM/DD/YYYY, DD/MM/YYYY, YYYY年MM月DD日, etc):

new Intl.DateTimeFormat("pt-BR").format(new Date("2026-12-25"));
// "25/12/2026"

new Intl.DateTimeFormat("en-US").format(new Date("2026-12-25"));
// "12/25/2026"

new Intl.DateTimeFormat("ja-JP").format(new Date("2026-12-25"));
// "2026/12/25" (japones usa YYYY/MM/DD)

new Intl.DateTimeFormat("pt-BR", {
  dateStyle: "long",
}).format(new Date("2026-12-25"));
// "25 de dezembro de 2026"

new Intl.DateTimeFormat("en-US", {
  dateStyle: "full",
}).format(new Date("2026-12-25"));
// "Friday, December 25, 2026"

// Com hora
new Intl.DateTimeFormat("pt-BR", {
  dateStyle: "short",
  timeStyle: "short",
}).format(new Date("2026-12-25T14:30:00"));
// "25/12/2026 14:30"

// Timezone
new Intl.DateTimeFormat("en-US", {
  timeStyle: "long",
  timeZone: "America/New_York",
}).format(new Date());
// "2:30:00 PM EST"

// Componentes especificos
new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
  weekday: "long",
  hour: "2-digit",
  minute: "2-digit",
}).format(new Date());
// "Friday, December 25, 2026 at 02:30 PM"

Intl.Collator - sort e busca locale-aware. Sort de strings nao e' trivial quando tem acentos, caracteres compostos, ou scripts diferentes:

const items = ["Zebra", "Äpfel", "Apfel", "Banana"];

// ❌ Sort default (Unicode code points)
items.sort();
// ["Apfel", "Banana", "Zebra", "Äpfel"]  (Ä vai pro fim em Unicode)

// ✅ Sort com Collator alemao
items.sort(new Intl.Collator("de-DE").compare);
// ["Äpfel", "Apfel", "Banana", "Zebra"]  (Ä vem antes de A em alemao)

// Compare com locale especifico
const collatorPt = new Intl.Collator("pt-BR", { sensitivity: "base" });
collatorPt.compare("Maça", "MACA");
// 0 (case-insensitive + accent-insensitive)

Intl.ListFormat - "A, B e C" em qualquer idioma. Ingles: "A, B, and C". Frances: "A, B et C". Japones: "A、B、C":

new Intl.ListFormat("en", { style: "long", type: "conjunction" })
  .format(["Ana", "Bruno", "Carla"]);
// "Ana, Bruno, and Carla"

new Intl.ListFormat("pt-BR", { style: "long", type: "conjunction" })
  .format(["Ana", "Bruno", "Carla"]);
// "Ana, Bruno e Carla"

new Intl.ListFormat("en", { style: "long", type: "disjunction" })
  .format(["Ana", "Bruno", "Carla"]);
// "Ana, Bruno, or Carla"

new Intl.ListFormat("ja", { style: "narrow", type: "unit" })
  .format(["10kg", "20kg", "30kg"]);
// "10kg、20kg、30kg"

A maquina de decisao: qual Intl usar.

Decision tree: qual Intl formatter usar pra cada caso. Regra pratica: 90% dos casos cai em NumberFormat (numeros/moedas), DateTimeFormat (datas), ou ListFormat (listas). Collator e' essencial pra sort correto; PluralRules e' o motor por tras do ICU pluralization.

Intl.PluralRules - o motor por tras do ICU plural. Antes de chamar ICU, voce precisa saber qual a regra de plural do locale:

new Intl.PluralRules("en-US").select(1);    // "one"
new Intl.PluralRules("en-US").select(5);    // "other"
new Intl.PluralRules("pl-PL").select(1);    // "one"
new Intl.PluralRules("pl-PL").select(5);    // "many" (5-21 em polones!)
new Intl.PluralRules("pl-PL").select(22);   // "few"  (22-25)
new Intl.PluralRules("pl-PL").select(100);  // "many" (26+)
new Intl.PluralRules("ar-EG").select(1);    // "one"
new Intl.PluralRules("ar-EG").select(2);    // "two"
new Intl.PluralRules("ar-EG").select(10);   // "many"
new Intl.PluralRules("ar-EG").select(100);  // "other"

Intl.RelativeTimeFormat - "5 minutos atras" sem dor. Sabe aquele 5min ago em ingles, há 5 min em PT, 5分前 em japones? Faca certo:

new Intl.RelativeTimeFormat("pt-BR", { numeric: "auto" }).format(-5, "minute");
// "há 5 minutos"

new Intl.RelativeTimeFormat("en-US", { numeric: "auto" }).format(-5, "minute");
// "5 minutes ago"

new Intl.RelativeTimeFormat("pt-BR").format(1, "day");
// "em 1 dia"

new Intl.RelativeTimeFormat("en-US", { numeric: "always" }).format(-1, "day");
// "1 day ago"  (numeric: 'always' = sempre com numero)

Intl.DisplayNames - nome de linguas/ paises. Mostrar "esse conteudo ta' disponivel em Ingles" na lingua nativa:

const dn = new Intl.DisplayNames(["pt-BR"], { type: "language" });
dn.of("en");    // "inglês"
dn.of("ja");    // "japonês"
dn.of("pt-BR"); // "português (Brasil)"

const dnRegion = new Intl.DisplayNames(["pt-BR"], { type: "region" });
dnRegion.of("US"); // "Estados Unidos"
dnRegion.of("BR"); // "Brasil"

Intl.Segmenter - quebrar texto em palavras/grafemas. Especialmente util em linguas sem espacos (chines, japones, tailandes):

const segmenter = new Intl.Segmenter("ja-JP", { granularity: "word" });
const text = "こんにちは世界";
[...segmenter.segment(text)].map((s) => s.segment);
// ["こんにちは", "世界"]

Performance: reuse a instancia. Criar um Intl.NumberFormat tem custo (carrega CLDR data). Crie uma vez, reuse:

// ❌ Cria instancia nova a cada format
function formatPrice(value) {
  return new Intl.NumberFormat("pt-BR", {
    style: "currency",
    currency: "BRL",
  }).format(value);
}

// ✅ Cria uma vez, cache
const priceFormatter = new Intl.NumberFormat("pt-BR", {
  style: "currency",
  currency: "BRL",
});
function formatPrice(value) {
  return priceFormatter.format(value);
}

// Em React, use useMemo ou um singleton
const useFormatter = (locale, options) =>
  useMemo(() => new Intl.NumberFormat(locale, options), [locale, JSON.stringify(options)]);

Detectar locale do user. A escolha do locale vem de varias fontes:

function getUserLocale(): string {
  // 1. URL (mais comum em apps com i18n)
  const urlLocale = new URLSearchParams(window.location.search).get("lang");
  if (urlLocale) return urlLocale;

  // 2. localStorage (escolha do user)
  const stored = localStorage.getItem("preferred-locale");
  if (stored) return stored;

  // 3. Browser (Accept-Language)
  return navigator.language; // "pt-BR", "en-US", etc
}

Aprofundamento 🟡

Performance real: Intl.NumberFormat e lento na primeira chamada. O primeiro new Intl.NumberFormat("ar-SA") pode levar 5-50ms (carrega CLDR data praquele locale). Chamadas subsequentes sao sub-millisecond. Solucao em apps com muitos locales: preload os mais comuns no boot:

// preload no boot
["pt-BR", "en-US", "es-ES", "ja-JP"].forEach((locale) => {
  new Intl.NumberFormat(locale, { style: "currency", currency: "BRL" });
});
// primeira format real fica rapida

Intl.NumberFormat options avancadas.

new Intl.NumberFormat("pt-BR", {
  style: "currency",
  currency: "BRL",
  currencyDisplay: "code",     // "BRL 99,90" em vez de "R$ 99,90"
  currencyDisplay: "symbol",   // "R$ 99,90" (default)
  currencyDisplay: "narrowSymbol", // "R$99,90" sem espaco (compact)
  useGrouping: "min2",         // agrupa so com >=2 casas
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  minimumSignificantDigits: 1, // 1-3 digitos significativos
  maximumSignificantDigits: 3,
  localeMatcher: "best fit",   // ou "lookup"
  roundingMode: "halfEven",    // banker's rounding
}).format(99.9);

notation: 'compact' - 1.2K, 3.4M.

new Intl.NumberFormat("en-US", { notation: "compact" }).format(1234);
// "1.2K"

new Intl.NumberFormat("en-US", { notation: "compact" }).format(1234567);
// "1.2M"

new Intl.NumberFormat("pt-BR", { notation: "compact" }).format(1234);
// "1,2 mil"

unit - unidades de medida. Formatar distancias, pesos, etc com a unidade traduzida:

new Intl.NumberFormat("en-US", {
  style: "unit",
  unit: "kilometer",
  unitDisplay: "long",
}).format(5);
// "5 kilometers"

new Intl.NumberFormat("pt-BR", {
  style: "unit",
  unit: "kilometer",
  unitDisplay: "long",
}).format(5);
// "5 quilometros"

new Intl.NumberFormat("pt-BR", {
  style: "unit",
  unit: "celsius",
  unitDisplay: "short",
}).format(25);
// "25 °C"

ICU MessageFormat vs Intl. Sao diferentes mas complementares:

  • Intl: formatacao de valores (numeros, datas, listas).
  • ICU MessageFormat: mensagens inteiras com placeholders, plural, selecao ("voce tem 5 items" / "voce nao tem items" / "voce tem 1 item").

Use Intl pra formatar valores isolados. Use ICU pra mensagens compostas que incluem plural e selecao.

Pra quem quer ir mais alem 🔴

Intl.NumberFormat v3 - novidades 2024+ (ES2023). Stage 4 e' Rounding Modes (halfEven, halfExpand, etc), e useGrouping: "min2" | "always" | "auto" granular (era boolean). Stage 3 inclui enum API (Intl.supportedValuesOf("calendar")), Intl.Locale enhancements (extractor de time zone, calendario, numbering system da BCP 47 tag).

Intl.DurationFormat - "1h 30min 15s". Experimental (Chrome 129+, Safari parcial). Formata duracoes ISO 8601 em formato human-readable, com pluralizacao e traducao automatica.

Intl.Segmenter para truncamento inteligente. "Ler mais..." em ingles. Cortar na fronteira de palavra/grafema correta (nao no meio de emoji ou caracter composto):

const segmenter = new Intl.Segmenter("pt-BR", { granularity: "word" });
function truncate(text, maxChars) {
  let result = "";
  for (const s of segmenter.segment(text)) {
    if ((result + s.segment).length > maxChars) break;
    result += s.segment;
  }
  return result + "...";
}

Polyfill: Intl.PluralRules em Node 10- (sem suporte nativo). Em 2026, suporte e' universal (Node 18+, todos browsers). Polyfills so' pra apps legados.

Carregar CLDR customizado. Se voce quer usar regras de plural customizadas (raro), use a lib intl-pluralrules (FormatJS polyfill) que aceita regras custom via CLDR data files.

Intl.Locale - introspeccao de tags.

const loc = new Intl.Locale("pt-BR");
loc.language;      // "pt"
loc.region;        // "BR"
loc.baseName;      // "pt-BR"
loc.calendar;      // "gregory"
loc.numberingSystem; // "latn"
loc.getWeekInfo(); // { firstDay: 7, weekend: [6, 0] } (BR: semana comeca domingo)

Use pra detectar configuracao do locale automaticamente e adaptar UI.

Leitura recomendada:

Dica: o erro mais comum em Intl e' criar instancia nova a cada format. new Intl.NumberFormat(...) carrega CLDR data e tem custo de 5-50ms na primeira vez. Em loops (ex: render 1000 rows de tabela com precos), isso trava a UI. Sempre reuse: crie o formatter uma vez, use .format() em loop. Em React, use useMemo ou um singleton. Cache a nivel de modulo e' o padrao.

No proximo no, vamos ICU MessageFormat: a linguagem de pluralizacao, selecao, e argumentos que resolve "1 item" / "2 items" / "5 items" / "22 items" / "items" em qualquer idioma. Cobre a parte de mensagens compostas.

// Quiz

Por que `Intl.NumberFormat` e' melhor que template literals pra formatar numeros/moedas em apps i18n?

Escolha uma alternativa

// recursos

// avaliação da trilha

—
ainda sem avaliações