IntlPull
Guide
16 min read

react-intl (FormatJS): La guía completa para la internacionalización de React en 2026

Domina react-intl de FormatJS para React i18n. Tutorial completo que cubre IntlProvider, FormattedMessage, sintaxis ICU, ganchos y mejores prácticas de producción.

IntlPull Team
IntlPull Team
03 Feb 2026, 11:44 AM [PST]
On this page
Summary

Domina react-intl de FormatJS para React i18n. Tutorial completo que cubre IntlProvider, FormattedMessage, sintaxis ICU, ganchos y mejores prácticas de producción.

Respuesta rápida

react-intl es la vinculación de React para FormatJS, proporcionando soporte de formato de mensaje ICU para la internacionalización. Instalar con npm install react-intl, wrap your app with <IntlProvider locale="en" messages={messages}>, then use <FormattedMessage id="greeting" /> or the useIntl() hook to display translations. Choose react-intl when you need strict ICU compliance or built-in date/number formatting. For simpler needs, react-i18next may be easier.


What is react-intl?

react-intl is part of the FormatJS ecosystem, an official implementation of the ICU Message Format for JavaScript. It provides:

  • ICU Message Format: Industry-standard syntax for plurals, gender, and complex messages
  • Built-in formatters: Dates, numbers, currencies, relative time
  • TypeScript support: Type-safe message extraction and formatting
  • Production-tested: Used by Yahoo, Microsoft, Airbnb

react-intl vs react-i18next

Featurereact-intlreact-i18next
Message FormatICU (strict)JSON (flexible)
Bundle Size~25KB~15KB
PluralsICU syntaxSuffix keys
FormattersBuilt-inPlugin/manual
Learning CurveHigherLower
TypeScriptExcellentExcellent

Choose react-intl when:

  • You need strict ICU compliance
  • Your team knows ICU syntax
  • You want built-in date/number formatting
  • You're integrating with translation tools that use ICU

Choose react-i18next when:

  • You want simpler syntax
  • You need smaller bundle size
  • Your team prefers JSON-based keys

Installation

Terminal
1npm install react-intl
2# or
3yarn add react-intl
4# or
5pnpm add react-intl

Optional CLI Tools

Terminal
1# Extract messages from code
2npm install -D @formatjs/cli
3
4# Compile messages for production
5npm install -D @formatjs/cli

Basic Setup

Step 1: Create Translation Files

src/
├── lang/
│   ├── en.json
│   ├── es.json
│   └── fr.json
└── App.tsx

en.json:

JSON
1{
2  "app.greeting": "Welcome to our application!",
3  "app.nav.home": "Home",
4  "app.nav.about": "About",
5  "app.buttons.submit": "Submit",
6  "app.buttons.cancel": "Cancel"
7}

es.json:

JSON
1{
2  "app.greeting": "¡Bienvenido a nuestra aplicación!",
3  "app.nav.home": "Inicio",
4  "app.nav.about": "Acerca de",
5  "app.buttons.submit": "Enviar",
6  "app.buttons.cancel": "Cancelar"
7}

Step 2: Configure IntlProvider

TSX
1// src/App.tsx
2import { IntlProvider } from 'react-intl';
3import { useState } from 'react';
4
5import enMessages from './lang/en.json';
6import esMessages from './lang/es.json';
7
8const messages: Record<string, Record<string, string>> = {
9  en: enMessages,
10  es: esMessages,
11};
12
13function App() {
14  const [locale, setLocale] = useState('en');
15
16  return (
17    <IntlProvider
18      locale={locale}
19      messages={messages[locale]}
20      defaultLocale="en"
21      onError={(err) => {
22        if (err.code !== 'MISSING_TRANSLATION') {
23          console.error(err);
24        }
25      }}
26    >
27      <MainApp onLocaleChange={setLocale} />
28    </IntlProvider>
29  );
30}

Step 3: Use FormattedMessage

TSX
1import { FormattedMessage } from 'react-intl';
2
3function Header() {
4  return (
5    <header>
6      <h1>
7        <FormattedMessage id="app.greeting" />
8      </h1>
9      <nav>
10        <a href="/"><FormattedMessage id="app.nav.home" /></a>
11        <a href="/about"><FormattedMessage id="app.nav.about" /></a>
12      </nav>
13    </header>
14  );
15}

The useIntl Hook

For programmatic access to formatting:

TSX
1import { useIntl } from 'react-intl';
2
3function Form() {
4  const intl = useIntl();
5
6  const handleSubmit = () => {
7    const confirmMessage = intl.formatMessage({ id: 'app.confirm' });
8    if (confirm(confirmMessage)) {
9      // Submit form
10    }
11  };
12
13  return (
14    <form onSubmit={handleSubmit}>
15      <button type="submit">
16        {intl.formatMessage({ id: 'app.buttons.submit' })}
17      </button>
18    </form>
19  );
20}

