ICU message format internationalization
Quick Guide:
FormattedMessagerenders translated text in JSX,useIntlreturns strings for attributes and programmatic use, anddefineMessagesproduces descriptors the FormatJS CLI can extract.IntlProvidersupplies the context, and itsonErroris what separates a missing translation from a real failure. Everypluralandselectneeds anotherbranch. Version boundary: v7.x runs on React 16.6 through 19; v8 and later require React 19.
Detailed Resources:
createIntl, locale switching, types, lazy loadingFormattedMessage, including rich text with tag values. Follow
examples/core.md.useIntl().formatMessage(). Also in
examples/core.md.createIntl from
@formatjs/intl, optionally re-supplied through RawIntlProvider. In
examples/core.md.<critical_requirements>
Wrap the tree in IntlProvider with locale, messages and defaultLocale. Every
FormattedMessage and useIntl call reads that context, and defaultLocale is what a missing
translation falls back to instead of surfacing the raw ID.
Give every plural and select an other branch. ICU requires it, and a message without one
throws when formatted rather than when authored.
Match the major version to the React version in the project. v7.x covers React 16.6 through 19; v8 and later dropped everything before React 19.
</critical_requirements>
Auto-detection: react-intl, FormatJS, FormattedMessage, useIntl, IntlProvider, RawIntlProvider, defineMessages, defineMessage, createIntl, formatMessage, FormattedDate, FormattedNumber, FormattedRelativeTime, ICU message format
Applies to:
Handled elsewhere:
ICU Message Format is the point: it is what translation vendors already speak, so a message written
in it moves through a professional workflow without a conversion step. Everything else follows.
Formatting is delegated to the browser's own Intl APIs rather than reimplemented, which is why
locale-specific behaviour is correct for locales nobody tested. And the API is deliberately doubled —
a component for JSX and a hook for strings — because a ReactNode cannot be put in an attribute.
onError is where a missing translation is separated from a real failure, and
defaultRichTextElements gives <b>, <i> and <br> one definition for the whole app.
<IntlProvider
locale={locale}
defaultLocale={DEFAULT_LOCALE}
messages={messages}
defaultRichTextElements={DEFAULT_RICH_TEXT_ELEMENTS}
onError={(err) => {
if (err.code === "MISSING_TRANSLATION") return;
throw err;
}}
>
{children}
</IntlProvider>
Full code: examples/core.md
For text rendered directly in JSX, including messages carrying ICU syntax.
<FormattedMessage
id="greeting.unread"
defaultMessage="{count, plural, =0 {No messages} one {# message} other {# messages}}"
values={{ count: unreadCount }}
/>
It returns a ReactNode, so an attribute — placeholder, aria-label, title — needs Pattern 3
instead.
Full code: examples/core.md
For any context that needs a string: attributes, document titles, third-party props, or a value the code then compares.
const intl = useIntl();
const placeholder = intl.formatMessage({
id: "search.placeholder",
defaultMessage: "Search products...",
});
Full code: examples/core.md
Descriptors the CLI can find statically. description is the only channel a translator has for
context.
export const productMessages = defineMessages({
reviewCount: {
id: "product.reviewCount",
defaultMessage:
"{count, plural, =0 {No reviews} one {# review} other {# reviews}}",
description: "Number of product reviews with pluralization",
},
});
Spread a descriptor into FormattedMessage, or pass it to intl.formatMessage.
Full code: examples/core.md
Tags in the message map to values, so the sentence stays in one translation unit and the translator can reorder the tags to fit the target grammar.
<FormattedMessage
id="terms.notice"
defaultMessage="You agree to our <terms>Terms</terms> and <privacy>Privacy Policy</privacy>."
values={{
terms: (chunks) => <a href="/terms">{chunks}</a>,
privacy: (chunks) => <a href="/privacy">{chunks}</a>,
}}
/>
Full code: examples/core.md
A component per value kind, each with an imperative twin on useIntl for string contexts.
<FormattedDate value={date} year="numeric" month="long" day="numeric" />
<FormattedNumber value={amount} style="currency" currency={currency} />
<FormattedList type="conjunction" value={names} />
Full code: examples/formatting.md
Augment the FormatjsIntl.Message interface and a wrong ID becomes a compile error.
declare global {
namespace FormatjsIntl {
interface Message {
ids: keyof typeof messages;
}
}
}
Add "esnext.intl" to compilerOptions.lib.
Full code: examples/core.md
Extract descriptors, send the JSON out for translation, compile what comes back.
formatjs extract 'src/**/*.{ts,tsx}' --out-file lang/en.json
formatjs compile lang/en.json --out-file compiled/en.json --ast
Compiling to AST skips parsing at runtime — 30-50% off first render for a large catalog.
Plural category counts differ by language: English has one/other, Russian adds few and many,
Arabic adds zero and two.
{count, plural, =0 {No items} one {# item} other {# items}}
{position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}
{gender, select, male {He} female {She} other {They}} liked your post.
Full code: examples/pluralization.md
</patterns>Compile to AST at build time — Pattern 8's formatjs compile --ast step, which is where the
largest single win is.
Load one locale at a time with dynamic imports, and cache what comes back so a switch back is free. Implementation in examples/core.md.
Define messages outside the component. An object literal passed inline to FormattedMessage is
a new reference every render, which defeats memoization.
createIntl + createIntlCache + RawIntlProvider puts the intl object under explicit control
when you want to memoize it yourself.
<red_flags>
Breaks at runtime:
plural or select with no other branch — ICU requires it and formatting throwsuseIntl or FormattedMessage outside IntlProvider — there is no context to readFormattedMessage in an attribute — it is a ReactNode, so placeholder, aria-label and
title receive an objectinjectIntl — removed in v10; useIntl replaces itSurprising behaviour:
formatMessage returns string normally, but string | ReactNode[] as soon as a value is a rich
text tag functionchunks as an array, not a single elementformatNumber with style: "percent" expects a fraction — 0.25 renders as 25%formatRelativeTime is relative to now: negative is past, positive is future' escapes the next special character and '' produces a
literal apostrophe, so an unescaped apostrophe in an English message can swallow the rest of itdefaultLocale, a missing translation renders the raw message IDonError, every missing translation writes to the console{count} on its own never pluralizes, and a plural branch without # renders no number at allonWarn on IntlProvider is what quiets defaultRichTextElements warnings when messages are not
pre-compiledAnti-patterns with the code that fixes them: reference.md.
</red_flags>
npx skills add agents-inc/web-i18n-react-intl下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer