Skip to main content

React Apps Internationalization

In this tutorial, we'll learn how to add internationalization (i18n) to an existing React JS application. We'll focus on the most common patterns and best practices for using Lingui in React.

Example

If you're looking for a working solution, check out the Examples page. It contains several sample projects with the complete setup using Lingui and React.

It includes examples for Create React App, React with Vite and Babel, React with Vite and SWC, and more.

Installing Lingui

  1. Follow the Installation and Setup page for initial setup.
  2. Install the @lingui/core and @lingui/react packages.

Example Component

We're going to translate the following one-page mailbox application:

src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import Inbox from "./Inbox";

const App = () => <Inbox />;

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
src/Inbox.js
import React from "react";

export default function Inbox() {
const messages = [{}, {}];
const messagesCount = messages.length;
const lastLogin = new Date();
const markAsRead = () => {
alert("Marked as read.");
};

return (
<div>
<h1>Message Inbox</h1>

<p>
See all <a href="/unread">unread messages</a>
{" or "}
<a onClick={markAsRead}>mark them</a> as read.
</p>

<p>
{messagesCount === 1
? `There's ${messagesCount} message in your inbox.`
: `There are ${messagesCount} messages in your inbox.`}
</p>

<footer>Last login on {lastLogin.toLocaleDateString()}.</footer>
</div>
);
}

This application is a simple mailbox with a header, a paragraph with a link and a button, another paragraph with a message count, and a footer with the last login date. We will use it as the basis for our tutorial.

Setup

We will start translating the Inbox component right away, but we need to do one more step to set up our application.

Components need to read information about current language and message catalogs from the i18n instance. Lingui uses the I18nProvider to pass the i18n instance to your React components.

Let's add all required imports and wrap our app inside I18nProvider:

src/index.js
import React from "react";
import ReactDOM from "react-dom/client";

import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { messages } from "./locales/en/messages";
import Inbox from "./Inbox";

i18n.load("en", messages);
i18n.activate("en");

const App = () => (
<I18nProvider i18n={i18n}>
<Inbox />
</I18nProvider>
);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
info

You might be wondering: how are we going to change the active language? That's what the I18n.load and i18n.activate calls are for! However, we cannot change the language unless we have the translated message catalog. And to get the catalog, we first need to extract all messages from the source code.

Introducing Internationalization

Now we're finally going to translate our application. Actually, we're not going to translate from one language to another right now. Instead, we're going to prepare our app for translation. This process is called internationalization.

Let's start with the basics - static messages. These messages don't have any variables, HTML or components inside. Just some text:

<h1>Message Inbox</h1>

To make this heading translatable, simply wrap it in the Trans macro:

import { Trans } from "@lingui/react/macro";

<h1>
<Trans>Message Inbox</Trans>
</h1>;

Using JSX Macros is the easiest way to translate your React components. It handles translations of messages, including variables and other React components.

Macros vs. Components

If you're wondering what Macros are and the difference between macros and runtime components, here's a quick explanation.

In general, macros are executed at compile time and serve to transform the source code to make the message writing process easier. Under the hood, all JSX macros are transformed into the runtime component Trans (imported from @lingui/react).

Below is a brief example demonstrating this transformation:

import { Trans } from "@lingui/react/macro";

<Trans>Hello {name}</Trans>;

// ↓ ↓ ↓ ↓ ↓ ↓

import { Trans } from "@lingui/react";

<Trans id="OVaF9k" message="Hello {name}" values={{ name }} />;

As you can see, the Trans runtime component gets id and message props with a message in ICU MessageFormat syntax. We could write it manually, but it's just easier and shorter to write JSX as we're used to and let macros generate the message for us.

Bundle Size Impact

Another advantage of using macros is that all non-essential properties are excluded from the production build. This results in a significant reduction in the size footprint for internationalization:

// NODE_ENV=production
import { Trans } from "@lingui/react";

<Trans id="OVaF9k" values={{ name }} />;

Extracting Messages

Back to our project. It's nice to use JSX and let macros generate messages under the hood. Let's check that it actually works correctly.

