Zoran Luledzija
Zoran Luledzija
September 04, 2024
23 min read
September 04, 2024
23 min read

React internationalization with react-intl 

React internationalization with react-intl

Reaching a global audience with your web application necessitates support for multiple languages. Fortunately, adding multilingual support is typically straightforward and offers numerous benefits, such as increased user engagement and enhanced accessibility. In this post, we'll delve into internationalizing (i18n) React web applications using the React Intl library, focusing on crucial aspects like message localization, formatting, language selection, and more.

The latest React documentation appears to favor the use of React-based frameworks such as Next.js, Remix, and Gatsby, when starting with React. However, in this post, our focus will be on the internationalization (i18n) of plain React applications developed using Vite and Create React App. For those interested in delving into internationalization within Next.js projects, we have a dedicated post on that topic.

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

Create a new React project 

Beginning with the creation of a new React project seems like the natural first step. In this guide, we'll use Vite, a build tool that has quickly become popular within the JavaScript community for its efficiency and speed.

To create a new project, run the following command:

npm create vite@latest -- --template react

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

Note: Executing the command above for the first time will require installing Vite as a prerequisite for project creation.

Add the 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 the ICU Message syntax, offers a wide range of formatting options, and is compatible with major browsers.

To incorporate the React Intl library into 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, respectively.

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've added with some data.

The ar.json file:

{
  "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}"
}

The en.json file:

{
  "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}"
}

The es-MX.json file:

{
  "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 is 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 entire 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 the 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 default to 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 load 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.

Note: If you need to manage a large number of languages on your website and are unsure about the text direction for each, consider using packages like rtl-detect for assistance.

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 considered good practice. To achieve this, we will extend the LocalizationWrapper component to include the currently selected locale. This will enable us to update the entire app upon a language change. Additionally, we will add a basic language switcher within the App component and pass the necessary props to it to ensure seamless functionality.

In case you would like to place a language switcher somewhere deeper in a tree of components, you can use React Context to avoid prop drilling.

Note: To enhance user experience, it is advisable to store the selected locale in a cookie or on the server side to ensure the proper language is automatically preselected on the next visit.

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 development process is to implement the localization messages we've earlier prepared and observe how our app functions afterward. With the help of the useIntl hook and the Formatted* components from the react-intl library, we can easily access these localization messages. The provided example demonstrates two different methods of access: an imperative and a declarative approach. In this section, we will demonstrate how to use these two methods to access the localization messages, with more details 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 either the imperative or 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).
  • Achieving better performance (e.g., rendering large tables of data).

Use the declarative approach for:

  • Seamlessly composing with other React components.
  • Supporting rich-text formatting.
  • Utilizing 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 beneficial to start with internationalization from the very beginning of 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 for React Intl is feasible using the @formatjs/cli package. Although this is a separate topic, being aware of it is beneficial. Generally, the process involves declaring messages in your React app, then using @formatjs/cli to extract these declared messages, translating them into other languages, and finally, integrating everything back together.

Conclusion 

In this post, we explain how to internationalize React applications using the React Intl library. We delve into the two predominant methods for developing plain React applications, using Vite and Create React App tools. Additionally, we explain how to adapt your React project for internationalization, organize your localization files, load translations dynamically, detect the user's preferred language, and much more.

However, adapting your app for multiple languages involves not just technical adjustments but also the translation of all localization messages. This can complicate your development workflow. Fortunately, the Localizely platform streamlines software localization. It simplifies collaboration among developers, translators, designers, and project managers, offering features like machine translation, translation memory, glossary, AWS S3 integration for over-the-air updates, among other features. It offers a free plan (no credit card needed) suitable for smaller projects and is completely free for open-source projects.

Try Localizely.

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

Next.js internationalization (i18n) tutorial
August 26, 2024
In “Coding
Flutter localization: step-by-step
August 20, 2024
In “Coding
Angular internationalization (i18n) tutorial
November 21, 2023
In “Coding
Navigating localization success with Localizely

Step into the world of easy localization

Copyrights 2024 © Localizely