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.
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.
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
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}"
}
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.
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>;
}
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) { ... }
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) { ... }
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>;
}
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>
);
}
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>
);
}
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:
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" />
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} />
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.
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.
All code samples used in this section are available on the GitHub repo.
See an online demo.
Beginning with the creation of a new React project seems like the natural first step. In this guide, we'll use Create React App, a tool that has long been the go-to for most developers to scaffold React projects.
To create a new project, run the following command:
npx create-react-app react-intl-example
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
Note: If installing the react-intl
fails, remove package-lock.json
and node_modules
and try again.
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
|-- src
| |-- lang
| |-- ar.json
| |-- en.json
| |-- es-MX.json
| |-- App.js
| |-- App.test.js
| |-- index.js
| |-- ...
|-- ...
|-- 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}"
}
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.
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.js
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>;
}
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.js
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) { ... }
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.js
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(setMessages);
}, [locale]);
return messages ? (
<IntlProvider locale={locale} messages={messages}>
<App />
</IntlProvider>
) : null;
}
export default LocalizationWrapper;
function App(props) { ... }
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.js
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(setMessages);
}, [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>;
}
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.js
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(setMessages);
}, [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>
);
}
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.js
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>
);
}
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:
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" />
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} />
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.
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.
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.
Step into the world of easy localization