All messages from the source code must be extracted into external message catalogs. Message catalogs are interchange files between developers and translators. We're going to have one file per language.

info

Refer to the Message Extraction guide for more information about various message extraction concepts and strategies.

Let's switch to the command line for a moment. Execute the extract CLI command. If everything is set up correctly, you should see the extracted message statistics in the output:

> lingui extract

Catalog statistics:
┌──────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├──────────┼─────────────┼─────────┤
│ cs │ 11
│ en │ 11
└──────────┴─────────────┴─────────┘

As a result, we have two new files in the locales directory: en/messages.po and cs/messages.po. These files contain extracted messages from the source code.

Let's take a look at the Czech message catalog:

src/locales/cs/messages.po
msgid ""
msgstr ""
"POT-Creation-Date: 2021-07-22 21:44+0900\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: cs\n"

#: src/Inbox.js:12
msgid "Message Inbox"
msgstr ""

It contains the message we wrapped in the Trans macro. Let's add the Czech translation:

src/locales/cs/messages.po
#: src/Inbox.js:12
msgid "Message Inbox"
msgstr "Příchozí zprávy"

If we run the extract command again, we'll see that all the Czech messages have been translated:

> lingui extract

Catalog statistics:
┌──────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├──────────┼─────────────┼─────────┤
│ cs │ 10
│ en │ 11
└──────────┴─────────────┴─────────┘

That's great! So how do we load it into your application? Lingui introduces the concept of compiled message catalogs. Before we load messages into our application, we need to compile them.

Use the compile command to do this:

> lingui compile

Compiling message catalogs…
Done!

If you look inside the locales/<locale> directory, you'll see that there is a new file for each locale: messages.js. This file contains the compiled message catalog.

tip

If you use TypeScript, you can add the --typescript flag to the compile command to produce compiled message catalogs with TypeScript types.

Let's load this file into our app and set active language to cs:

src/index.js
import React from "react";
import ReactDOM from "react-dom/client";

import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { messages as enMessages } from "./locales/en/messages";
import { messages as csMessages } from "./locales/cs/messages";
import Inbox from "./Inbox";

i18n.load({
en: enMessages,
cs: csMessages,
});
i18n.activate("cs");

const App = () => (
<I18nProvider i18n={i18n}>
<Inbox />
</I18nProvider>
);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

When we run the app, we see the inbox header is translated into Czech.

tip

Alternatively, you can load catalogs dynamically using the @lingui/loader or @lingui/vite-plugin without the need to import compiled messages manually.

Summary of Basic Workflow

Let's go through the workflow again:

  1. Add an I18nProvider, this component provides the active language and catalog(s) to other components.
  2. Wrap messages in the Trans macro.
  3. Run extract command to generate message catalogs.
  4. Translate message catalogs (send them to translators usually).
  5. Run compile to create runtime catalogs.
  6. Load runtime catalog.
  7. Profit! 🎉

It's not necessary to extract/translate messages one by one. This is usually done in batches. When you finish your work or PR, run extract to generate the latest message catalogs, and before building the application for production, run compile.

Formatting

Let's move on to another paragraph in our project. The following paragraph has some variables, some HTML and components inside:

<p>
See all <a href="/unread">unread messages</a>
{" or "}
<a onClick={markAsRead}>mark them</a> as read.
</p>

Although it looks complex, there's really nothing special here. Just wrap the content of the paragraph in Trans and let the macro do the magic:

<p>
<Trans>
See all <a href="/unread">unread messages</a>
{" or "}
<a onClick={markAsRead}>mark them</a> as read.
</Trans>
</p>

Let's see how this message actually looks in the message catalog. Run the extract command and take a look at the message:

#: src/Inbox.js:20
msgid "See all <0>unread messages</0> or <1>mark them</1> as read."
msgstr ""

You may notice that components and html tags are replaced with indexed tags (<0>, <1>). This is a little extension to the ICU MessageFormat which allows rich-text formatting inside translations. Components and their props remain in the source code and don't scare our translators. Also, in case we change a className, we don't need to update our message catalogs.

JSX to MessageFormat Transformations

