Ajukan pertanyaan Anda dan dapatkan ringkasan dokumen dengan merujuk halaman ini dan penyedia AI pilihan Anda
Konten halaman ini diterjemahkan menggunakan AI.
Lihat versi terakhir dari konten aslinya dalam bahasa InggrisJika Anda memiliki ide untuk meningkatkan dokumentasi ini, silakan berkontribusi dengan mengajukan pull request di GitHub.
Tautan GitHub ke dokumentasiSalin Markdown dokumentasi ke clipboard
ICU Message Format: the syntax, and the parts that trip people up
ICU MessageFormat is a string syntax that lets a translation contain its own branching logic: plurals, gendered forms, number and date formatting. It exists because grammar belongs to the translator, not to the developer writing if (count === 1). This post covers the syntax, the language-dependent parts that break naive implementations, and how the JS ecosystem handles it.
Table of Contents
The problem, concretely
Here is the code almost everyone writes first:
Salin kode ke clipboard
This works in English and breaks everywhere else:
- Russian and Polish need three or four forms, not two.
- Japanese needs one, and the space you concatenated is wrong.
- Arabic needs six, and the number itself should be rendered in the locale's numbering system.
- French puts a non-breaking space before some punctuation, which your
+ " "just destroyed.
The deeper problem is that the sentence has been cut into fragments. A translator sees item and items with no context and no ability to reorder the sentence. ICU MessageFormat fixes this by keeping the whole sentence in one translatable string and giving the translator the branching operators.
Simple arguments
The smallest unit is a placeholder in single braces:
Salin kode ke clipboard
You pass { name: "Alice" } at format time and get Hello, Alice!. Braces are the only special characters; to print a literal brace you wrap it in single quotes: '{'.
That is the entire "interpolation" feature. Everything else in ICU is built on top of it.
Plural
plural selects a branch based on a number:
Salin kode ke clipboard
Three things to know:
#is replaced by the formatted value ofcount, locale-formatted, so1234becomes1,234inen-USand1 234infr-FR.otheris mandatory. Every ICU implementation will throw or fail validation without it. It is the fallback when no category matches.=0,=1, … match exact values and are checked before the CLDR categories. Use them for special-cased copy ("No messages"), not as a substitute forone.
Salin kode ke clipboard
offset
offset:n subtracts n from the value before both category selection and # substitution. It exists for the "Alice and 3 others liked this" pattern:
Salin kode ke clipboard
With count: 4, the # renders 3. offset is genuinely useful and genuinely under-supported, so check your runtime before relying on it.
Plural categories are language-dependent
This is the part people get wrong. The category names zero, one, two, few, many, other are not universal buckets you fill in for every language. Each locale uses a subset, defined by the CLDR plural rules, and the rules are grammatical, not intuitive.
Buka tabel dalam modal untuk melihat semua isi data dengan jelas
| Language | Tag | Categories used | Count |
|---|---|---|---|
| Japanese | ja | other | 1 |
| Chinese | zh | other | 1 |
| English | en | one, other | 2 |
| German | de | one, other | 2 |
| French | fr | one, many, other | 3 |
| Czech | cs | one, few, many, other | 4 |
| Polish | pl | one, few, many, other | 4 |
| Russian | ru | one, few, many, other | 4 |
| Arabic | ar | zero, one, two, few, many, other | 6 |
| Welsh | cy | zero, one, two, few, many, other | 6 |
Two consequences that surprise people:
onedoes not mean "1". In Russian,onecovers 1, 21, 31, 101: any number ending in 1 except those ending in 11. In French,0falls intoone.- Adding a category to the English source does nothing. The English message only needs
oneandother; the Polish translation needs four branches, and that structure lives in the Polish string, not the English one. Any format that forces all locales to share one key shape will fight you here.
You can check what a runtime actually does without installing anything:
Salin kode ke clipboard
Intl.PluralRules ships CLDR data in every modern browser and in Node. A library claiming CLDR pluralization is almost always calling it underneath.
select and selectordinal
select branches on an arbitrary string: a gender, a role, a status, a plan tier.
Salin kode ke clipboard
Keys are matched literally and other is mandatory here too. select is the right tool any time sentence structure depends on an enum value, because languages disagree about which enums affect grammar.
selectordinal is the same shape as plural but uses the ordinal plural rules, which are a different table from the cardinal ones:
Salin kode ke clipboard
English uses four ordinal categories (1st, 2nd, 3rd, 4th) even though it only uses two cardinal ones. That asymmetry is exactly why the two operators are separate.
Number, date and time arguments
ICU can format the value it interpolates:
Salin kode ke clipboard
The modern form is the skeleton, introduced with ICU 60 and marked by a :: prefix. Skeletons are far more expressive than the legacy style names:
Salin kode ke clipboard
Skeleton support is the most uneven part of the ecosystem. FormatJS implements them; several other runtimes only accept the legacy number, currency / date, long forms. Verify :: support in your actual runtime before shipping.
Nesting, and where it stops being readable
ICU composes. A plural branch can contain a select, which can contain another plural:
Salin kode ke clipboard
This is the canonical ICU example and also the canonical argument against deep nesting. Two levels is where translators start making brace errors and where TMS editors stop helping. Nest at most two levels; if you need a third, split the sentence into two messages.
How JS libraries handle ICU
Buka tabel dalam modal untuk melihat semua isi data dengan jelas
| Library | ICU support | What you actually write |
|---|---|---|
| react-intl (FormatJS) | Native, full | ICU strings, including skeletons and rich-text tags |
| next-intl | Native | ICU strings, via FormatJS's intl-messageformat |
| i18next | Plugin required | key_one / key_other suffix keys and {{name}}; ICU via i18next-icu |
| vue-i18n | Partial / its own | {name} interpolation and pipe-separated plural branches |
Angular ($localize) | Subset | ICU plural / select inside templates, extracted to XLIFF |
A few notes so the table is not misleading:
- i18next's default syntax is not ICU and is not worse for it. Suffix keys (
item_one,item_few) map ontoIntl.PluralRulescategories and are arguably easier for translators to edit in flat JSON. Butselectand nested branching are not part of it, so you either addi18next-icuor you write the logic in code. - vue-i18n's pipe plurals use a per-locale rule function, not CLDR categories by default. That works, but the plural rule lives in your app config rather than in the data.
- FormatJS is the reference implementation in JS. When people say "ICU MessageFormat" in a JS context, they usually mean what FormatJS accepts.
How Intlayer handles it
Intlayer does not use a string DSL. The branching operators are functions in a content declaration file, so the structure is typed and each locale declares only the categories its grammar needs:
Salin kode ke clipboard
Salin kode ke clipboard
The mapping to ICU concepts is direct:
Buka tabel dalam modal untuk melihat semua isi data dengan jelas
| ICU construct | Intlayer |
|---|---|
{name} | insert("Hello {{name}}"), or auto-detected |
{count, plural, …} | plural({ one, few, many, other }) |
{value, select, …} | select({ draft, published, fallback }) |
gender branch of select | gender({ male, female, fallback }) |
boolean branch of select | cond({ true, false }) |
| numeric ranges (non-CLDR) | enu({ "0": …, ">5": …, fallback: … }) |
{n, number, ::currency/EUR} | useCurrency()(1234.5, { currency: "EUR" }) |
plural delegates category selection to Intl.PluralRules, so the CLDR table above applies unchanged. Formatting stays separate: numbers, dates, currencies and lists go through the formatter hooks instead of being embedded in the message.
Honest limits:
- Intlayer requires a build step; the compiler extracts declarations at build time. If you want plain JSON loaded at runtime, that is a different model.
pluralcannot nest at()inside its branches yet; you wrappluralint(), not the other way round.- The ecosystem is smaller than i18next's. Fewer TMS integrations, fewer StackOverflow answers.
If you are coming from a codebase that already contains real ICU strings, the react-intl compat adapter parses them directly: plural, select, selectordinal, #, and the legacy number / date / time arguments. Skeletons and offset: are not covered by that resolver, so check those messages when you migrate. The i18next adapter resolves the suffix form (key_one, key_male) against Intl.PluralRules instead.
Common mistakes
- Hardcoding plural logic in JS.
count === 1 ? a : bproduces the wrong output for 8 of the 10 languages in the table above. Once the ternary is in code, no translator can fix it. - Concatenating translated fragments. Word order, agreement and punctuation spacing are all locale-dependent. Keep the sentence whole.
- Omitting
other. It is required by the spec, not a convention. Most parsers reject the message; the ones that do not will render nothing. - Assuming your categories generalize. An English source with
one/otherdoes not mean the Polish file has two branches. Let each locale declare its own. See per-locale content declaration. - Using
=1where you meantone.=1matches only the literal 1. In Russian, 21 needsone, and=1will never fire for it. - Putting
#outside a plural branch. It is only special insideplural/selectordinal. Elsewhere it is a literal hash. - Forgetting that
#is formatted. If you want the raw number, interpolate the argument by name instead.
Going further
- Plural content in Intlayer: the CLDR-backed
pluralnode and its category table. - Select-based content: the equivalent of ICU
select, and when to useenuorcondinstead. - Insertion placeholders:
{{name}}interpolation and automatic detection. - i18n library benchmark: bundle size and runtime cost across the libraries listed above.
- react-i18next vs react-intl vs Intlayer: a fuller comparison of the three message models.
- What is internationalization?: the wider scope beyond message formatting.
- i18n meaning: where the term comes from and how i18n differs from l10n.
Komentar
Belum ada komentar. Jadilah yang pertama membagikan pemikiran Anda.
