Translating your React app is essential for reaching a global audience. react-intl is a powerful internationalization library from FormatJS that provides React components and an API for formatting dates, numbers, and strings, including pluralization and handling translations. This guide will walk you through the process of translating a React app using react-intl.
Understanding React Localization with react-intl
react-intl is part of the FormatJS suite and provides React components and an API for internationalization. It uses ICU message format and provides excellent support for pluralization, date/time formatting, and number formatting.
Step 1: Install Dependencies
First, install the necessary packages:
npm install react-intl
Or with yarn:
yarn add react-intl
Step 2: Create Translation Files
Create a locales directory in your src folder and add translation files for each language:
src/locales/en.json (English - default):
{
"app.welcome": "Welcome to our app!",
"button.submit": "Submit",
"button.cancel": "Cancel",
"button.delete": "Delete",
"error.network": "Network error. Please try again.",
"error.notFound": "Page not found",
"items.count": "{count, plural, =0 {No items} one {# item} other {# items}}",
"user.greeting": "Hello, {name}! You have {count, plural, =0 {no messages} one {# message} other {# messages}}.",
"date.today": "Today is {date, date, full}",
"currency.price": "Price: {amount, number, currency}"
}
src/locales/es.json (Spanish):
{
"app.welcome": "¡Bienvenido a nuestra aplicación!",
"button.submit": "Enviar",
"button.cancel": "Cancelar",
"button.delete": "Eliminar",
"error.network": "Error de red. Por favor, inténtalo de nuevo.",
"error.notFound": "Página no encontrada",
"items.count": "{count, plural, =0 {No hay elementos} one {# elemento} other {# elementos}}",
"user.greeting": "¡Hola, {name}! Tienes {count, plural, =0 {no hay mensajes} one {# mensaje} other {# mensajes}}.",
"date.today": "Hoy es {date, date, full}",
"currency.price": "Precio: {amount, number, currency}"
}
src/locales/fr.json (French):
{
"app.welcome": "Bienvenue dans notre application!",
"button.submit": "Soumettre",
"button.cancel": "Annuler",
"button.delete": "Supprimer",
"error.network": "Erreur réseau. Veuillez réessayer.",
"error.notFound": "Page non trouvée",
"items.count": "{count, plural, =0 {Aucun élément} one {# élément} other {# éléments}}",
"user.greeting": "Bonjour, {name}! Vous avez {count, plural, =0 {aucun message} one {# message} other {# messages}}.",
"date.today": "Aujourd'hui est {date, date, full}",
"currency.price": "Prix: {amount, number, currency}"
}
Step 3: Load Translation Messages
Create a utility file to load translations:
src/locales/index.js:
import en from './en.json';
import es from './es.json';
import fr from './fr.json';
export const messages = {
en,
es,
fr
};
export const locales = ['en', 'es', 'fr'];
export const defaultLocale = 'en';
Or with TypeScript (src/locales/index.ts):
import en from './en.json';
import es from './es.json';
import fr from './fr.json';
export const messages = {
en,
es,
fr
} as const;
export const locales = ['en', 'es', 'fr'] as const;
export const defaultLocale = 'en' as const;
Step 4: Configure IntlProvider
Wrap your app with IntlProvider in your main App component:
src/App.js:
import React, { useState } from 'react';
import { IntlProvider } from 'react-intl';
import { messages, defaultLocale, locales } from './locales';
import HomePage from './components/HomePage';
function App() {
const [locale, setLocale] = useState(defaultLocale);
return (
<IntlProvider
locale={locale}
messages={messages[locale]}
defaultLocale={defaultLocale}
>
<div className="App">
<HomePage locale={locale} setLocale={setLocale} />
</div>
</IntlProvider>
);
}
export default App;
Or with TypeScript (src/App.tsx):
import React, { useState } from 'react';
import { IntlProvider } from 'react-intl';
import { messages, defaultLocale, locales } from './locales';
import HomePage from './components/HomePage';
const App: React.FC = () => {
const [locale, setLocale] = useState<string>(defaultLocale);
return (
<IntlProvider
locale={locale}
messages={messages[locale as keyof typeof messages]}
defaultLocale={defaultLocale}
>
<div className="App">
<HomePage locale={locale} setLocale={setLocale} />
</div>
</IntlProvider>
);
};
export default App;
Step 5: Use FormattedMessage Component
Use the FormattedMessage component for translations:
Before:
function Welcome() {
return <h1>Welcome to our app!</h1>;
}
After:
import { FormattedMessage } from 'react-intl';
function Welcome() {
return (
<h1>
<FormattedMessage id="app.welcome" />
</h1>
);
}
Step 6: Use useIntl Hook
For more flexibility, use the useIntl hook:
import { useIntl } from 'react-intl';
function Welcome() {
const intl = useIntl();
return <h1>{intl.formatMessage({ id: 'app.welcome' })}</h1>;
}
With TypeScript:
import { useIntl } from 'react-intl';
const Welcome: React.FC = () => {
const intl = useIntl();
return <h1>{intl.formatMessage({ id: 'app.welcome' })}</h1>;
};
Step 7: String Interpolation with Variables
Pass variables to translations using ICU message format:
translation.json:
{
"user.greeting": "Hello, {name}!"
}
Component:
import { FormattedMessage } from 'react-intl';
function UserGreeting({ name }) {
return (
<p>
<FormattedMessage
id="user.greeting"
values={{ name: name }}
/>
</p>
);
}
// Usage: <UserGreeting name="John" />
// English: "Hello, John!"
// Spanish: "¡Hola, John!"
Or with useIntl:
import { useIntl } from 'react-intl';
function UserGreeting({ name }) {
const intl = useIntl();
return (
<p>
{intl.formatMessage(
{ id: 'user.greeting' },
{ name: name }
)}
</p>
);
}
Step 8: Pluralization
react-intl uses ICU message format for pluralization:
translation.json:
{
"items.count": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
Component:
import { FormattedMessage } from 'react-intl';
function ItemCount({ count }) {
return (
<p>
<FormattedMessage
id="items.count"
values={{ count: count }}
/>
</p>
);
}
// Usage examples:
// <ItemCount count={0} /> → "No items" (English) / "No hay elementos" (Spanish)
// <ItemCount count={1} /> → "1 item" (English) / "1 elemento" (Spanish)
// <ItemCount count={5} /> → "5 items" (English) / "5 elementos" (Spanish)
ICU Plural Rules:
=0- Exactly zeroone- Singular (1)other- Plural (2, 3, 4, etc.)#- Shorthand for the number value
Step 9: Format Dates
Use FormattedDate component or formatDate API:
Component approach:
import { FormattedDate } from 'react-intl';
function DateDisplay({ date }) {
return (
<p>
<FormattedDate
value={date}
year="numeric"
month="long"
day="numeric"
weekday="long"
/>
</p>
);
}
// English: "Wednesday, March 20, 2025"
// Spanish: "miércoles, 20 de marzo de 2025"
Hook approach:
import { useIntl } from 'react-intl';
function DateDisplay({ date }) {
const intl = useIntl();
const formattedDate = intl.formatDate(date, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
return <p>{formattedDate}</p>;
}
With ICU message format:
{
"date.today": "Today is {date, date, full}"
}
import { FormattedMessage, FormattedDate } from 'react-intl';
function TodayDate() {
const today = new Date();
return (
<p>
<FormattedMessage
id="date.today"
values={{ date: today }}
/>
</p>
);
}
Step 10: Format Numbers
Use FormattedNumber component or formatNumber API:
Component approach:
import { FormattedNumber } from 'react-intl';
function NumberDisplay({ number }) {
return (
<p>
<FormattedNumber value={number} />
</p>
);
}
// English: "1,234.56"
// Spanish: "1.234,56"
Hook approach:
import { useIntl } from 'react-intl';
function NumberDisplay({ number }) {
const intl = useIntl();
const formattedNumber = intl.formatNumber(number);
return <p>{formattedNumber}</p>;
}
Currency formatting:
import { FormattedNumber } from 'react-intl';
function PriceDisplay({ amount }) {
return (
<p>
<FormattedNumber
value={amount}
style="currency"
currency="USD"
/>
</p>
);
}
// English (US): "$1,234.56"
// Spanish (ES): "1.234,56 €"
With ICU message format:
{
"currency.price": "Price: {amount, number, currency}"
}
import { FormattedMessage } from 'react-intl';
function Price({ amount }) {
return (
<p>
<FormattedMessage
id="currency.price"
values={{ amount: amount }}
/>
</p>
);
}
Step 11: Format Relative Time
Use FormattedRelativeTime for relative dates:
import { FormattedRelativeTime } from 'react-intl';
function RelativeTime({ date }) {
const now = new Date();
const diffInSeconds = Math.floor((date - now) / 1000);
return (
<p>
<FormattedRelativeTime
value={diffInSeconds}
numeric="auto"
updateIntervalInSeconds={60}
/>
</p>
);
}
// Examples: "in 2 hours", "2 hours ago", "yesterday"
Step 12: Change Language Programmatically
Allow users to switch languages:
import { useIntl } from 'react-intl';
function LanguageSwitcher({ locale, setLocale }) {
const intl = useIntl();
const changeLanguage = (newLocale) => {
setLocale(newLocale);
// Optionally save to localStorage
localStorage.setItem('language', newLocale);
};
return (
<div>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('es')}>Español</button>
<button onClick={() => changeLanguage('fr')}>Français</button>
</div>
);
}
With language detection:
import { useState, useEffect } from 'react';
function App() {
const [locale, setLocale] = useState(() => {
// Try to get saved language from localStorage
const saved = localStorage.getItem('language');
if (saved) return saved;
// Detect browser language
const browserLang = navigator.language.split('-')[0];
return ['en', 'es', 'fr'].includes(browserLang) ? browserLang : 'en';
});
useEffect(() => {
localStorage.setItem('language', locale);
}, [locale]);
return (
<IntlProvider locale={locale} messages={messages[locale]}>
{/* Your app */}
</IntlProvider>
);
}
Step 13: Handle Right-to-Left (RTL) Languages
For RTL languages like Arabic and Hebrew:
import { useEffect } from 'react';
import { useIntl } from 'react-intl';
function App() {
const intl = useIntl();
const locale = intl.locale;
useEffect(() => {
const isRTL = ['ar', 'he', 'fa'].includes(locale);
document.documentElement.dir = isRTL ? 'rtl' : 'ltr';
document.documentElement.lang = locale;
}, [locale]);
return (
<div className="App">
{/* Your app content */}
</div>
);
}
Best Practices
1. Use Descriptive Message IDs
Bad:
{
"msg1": "Submit"
}
Good:
{
"button.submit": "Submit"
}
2. Organize Messages by Feature
translation.json:
{
"auth": {
"login": "Login",
"logout": "Logout",
"signup": "Sign Up"
},
"dashboard": {
"title": "Dashboard",
"welcome": "Welcome back!"
}
}
3. Use ICU Message Format
Always use ICU format for complex messages:
{
"user.status": "{name} has {count, plural, =0 {no tasks} one {# task} other {# tasks}}"
}
4. Provide Default Messages
Use defaultMessage prop for development:
<FormattedMessage
id="app.welcome"
defaultMessage="Welcome to our app!"
/>
5. Extract Messages for Translation
Use @formatjs/cli to extract messages:
npm install --save-dev @formatjs/cli
# Extract messages
formatjs extract "src/**/*.{js,jsx,ts,tsx}" --out-file locales/en.json
6. Test String Lengths
Some languages are longer than others. Design your UI to accommodate:
- German and Finnish: 30-50% longer than English
- Asian languages: May need more vertical space
Common Pitfalls
1. Forgetting to Wrap App with IntlProvider
Always wrap your app with IntlProvider:
// ❌ Won't work
function App() {
return <Welcome />;
}
// ✅ Correct
function App() {
return (
<IntlProvider locale="en" messages={messages.en}>
<Welcome />
</IntlProvider>
);
}
2. Not Providing Messages for Locale
Make sure messages exist for the selected locale:
// ❌ Will show message IDs if messages.es doesn't exist
<IntlProvider locale="es" messages={messages.en}>
// ✅ Correct
<IntlProvider locale="es" messages={messages.es}>
3. Hardcoding Format Strings
Bad:
const price = `$${amount.toFixed(2)}`;
Good:
<FormattedNumber
value={amount}
style="currency"
currency="USD"
/>
4. Not Handling Missing Messages
Provide fallbacks:
<FormattedMessage
id="app.welcome"
defaultMessage="Welcome"
/>
Advanced: Rich Text Formatting
Use FormattedMessage with rich text:
{
"welcome": "Welcome to <bold>our app</bold>!"
}
import { FormattedMessage } from 'react-intl';
function Welcome() {
return (
<FormattedMessage
id="app.welcome"
values={{
bold: (chunks) => <strong>{chunks}</strong>
}}
/>
);
}
Advanced: Message Descriptions
Add descriptions to help translators:
{
"button.delete": "Delete",
"@button.delete": {
"description": "Button to delete an item. Shown in the item detail view."
}
}
Advanced: TypeScript Support
For better TypeScript support:
src/types/react-intl.d.ts:
import { Messages } from '@formatjs/intl';
declare module 'react-intl' {
interface IntlMessages extends Messages {
'app.welcome': string;
'button.submit': string;
// ... other message keys
}
}
Advanced: Lazy Loading Messages
Load translations on demand:
import { useState, useEffect } from 'react';
import { IntlProvider } from 'react-intl';
function App() {
const [locale, setLocale] = useState('en');
const [messages, setMessages] = useState({});
useEffect(() => {
import(`./locales/${locale}.json`)
.then((module) => setMessages(module.default))
.catch(() => setMessages({}));
}, [locale]);
return (
<IntlProvider locale={locale} messages={messages}>
{/* Your app */}
</IntlProvider>
);
}
Using with Next.js
For Next.js applications, use next-intl or configure manually:
pages/_app.js:
import { IntlProvider } from 'react-intl';
import { useRouter } from 'next/router';
import { messages } from '../locales';
export default function App({ Component, pageProps }) {
const { locale } = useRouter();
return (
<IntlProvider
locale={locale}
messages={messages[locale]}
>
<Component {...pageProps} />
</IntlProvider>
);
}
Conclusion
Localizing your React app with react-intl is straightforward when you follow these steps:
- Install
react-intlpackage - Create translation JSON files for each language using ICU message format
- Load translations and wrap your app with
IntlProvider - Use
FormattedMessagecomponent oruseIntlhook - Handle pluralization with ICU format
- Format dates, numbers, and currency using built-in components
- Allow users to switch languages
- Test thoroughly in all supported languages
By following these practices, you’ll create an app that provides a native experience for users worldwide, significantly expanding your potential user base.
Streamline Your React Localization Workflow
Managing translations for multiple languages can become complex as your app grows. Consider using a translation management platform to:
- Collaborate with translators
- Keep translations in sync with your codebase
- Automate the translation workflow
- Maintain consistency across all languages
- Generate translation files automatically
- Integrate with your CI/CD pipeline
Ready to take your React app global? Explore AZbox’s localization platform and streamline your translation workflow: