Zoran Luledzija
Zoran Luledzija
April 14, 2021
9 min read
April 14, 2021
9 min read

React internationalization with react-intl

Supporting multiple languages in an application is a great way to make it more appealing to an international audience. In this article, we will show you how to internationalize (i18n) your React app with the React Intl library.

React is one of the most popular JavaScript libraries for building user interfaces. It is recognizable by its efficiency, flexibility, and declarative way of usage. The large community around React has spawned many great libraries for it. Some of them are created to help with internationalization.

The most popular libraries out there for React internationalization are:

Deciding which one to use is not an easy task. Each has its pros and cons. In the following, we will focus only on one of them, the react-intl.

The react-intl is a part of Format.JS, a set of JavaScript libraries for internationalization and formatting. It is a well-documented and maintained library. It works well in all major browsers. For older browsers, there are polyfills. Plenty of formatting options and support for the ICU Message syntax makes it very handy.

In the coming chapters, you will find more about how to use it in your project.

All code samples used in this article are available on the GitHub repo.
The working demo can be found here.

Create new React project

First things first, let’s create a new React app with Create React App.

npx create-react-app react-intl-example

Add React Intl dependency

To use the react-intl library in the project, you need to add it as a dependency.

cd react-intl-example
npm i --save react-intl

Create localization files

The next step is to create the localization files for the required locales. It is good practice to keep all localization files in one place (e.g. src/lang). In this example, we will add three JSON files under the lang directory: ar.json, en.json, and es-MX.json. These files are going to hold translations for the Arabic, English, and Spanish (Mexico).

Below, you can see how the project structure should look after adding mentioned files.

react-intl-example
|-- lang
|   |-- ar.json
|   |-- en.json
|   |-- es-MX.json
|-- src
|   |-- App.js
|   |-- App.test.js
|   |-- index.js
|   |-- ...
|-- ...
|-- package.json
|-- package-lock.json

As we are going to use localization messages later, let’s populate added files with some examples.

ar.json:

{
  "message.simple": "رسالة بسيطة.",
  "message.argument": "مرحبًا {name}! 👋",
  "message.plural": "{count, plural, zero {لا توجد عناصر} one {# بند} two {# بنود} few {# عناصر} many {# بندا} other {# قطعة}}",
  "message.select": "{gender, select, male {السيد} female {السيدة} other {المستعمل}}",
  "message.text-format": "مرحبًا <b>John</b>!",
  "message.number-format": "عدد مهيأ: {num, number, ::K}",
  "message.currency-format": "عملة منسقة: {amount, number, ::currency/USD}"
}

en.json:

{
  "message.simple": "A simple message.",
  "message.argument": "Hi, {name}! 👋",
  "message.plural": "{count, plural, one {# item} other {# items}}",
  "message.select": "{gender, select, male {Mr} female {Mrs} other {User}}",
  "message.text-format": "Hi, <b>John</b>!",
  "message.number-format": "Formatted number: {num, number, ::K}",
  "message.currency-format": "Formatted currency: {amount, number, ::currency/USD}"
}

es-MX.json:

{
  "message.simple": "Un mensaje sencillo.",
  "message.argument": "¡Hola, {name}! 👋",
  "message.plural": "{count, plural, one {# articulo} other {# artículos}}",
  "message.select": "{gender, select, male {Sr.} female {Sra.} other {Usuario}}",
  "message.text-format": "¡Hola, <b>John</b>!",
  "message.number-format": "Número formateado: {num, number, ::K}",
  "message.currency-format": "Moneda formateada: {amount, number, ::currency/USD}"
}

Adapt the main app file

Although we have done most of the required things, we still need to adapt the main app file for internationalization. Therefore, we are going to update the content of the App.js file.
To put it another way, we will wrap the app with the IntlProvider. That way, we will be able to use localization features through all subcomponents in the app tree. Also, we will add detection of the user’s preferred language and optimize performances by loading only needed translations. And finally, we will add a language switcher and handle the setting of appropriate text direction.

Wrap the app with the IntlProvider

The react-intl library uses the provider pattern to pass the needed configuration through the tree of descendant nodes. That way, throughout the whole app, we can use localized messages and proper formatting. As you can see in the below-posted example, the top-level app component is wrapped with the IntlProvider from the react-intl. There, we are passing two props to the IntlProvider, the locale, and the messages. As the name implies, these props expect selected locale within the app and appropriate messages for that locale.

