Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

React Intl: Internationalize your React apps

14 min read 4113

React Intl Internationalize Your Apps

As globalization increases, writing React applications for a wide-ranging audience in different regions and locales means making it accessible across languages.

With internationalization capabilities, the React Intl library provides the mechanism to properly translate file text into other languages.

In this guide, we’ll learn how to use the React Intl library to set up internationalization in a React project. We’ll create a simple app that allows users to select and see their preferred language when viewing the app.

We’ll also persist the selected locale in the browser storage so the contents are still available after a page refresh or on a subsequent visit.

Below is the end product of what we will build together:

React Intl End Product

Interact with the finalized webpage to familiarize yourself with the interface, and with that, let’s get started!

Internationalization and localization

As mentioned earlier, React Intl allows us to set up internationalization in a React app. But what exactly is internationalization?

Internationalization is the process of designing a product — in this case, a React app — to be used within different locales. It is often abbreviated as Intl or i18n.

In contrast, localization, abbreviated l10n, focuses on translating an internationalized app from its original language to a specific language.

We made a custom demo for .
No really. Click here to check it out.

But hey, translation is more than just transforming the text or message to a target language. Cultural differences must also be considered, like how numbers and dates are written or how units and currencies are placed for different locales.

Fortunately, with the React Intl library, we can achieve our desired result seamlessly.

Setting up the React project

Let’s start by getting our simple React project — with its default content in English — from this GitHub repository. Then, we can add support for three additional languages: French, German, and Japanese.

So, head over to the terminal, switch to a directory to save the project, and run the following command to download the starter files:

git clone https://github.com/Ibaslogic/i18n_react_intl_starter

Once the project files have been bootstrapped, open them with a code editor. We must be inside the project directory and run npm install to create a node_modules folder for the project.

Ensure Node.js is installed on the computer to complete this tutorial.

Now, start the development server using npm start. We should see our app loaded in the browser at http://localhost:3000.

App loaded in the browser

If we take a look at the project’s file structure, we should have the following:

project_folder
    ├── node_modules
    ├── public
    ├── src
    │    ├── components
    │    │      ├── App.js
    │    │      ├── Content.js
    │    │      ├── Footer.js
    │    │      └── Header.js
    │    ├── index.css
    │    └── index.js
    ├── .gitignore
    ├── package-lock.json
    ├── package.json
    ├── README.md
    └── yarn.lock

Great. Now that we are on the same page, let’s see how we can use the React Intl library within our app.

Setting up the React Intl library

To use this library, we must install it by stopping the development server and running the following command from the terminal:

npm install react-intl

This library gives us all the APIs and components needed to implement internationalization into our app.

Let’s wrap the top-level app or the root component of our app in a provider component from the library; in this case, we’ll use <IntlProvider>.

What is the IntlProvider component?

As the name implies, IntlProvider ensures that important configurations are provided or made available to the subcomponents in the tree.

These subcomponents from React Intl are called formatted components. They are responsible for the proper translation and formatting at runtime. These components are:

  • FormattedMessage
  • FormattedNumber
  • FormattedDate
  • FormattedPlural
  • And many more

For internationalization projects using the React Intl library, the FormattedMessage component is often used, allowing users to translate and format simple to complex strings and messages. We’ll be using this in a moment.

Back in our project, let’s open the parent component file src/components/App.js and wrap the children components with IntlProvider.

Ensure to import IntlProvider from react-intl at the top of the file.

Our file should look like this:

...
import { IntlProvider } from "react-intl";

const App = () => {
  return (
    <IntlProvider>
      <div>
        <Header />
        <Content />
        <Footer />
      </div>
    </IntlProvider>
  );
};

export default App;

Let’s take a moment and check what the IntlProvider component returns. First, log the component:

...
const App = () => {
  console.log(IntlProvider);

  return (
    ...
  );
};
...

Then, check the browser console:

Default Configuration Props

By focusing on the defaultProps, we see all the default configuration props used by IntlProvider. The component is also telling us to configure a locale.

So, let’s add the required props to identify and display translations to IntlProvider by updating the component to include the following props:

return (
  <IntlProvider messages={{}} locale="en" defaultLocale="en">
    <div>
      <Header />
      <Content />
      <Footer />
    </div>
  </IntlProvider>
);

Immediately by adding the locale prop and saving, the error goes away.

This locale, which accepts a string, determines what language our app is rendered in. We’ll dynamically add the value based on what users select in the frontend.

The messages object contains a set of translated strings ready to display in the frontend. These will also be dynamically added based on the current locale.