Hook Methods

MethodPurposeExample
formatMessageTranslate messageintl.formatMessage({ id: 'key' })
formatDateFormat dateintl.formatDate(new Date())
formatTimeFormat timeintl.formatTime(new Date())
formatNumberFormat numberintl.formatNumber(1234.56)
formatRelativeTimeRelative timeintl.formatRelativeTime(-1, 'day')

ICU Message Format

ICU (International Components for Unicode) is the industry standard for complex messages.

Variables (Arguments)

JSON
1{
2  "greeting": "Hello, {name}!",
3  "cartInfo": "You have {count} items worth {total}"
4}
TSX
1<FormattedMessage
2  id="greeting"
3  values={{ name: user.name }}
4/>
5
6<FormattedMessage
7  id="cartInfo"
8  values={{ count: 5, total: '$99.99' }}
9/>

Pluralization

ICU handles complex plural rules for all languages:

JSON
{
  "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
TSX
1<FormattedMessage id="items" values={{ count: 0 }} />
2// "No items"
3
4<FormattedMessage id="items" values={{ count: 1 }} />
5// "1 item"
6
7<FormattedMessage id="items" values={{ count: 5 }} />
8// "5 items"

Complex plural forms (Russian):

JSON
{
  "items": "{count, plural, one {# товар} few {# товара} many {# товаров} other {# товаров}}"
}

Select (Gender, Choice)

JSON
{
  "pronoun": "{gender, select, male {He} female {She} other {They}} liked your post"
}
TSX
1<FormattedMessage
2  id="pronoun"
3  values={{ gender: 'female' }}
4/>
5// "She liked your post"

Nested Plural + Select

JSON
{
  "invites": "{gender, select, male {{count, plural, one {He invited # person} other {He invited # people}}} female {{count, plural, one {She invited # person} other {She invited # people}}} other {{count, plural, one {They invited # person} other {They invited # people}}}}"
}

Rich Text (HTML Components)

JSON
1{
2  "terms": "By signing up, you agree to our <link>Terms of Service</link>",
3  "welcome": "Welcome <bold>{name}</bold> to our platform!"
4}
TSX
1<FormattedMessage
2  id="terms"
3  values={{
4    link: (chunks) => <a href="/terms">{chunks}</a>
5  }}
6/>
7
8<FormattedMessage
9  id="welcome"
10  values={{
11    name: user.name,
12    bold: (chunks) => <strong>{chunks}</strong>
13  }}
14/>

Built-in Formatters

Date Formatting

TSX
1import { FormattedDate } from 'react-intl';
2
3<FormattedDate
4  value={new Date()}
5  year="numeric"
6  month="long"
7  day="2-digit"
8/>
9// "January 17, 2026" (en-US)
10// "17 de enero de 2026" (es)

Time Formatting

TSX
1import { FormattedTime } from 'react-intl';
2
3<FormattedTime
4  value={new Date()}
5  hour="numeric"
6  minute="numeric"
7  timeZoneName="short"
8/>
9// "2:30 PM EST" (en-US)
10// "14:30 EST" (es)

Number Formatting

TSX
1import { FormattedNumber } from 'react-intl';
2
3// Plain number
4<FormattedNumber value={1234567.89} />
5// "1,234,567.89" (en-US)
6// "1.234.567,89" (de)
7
8// Currency
9<FormattedNumber
10  value={99.99}
11  style="currency"
12  currency="EUR"
13/>
14// "€99.99" (en)
15// "99,99 €" (de)
16
17// Percentage
18<FormattedNumber value={0.25} style="percent" />
19// "25%"

Relative Time

TSX
1import { FormattedRelativeTime } from 'react-intl';
2
3<FormattedRelativeTime value={-1} unit="day" />
4// "1 day ago" or "yesterday" depending on locale
5
6<FormattedRelativeTime value={3} unit="hour" />
7// "in 3 hours"

TypeScript Integration

Define Message Types

TypeScript
1// src/types/intl.d.ts
2import en from '../lang/en.json';
3
4type Messages = typeof en;
5
6declare global {
7  namespace FormatjsIntl {
8    interface Message {
9      ids: keyof Messages;
10    }
11  }
12}

Now TypeScript autocompletes and validates message IDs:

TSX
<FormattedMessage id="app.greeting" />  // ✅ Valid
<FormattedMessage id="invalid.key" />   // ❌ TypeScript error

defineMessages for Extraction

TSX
1import { defineMessages, useIntl } from 'react-intl';
2
3const messages = defineMessages({
4  title: {
5    id: 'app.title',
6    defaultMessage: 'My Application',
7    description: 'Main application title',
8  },
9  greeting: {
10    id: 'app.greeting',
11    defaultMessage: 'Hello, {name}!',
12    description: 'User greeting message',
13  },
14});
15
16function Header({ name }: { name: string }) {
17  const intl = useIntl();
18
19  return (
20    <header>
21      <h1>{intl.formatMessage(messages.title)}</h1>
22      <p>{intl.formatMessage(messages.greeting, { name })}</p>
23    </header>
24  );
25}

Message Extraction

Extract with FormatJS CLI

Terminal
# Extract messages from source files
npx formatjs extract 'src/**/*.ts*' --out-file lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'

Compile for Production

Terminal
# Compile to AST for better performance
npx formatjs compile lang/en.json --out-file lang/compiled/en.json
npx formatjs compile lang/es.json --out-file lang/compiled/es.json

Using compiled messages:

TSX
1import compiledMessages from './lang/compiled/en.json';
2
3<IntlProvider
4  locale="en"
5  messages={compiledMessages}
6/>

Compiled messages are pre-parsed, improving runtime performance by 30-50%.

Language Switching

TSX
1import { useState, useCallback } from 'react';
2import { IntlProvider } from 'react-intl';
3
4const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'de'] as const;
5type Locale = typeof SUPPORTED_LOCALES[number];
6
7function App() {
8  const [locale, setLocale] = useState<Locale>('en');
9  const [messages, setMessages] = useState(enMessages);
10
11  const handleLocaleChange = useCallback(async (newLocale: Locale) => {
12    // Dynamically import translations
13    const newMessages = await import(`./lang/${newLocale}.json`);
14    setMessages(newMessages.default);
15    setLocale(newLocale);
16
17    // Persist preference
18    localStorage.setItem('locale', newLocale);
19    document.documentElement.lang = newLocale;
20  }, []);
21
22  return (
23    <IntlProvider locale={locale} messages={messages}>
24      <LocaleSwitcher
25        current={locale}
26        available={SUPPORTED_LOCALES}
27        onChange={handleLocaleChange}
28      />
29      <MainContent />
30    </IntlProvider>
31  );
32}
33
34function LocaleSwitcher({ current, available, onChange }) {
35  return (
36    <select value={current} onChange={(e) => onChange(e.target.value)}>
37      {available.map((locale) => (
38        <option key={locale} value={locale}>
39          {new Intl.DisplayNames([locale], { type: 'language' }).of(locale)}
40        </option>
41      ))}
42    </select>
43  );
44}