At first glance, these transformations might seem somewhat unconventional; however, they are straightforward, intuitive, and align well with React principles. There is no need to focus on MessageFormat, as the library handles its creation for us. We can write our components as we typically would and simply wrap the text in the Trans macro.

Let's see some examples with MessageFormat equivalents:

<Trans>Hello {name}</Trans>
// Hello {name}

Any expressions are allowed, not just simple variables. The only difference is, only the variable name will be included in the extracted message:

  • Any expression -> positional argument:

    <Trans>Hello {user.name}</Trans>
    // Hello {0}
  • Object, arrays, function calls -> positional argument:

    <Trans>The random number is {Math.rand()}</Trans>
    // The random number is {0}
  • Components might get tricky, but like we saw, it's really easy:

    <Trans>
    Read <a href="/more">more</a>.
    </Trans>
    // Read <0>more</0>.
    <Trans>
    Dear Watson,
    <br />
    it's not exactly what I had in my mind.
    </Trans>
    // Dear Watson,<0/>it's not exactly what I had in my mind.
caution

Try to keep your messages simple and avoid complex expressions. During extraction, these expressions will be replaced by placeholders, resulting in a lack of context for translators. There is also a special rule in Lingui ESLint Plugin to catch these cases: no-expression-in-message.

Dates and Numbers

Take a look at the message in the footer of our component. It is a bit special because it contains a date:

<footer>Last login on {lastLogin.toLocaleDateString()}.</footer>

Dates (as well as numbers) are formatted differently in different languages, but we don't have to do this manually. The heavy lifting is done by the Intl object, we'll just use the i18n.date() function.

The i18n object can be accessed with the useLingui hook:

src/Inbox.js
import { useLingui } from "@lingui/react";

export default function Inbox() {
const { i18n } = useLingui();

return (
<div>
<footer>
<Trans>Last login on {i18n.date(lastLogin)}.</Trans>
</footer>
</div>
);
}

This will format the date using the conventional format for the active language. To format numbers, use the i18n.number() function.

Message ID

At this point, we'll explain what the message ID is and how to set it manually. Translators work with message catalogs. No matter what format we use, it's just a mapping of a message ID to the translation.

Here's an example of a simple message catalog in Czech language:

Message IDTranslation
MondayPondělí
TuesdayÚterý
WednesdayStředa

... and the same catalog in French language:

Message IDTranslation
MondayLundi
TuesdayMardi
WednesdayMercredi

The message ID is what all catalogs have in common – "Lundi" and "Pondělí" represent the same message in different languages.

There are two approaches for creating a message ID:

  • Automatically generated from message (e.g. Monday) and context, if available.
  • Explicit message ID set by the developer (e.g. days.monday).
info

Refer to the Explicit vs Generated IDs guide for more information about the pros and cons of each approach.

Plurals

Let's take a closer look at the following code in our component:

<p>
{messagesCount === 1
? `There's ${messagesCount} message in your inbox.`
: `There are ${messagesCount} messages in your inbox.`}
</p>

This message is a bit special, because it depends on the value of the messagesCount variable. Most languages use different forms of words when describing quantities - this is called pluralization.

What's tricky is that different languages use different number of plural forms. For example, English has only two forms - singular and plural - as we can see in the example above. However, Czech language has three plural forms. Some languages have up to 6 plural forms and some don't have plurals at all!

info

Lingui uses Intl.PluralRules which is supported in every modern browser and can be polyfilled for older. So you don't need to setup anything special.

English Plural Rules

How do we know which plural form we should use? It's very simple: we, as developers, only need to know plural forms of the language we use in our source. Our component is written in English, so looking at English plural rules we'll need just two forms:

one

Singular form

other

Plural form

We don't need to select these forms manually. We'll use Plural component, which takes a value prop and based on the active language, selects the right plural form:

import { Plural } from "@lingui/react/macro";

<p>
<Plural value={messagesCount} one="There's # message in your inbox" other="There are # messages in your inbox" />
</p>;

This component will render There's 1 message in your inbox when messageCount = 1 and There are # messages in your inbox for any other values of messageCount. # is a placeholder, which is replaced with value.

