Intl APIs: NumberFormat, DateTimeFormat, Collator, ListFormat
4 min de leitura
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.
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:
- MDN - Intl.NumberFormat - referencia completa com todas as options.
- MDN - Intl.DateTimeFormat - todas as opcoes de data/hora.
- Format.JS - Guides - guias praticos, polyfills, e integracao com React.
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, useuseMemoou 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?