ICU MessageFormat: pluralizacao, selecao, argumentos, MessageFormat 2
4 min de leitura
Voce ja' sabe formatar numeros, datas,
moedas com Intl.*. Agora vem o
problema de i18n que 80% dos apps
implementam errado: pluralizacao e
selecao. Ingles e' facil: "1 item" /
"2 items" (2 formas). Mas polones tem
3 formas (1 / 2-4 / 5+), arabe tem
6 formas (1 / 2 / 3-10 / 11-99 / 100
/ 101+), e japones so' tem 1 forma.
Esse no cobre ICU MessageFormat: a
linguagem padrao pra escrever
mensagens que lidam com plural, selecao
("male/female/other"), e argumentos
nomeados - traduzida corretamente em
qualquer idioma.
Intl.PluralRules (no anterior) diz
qual categoria de plural usar
(one/other/few/many/etc). ICU
MessageFormat combina isso com
templates de mensagem - vc escreve
"Voce tem {count, plural, one {# item}
other {# items}}" e o ICU resolve
corretamente pro locale.
Voce sai de "template literal com if(count===1)" (quebra em polones) pra "mensagens ICU que funcionam em qualquer idioma".
O essencial 🟢
O problema classico: pluralizacao que quebra em i18n.
// ❌ Errado: assume "1 = singular, 2+ = plural"
const mensagem = `Voce tem ${count} ${count === 1 ? "item" : "items"}`;
// Quebra em:
// polones: 1 = 1 forma, 2-4 = outra, 5+ = outra
// arabe: 1 = 1 forma, 2 = 2 forma, 3-10 = 3 forma, ...
// japones: 1 = mesma forma (1 item, 2 item, 5 item...)
// chines: 1 = mesma forma
// ❌ Tambem errado: plural em ingles hardcoded
const mensagem = `${count} item${count === 1 ? "" : "s"}`;
// Quebra em qualquer outro idioma alem de ingles
ICU MessageFormat 1.x - a sintaxe classica. Mensagens com placeholders, plural, select, e combinacoes:
{chave, tipo, valor}
{total, plural, =0 {Sem items} one {1 item} other \{# items\}}
{gender, select, male {Ele} female {Ela} other {Eles}}
{price, number, currency}
{date, date, long}
Exemplos praticos:
// Plural basico
const m1 = `Voce tem {count, plural,
one {1 item}
other {# items}
}`;
// count=1: "Voce tem 1 item"
// count=5: "Voce tem 5 items"
// Plural em polones
const m2 = `Masz {count, plural,
one {1 przedmiot}
few {# przedmioty}
many {# przedmiotów}
other {# przedmiotu}
}`;
// count=1: "Masz 1 przedmiot"
// count=2: "Masz 2 przedmioty" (few)
// count=5: "Masz 5 przedmiotów" (many)
// count=22: "Masz 22 przedmioty" (few - 22-25)
// Select por genero
const m3 = `{gender, select,
male {Ele enviou uma mensagem}
female {Ela enviou uma mensagem}
other {Enviaram uma mensagem}
}`;
// Combinando plural + select
const m4 = `{gender, select,
male {Ele tem {count, plural, one {1 amigo} other {# amigos}}}
female {Ela tem {count, plural, one {1 amiga} other {# amigas}}}
}`;
// gender=male, count=1: "Ele tem 1 amigo"
// gender=female, count=3: "Ela tem 3 amigas"
// Argumentos nomeados
const m5 = `Ola {nome}, voce tem {count, plural, one {1 mensagem} other {# mensagens}}`;
// "Ola Ana, voce tem 1 mensagem"
// "Ola Bruno, voce tem 5 mensagens"
// Number format inline
const m6 = `Total: {total, number, ::currency/EUR}`;
// "Total: €99,90"
Usando em JS - intl-messageformat (npm).
pnpm add intl-messageformat
import IntlMessageFormat from "intl-messageformat";
const msg = new IntlMessageFormat(
"Voce tem {count, plural, one {1 item} other \{# items\}}",
"pt-BR"
);
msg.format({ count: 1 }); // "Voce tem 1 item"
msg.format({ count: 5 }); // "Voce tem 5 items"
const plMsg = new IntlMessageFormat(
"Masz {count, plural, one {1 przedmiot} few {# przedmioty} many {# przedmiotów} other {# przedmiotu}}",
"pl-PL"
);
plMsg.format({ count: 1 }); // "Masz 1 przedmiot"
plMsg.format({ count: 3 }); // "Masz 3 przedmioty" (few)
plMsg.format({ count: 5 }); // "Masz 5 przedmiotów" (many)
plMsg.format({ count: 22 }); // "Masz 22 przedmioty" (few)
Intl.PluralRules - o motor por tras.
const pr = new Intl.PluralRules("pl-PL");
pr.select(1); // "one"
pr.select(2); // "few"
pr.select(5); // "many"
pr.select(22); // "few" (22-25 sao few)
pr.select(100); // "many"
const prAr = new Intl.PluralRules("ar-EG");
prAr.select(1); // "one"
prAr.select(2); // "two"
prAr.select(3); // "few" (3-10)
prAr.select(11); // "many" (11-99)
prAr.select(100); // "other"
Categorias CLDR padrao:
zero- arabe (count=0)one- ingles (1), polones (1)two- arabe (2)few- polones (2-4, 22-25), arabe (3-10)many- polones (5-21, 26+), arabe (11-99)other- default (sempre disponivel)
=N exato - "exatamente 0 items", "exatamente
1 item". Para casos onde o plural CLDR nao
captura o que voce quer:
{count, plural,
=0 {Voce nao tem items}
one {Voce tem 1 item}
other {Voce tem # items}
}
Select aninhado com plural.
{gender, select,
male {{count, plural,
one {Ele tem 1 amigo}
other {Ele tem # amigos}
}}
female {{count, plural,
one {Ela tem 1 amiga}
other {Ela tem # amigas}
}}
}
# - o valor atual. Dentro de
{count, plural, ...}, o # e' substituido
pelo valor de count. Util quando voce nao
quer escrever {count} de novo:
{count, plural,
one \{# item}
other {# items}
}
ICU em i18next, FormatJS, react-intl.
Todas as libs de i18n populares usam ICU
como motor. i18next tem i18next-icu
plugin, FormatJS usa ICU nativo. Em
99% dos casos voce nao escreve ICU na
mao - usa uma lib que parseia e resolve.
// i18next com i18next-icu
import i18next from "i18next";
import ICU from "i18next-icu";
i18next.use(ICU).init({
resources: {
"pt-BR": {
translation: {
cart_items: "Voce tem {{count, plural, one {1 item} other \{# items\}}}",
},
},
"pl-PL": {
translation: {
cart_items: "Masz {{count, plural, one {1 przedmiot} few {# przedmioty} many {# przedmiotów} other {# przedmiotu}}}",
},
},
},
});
i18next.t("cart_items", { count: 1 }); // "Voce tem 1 item"
i18next.t("cart_items", { count: 5 }); // "Voce tem 5 items"
Aprofundamento 🟡
MessageFormat 2 (MF2) - o futuro (2024+). A sintaxe 1.x funciona, mas tem limitacoes: verbosa, dificil de aninhar, type-unsafe. MF2 (a nova versao, em rollout em 2024-2026) simplifica:
// ICU 1.x: verboso
{chose, select, male {his} female {her} other {their}}
// MF2: declarativo
.input {$chose :gender}
.match $chose
male {{Ele enviou}}
female {{Ela enviou}}
* {{Enviaram}}
MF2 tem sintaxe type-safe (compilador valida placeholders), mais expressiva (local variants, custom functions), e backward-compatible com 1.x. Em 2026, use 1.x (estavel) a menos que esteja comecando projeto novo e queira apostar em MF2 (compiler + parser disponivel).
Custom functions em MF2. Alem de
plural/select/number/date, MF2
aceita functions custom (registered
no compilador). Ex: :currency,
:relativeTime, :emoji - voce cria
suas proprias.
ICU em libs de UI: react-intl, FormatJS, Lingui, i18next-icu.
| Lib | ICU nativo | Sintaxe | Bundle |
|---|---|---|---|
| i18next-icu | via plugin | ICU 1.x | +12KB |
| FormatJS | nativo | ICU 1.x | +30KB |
| react-intl | nativo | ICU 1.x | +30KB |
| Lingui | compila | ICU-like | +10KB |
| @messageformat/parse | puro | ICU 1.x | +8KB |
Lingui compila ICU em JS estatico - bundle minimo, mas tem build step. Escolha depende de stack: i18next (mais popular JS), FormatJS (React first), Lingui (performance).
Intl.MessageFormat proposal - nativo no
JS. Stage 3 (proposta TC39). Em 2026,
nao no spec - ainda depende de libs.
Acompanhe
github.com/tc39/proposal-intl-messageformat.
Argumentos nomeados vs posicionais.
// Posicional
"Hello {0}, you have {1} items" (i18next: {{0}}, {{1}})
// Nomeado
"Hello {name}, you have {count} items"
Nomeado e' mais legivel e mais facil de traduzir (tradutor ve "name" e "count" em vez de numeros).
Escape com aspas simples. Pra incluir
{ ou } literal em mensagem ICU, use
' como escape:
"Mostre '{name}' no console" -> "Mostre {name} no console"
"Use '{' e '}'" -> "Use { e }"
Plural + offset. Pra "voce, mais 4 amigos" (count inclui voce), use offset:
{count, plural, offset:1
=0 {Voce nao tem amigos}
=1 {Voce tem 1 amigo (so' voce)}
one {Voce e # outro amigo}
other {Voce e # outros amigos}
}
// count=1: "Voce tem 1 amigo (so' voce)"
// count=5: "Voce e 4 outros amigos"
Pra quem quer ir mais alem 🔴
ICU em SSR (Next.js, Remix, etc). ICU
parser precisa de CLDR data (vem com
Node/browser). Em serverless (Vercel
Edge, Cloudflare Workers), pode ser
limitado. Solucao: polyfill com
@formatjs/intl-pluralrules ou use
Lingui que compila em estatico.
Custom CLDR rules - quando o seu produto e' diferente. Raro, mas se voce tem categoria de plural propria (ex: "pro" vs "free" user), implemente custom select:
const customRules = {
pro: (n) => n > 0,
free: () => true,
};
Use @messageformat/runtime com
custom select function.
i18n em Web Components / Shadow DOM.
ICU nao tem nada especifico pra Shadow DOM,
mas Intl.PluralRules e
Intl.NumberFormat funcionam global.
Cuidado com lang attribute: se voce
nao define <html lang>, o browser usa
default (en-US) e formata errado.
Number skeletons (LDML). Formatacao avancada de numeros inline em ICU:
{price, number, ::currency/EUR .##}
{count, number, ::percent .00}
{bytes, number, ::unit/megabyte .0#}
Skeletons sao strings que descrevem
formatacao, alternativa a options object.
Use quando precisa de formatacao complexa
inline em mensagem.
Date skeletons:
{date, date, ::yyyyMMdd}
{now, date, ::hms}
{deadline, date, ::yMMMMd}
Leitura recomendada:
- ICU MessageFormat - spec oficial, sintaxe, exemplos.
- Format.JS - Message Format - uso pratico em React, polyfills.
- i18next - Interpolation - integracao i18next + ICU.
Dica: o erro mais comum em pluralizacao i18n e' hardcoded if/else baseado em ingles. "1 = singular, 2+ = plural" funciona em ingles, quebra em polones (1 = singular, 2-4 = "few", 5+ = "many"), arabe (6 formas!), e japones (1 forma so'). Use ICU + Intl.PluralRules: o motor consulta o CLDR e escolhe a categoria certa pro locale. Em ingles:
one/other. Em polones:one/few/many/other. Escreva a mensagem com ICU e o parser resolve.
No proximo no, vamos RTL e
bidirecionalidade: como tornar sua UI
correta em arabe, hebraico, e outros
idiomas RTL. Cobre dir, icones
espelhados, e CSS logical properties
que tornam o layout agnostico de
direcao.
// Quiz
Por que pluralizacao hardcoded com if/else (1 ? 'item' : 'items') quebra em apps i18n?