Let's run the extract command to see the extracted message:

{messagesCount, plural,
one {There's # message in your inbox}
other {There are # messages in your inbox}
}

In the catalog, you'll see the message in a single line. Here we have wrapped it to make it more readable.

Beware of Zeroes!

Just a short detour, because it's a common misunderstanding. You may wonder why the following code doesn't work as expected:

<p>
<Plural
value={messagesCount}
zero="There are no messages"
one="There's # message in your inbox"
other="There are # messages in your inbox"
/>
</p>

This component will render There are 0 messages in your inbox for messagesCount = 0. Why so? Because English doesn't have zero plural form. Looking at English plural rules, it's:

NForm
0other
1one
nother (anything else)

However, decimal numbers (even 1.0) always use the other form:

There are 0.0 messages in your inbox.

Exact Forms

Going back to our example, what if we specifically want to display There are no messages when messagesCount = 0? This is where exact forms come in handy:

<p>
<Plural
value={messagesCount}
_0="There are no messages"
one="There's # message in your inbox"
other="There are # messages in your inbox"
/>
</p>
tip

MessageFormat allows exact forms, like =0. However, React props can't start with = and can't be numbers either, so we need to write _N instead of =0.

It works with any number, allowing for extensive customization as follows:

<p>
<Plural
value={messagesCount}
_0="There are no messages"
_1="There's one message in your inbox"
_2="There are two messages in your inbox, that's not much!"
other="There are # messages in your inbox"
/>
</p>

Exact matches always take precedence over plural forms.

Variables and Components

Let's go back to our original pluralized message:

<p>
<Plural value={messagesCount} one="There's # message in your inbox" other="There are # messages in your inbox" />
</p>

To include variables or components within messages, simply wrap them in the Trans macro or use template literals (for example, with a variable name):

<p>
<Plural
value={messagesCount}
one={`There's # message in your inbox, ${name}`}
other={
<Trans>
There are <strong>#</strong> messages in your inbox, {name}
</Trans>
}
/>
</p>

Nested macros, components, variables, and expressions are all supported, providing the flexibility needed for any use case.

Internationalization Outside of React

So far, we have learned how to translate strings within a JSX element. However, what if we need to translate content that is outside JSX or pass a translation as a prop to another component?

In our example, we have the following code:

const markAsRead = () => {
alert("Marked as read.");
};

To translate it, we will use the useLingui macro hook:

import { useLingui } from "@lingui/react/macro";

const { t } = useLingui();

const markAsRead = () => {
alert(t`Marked as read.`);
};

Now the Marked as read. message would be picked up by the extractor, and available for translation in the catalog.

You could also pass variables and use any other macro in the message:

const { t } = useLingui();

const markAsRead = () => {
const userName = "User1234";
alert(t`Hello ${userName}, your messages marked as read!`);
};
tip

You can also use this approach to translate element attributes, such as alt in an img tag:

import { useLingui } from "@lingui/react/macro";

export default function ImageWithCaption() {
const { t } = useLingui();

return <img src="..." alt={t`Image caption`} />;
}

Review

After all modifications, the final i18n-ready component looks like this:

src/Inbox.js
import React from "react";
import { Trans, Plural, useLingui } from "@lingui/react/macro";

export default function Inbox() {
const { i18n, t } = useLingui();
const messages = [{}, {}];
const messagesCount = messages.length;
const lastLogin = new Date();
const markAsRead = () => {
alert(t`Marked as read.`);
};

return (
<div>
<h1>
<Trans>Message Inbox</Trans>
</h1>

<p>
<Trans>
See all <a href="/unread">unread messages</a>
{" or "}
<a onClick={markAsRead}>mark them</a> as read.
</Trans>
</p>

<p>
<Plural
value={messagesCount}
one="There's # message in your inbox"
other="There are # messages in your inbox"
/>
</p>

<footer>
<Trans>Last login on {i18n.date(lastLogin)}.</Trans>
</footer>
</div>
);
}

That's it for this tutorial! For more details, see the reference documentation or check out additional tutorials. Happy Internationalizing!