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
| Feature | react-intl | react-i18next |
|---|---|---|
| Message Format | ICU (strict) | JSON (flexible) |
| Bundle Size | ~25KB | ~15KB |
| Plurals | ICU syntax | Suffix keys |
| Formatters | Built-in | Plugin/manual |
| Learning Curve | Higher | Lower |
| TypeScript | Excellent | Excellent |
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
Terminal1npm install react-intl 2# or 3yarn add react-intl 4# or 5pnpm add react-intl
Optional CLI Tools
Terminal1# 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:
JSON1{ 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:
JSON1{ 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
TSX1// 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
TSX1import { 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:
TSX1import { 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
| Method | Purpose | Example |
|---|---|---|
formatMessage | Translate message | intl.formatMessage({ id: 'key' }) |
formatDate | Format date | intl.formatDate(new Date()) |
formatTime | Format time | intl.formatTime(new Date()) |
formatNumber | Format number | intl.formatNumber(1234.56) |
formatRelativeTime | Relative time | intl.formatRelativeTime(-1, 'day') |
ICU Message Format
ICU (International Components for Unicode) is the industry standard for complex messages.
Variables (Arguments)
JSON1{ 2 "greeting": "Hello, {name}!", 3 "cartInfo": "You have {count} items worth {total}" 4}
TSX1<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}}" }
TSX1<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" }
TSX1<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)
JSON1{ 2 "terms": "By signing up, you agree to our <link>Terms of Service</link>", 3 "welcome": "Welcome <bold>{name}</bold> to our platform!" 4}
TSX1<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
TSX1import { 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
TSX1import { 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
TSX1import { 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
TSX1import { 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
TypeScript1// 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
TSX1import { 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:
TSX1import 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
TSX1import { 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
TSX1// 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
TSX1// 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
TSX1import { 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
JSON1{ 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
TSX1<FormattedMessage 2 id="app.greeting" 3 defaultMessage="Welcome to our app" 4/>
Default messages serve as fallback and documentation.
3. Handle Missing Translations
TSX1<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
TSX1const 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
Terminal1# 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:
| Aspect | Details |
|---|---|
| Installation | npm install react-intl |
| Provider | <IntlProvider locale messages> |
| Component | <FormattedMessage id values /> |
| Hook | useIntl().formatMessage() |
| Message Format | ICU (standard) |
| Formatters | Built-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.