Lastly, the defaultLocale prop is the default locale and should match the app’s default language.

Before we load the translated messages for our project, let’s play around and get familiar with the formatted components.

Open the src/components/Footer.js file and add these formatted components in the return statement:

// ...
import { FormattedDate, FormattedNumber, FormattedPlural } from "react-intl";

const Footer = () => {
  // ...
  return (
    <div className="container mt">
      {/* ... */}

      <FormattedDate value={Date.now()} />
      <br />
      <FormattedNumber value={2000} />
      <br />
      <FormattedPlural value={5} one="1 click" other="5 clicks" />
    </div>
  );
};

// ...

Next, save the file and check the frontend. We should have this displayed:

5/29/2021 // your current date
2,000
5 clicks

If we change the locale prop of the IntlProvider to de and save the file once again, we have the data formatted in German:

29.5.2021

2.000

This is awesome. React Intl is formatting the date and number based on the selected region through the formatted components (FormattedDate and FormattedNumber, respectively).

These formatted components have a value prop that accepts the data to be formatted.

We can further customize these components by taking advantage of its other props. For instance, we can have the following:

return (
  <div className="container mt">
    {/* ... */}

    <FormattedDate
      value={Date.now()}
      year="numeric"
      month="long"
      day="2-digit"
    />
    <br />
    <FormattedNumber value={2000} style={`currency`} currency="USD" />
    <br />
    {/* ... */}
  </div>
);

Then, we’ll get this for English (en):

May 29, 2021 // your current date

$2,000.00

And then we’ll get this for German (de):

29. Mai 2021

2.000,00 $

Apart from these two components, we also have the FormattedPlural component that allows us to handle plurals in our application. As seen in the sample above, the component selects a plural category based on the value it received.

Using the React Intl API

Another way we can format our data is through the React Intl API. Though using the component method is preferred in React because it works seamlessly with other React components, there are cases where the API method is needed, such as when the placeholder, title, or aria-label of an element must be translated.

Anytime we use formatted components to render a React element, they use the React Intl API behind the scenes.

Let’s see how this API works. Still in the Footer.js file, import the useIntl Hook from react-intl:

import { FormattedDate, FormattedNumber, FormattedPlural, useIntl } from "react-intl";

Ensure this is done in a functional React component.

If we log the useIntl Hook in the console, we should see all the available functions needed to format our data:

// ...
const Footer = () => {
  // ...
  const intl = useIntl();
  console.log(intl);

  return (
    <div className="container mt">
      {/* ... */}
    </div>
  );
};
// ...
>

One of these functions is the formatDate; let’s apply it.

Update the return statement in the Footer.js file to include the function:

// ...
const Footer = () => {
  // ...
  const intl = useIntl();
  return (
    <div className="container mt">
      {/* <... */}
      <br />
      <input placeholder={intl.formatDate(Date.now())} />
    </div>
  );
};
// ...

Check the frontend to see the input field. Remember to change the locale value of the IntlProvider to see how it works.

We can customize further by adding the optional config:

<input
  placeholder={intl.formatDate(Date.now(), {
    year: "numeric",
    month: "long",
    day: "2-digit",
  })}
/>

Now that we are familiar with these formatters, let’s head back to our project.

Translating the app’s source text string

To support other locales, we must translate the source text. For this tutorial, let’s use Google Translate.

Create a folder named i18n in the src directory. In this folder, create two different files named locales.js and messages.js. The locales.js file will hold our supported locales while the messages.js file will hold the corresponding translations.

Add this in the i18n/locales.js file:

export const LOCALES = {
  ENGLISH: "en-US",
  JAPANESE: "ja-JA",
  FRENCH: "fr-FR",
  GERMAN: "de-DE",
};

This is necessary to avoid manually adding the locales. To access en-US, for example, we’ll use [LOCALES.ENGLISH].

Where is the code coming from?

The code en-US comes from the language-COUNTRY code, although we can also just use the language code without adding the COUNTRY code.

However, in instances when multiple countries have the same language, it can be useful to add the COUNTRY clarifier. For example, English in the United States would be en-US, and English in the United Kingdom would be en-GB.

Translations in React

Now, in the i18n/messages.js file, add the following translations:

import { LOCALES } from "./locales";