Detect user's preferred language

The initial locale of the app is retrieved from the user’s preferred language (the language of the browser). If the user’s preferred language is not supported, the app will use the default, English. The presented locale resolution logic is trivial. In practice, this would probably be a slightly more robust check that would include a region control as well.

Load translations dynamically

Using dynamic imports, we are loading localization messages only when they are needed. Moreover, that way, we are improving app performance, as only required data is loaded. The below-posted example contains the loadMessages function that returns localization messages as a Promise for the passed locale.

Add language switcher

Although setting the initial language according to the browser’s language works in most cases, it is good practice to provide an option for manual config as well. To achieve that, we wrapped the IntlProvider with the LocalizationWrapper component. This component will hold the currently selected locale and messages, and in case of change, it will update its state, which consequentially will update the rest of the app. As we are going to render language switcher somewhere down in the app tree, we are passing locale and onLocaleChange props to the App component.

Set text direction

Since one of the added languages (Arabic) has a non-default text direction (rtl), explicit handling of the same is needed. For that reason, we are going to add the getDirection function that will return the appropriate direction for the passed locale.
Roughly speaking, you can set the text direction in two ways, with the HTML dir attribute or with the CSS direction property. In this example, we are going to use the first option, the dir attribute. To demonstrate how this works in practice, we will adjust the dir attribute of the container element within the App component. Note that you can also set the dir attribute on the html element and thus customize the content of the entire HTML page, of course, if something like that suits your use case.

App.js (first part):

import React, { useState, useEffect } from "react";
import {
  useIntl,
  IntlProvider,
  FormattedMessage,
  FormattedDate,
  FormattedTime,
  FormattedRelativeTime,
  FormattedNumber,
  FormattedList,
} from "react-intl";

let initLocale = "en";
if (navigator.language === "es-MX") {
  initLocale = "es-MX";
} else if (navigator.language === "ar") {
  initLocale = "ar";
}

function loadMessages(locale) {
  switch (locale) {
    case "ar":
      return import("./lang/ar.json");
    case "en":
      return import("./lang/en.json");
    case "es-MX":
      return import("./lang/es-MX.json");
    default:
      return import("./lang/en.json");
  }
}

function getDirection(locale) {
  switch (locale) {
    case "ar":
      return "rtl";
    case "en":
      return "ltr";
    case "es-MX":
      return "ltr";
    default:
      return "ltr";
  }
}

function LocalizationWrapper() {
  const [locale, setLocale] = useState(initLocale);
  const [messages, setMessages] = useState(null);

  useEffect(() => loadMessages(locale).then(setMessages), [locale]);

  return messages ? (
    <IntlProvider locale={locale} messages={messages}>
      <App locale={locale} direction={getDirection(locale)} onLocaleChange={(locale) => setLocale(locale)} />
    </IntlProvider>
  ) : null;
}
export default LocalizationWrapper;

The App component

The App component is very primitive. It contains just a locale picker and message examples. With the help of the useIntl hook and the Formatted* components from the react-intl library, we are able to access the localization messages. The posted example contains two different ways for accessing, an imperative and a declarative way. More words about these two ways will be in the next chapter.

App.js (second part):

function App({ locale, direction, onLocaleChange }) {
  const intl = useIntl();

  return (
    <div>
      <div style={{ textAlign: "center" }}>
        <select value={locale} onChange={(e) => onLocaleChange(e.target.value)}>
          <option value="en">en</option>
          <option value="es-MX">es-MX</option>
          <option value="ar">ar</option>
        </select>
      </div>

      <div dir={direction} style={{ padding: 20 }} data-testid="examples">
        <h3>Declarative examples</h3>
        <FormattedMessage id="message.simple" />
        <br />
        <FormattedMessage id="message.argument" values={{ name: "John" }} />
        <br />
        <FormattedMessage id="message.plural" values={{ count: 6 }} />
        <br />
        <FormattedMessage id="message.select" values={{ gender: "female" }} />
        <br />
        <FormattedMessage id="message.text-format" values={{ b: (value) => <b>{value}</b> }} />
        <br />
        <FormattedMessage id="message.number-format" values={{ num: 7500 }} />
        <br />
        <FormattedMessage id="message.currency-format" values={{ amount: 7.5 }} />
        <br />
        <FormattedList type="conjunction" value={["foo", "bar", "baz"]} />

        <h3>Imperative examples</h3>
        {intl.formatMessage({ id: "message.simple" })}
        <br />
        {intl.formatMessage({ id: "message.argument" }, { name: "John" })}
        <br />
        {intl.formatMessage({ id: "message.plural" }, { count: 5 })}
        <br />
        {intl.formatMessage({ id: "message.select" }, { gender: "female" })}
        <br />
        {intl.formatMessage({ id: "message.text-format" }, { b: (value) => <b>{value}</b> })}
        <br />
        {intl.formatMessage({ id: "message.number-format" }, { num: 7500 })}
        <br />
        {intl.formatMessage({ id: "message.currency-format" }, { amount: 7.5 })}
        <br />
        {intl.formatList(["foo", "bar", "baz"], { type: "conjunction" })}
      </div>
    </div>
  );
}