Server-Side Rendering

Next.js Integration

TSX
1// app/[locale]/layout.tsx
2import { IntlProvider } from 'react-intl';
3
4async function loadMessages(locale: string) {
5  return (await import(`../../lang/${locale}.json`)).default;
6}
7
8export default async function LocaleLayout({
9  children,
10  params: { locale },
11}: {
12  children: React.ReactNode;
13  params: { locale: string };
14}) {
15  const messages = await loadMessages(locale);
16
17  return (
18    <IntlProvider locale={locale} messages={messages}>
19      {children}
20    </IntlProvider>
21  );
22}

Note: For Next.js App Router, consider next-intl which provides better RSC integration.

Testing

Test Setup

TSX
1// test/test-utils.tsx
2import { render } from '@testing-library/react';
3import { IntlProvider } from 'react-intl';
4
5const messages = {
6  'app.greeting': 'Hello, {name}!',
7  'app.buttons.submit': 'Submit',
8};
9
10function Wrapper({ children }: { children: React.ReactNode }) {
11  return (
12    <IntlProvider locale="en" messages={messages}>
13      {children}
14    </IntlProvider>
15  );
16}
17
18export function renderWithIntl(ui: React.ReactElement) {
19  return render(ui, { wrapper: Wrapper });
20}

Writing Tests

TSX
1import { renderWithIntl } from './test-utils';
2import { screen } from '@testing-library/react';
3import Header from './Header';
4
5describe('Header', () => {
6  it('renders greeting with user name', () => {
7    renderWithIntl(<Header name="John" />);
8    expect(screen.getByText('Hello, John!')).toBeInTheDocument();
9  });
10});

Production Best Practices

1. Message ID Convention

JSON
1{
2  "namespace.component.element": "Value",
3  "auth.login.title": "Sign In",
4  "auth.login.button": "Continue",
5  "dashboard.stats.users": "Total Users",
6  "common.errors.network": "Network error"
7}

2. Always Provide Default Messages

TSX
1<FormattedMessage
2  id="app.greeting"
3  defaultMessage="Welcome to our app"
4/>

Default messages serve as fallback and documentation.

3. Handle Missing Translations