export const messages = {
  [LOCALES.ENGLISH]: {
    learn_to: "Hello, let's learn how to use React-Intl",
    price_display:
      "How {n, number, ::currency/USD} is displayed in your selected language",
    number_display:
      "This is how {n, number} is formatted in the selected locale",
    start_today: "Start Today: {d, date}",
    // menu
    about_project: "About the project",
    contact_us: "Contact us",
  },
  [LOCALES.FRENCH]: {
    learn_to: "Bonjour, apprenons à utiliser React-Intl",
    price_display:
      "Comment {n, number, ::currency/USD} $ s'affiche dans la langue sélectionnée",
    number_display:
      "Voici comment {n, number} sont formatés dans les paramètres régionaux sélectionnés ",
    start_today: "Commencez aujourd'hui: {d, date}",
    // menu
    about_project: "À propos du projet",
    contact_us: "Contactez-nous",
  },
  [LOCALES.GERMAN]: {
    learn_to: "Hallo, lass uns lernen, wie man React-Intl benutzt",
    price_display:
      "Wie {n, number, ::currency/USD} in Ihrer ausgewählten Sprache angezeigt wird",
    number_display:
      "Auf diese Weise werden {n, number} im ausgewählten Gebietsschema formatiert",
    start_today: "Beginnen Sie heute: {d, date}",
    // menu
    about_project: "Über das Projekt",
    contact_us: "Kontaktiere uns",
  },
  [LOCALES.JAPANESE]: {
    learn_to: "こんにちは、React-Intlの使い方を学びましょう",
    price_display:
      "選択した言語で{n, number, ::currency/USD}がどのように表示されるか",
    number_display:
      "これは、選択したロケールで{n, number}がフォーマットされる方法です。",
    start_today: "今日から始める:{d, date}",
    // menu
    about_project: "プロジェクトについて",
    contact_us: "お問い合わせ",
  },
};

This file comprises our application source text and the translations for the supported locales. Note each of the unique IDs and keys, which can be named anything; we will use them to inject their corresponding strings into our application.

For now, let’s focus on the simple strings and ignore the arguments, also known as placeholders, in the curly brackets.

Next, let’s load the data in the message prop of the IntlProvider component.

As we can see from the data, we can access the English translation object like so:

messages[LOCALES.ENGLISH]

The same applies to the other locales.

So, let’s assign this English translation to the messages prop of the provider component. Later in this guide, we will define a logic that dynamically injects translated messages based on the user’s selected locale.

In the components/App.js file, import the LOCALES and messages at the top of the file:

import { LOCALES } from "../i18n/locales";
import { messages } from "../i18n/messages";

Then, update the <IntlProvider> so we have the following:

const App = () => {
  const locale = LOCALES.ENGLISH;

  return (
    <IntlProvider
      messages={messages[locale]}
      locale={locale}
      defaultLocale={LOCALES.ENGLISH}
    >
      ...
    </IntlProvider>
  );
};

export default App;

From the code, we can deduce that the English data is loaded in the provider component and can be accessed through the subcomponent.

As mentioned earlier, we will use the FormattedMessage to format these complex strings. This component is ideal if our message combines dates, numbers, or just simple messages.

We will start by transforming the simple string, “Hello, let’s learn how to use React-Intl.”

Head over to the components/Content.js file and import the FormattedMessage:

import { FormattedMessage } from "react-intl";

Then, replace the string with the component:

import { FormattedMessage } from "react-intl";

const Content = () => {
  return (
    <div className="container hero">
      <h1><FormattedMessage id="learn_to" /></h1>
      {/* ... */}
    </div>
  );
};

export default Content;

It’s that simple. To test our work, we can manually change the locale in the App.js file to JAPANESE:

const locale = LOCALES.JAPANESE;

Save and see the changes in the frontend:

Return The Corresponding Key To The Current Locale

The FormattedMessage used in the code above takes an id prop whose value matches a specific key in the translation file and then returns the corresponding string for the current locale. Remember, this id is unique across all the supported locales in the translation file.

Using arguments

When we see a message that includes a date, amount, or number, we want to replace them with arguments.

Earlier, we learned how to format these types of values using the FormattedDate and FormattedNumber. But here we will use the FormattedMessage since the values are part of a string.

If we take a look at the translation file, we are replacing these value types by following this pattern: { key, type, format }.

For instance, we have something like this:

price_display:
  "How {n, number, ::currency/USD} is displayed in your selected language",

The first element, n, is the key, and it looks up to the data object to find the appropriate data. The second element is the type of data to interpret, and it conforms to a specific locale.

The third argument — which is optional — allows us to specify additional information about the element type.

In the case above, we are saying that the placeholder of the number must be formatted as USD currency. Then, React Intl positions the currency. You can find the list of currency codes here.

To transform this type of string using the FormattedMessage, we’ll have something like this:

<FormattedMessage id="price_display" values={{ n: 59.99 }} />

