Zoran Luledzija
Zoran Luledzija
November 15, 2023
21 min read
November 15, 2023
21 min read

React internationalization with react-intl

React internationalization with react-intl

Reaching a global audience with your web application requires support for multiple languages. The good news is that adding multilingual support is not usually complex and brings a plethora of benefits, including increased user engagement and enhanced accessibility, among others. In this post, we'll explain how to internationalize (i18n) React web applications using the React Intl library, focusing on key aspects such as message localization, formatting, language selection, and other important elements.

Keeping up with the latest updates and guidance from the React team, this post will explore the internationalization process of React applications developed using Vite and Create React App. If you're interested in learning about supporting internationalization in Next.js projects, we invite you to consult our dedicated post on that topic.

All code samples used in this section are available on the GitHub repo.
See an online demo.

Create new React project

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

npm create vite@latest -- --template react

Since this command operates in interactive mode, for the project name enter react-intl-example.

Add React Intl dependency

React Intl is part of Format.JS, a suite of JavaScript libraries designed for internationalization and formatting. This well-documented and maintained library supports ICU Message syntax, a wide range of formatting options, and is compatible with major browsers.

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

cd react-intl-example
npm i 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 Mexican Spanish languages.

Below, you can see how the project structure should look after you have added these files.

react-intl-example
|-- public
|-- src
|   |-- assets
|   |-- lang
|       |-- ar.json
|       |-- en.json
|       |-- es-MX.json
|   |-- App.jsx
|   |-- main.jsx
|   |-- ...
|-- ...
|-- package.json
|-- package-lock.json

As we will be using localization messages later, let's populate the files we added 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

While we have completed most of the necessary tasks, there's still a need to modify the main app file to support internationalization. To achieve this, our next step involves updating the App.jsx file. More concretely, this means wrapping the whole app with the IntlProvider, which will enable us to use localization features across all app subcomponents. Furthermore, we will incorporate a feature to detect the user's preferred language and optimize performance by loading only necessary translations. Lastly, we will add a language switcher and configure the appropriate text direction settings.

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. This approach ensures that throughout the entire app, we can use localized messages and proper formatting. As you can see in the following example, the top-level app component is wrapped with the IntlProvider from react-intl. There, we pass two props to the IntlProvider, locale and messages. As their names suggest, the locale prop specifies the selected locale within the app, and the messages prop contains the messages corresponding to that locale.

The App.jsx file:

import React from "react";
import { IntlProvider } from "react-intl";
import messages from "./lang/en.json";

let locale = "en";

function LocalizationWrapper() {
  return (
    <IntlProvider locale={locale} messages={messages}>
      <App />
    </IntlProvider>
  );
}
export default LocalizationWrapper;

function App(props) {
  return <div>My App</div>;
}

Detect user's preferred language

By detecting the user’s preferred language (the language of the browser), we are going to set the initial locale of the app. If the user’s preferred language is not supported, the app will use the default, English. The locale resolution logic presented here is basic. In practice, a more comprehensive check would likely be implemented, possibly including region control as well.

The App.jsx file:

import React from "react";
import { IntlProvider } from "react-intl";
import messagesAr from "./lang/ar.json";
import messagesEn from "./lang/en.json";
import messagesEsMx from "./lang/es-MX.json";

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 messagesAr;
    case "en":
      return messagesEn;
    case "es-MX":
      return messagesEsMx;
    default:
      return messagesEn;
  }
}

function LocalizationWrapper() {
  return (
    <IntlProvider locale={initLocale} messages={loadMessages(initLocale)}>
      <App />
    </IntlProvider>
  );
}
export default LocalizationWrapper;

function App(props) { ... }

Load translations dynamically

Using dynamic imports, we are loading localization messages only when they are needed. Moreover, this approach improves app performance since only the necessary data is loaded. The example below contains a slightly updated loadMessages function, which returns localization messages as a Promise for the specified locale.

The App.jsx file:

import React, { useState, useEffect } from "react";
import { IntlProvider } 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 LocalizationWrapper() {
  const [locale, setLocale] = useState(initLocale);
  const [messages, setMessages] = useState(null);

  useEffect(() => {
    loadMessages(locale).then((data) => setMessages(data.default));
  }, [locale]);

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

function App(props) { ... }

Set text direction

Since one of the added languages, Arabic, uses a non-default text direction (rtl), explicit handling of this is needed. For that reason, we will add the getDirection function, which will return the appropriate direction for the given locale. 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 to adjust the content of the entire HTML page. However, for simplicity, we will not address that in this post.

The App.jsx file:

import React, { useState, useEffect } from "react";
import { IntlProvider } 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((data) => setMessages(data.default));
  }, [locale]);

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

function App({ direction }) {
  return <div dir={direction}>My App</div>;
}

Add language switcher

While automatically setting the app's initial language based on the browser's settings is effective in most scenarios, offering a manual language selection option is also a good practice. To achieve this, we will extend the LocalizationWrapper component to include the currently selected locale. This will allow us to update the whole app on a language change. We will also add a basic language switcher within the App component, and pass the necessary props to it to ensure seamless functionality.

The App.jsx file:

import React, { useState, useEffect } from "react";
import { IntlProvider } from "react-intl";

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

function loadMessages(locale) { ... }

function getDirection(locale) { ... }

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

  useEffect(() => {
    loadMessages(locale).then((data) => setMessages(data.default));
  }, [locale]);

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

function App({ locale, direction, onLocaleChange }) {
  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}>My App</div>
    </div>
  );
}

Add localization messages

The final step in our app is to implement the localization messages we've prepared, and observe how our app functions. With the help of the useIntl hook and the Formatted* components from the react-intl library, we are able to access these localization messages. The posted example demonstrates two different methods of access: an imperative and a declarative approach. Here, we will only demonstrate how to use these two methods to access the localization messages, with more details about them to be provided in the following section.

The App.jsx file:

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

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

function loadMessages(locale) { ... }

function getDirection(locale) { ... }

function LocalizationWrapper() { ... }
export default LocalizationWrapper;

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. Here are some guidelines:

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

Below are some examples of the most common formatting options.

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. Various factors such as shifting priorities, divergent planning, and user requirements contribute to this. It's important to note, however, that extracting localization messages via React Intl is feasible using the @formatjs/cli package. While this subject is distinct and won't be covered in this post, it's beneficial to be aware of it.

Conclusion

This post provided an overview of the react-intl library, illustrating its application in React projects. We delved into the two predominant methods for developing React applications, emphasizing the integration of internationalization. It's crucial to understand that the process begins with enabling the app for multilingual support, followed by the translation of all messages into your chosen languages. Typically, this involves teaming up with translators, assessing the quality of translations, implementing over-the-air translation updates through an S3 bucket, and other related activities. Fortunately, the Localizely platform is designed to facilitate these tasks. We invite you to try its free version and explore how it can boost your efficiency.

Try it for free.

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.

Related

Angular internationalization (i18n) tutorial
November 21, 2023
In “Coding
Flutter localization: step-by-step
November 09, 2023
In “Coding
Next.js internationalization (i18n) tutorial
November 07, 2023
In “Coding
Copyrights 2024 © Localizely