web-i18n-react-intl · git:20260906.3dc53ce · 2026-09-06 · sha256 9560de0b52d728d0
web-i18n-react-intl git:20260906.3dc53ceA
Immutable. This exact content is served forever at /api/v1/blob/9560de0b52d728d0.
---
name: web-i18n-react-intl
description: ICU message format internationalization for React — FormattedMessage, useIntl, defineMessages, and the FormatJS extraction workflow. Load when a project imports react-intl.
---
# React-Intl (FormatJS) Internationalization Patterns
> **Quick Guide:** `FormattedMessage` renders translated text in JSX, `useIntl` returns strings for
> attributes and programmatic use, and `defineMessages` produces descriptors the FormatJS CLI can
> extract. `IntlProvider` supplies the context, and its `onError` is what separates a missing
> translation from a real failure. Every `plural` and `select` needs an `other` branch. Version
> boundary: v7.x runs on React 16.6 through 19; v8 and later require React 19.
**Detailed Resources:**
- [examples/core.md](examples/core.md) — provider setup, FormattedMessage, useIntl, defineMessages, `createIntl`, locale switching, types, lazy loading
- [examples/formatting.md](examples/formatting.md) — date, time, number, currency, relative time, list and display-name formatting
- [examples/pluralization.md](examples/pluralization.md) — plural, ordinal, select, nested patterns, per-language categories, ICU escaping
- [reference.md](reference.md) — decision trees, ICU syntax tables, API tables, anti-pattern code, checklists
---
## Which path applies
- **Text rendered inside JSX** — `FormattedMessage`, including rich text with tag values. Follow
[examples/core.md](examples/core.md).
- **A string is needed** — an attribute, a document title, a value passed to a third-party
component, or a comparison — `useIntl().formatMessage()`. Also in
[examples/core.md](examples/core.md).
- **Outside a React tree** — a server render or a plain module — `createIntl` from
`@formatjs/intl`, optionally re-supplied through `RawIntlProvider`. In
[examples/core.md](examples/core.md).
---
<critical_requirements>
## Before writing react-intl code
**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:**
- Rendering messages with ICU interpolation, pluralization, select and rich text
- Formatting dates, numbers, currency, relative time, lists and display names per locale
- Structuring messages as descriptors so the CLI can extract and compile them
- Typing message IDs so a typo fails at compile time
- Loading a locale's messages on demand rather than bundling all of them
**Handled elsewhere:**
- Where the locale value comes from and how it is persisted — this skill consumes a locale and
settles nothing about detection, routing or storage
- A framework's own built-in i18n — a framework that resolves locale and messages per request will
do that better than a client-side provider, and this skill does not compete with it
- Translation vendor workflow — the CLI produces and consumes JSON, and what happens to it in
between is not this skill's concern
- Rendering and state — components receive messages through context and are otherwise ordinary
---
<philosophy>
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.
</philosophy>
---
<patterns>
## Core patterns
### Pattern 1: IntlProvider setup
`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.
```typescript
<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](examples/core.md)
### Pattern 2: FormattedMessage
For text rendered directly in JSX, including messages carrying ICU syntax.
```typescript
<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](examples/core.md)
### Pattern 3: useIntl
For any context that needs a string: attributes, document titles, third-party props, or a value the
code then compares.
```typescript
const intl = useIntl();
const placeholder = intl.formatMessage({
id: "search.placeholder",
defaultMessage: "Search products...",
});
```
Full code: [examples/core.md](examples/core.md)
### Pattern 4: defineMessages
Descriptors the CLI can find statically. `description` is the only channel a translator has for
context.
```typescript
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](examples/core.md)
### Pattern 5: Rich text
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.
```typescript
<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](examples/core.md)
### Pattern 6: Formatting
A component per value kind, each with an imperative twin on `useIntl` for string contexts.
```typescript
<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](examples/formatting.md)
### Pattern 7: Type-safe message IDs
Augment the `FormatjsIntl.Message` interface and a wrong ID becomes a compile error.
```typescript
declare global {
namespace FormatjsIntl {
interface Message {
ids: keyof typeof messages;
}
}
}
```
Add `"esnext.intl"` to `compilerOptions.lib`.
Full code: [examples/core.md](examples/core.md)
### Pattern 8: Extraction workflow
Extract descriptors, send the JSON out for translation, compile what comes back.
```bash
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.
### Pattern 9: ICU pluralization
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](examples/pluralization.md)
</patterns>
---
<performance>
## Performance
**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](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.
</performance>
---
<red_flags>
## Red flags
**Breaks at runtime:**
- A `plural` or `select` with no `other` branch — ICU requires it and formatting throws
- Any `useIntl` or `FormattedMessage` outside `IntlProvider` — there is no context to read
- `FormattedMessage` in an attribute — it is a `ReactNode`, so `placeholder`, `aria-label` and
`title` receive an object
- `injectIntl` — removed in v10; `useIntl` replaces it
**Surprising behaviour:**
- `formatMessage` returns `string` normally, but `string | ReactNode[]` as soon as a value is a rich
text tag function
- Rich text tag functions receive `chunks` as an array, not a single element
- `formatNumber` with `style: "percent"` expects a fraction — `0.25` renders as 25%
- `formatRelativeTime` is relative to now: negative is past, positive is future
- ICU escaping runs on single quotes — `'` escapes the next special character and `''` produces a
literal apostrophe, so an unescaped apostrophe in an English message can swallow the rest of it
- Without `defaultLocale`, a missing translation renders the raw message ID
- Without `onError`, every missing translation writes to the console
- Concatenating two translated strings assumes English word order, which most languages do not share
- `{count}` on its own never pluralizes, and a plural branch without `#` renders no number at all
- `onWarn` on `IntlProvider` is what quiets `defaultRichTextElements` warnings when messages are not
pre-compiled
Anti-patterns with the code that fixes them: [reference.md](reference.md).
</red_flags>