react-intl Guide: React Internationalization Setup

Set up FormatJS react-intl in your React app with IntlProvider, FormattedMessage, useIntl, ICU message format, and automated translations.

Using react-i18next instead? See our react-i18next guide

1

Install react-intl

react-intl is part of the FormatJS project. It provides React components and hooks for formatting strings, numbers, dates, and plurals using the ICU MessageFormat standard.

react-intl has zero runtime dependencies beyond React. It uses the browser's built-in Intl API for number and date formatting, and ships its own ICU MessageFormat parser for plurals, select, and rich text.
Terminal
npm install react-intl
2

Configure IntlProvider

Wrap your app with IntlProvider at the root. Pass the active locale and a flat messages object. Every component below can then access translations via FormattedMessage or useIntl.

src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { IntlProvider } from 'react-intl';
import App from './App';
import enMessages from './messages/en.json';
import deMessages from './messages/de.json';

const messages: Record<string, Record<string, string>> = {
  en: enMessages,
  de: deMessages,
};

// Detect locale from browser or your routing layer
const locale = navigator.language.split('-')[0] || 'en';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <IntlProvider locale={locale} messages={messages[locale] || messages.en}>
      <App />
    </IntlProvider>
  </React.StrictMode>
);
IntlProvider requires a flat key-value messages object (e.g., { "app.greeting": "Hello" }). Nested JSON must be flattened before passing to IntlProvider, or use a utility like flat to convert nested structures.

Message Files

Create one JSON file per locale. react-intl uses ICU MessageFormat syntax natively — plurals, select, and variables are all expressed inline in message strings.

messages/en.json & messages/de.json
// messages/en.json
{
  "app.greeting": "Hello, {name}!",
  "nav.home": "Home",
  "nav.about": "About",
  "nav.settings": "Settings",
  "cart.itemCount": "{count, plural, one {# item} other {# items}} in your cart"
}

// messages/de.json
{
  "app.greeting": "Hallo, {name}!",
  "nav.home": "Startseite",
  "nav.about": "Über uns",
  "nav.settings": "Einstellungen",
  "cart.itemCount": "{count, plural, one {# Artikel} other {# Artikel}} in Ihrem Warenkorb"
}
Use dot-separated IDs like "nav.home" for organization. Unlike react-i18next, react-intl expects a flat messages object — you flatten the keys, not the structure.
3

Use Translations in Components

react-intl gives you two primary APIs: the FormattedMessage component for rendering translated JSX, and the useIntl hook for imperative access (placeholders, aria labels, programmatic formatting).

FormattedMessage Component

Use FormattedMessage for declarative translations in JSX. Pass the message ID and any interpolation values. It renders the translated string directly.

Greeting.tsx
import { FormattedMessage } from 'react-intl';

function Greeting({ userName }: { userName: string }) {
  return (
    <div>
      <h1>
        <FormattedMessage
          id="app.greeting"
          values={{ name: userName }}
        />
      </h1>
      <nav>
        <a href="/"><FormattedMessage id="nav.home" /></a>
        <a href="/about"><FormattedMessage id="nav.about" /></a>
      </nav>
    </div>
  );
}

useIntl Hook

Use useIntl() when you need the translated string as a plain value — for input placeholders, aria-labels, document.title, or when passing strings to non-React APIs. It also provides formatNumber, formatDate, and formatRelativeTime.

SearchBar.tsx
import { useIntl } from 'react-intl';

function SearchBar() {
  const intl = useIntl();

  return (
    <input
      type="search"
      placeholder={intl.formatMessage({ id: 'search.placeholder' })}
      aria-label={intl.formatMessage({ id: 'search.ariaLabel' })}
    />
  );
}

// useIntl also gives you formatNumber, formatDate, formatRelativeTime:
function PriceTag({ amount, currency }: { amount: number; currency: string }) {
  const intl = useIntl();
  return (
    <span>{intl.formatNumber(amount, { style: 'currency', currency })}</span>
  );
}

Rich Text (HTML in Translations)

Embed JSX inside translations using XML-like tags in your message strings. Pass tag handlers via the values prop to render links, bold text, or any React component within a translated message.

SignUp.tsx
import { FormattedMessage } from 'react-intl';

// Message: "By signing up, you agree to our <link>Terms</link>."
// Key: "signup.terms"
// Value: "By signing up, you agree to our <link>Terms</link>."

