Internationalization of React apps
In this tutorial, we'll learn how to add internationalization (i18n) to an existing React JS application.
Let's Start
We're going to translate the following app:
import React from "react";
import { render } from "react-dom";
import Inbox from "./Inbox";
const App = () => <Inbox />;
render(<App />, document.getElementById("root"));
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>
);
}
As you can see, it's a simple mailbox application with only one page.
Installing LinguiJS
Follow the React projects setup guide.
Setup
We will directly start translating the Inbox
component, but we need to complete one more step to setup our application.
Components need to read information about current language and message catalogs from i18n
instance. Initially, you can use the one created and exported from @lingui/core
and later you can replace with your one if such need arises.
Lingui uses the I18nProvider
to pass the instance i18n
to your React components.
Let's add all required imports and wrap our app inside I18nProvider
:
import React from "react";
import { render } from "react-dom";
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>
);
render(<App />, document.getElementById("root"));
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.
Let's deal with language switching later... but if you're still curious, take a look at example with Redux and Webpack.
Introducing internationalization
Now we're finally going to translate our app. Actually, we aren't 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 and you should practice saying this word aloud until you're able to say it three times very quickly.
From now on, internationalization will be shortened to a common numeronym i18n.
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>
All we need to make this heading translatable is wrap it in Trans
macro:
import { Trans } from "@lingui/react/macro";
<h1>
<Trans>Message Inbox</Trans>
</h1>;
Macros vs. Components
If you're wondering what macros are and what's the difference between macros and components, this short paragraph is for you.
In general, macros are executed at compile time and they transform source code in some way. We use this feature in LinguiJS to simplify writing messages.
Under the hood, all JSX macros are transformed into Trans
component. Take a look at this short example. This is what we write:
import { Trans } from "@lingui/react/macro";
<Trans>Hello {name}</Trans>;
And this is how the code is transformed:
import { Trans } from "@lingui/react";
<Trans id="OVaF9k" message="Hello {name}" values={{ name }} />;
See the difference? Trans
component receives 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. Let's enter command line for a while.
We're going to use CLI again. Run extract
command to extract messages:
> lingui extract
Lingui was unable to find a config!
Create 'lingui.config.js' file with LinguiJS configuration in root of your project (next to package.json). See https://lingui.dev/ref/conf
We need to create the lingui.config.js
file:
/** @type {import('@lingui/conf').LinguiConfig} */
const config = {
locales: ["cs", "en"],
catalogs: [
{
path: "<rootDir>/src/locales/{locale}/messages",
include: ["src"],
},
],
compileNamespace: "es",
};
export default config;
After adding the configuration file, let's run extract
command again:
> lingui extract
Catalog statistics:
┌──────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├──────────┼─────────────┼─────────┤
│ cs │ 1 │ 1 │
│ en │ 1 │ 1 │