TSX
1<IntlProvider
2  onError={(err) => {
3    if (err.code === 'MISSING_TRANSLATION') {
4      // Log but don't crash
5      console.warn(`Missing translation: ${err.message}`);
6      return;
7    }
8    throw err;
9  }}
10>

4. Lazy Load Translations

TSX
1const loadMessages = async (locale: string) => {
2  const messages = await import(`./lang/${locale}.json`);
3  return messages.default;
4};

5. Use Compiled Messages in Production

Compiled AST messages skip runtime parsing, improving performance.

Integrating with IntlPull

IntlPull works seamlessly with react-intl and ICU format:

CLI Workflow

Terminal
1# Extract messages
2npx formatjs extract 'src/**/*.ts*' --out-file lang/extracted.json
3
4# Upload to IntlPull
5npx @intlpullhq/cli upload --file lang/extracted.json --format icu
6
7# AI translate
8npx @intlpullhq/cli translate --target es,fr,de
9
10# Download translations
11npx @intlpullhq/cli download --output lang/ --format json

Benefits

  • ICU support: IntlPull preserves ICU syntax perfectly
  • AI translation: GPT-4 and Claude understand ICU placeholders
  • Validation: Catches ICU syntax errors before deployment
  • Translation memory: Reuses ICU segments across projects

Frequently Asked Questions

What is react-intl?

react-intl is the React binding for FormatJS, providing internationalization with ICU Message Format support. It offers components like FormattedMessage, FormattedDate, and FormattedNumber, plus the useIntl hook. It's used by major companies and is the go-to choice for projects requiring strict ICU compliance.

What is the difference between react-intl and react-i18next?

react-intl uses ICU Message Format with strict syntax for plurals and selections. react-i18next uses simpler JSON keys with suffix-based plurals. react-intl has built-in formatters for dates/numbers; react-i18next requires plugins. Choose react-intl for ICU compliance; choose react-i18next for simpler syntax.

How do I use FormattedMessage with variables?

Use curly brace syntax in ICU messages: "greeting": "Hello, {name}!". Then pass values: <FormattedMessage id="greeting" values={{ name: 'John' }} />. For plurals: "{count, plural, one {# item} other {# items}}" with values={{ count: 5 }}.

How does pluralization work in react-intl?

Use ICU plural syntax: "{count, plural, one {# item} other {# items}}". ICU supports zero, one, two, few, many, other categories and exact matches like =0. The correct form is chosen automatically based on locale plural rules. Pass values={{ count: n }} to the component.

How do I format dates and numbers in react-intl?

Use built-in formatter components: <FormattedDate value={date} dateStyle="long" />, <FormattedNumber value={1234.56} style="currency" currency="USD" />. Or use hooks: intl.formatDate(date), intl.formatNumber(1234.56, { style: 'currency', currency: 'USD' }). Formatting is locale-aware automatically.

How do I extract messages from code?

Use FormatJS CLI: npx formatjs extract 'src/**/*.ts*' --out-file lang/en.json. Define messages with defineMessages() for best extraction. The CLI extracts IDs, default messages, and descriptions. Upload extracted files to IntlPull for translation management.

Should I compile messages for production?

Yes, compile messages for 30-50% better performance. Run npx formatjs compile lang/en.json --out-file lang/compiled/en.json. Compiled messages are pre-parsed ASTs, skipping runtime parsing. This is especially important for large translation files or performance-critical apps.

How do I handle missing translations?

Use defaultMessage and onError handler. Always provide defaultMessage: <FormattedMessage id="key" defaultMessage="Fallback" />. Handle missing translations gracefully with onError on IntlProvider to log warnings instead of crashing.

Can I use react-intl with Next.js?

Yes, but consider next-intl for App Router projects. react-intl works with Next.js Pages Router by passing messages in getStaticProps/getServerSideProps. For App Router with Server Components, next-intl provides better integration. react-intl remains a good choice for client-heavy Next.js apps.

How do I add TypeScript support?

Extend FormatjsIntl namespace with your message types. Import your English JSON, create a type from it, and declare it in FormatjsIntl.Message.ids. TypeScript will then autocomplete and validate all message IDs, catching typos at compile time rather than runtime.

Summary

react-intl provides robust internationalization with ICU Message Format:

AspectDetails
Installationnpm install react-intl
Provider<IntlProvider locale messages>
Component<FormattedMessage id values />
HookuseIntl().formatMessage()
Message FormatICU (standard)
FormattersBuilt-in date, number, currency
Bundle~25KB gzipped

Best for: Teams needing ICU compliance, built-in formatters, or integration with CAT tools that use ICU.

For simpler needs, consider react-i18next.

Ready to manage react-intl translations? Start free with IntlPull — full ICU support with AI translation.

Tags
react-intl
formatjs
react
i18n
internationalization
icu
2026
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.