As expected, the id locates the translation for the current locale, and the placeholder is replaced by the key value.

If we apply this logic in our project, the components/Content.js file now looks like this:

import { FormattedMessage } from "react-intl";

const Content = () => {
  return (
    <div className="container hero">
      <h1><FormattedMessage id="learn_to" /></h1>
      <p><FormattedMessage id="price_display" values={{ n: 59.99 }} /></p>
      <p><FormattedMessage id="number_display" values={{ n: 2000 }} /></p>
      <p><FormattedMessage id="start_today" values={{ d: new Date() }} /></p>
    </div>
  );
};

export default Content;

Save the file and test the app by changing the locale in the component/App.js file to another supported language:

const locale = LOCALES.JAPANESE;

Our app should look like this:

 

Change The Locale To Another Language

Pluralization with React Intl

Earlier, we learned how to use the <FormattedPlural> component to handle a simple plural text. In this section, we will use the <FormattedMessage> to achieve plurals in a text-rich message.

Presently, if we click on the count button in the frontend, we don’t have logic that displays the correct text when the count is singular, that is, when it’s at 1.

When we formatted the currency using the { key, type, format } pattern, we can use the same pattern but use plural and matches to replace type and format, respectively.

By applying this pattern, we can add the following to our English translation object:

click_count: "You clicked {count, plural, one {# time} other {# times}}"

The plural categories, one and other, match the singular and the plural forms within the first and second curly brackets, preceded by #, which represents the count number.

If we translate the message to other locales and apply the same logic, we’ll have the following for German:

click_count:
      "Sie haben {count, plural, one {# Mal} other {# Mal}} geklickt",

For French:

click_count:
  "Vous avez cliqué {count, plural, one {# fois} other {# fois}}",

For Japanese:

click_count: "{count, plural, one {# 回} other {# 回}}クリックしました",

Save the file and open the components/Footer.js file to render the translation using the FormattedMessage component.

Then, find this element:

<p>You clicked {count} times</p>

Replace the above with the following:

<p>
  <FormattedMessage id="click_count" values={{ count: count }} />
</p> 

We must also import the FormattedMessage component from react-intl:

import {
  // ...
  FormattedMessage,
} from "react-intl";

Save the file again. Let’s reload the frontend and test our work.

We can now add translation support for the menu items. First, import the FormattedMessage in the components/Header.js file like so:

import { FormattedMessage } from "react-intl";

We need access to the individual menu key used in the translation file. To do this, let’s update the menu array to include the key:

const menu = [
  {
    key: "about_project",
    // ...
  },
  {
    key: "contact_us",
    // ...
  },
];

We can now use the keys to access the translations in the FormattedMessage.

Begin by finding the following code:

{menu.map(({ title, path }) => (
  <li key={title}>
    <a href={path}>{title}</a>
  </li>
))}

Then, replace with this:

{menu.map(({ title, path, key }) => (
  <li key={title}>
    <a href={path}>
      <FormattedMessage id={key} />
    </a>
  </li>
))}

Save the file and test the application.

To localize the frontend button and its call to action text, let’s assign keys to the button in our i18n/message.js file.

For the English object, add the following:

click_button: "Please click the button below",
click_here: "click here",

For French, add this:

click_button: "Veuillez cliquer sur le bouton ci-dessous",
click_here: "Cliquez ici",

Add translations for the other locales, such as Japanese and German, using the above code and Google translate.

Once we are done, let’s open the components/Footer.js and replace the text strings with the FormattedMessage:

return (
  <div className="container mt">
    {/* Footer content here */}
    <p><FormattedMessage id="click_button" /></p>
    <button onClick={onChange}>
      <FormattedMessage id="click_here" />
    </button>
    {/* ... */}
  </div>
);

Save the files and reload the frontend. Notice that the button’s description and the button’s text updated in French.

Updated in French app

Adding the option to switch locales in the frontend

To provide an option to switch locales in the frontend, we must open the components/Header.js file and add the following above the return statement:

// Languages
const languages = [
  { name: "English", code: LOCALES.ENGLISH },
  { name: "日本語", code: LOCALES.JAPANESE },
  { name: "Français", code: LOCALES.FRENCH },
  { name: "Deutsche", code: LOCALES.GERMAN },
];

Then, import the LOCALES at the top of the file:

import { LOCALES } from "../i18n/locales";

Next, we will loop through the languages array to generate our dropdown.

Still in the file, locate this div container element:

<div className="switcher">
  {/* Language switch dropdown here */}
</div>

Then, update it to see the following:

<div className="switcher">
  {/* Language switch dropdown here */}
  Languages{" "}
  <select>
    {languages.map(({ name, code }) => (
      <option key={code} value={code}>
        {name}
      </option>
    ))}
  </select>
</div>

Save it and add this simple style in the src/index.css file:

.switcher select {
  width: 99px;
  height: 30px;
}

Save the file again and view the dropdown in the frontend. At the moment, switching between languages doesn’t change the page content, so let’s address that.

If familiar with form handling in React, this process should be a piece of cake. If not, that’s fine because we will do it together.

In React, the form input should be a controlled input unlike that of the HTML form.

To do this, we will add a value prop and an onChange event to the select element. The value prop is assigned the current locale while the onChange updates the value based on the user’s selection.

For the meantime, update the select element to include these props:

<select onChange="" value="">
  ..
</select>

Since the IntlProvider is in the parent component, App.js must also know the current locale, so let’s set up the logic in the file.

We can pass the necessary data down to the Header child component through the props. This process is called props drilling.

Open the components/App.js and add a state whose default language is English:

import { useState } from "react";
...
const App = () => {
  const locale = LOCALES.ENGLISH;

  const [currentLocale, setCurrentLocale] = useState(locale);

  return (
    <IntlProvider
      messages={messages[currentLocale]}
      locale={currentLocale}
      defaultLocale={LOCALES.ENGLISH}
    >
      ...
    </IntlProvider>
  );
};

export default App;

Don’t forget to import the useState Hook from React to get the current locale.

Now, let’s pass the locale to the Header child component and use it in the select element.

In the App.js file, update the <Header /> instance in <IntlProvider /> so we have the following:

<Header currentLocale={currentLocale}/>

Save the file and open the Header.js to access the data; pass it to the value prop of the select element:

...
const Header = (props) => {
  ...
  return (
    <header>
      <div className="container header_content">
        ...
        <nav>
          ...
        </nav>
        <div className="spacer"></div>
        <div className="switcher">
          {/* Language switch dropdown here */}
          Languages{" "}
          <select onChange="" value={props.currentLocale}>
           ...
          </select>
        </div>
      </div>
    </header>
  );
};

export default Header;

Call props the following:

const Header = (props) => { 

Next, we will use the onChange event to handle updates.

In the App.js file, add the following handler above the return statement and pass it down to the Header component through the props:

const handleChange = (e) => {
  setCurrentLocale(e.target.value);
};

The code now looks like this:

...
const App = () => {
  ...
  const handleChange = (e) => {
    setCurrentLocale(e.target.value);
  };

  return (
    ...
      <div>
        <Header currentLocale={currentLocale} handleChange={handleChange} />
        ...
      </div>
    ...
  );
};

Save the file and access the handler inside the Header component.

Simply update the select element so we have the following:

<select onChange={props.handleChange} value={props.currentLocale}>

Now we can test our application. It works! Awesome.

Ensure to translate the Languages text beside the dropdown list as well.

Persisting the selected locale in the browser local storage

Presently, if we switch from the default locale and reload the page, the content goes back to default. We don’t want that.

In this section, we’ll see how to easily persist the selected locale to the browser storage. This means the content in the user’s preferred language is still visible on the page when reloaded and on a subsequent visit, further improving the user’s experience.

First, open the App.js file and add this code above the return statement:

//localstorage
function getInitialLocal() {
  // getting stored items
  const savedLocale = localStorage.getItem("locale");
  return savedLocale || LOCALES.ENGLISH;
}

Then, update the state to use this function:

const [currentLocale, setCurrentLocale] = useState(getInitialLocal());

Finally, update the handleChange to include the storage object:

const handleChange = (e) => {
  setCurrentLocale(e.target.value);
  // storing locale in the localstorage
  localStorage.setItem("locale", e.target.value);
};

Remove this line from the App.js file because it is now redundant:

const locale = LOCALES.ENGLISH;

Now, save the file and test the application. Ensure to reload the page to see that the data persists in the storage.

Because we started by accessing the local storage using the getItem() method to check for any saved locale or return a fallback, the returned locale was assigned to the state variable, currentLocale.

Likewise, we are also using the setItem() method to save the user’s selected locale to the Storage object.

Read about how to persist state of your React component in the local storage to learn more.

Conclusion

We’ve covered pretty much everything we need to know about the React Intl library. We’ve learned how to apply the library to a React application and how to persist the user’s selected locale in the local storage.

I hope you found this guide helpful. Should you have questions or contributions, I am in the comment section. You can also find the entire project source code in this GitHub repository.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Leave a Reply