function SignUp() {
  return (
    <FormattedMessage
      id="signup.terms"
      values={{
        link: (chunks) => <a href="/terms" className="underline">{chunks}</a>,
      }}
    />
  );
}
FormattedMessage renders a React Fragment by default. If you need a specific wrapper element, pass the textComponent prop to IntlProvider or wrap FormattedMessage in your own element.

Message Extraction with @formatjs/cli

FormatJS provides a CLI to automatically extract message IDs from your source code into a JSON file. This ensures your messages file stays in sync with your components without manual bookkeeping.

Terminal
# Install the CLI
npm install -g @formatjs/cli

# Extract messages from source code into a JSON file
formatjs extract 'src/**/*.tsx' --out-file messages/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'

# Or use explicit IDs (recommended):
formatjs extract 'src/**/*.tsx' --out-file messages/en.json

# Compile messages for production (optional, improves perf)
formatjs compile messages/en.json --out-file compiled/en.json
formatjs compile messages/de.json --out-file compiled/de.json
4

Plurals and ICU Select

react-intl uses ICU MessageFormat natively. Plurals, gender-based select, and nested formatting are all expressed directly in message strings — no suffix conventions or separate keys needed.

ICU plural syntax by language
// ICU MessageFormat syntax — react-intl uses this natively
// English
{
  "cart.itemCount": "{count, plural, one {# item} other {# items}} in your cart",
  "inbox.unread": "You have {count, plural, =0 {no unread messages} one {# unread message} other {# unread messages}}"
}

// Arabic — 6 plural forms
{
  "cart.itemCount": "{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}} في سلتك"
}

// Japanese — 1 form (other)
{
  "cart.itemCount": "カートに{count}個の商品があります"
}
Never hardcode plural logic in JavaScript. Languages like Arabic have 6 plural forms, French treats 0 as singular, and Japanese has no plural distinction. Let ICU MessageFormat handle the rules — just pass the count value.

ICU Select for Gender and Roles

Use ICU select syntax for context-dependent translations like gender, user roles, or status values. The select expression picks the right variant based on the provided value.

ICU select syntax
// Gender-dependent messages using ICU select
{
  "user.greeting": "{gender, select, male {He} female {She} other {They}} liked your post.",
  "user.invitation": "{role, select, admin {You can manage all settings.} editor {You can edit content.} other {You can view content.}}"
}

// Usage:
<FormattedMessage
  id="user.greeting"
  values={{ gender: user.gender }}
/>

Automate Translation Quality

Catch missing keys and broken placeholders before they ship with i18n-validate. Test your UI with pseudo-translations using i18n-pseudo before real translations arrive.

Common Pitfalls

Over-Relying on defaultMessage

defaultMessage is a development fallback, not a translation strategy. If you use defaultMessage for all strings, your message extraction output will contain the English text but translators may miss new keys. Always extract and maintain a complete source locale file.

Nested Objects Instead of Flat Keys

IntlProvider expects a flat Record<string, string> for messages. If you pass nested JSON like { nav: { home: "Home" } }, react-intl won't find the key "nav.home". Flatten your messages before passing them, or use a library like flat.

IntlProvider Causing Re-renders

If you create the messages object inline inside the render function, IntlProvider receives a new object reference every render, causing all consumers to re-render. Memoize messages with useMemo or define them outside the component.

Missing IntlProvider in Tests

Components using FormattedMessage or useIntl will throw if rendered without an IntlProvider ancestor. In tests, wrap your component in IntlProvider with locale="en" and an empty or minimal messages object.

Recommended File Structure

Project Structure
my-react-app/
├── messages/
│   ├── en.json              # Source of truth (English)
│   ├── de.json              # German
│   ├── ja.json              # Japanese
│   └── es.json              # Spanish
├── compiled/                # Optional: compiled messages for prod
│   ├── en.json
│   └── ...
├── src/
│   ├── main.tsx             # App entry with IntlProvider
│   ├── App.tsx
│   └── components/
│       ├── Greeting.tsx      # Uses FormattedMessage
│       └── SearchBar.tsx     # Uses useIntl
└── package.json

Try i18n Agent Now

Drop your translation file here

JSON, YAML, PO, XML, CSV, Markdown, Properties

or click to browse

Target languages

No signup requiredInstant estimate

Frequently Asked Questions