Imperative vs Declarative usage of React Intl

Depending on your needs, you can format a message in the imperative and the declarative way. In most cases, they should have the same capabilities. Some hints are given below.

Use the imperative approach for:

  • setting text attributes (e.g. title, aria-label)
  • formatting messages in environments that do not support React components (e.g. Redux store)
  • better performance (e.g. rendering large tables of data)

Use the declarative approach for:

  • seamlessly composing with other React components
  • supporting rich-text formatting
  • advanced features (e.g. formatted relative time that’s updating over time, etc.)

Imperative usage example:

intl.formatMessage({ id: "common.aria-label.confirm" })

Declarative usage example:

<FormattedMessage id="page.home.greeting" />

React Intl formatting options

Some examples of the most common formatting options are listed below.

Simple message example:

// "message.simple": "A simple message."
intl.formatMessage({ id: "message.simple" })
<FormattedMessage id="message.simple" />

Argument message example:

// "message.argument": "Hi, {name}! 👋"
intl.formatMessage({ id: "message.argument" }, { name: "John" })
<FormattedMessage id="message.argument" values={{ name: "John" }} />

Plural message example:

// "message.plural": "{count, plural, one {# item} other {# items}}"
intl.formatMessage({ id: "message.plural" }, { count: 5 })
<FormattedMessage id="message.plural" values={{ count: 5 }} />

Select message example:

// "message.select": "{gender, select, male {Mr} female {Mrs} other {User}}"
intl.formatMessage({ id: "message.select" }, { gender: "female" })
<FormattedMessage id="message.select" values={{ gender: "female" }} />

Text formatting example:

// "message.text-format": "Hi, <b>John</b>!"
intl.formatMessage({ id: "message.text-format" }, { b: (value) => <b>{value}</b> })
<FormattedMessage id="message.text-format" values={{ b: (value) => <b>{value}</b> }} />

Currency formatting example:

intl.formatNumber(7.5, { style: "currency", currency: "USD" })
<FormattedNumber value={7.5} style="currency" currency="USD" />

Date formatting example:

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

Time formatting example:

<FormattedTime value={Date.now()} />
{intl.formatTime(Date.now())}

Relative time formatting example:

intl.formatRelativeTime(-5, "second", { style: "narrow" })
<FormattedRelativeTime value={0} numeric="auto" updateIntervalInSeconds={1} />

Message extraction

While it is good to start with internationalization from the very beginning of the app development, it is not always feasible. The reasons for this can be various, such as priorities, different plans, user needs, etc. Hence, it is worth mentioning that it is possible to extract localization messages based on the react-intl with the help of the @formatjs/cli package. Since this is a separate topic, we will not deal with it in this post.

Conclusion

In this article, we showed some basic examples of the react-intl library.
Preparing your app to support various languages is the first part of the task. The second one is translating all app messages into languages that you want to support. In most cases, this implies collaboration with translators, reviewing translations, Over-the-air translation updates via S3 bucket, and much more. Luckily, the Localizely platform can help you with that.

Like this article? Share it!


Zoran Luledzija
Zoran Luledzija

Zoran is a Software Engineer at Localizely. His primary interest is web development, but he also has a solid background in other technologies. For the last few years, he has been involved in the development of software localization tools.

Enjoying the read?

Subscribe to the Localizely blog newsletter for quality product content in your inbox.

Copyrights 2023 © Localizely