使用您最喜欢的AI助手总结文档,并引用此页面和AI提供商
此页面的内容已使用 AI 翻译。
查看英文原文的最新版本如果您有改善此文档的想法,请随时通过在GitHub上提交拉取请求来贡献。
文档的 GitHub 链接复制文档 Markdown 到剪贴板
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:
复制代码到剪贴板
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:
复制代码到剪贴板
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:
复制代码到剪贴板
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.
复制代码到剪贴板
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:
复制代码到剪贴板
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.
在弹窗中打开表格以清晰地查看所有数据
| 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:
复制代码到剪贴板
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.
复制代码到剪贴板
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:
复制代码到剪贴板
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:
复制代码到剪贴板
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:
复制代码到剪贴板
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:
复制代码到剪贴板
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
在弹窗中打开表格以清晰地查看所有数据
| 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:
复制代码到剪贴板
复制代码到剪贴板
The mapping to ICU concepts is direct:
在弹窗中打开表格以清晰地查看所有数据
| 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.
评论
暂无评论。成为第一个分享您想法的人吧。
