How to Translate a React App with react-intl: Complete Guide

Learn how to localize your React app using react-intl (FormatJS). This comprehensive guide covers string translation, pluralization, date formatting, and best practices for React app transla

  • date icon

    Sunday, Nov 16, 2025

How to Translate a React App with react-intl: Complete Guide

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 zero
  • one - 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:

  1. Install react-intl package
  2. Create translation JSON files for each language using ICU message format
  3. Load translations and wrap your app with IntlProvider
  4. Use FormattedMessage component or useIntl hook
  5. Handle pluralization with ICU format
  6. Format dates, numbers, and currency using built-in components
  7. Allow users to switch languages
  8. 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:

View AZbox Plans and Pricing

Blog

Latest Posts

Discover our latest articles and updates.

Why Translations Have Always Been a Problem in Software Development
date icon

Sunday, Dec 21, 2025

Why Translations Have Always Been a Problem in Software Development

For decades, software developers have struggled with translations and localization. What should be a straightforward pro

Read More
Cómo Traducir una App Flutter con Azbox: Guía Completa
date icon

Saturday, Dec 20, 2025

Cómo Traducir una App Flutter con Azbox: Guía Completa

Traducir tu app Flutter es esencial para llegar a una audiencia global. Azbox proporciona un potente SDK de Flutter que

Read More
How to Translate a Flutter App with Azbox: Complete Guide
date icon

Saturday, Dec 20, 2025

How to Translate a Flutter App with Azbox: Complete Guide

Translating your Flutter app is essential for reaching a global audience. Azbox provides a powerful Flutter SDK that sim

Read More
cta-image

Start Global Growth Today

Join hundreds of successful companies already using AZbox to reach customers worldwide. Start with a free trial, no credit card required.

Get Started - It's Free