react-intl
react-intl is FormatJS's React binding for translating interface messages and formatting numbers, currencies, dates, times, ranges, lists, display names, and relative time with the platform Intl APIs. An IntlProvider supplies the active locale and message catalog; components such as FormattedMessage and the useIntl hook consume that context. Messages use ICU MessageFormat, so translators can control plural and select branches instead of receiving sentence fragments assembled in code. It handles runtime formatting, not translation authoring, catalog hosting, locale negotiation, or a translation-management service.
The strongest default for React teams that want ICU messages, mature extraction tooling, and standards-based formatting across client and server. It is a system you adopt, not a drop-in string dictionary, so budget for catalog delivery, polyfills, translator training, and major-version migrations.
Use it if
- You want standards-based ICU messages that let translators own plural, select, and rich-text sentence structure
- Your React 18+ application needs one typed API for translated strings plus locale-aware dates, numbers, lists, and display names
- You want build-time message extraction through the wider FormatJS CLI, Babel, TypeScript, SWC, or bundler tooling
- You need the same message catalog and formatting rules in client components, server rendering, and React Server Components
- You want translation loading, language detection, namespaces, fallback chains, and backend adapters bundled together; react-intl expects the application to load and choose message catalogs
- Your team will not adopt ICU MessageFormat; plural and select syntax is powerful but more demanding for developers and translators than plain key-to-string lookup
- You still depend on React 17 or class components using injectIntl; version 10 requires React 18 and removed injectIntl plus its WrappedComponentProps types
- Your target runtime lacks the required Intl APIs and you cannot ship ordered polyfills plus locale data, a documented concern for some React Native iOS and older browser environments
- Bundle weight is critical and you only need one or two native formatters; version 10.1.20 is 14.3 KB gzipped before application catalogs, polyfills, or locale data
Setup reality
Install react-intl alongside React 18 or newer; the current package also lists @types/react >=18 as a peer, which is relevant even though JavaScript-only apps may not otherwise install React types. Every rendered consumer must sit under IntlProvider or RawIntlProvider. Your application, not the library, must detect the locale, fetch the matching messages, choose fallbacks, persist the user's choice, and decide whether rendering waits for a catalog or briefly shows the source language. Set defaultLocale to the language used by defaultMessage values or fallback sentences can mix a source-language phrase with a date formatted in the active locale. Production work normally adds @formatjs/cli or a compiler plugin to extract descriptors, sends catalogs through translation, then validates and compiles returned messages; none of that workflow comes from installing react-intl alone. ICU quoting, plural offsets, exact-number branches, and rich-text tags need translator guidance and tests. The runtime relies on Intl.NumberFormat, Intl.DateTimeFormat, and Intl.PluralRules, with RelativeTimeFormat and DisplayNames needed when those features are used. Older browsers and some React Native runtimes need FormatJS polyfills loaded in the documented dependency order plus per-locale data, which can outweigh the core package. The docs also warn that FormatJS ESM dependencies may need transpilation in some build pipelines. Version 10 removed injectIntl and the global multiple-copy context workaround, so deduplicate react-intl and migrate class wrappers to useIntl. For Next.js React Server Components, import createIntl from react-intl/server rather than pulling the client-marked main entry into a server component. Decide how onError handles missing translations in production; silencing it globally can hide broken catalogs.
Patterns
Wrap the app with a locale and catalogprovide-locale-messages
import {IntlProvider} from 'react-intl';
import fr from './lang/fr.json';
root.render(
<IntlProvider locale="fr" defaultLocale="en" messages={fr}>
<App />
</IntlProvider>
);defaultLocale must match the language of defaultMessage strings. The application is responsible for loading and selecting the catalog.
Render a translated message with valuesformat-basic-message
import {FormattedMessage} from 'react-intl';
<FormattedMessage
id="account.greeting"
defaultMessage="Hello, {name}"
description="Greeting above the account page"
values={{name: user.displayName}}
/>Keep defaultMessage and description beside the call so FormatJS extraction tools can give translators useful source context.
Let ICU choose plural branchesformat-plural-message
<FormattedMessage
id="cart.items"
defaultMessage="{count, plural, =0 {Your cart is empty} one {# item} other {# items}}"
values={{count: items.length}}
/>Do not pluralize by concatenating translated fragments. ICU plural rules vary by locale, and exact branches such as =0 are checked before categories.
Map message tags to React elementsformat-rich-text-message
<FormattedMessage
id="terms.notice"
defaultMessage="Read our <terms>terms</terms> before continuing."
values={{
terms: (chunks) => <a href="/terms">{chunks}</a>,
}}
/>Tags are part of the ICU message, so translators can move the linked phrase. Supply functions, not raw HTML, for rich-text values.
Format text for an attribute with useIntlformat-imperative-message
import {useIntl} from 'react-intl';
function DeleteButton({name}) {
const intl = useIntl();
const label = intl.formatMessage(
{id: 'user.delete', defaultMessage: 'Delete {name}'},
{name}
);
return <button aria-label={label}>×</button>;
}Use the imperative API when a string must go into aria-label, title, placeholder, or a non-React API. The component still needs IntlProvider context.
Format a currency amountformat-currency
import {FormattedNumber} from 'react-intl';
<FormattedNumber
value={invoice.total}
style="currency"
currency={invoice.currency}
currencyDisplay="symbol"
/>The value is in major currency units, not minor units. Currency formatting does not convert exchange rates.
Render a date in an explicit time zoneformat-date-timezone
import {FormattedDate} from 'react-intl';
<FormattedDate
value={new Date(event.startsAt)}
dateStyle="long"
timeZone="America/New_York"
/>Without timeZone, output uses the runtime's zone, which can cause server and client markup to disagree during hydration.
Show relative time that updatesformat-relative-time
import {FormattedRelativeTime} from 'react-intl';
<FormattedRelativeTime
value={-5}
unit="minute"
numeric="auto"
updateIntervalInSeconds={60}
/>The value is relative to now in the selected unit. This feature requires Intl.RelativeTimeFormat or its polyfill.
Join a localized listformat-list
import {FormattedList} from 'react-intl';
<FormattedList
value={['Ada', 'Grace', 'Linus']}
type="conjunction"
style="long"
/>List punctuation and the final conjunction come from the active locale. This is safer than joining translated UI text with commas.
Format inside a React Server Componentcreate-server-formatter
import {createIntl, createIntlCache} from 'react-intl/server';
const cache = createIntlCache();
const intl = createIntl({locale: 'en', messages: {}}, cache);
export function Price({value}) {
return intl.formatNumber(value, {style: 'currency', currency: 'USD'});
}Version 10 provides react-intl/server so server components do not import the main entry marked use client. Recreate intl when locale or messages change.
Report missing messages without hiding other errorshandle-missing-translations
<IntlProvider
locale={locale}
defaultLocale="en"
messages={messages}
onError={(error) => {
if (error.code === 'MISSING_TRANSLATION') {
reportMissingMessage(error.message);
return;
}
console.error(error);
}}
>
<App />
</IntlProvider>Do not replace onError with an empty function. Parse failures and formatting errors need visibility even if missing translations use defaultMessage.
Extract declared messages for translationextract-message-catalog
npm install --save-dev @formatjs/cli
# package.json
# "scripts": {
# "i18n:extract": "formatjs extract 'src/**/*.{ts,tsx}' --out-file lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'"
# }
npm run i18n:extractreact-intl does not extract catalogs by itself. Keep the CLI version and ID strategy stable or message identifiers can churn across builds.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-i18next | npm | Choose it when language detection, resource backends, namespaces, and the wider i18next plugin model matter more than an ICU-first workflow |
| @lingui/react | npm | Choose it for a compile-time catalog workflow with React bindings and a smaller runtime-centered architecture |
| next-intl | npm | Choose it in a Next.js App Router project that wants routing, server-component, request-locale, and navigation conventions together |