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.
First things first, let’s create a new React app with Create React App.
npx create-react-app react-intl-example
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
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}"
}
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.
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.
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.
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.
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.
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 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>
);
}
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:
title
, aria-label
)Use the declarative approach for:
Imperative usage example:
intl.formatMessage({ id: "common.aria-label.confirm" })
Declarative usage example:
<FormattedMessage id="page.home.greeting" />
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} />
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.
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 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.
Subscribe to the Localizely blog newsletter for quality product content in your inbox.
Related