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.
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
- Follow the Installation and Setup page for initial setup.
- Install the
@lingui/core
and@lingui/react
packages.
Example Component
We're going to translate the following one-page mailbox application:
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>
);
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
:
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>
);
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.
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.
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 │ 1 │ 1 │
│ en │ 1 │ 1 │
└──────────┴─────────────┴─────────┘
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:
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/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 │ 1 │ 0 │
│ en │ 1 │ 1 │
└──────────┴─────────────┴─────────┘
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.
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
:
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.
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:
- Add an
I18nProvider
, this component provides the active language and catalog(s) to other components. - Wrap messages in the
Trans
macro. - Run
extract
command to generate message catalogs. - Translate message catalogs (send them to translators usually).
- Run
compile
to create runtime catalogs. - Load runtime catalog.
- 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.
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:
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 ID | Translation |
---|---|
Monday | Pondělí |
Tuesday | Úterý |
Wednesday | Středa |
... and the same catalog in French language:
Message ID | Translation |
---|---|
Monday | Lundi |
Tuesday | Mardi |
Wednesday | Mercredi |
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
).
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!
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:
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:
N | Form |
---|---|
0 | other |
1 | one |
n | other (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>
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!`);
};
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:
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!