web-i18n-react-intl · git:20260202.b236384 · 2026-02-02 · sha256 7865826d281af36e
web-i18n-react-intl git:20260202.b236384A
Immutable. This exact content is served forever at /api/v1/blob/7865826d281af36e.
---
name: web-i18n-react-intl
description: ICU message format internationalization
---
# React-Intl (FormatJS) Internationalization Patterns
> **Quick Guide:** Use react-intl for internationalization with ICU Message Format. `FormattedMessage` for JSX content, `useIntl` for string attributes and programmatic use, `defineMessages` for extractable message descriptors. Wrap app with `IntlProvider` and configure `onError` for missing translations.
>
> **Version Note:** react-intl v8+ requires React 19+. For React 18 projects, use react-intl v6.x.
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST wrap the application root with `IntlProvider` and configure locale, messages, and defaultLocale)**
**(You MUST include the `other` category in ALL plural and select ICU messages - omission causes runtime errors)**
**(You MUST use named constants for locale codes - NO inline locale strings)**
**(You MUST verify React version compatibility: v8+ requires React 19+, use v6.x for React 18)**
</critical_requirements>
---
**Auto-detection:** react-intl, FormatJS, FormattedMessage, useIntl, IntlProvider, defineMessages, ICU message format, formatMessage, FormattedDate, FormattedNumber
**When to use:**
- Implementing internationalization in React applications (non-Next.js)
- Rendering localized messages with ICU syntax (interpolation, pluralization, select)
- Formatting dates, numbers, currency, and relative time per locale
- Extracting and compiling translation messages for TMS workflows
- Building type-safe i18n with TypeScript augmentation
**Key patterns covered:**
- IntlProvider setup with error handling and default rich text elements
- FormattedMessage component for declarative JSX translations
- useIntl hook for imperative string formatting (attributes, props)
- defineMessages for static message extraction
- ICU Message Format syntax (plurals, select, ordinals, rich text)
- Date, time, number, and relative time formatting components
- TypeScript integration for type-safe message IDs
- Testing patterns with custom render wrapper
**When NOT to use:**
- Next.js App Router applications (use next-intl instead - better SSR integration)
- Simple single-locale applications (skip i18n complexity)
- Server Components without React context (use createIntl from @formatjs/intl)
**Detailed Resources:**
- For code examples, see [examples/](examples/) (core.md, formatting.md, pluralization.md)
- For decision frameworks and anti-patterns, see [reference.md](reference.md)
---
<philosophy>
## Philosophy
React-intl follows the principle of **ICU Message Format standardization** with both declarative and imperative APIs. Translations use industry-standard ICU syntax enabling compatibility with professional translation management systems. The library is built on browser-native `Intl` APIs for optimal performance and accurate locale-aware formatting.
**Core principles:**
1. **ICU Standard**: Use industry-standard ICU Message Format for professional translation workflows
2. **Dual API**: FormattedMessage for JSX content, useIntl for string contexts (attributes, programmatic use)
3. **Native Intl**: Built on browser Intl APIs for accurate locale-specific formatting
4. **Extractable**: defineMessages enables CLI extraction for translation management
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: IntlProvider Setup
Wrap your application root with `IntlProvider` to establish i18n context.
#### Constants
```typescript
// src/i18n/config.ts
export const SUPPORTED_LOCALES = ["en", "de", "fr", "es"] as const;
export const DEFAULT_LOCALE = "en";
export type Locale = (typeof SUPPORTED_LOCALES)[number];
export function isValidLocale(locale: string): locale is Locale {
return SUPPORTED_LOCALES.includes(locale as Locale);
}
```
#### Implementation
```typescript
// src/providers/intl-provider.tsx
import { IntlProvider } from "react-intl";
import type { ReactNode } from "react";
import { DEFAULT_LOCALE, type Locale } from "../i18n/config";
type Props = {
children: ReactNode;
locale: Locale;
messages: Record<string, string>;
};
export function AppIntlProvider({ children, locale, messages }: Props) {
return (
<IntlProvider
locale={locale}
defaultLocale={DEFAULT_LOCALE}
messages={messages}
onError={(err) => {
if (err.code === "MISSING_TRANSLATION") {
console.warn(`Missing translation: ${err.message}`);
return;
}
throw err;
}}
>
{children}
</IntlProvider>
);
}
export { AppIntlProvider };
```
**Why good:** named constants for locales enable type-safe usage throughout app, custom onError distinguishes missing translations from actual errors, defaultLocale provides fallback for missing messages
```typescript
// WRONG - No error handling, hardcoded locale
import { IntlProvider } from "react-intl";
function App({ children }) {
return (
<IntlProvider locale="en" messages={messages}>
{children}
</IntlProvider>
);
}
```
**Why bad:** hardcoded locale string prevents type safety, missing onError causes console noise for missing translations, no defaultLocale means no fallback behavior
---
### Pattern 2: FormattedMessage Component (Declarative)
Use `FormattedMessage` for rendering translated text directly in JSX.
```typescript
// src/components/greeting.tsx
import { FormattedMessage } from "react-intl";
type Props = {
userName: string;
unreadCount: number;
};
export function Greeting({ userName, unreadCount }: Props) {
return (
<div>
<h1>
<FormattedMessage
id="greeting.welcome"
defaultMessage="Welcome back, {name}!"
values={{ name: userName }}
/>
</h1>
<p>
<FormattedMessage
id="greeting.unread"
defaultMessage="{count, plural, =0 {No unread messages} one {# unread message} other {# unread messages}}"
values={{ count: unreadCount }}
/>
</p>
</div>
);
}
export { Greeting };
```
**Why good:** FormattedMessage renders directly in JSX, ICU plural syntax handles all count cases including zero, values are explicitly named for refactoring safety
**When to use:** Text content rendered directly in JSX, rich text with embedded formatting, when translation is the primary content of an element.
**When not to use:** String attributes like placeholder, aria-label, title (use useIntl instead).
---
### Pattern 3: useIntl Hook (Imperative)
Use `useIntl` when you need formatted strings for attributes, props, or programmatic use.
```typescript
// src/components/search-input.tsx
import { useIntl } from "react-intl";
export function SearchInput() {
const intl = useIntl();
const placeholder = intl.formatMessage({
id: "search.placeholder",
defaultMessage: "Search products...",
});
const ariaLabel = intl.formatMessage({
id: "search.ariaLabel",
defaultMessage: "Search for products in the catalog",
});
return (
<input
type="search"
placeholder={placeholder}
aria-label={ariaLabel}
/>
);
}
export { SearchInput };
```
**Why good:** useIntl returns strings suitable for HTML attributes, placeholder and aria-label require string values not ReactNode, message descriptors are type-safe
**When to use:** Input placeholders and ARIA labels, passing translated strings to third-party components, conditional rendering based on formatted values, programmatic string manipulation.
---
### Pattern 4: defineMessages for Static Extraction
Use `defineMessages` to declare messages that the CLI can extract.
```typescript
// src/messages/product.messages.ts
import { defineMessages } from "react-intl";
export const productMessages = defineMessages({
title: {
id: "product.title",
defaultMessage: "Product Details",
description: "Page title for product detail page",
},
addToCart: {
id: "product.addToCart",
defaultMessage: "Add to Cart",
description: "Button text for adding product to shopping cart",
},
outOfStock: {
id: "product.outOfStock",
defaultMessage: "Out of Stock",
description: "Badge text when product is unavailable",
},
priceLabel: {
id: "product.priceLabel",
defaultMessage: "Price: {price}",
description: "Price display with formatted currency",
},
reviewCount: {
id: "product.reviewCount",
defaultMessage:
"{count, plural, =0 {No reviews} one {# review} other {# reviews}}",
description: "Number of product reviews with pluralization",
},
});
```
**Why good:** centralizes related messages in one file, descriptions provide context for translators, CLI extracts these automatically, IDE autocomplete for message references
#### Usage
```typescript
// src/components/product-card.tsx
import { useIntl } from "react-intl";
import { productMessages } from "../messages/product.messages";
export function ProductCard({ product }: { product: Product }) {
const intl = useIntl();
return (
<article>
<h2>{intl.formatMessage(productMessages.title)}</h2>
<p>{intl.formatMessage(productMessages.reviewCount, { count: product.reviewCount })}</p>
<button>{intl.formatMessage(productMessages.addToCart)}</button>
</article>
);
}
```
---
### Pattern 5: Rich Text Formatting
Use XML-like tags in messages for embedded markup.
```typescript
// src/components/terms-notice.tsx
import { FormattedMessage } from "react-intl";
export function TermsNotice() {
return (
<p>
<FormattedMessage
id="terms.notice"
defaultMessage="By signing up, you agree to our <terms>Terms of Service</terms> and <privacy>Privacy Policy</privacy>."
values={{
terms: (chunks) => <a href="/terms">{chunks}</a>,
privacy: (chunks) => <a href="/privacy">{chunks}</a>,
}}
/>
</p>
);
}
export { TermsNotice };
```
**Why good:** translation string stays complete and translatable, markup tags are developer-defined, translators can reorder tags per language grammar
#### Default Rich Text Elements
Configure global tag handlers in `IntlProvider` for consistent styling:
```typescript
// src/providers/intl-provider.tsx
import { IntlProvider } from "react-intl";
import type { ReactNode } from "react";
const DEFAULT_RICH_TEXT_ELEMENTS = {
b: (chunks: ReactNode) => <strong>{chunks}</strong>,
i: (chunks: ReactNode) => <em>{chunks}</em>,
br: () => <br />,
};
export function AppIntlProvider({ children, locale, messages }: Props) {
return (
<IntlProvider
locale={locale}
messages={messages}
defaultRichTextElements={DEFAULT_RICH_TEXT_ELEMENTS}
>
{children}
</IntlProvider>
);
}
```
---
### Pattern 6: Formatting Components
Use dedicated components for locale-aware date, time, number, and list formatting.
#### Date and Time
```typescript
// src/components/event-date.tsx
import { FormattedDate, FormattedTime } from "react-intl";
export function EventDate({ date }: { date: Date }) {
return (
<time dateTime={date.toISOString()}>
<FormattedDate
value={date}
year="numeric"
month="long"
day="numeric"
weekday="long"
/>
{" at "}
<FormattedTime
value={date}
hour="numeric"
minute="numeric"
timeZoneName="short"
/>
</time>
);
}
// Output (en-US): "Monday, January 15, 2024 at 3:30 PM EST"
// Output (de-DE): "Montag, 15. Januar 2024 um 15:30 MEZ"
```
#### Numbers and Currency
```typescript
// src/components/product-price.tsx
import { FormattedNumber } from "react-intl";
const MIN_FRACTION_DIGITS = 2;
const MAX_FRACTION_DIGITS = 2;
export function ProductPrice({ amount, currency }: { amount: number; currency: string }) {
return (
<FormattedNumber
value={amount}
style="currency"
currency={currency}
minimumFractionDigits={MIN_FRACTION_DIGITS}
maximumFractionDigits={MAX_FRACTION_DIGITS}
/>
);
}
// Output (en-US, USD): "$1,234.56"
// Output (de-DE, EUR): "1.234,56 EUR"
```
#### Lists
```typescript
// src/components/contributors.tsx
import { FormattedList } from "react-intl";
export function Contributors({ names }: { names: string[] }) {
return (
<FormattedList
type="conjunction"
value={names}
/>
);
}
// Output (en): "Alice, Bob, and Charlie"
// Output (es): "Alice, Bob y Charlie"
```
---
### Pattern 7: TypeScript Integration
Enable type-safe message IDs with TypeScript augmentation.
```typescript
// src/types/intl.d.ts
import type messages from "../lang/en.json";
type MessageIds = keyof typeof messages;
declare global {
namespace FormatjsIntl {
interface Message {
ids: MessageIds;
}
}
}
```
**Why good:** typos in message IDs become compile-time errors, IDE autocomplete for message IDs, refactoring keys updates all usages
#### tsconfig.json Requirements
```json
{
"compilerOptions": {
"lib": ["esnext.intl", "es2017.intl", "es2018.intl"]
}
}
```
---
### Pattern 8: Message Extraction Workflow
Use FormatJS CLI for extracting and compiling messages.
#### Package.json Scripts
```json
{
"scripts": {
"intl:extract": "formatjs extract 'src/**/*.{ts,tsx}' --ignore='**/*.d.ts' --out-file lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
"intl:compile": "formatjs compile lang/en.json --out-file src/compiled-lang/en.json --ast",
"intl:compile:all": "formatjs compile-folder --ast lang src/compiled-lang"
}
}
```
#### Workflow
1. **Extract** messages from source code
2. **Send** to translation management system (TMS)
3. **Compile** translations to AST format for faster runtime
**Why compile to AST:** 30-50% faster initial render for large message catalogs - skips runtime parsing.
</patterns>
---
<performance>
## Performance Optimization
### Message Compilation (AST Pre-parsing)
Compile messages to AST format at build time to skip runtime parsing:
```bash
formatjs compile lang/en.json --out-file compiled/en.json --ast
```
```typescript
// Load compiled messages instead of raw JSON
import compiledMessages from "./compiled/en.json";
<IntlProvider locale="en" messages={compiledMessages}>
```
**Impact:** 30-50% faster initial render for large message catalogs.
### Lazy Loading Locale Data
```typescript
// src/utils/load-messages.ts
const messageLoaders: Record<string, () => Promise<Record<string, string>>> = {
en: () => import("../lang/compiled/en.json").then((m) => m.default),
de: () => import("../lang/compiled/de.json").then((m) => m.default),
fr: () => import("../lang/compiled/fr.json").then((m) => m.default),
};
const messageCache = new Map<string, Record<string, string>>();
export async function loadMessages(
locale: string,
): Promise<Record<string, string>> {
if (messageCache.has(locale)) {
return messageCache.get(locale)!;
}
const loader = messageLoaders[locale] ?? messageLoaders.en;
const messages = await loader();
messageCache.set(locale, messages);
return messages;
}
export { loadMessages };
```
### Avoid Inline Message Objects
```typescript
// WRONG - Creates new object on every render
function Bad() {
return (
<FormattedMessage
id="greeting"
defaultMessage="Hello!"
description="Greeting message"
/>
);
}
// CORRECT - Define messages outside component
const messages = defineMessages({
greeting: {
id: "greeting",
defaultMessage: "Hello!",
description: "Greeting message",
},
});
function Good() {
return <FormattedMessage {...messages.greeting} />;
}
```
</performance>
---
<integration>
## Integration Guide
**react-intl is context-based and React-specific.** It integrates with React's component tree via IntlProvider.
**Works with:**
- **React**: Designed specifically for React applications
- **Testing Library**: Custom render wrapper with IntlProvider
- **TypeScript**: Module augmentation for type-safe message IDs
**Component State Guidance:**
- Use IntlProvider at app root for global i18n context
- For non-React contexts (Node.js, SSR without context), use `createIntl` from `@formatjs/intl`
- Locale state is managed by your application - pass it to IntlProvider
**Replaces / Conflicts with:**
- **next-intl**: For Next.js App Router, prefer next-intl (better SSR integration)
- **i18next**: Different API and message format - choose one, not both
</integration>
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md**
**(You MUST wrap the application root with `IntlProvider` and configure locale, messages, and defaultLocale)**
**(You MUST include the `other` category in ALL plural and select ICU messages - omission causes runtime errors)**
**(You MUST use named constants for locale codes - NO inline locale strings)**
**(You MUST verify React version compatibility: v8+ requires React 19+, use v6.x for React 18)**
**Failure to follow these rules will cause runtime errors and broken internationalization.**
</critical_reminders>