React App Internationalization With LinguiJS | by Artem Diashkin | Mar, 2022

We will use a simple TypeScript CRA application without any additional dependencies and custom configurations:

yarn create react-app my-app --template typescript

Add @lingui/cli, @lingui/macro and @lingui/react packages to your project:

yarn add @lingui/cli @lingui/macro -D
yarn add @lingui/react

At this point, I will have these versions in my package.json file:

"dependencies": {
"@lingui/react": "^3.13.2",
...
},
"devDependencies": {
"@lingui/cli": "^3.13.2",
"@lingui/macro": "^3.13.2",
}

yarn.lock file dependencies:

"@lingui/react@^3.13.2":
version "3.13.2"
...
"@lingui/cli@^3.13.2":
version "3.13.2"
...
"@lingui/macro@^3.13.2":
version "3.13.2"

Step 1. Lingui config file

Create lingui.config.js file with content:

⚠️ NOTE: Each locale should be a valid BCP-47 code, like en for English and fr for French.

Configuration is read from 3 different sources (the first found wins):

  • from lingui section in package.json
  • from .linguirc
  • from lingui.config.js
  • from lingui.config.ts _(since 3.4.0)

In the case of TypeScript based configuration you can use ESM format and export default:

You can check more about configuration by this link:

This configuration will extract messages from source files inside src directory and write them into message catalogs in src/locales (English catalog would be in eg: src/locales/en/messages.po). Finally, PO format is recommended. See format documentation for other available formats.

Step 2: Extract and Compile

Add extract and compile scripts to your package.json file:

"scripts": {
"extract": "lingui extract",
"compile": "lingui compile"
},

Note: extract will parse app for messages and create/update .po files, while compile will convert PO message to JS object:

We will cover more about it later.

You can check more about extract and compile script by this :

Step 3: Code update

Let’s add a Context that will add I18nProvider from @lingui/react package and will handle locale state change in our app:

Content:

Note: We added plurals as well, we will cover that in the Plurals section.

After that, wrap index.tsx file with our created <I18nProvider/>:

Update App.tsx file with a <Trans/> component, useI18nContext hook, and <select> tag that will help us change locales:

Updated src/App.tsx component :

🟢 Run “extract”:

yarn extract

Result of the “extract” script and those PO files content:

Update French /locales/fr/messages.po file translations:

msgid "Edit <0>src/App.tsx</0> and save to reload."
msgstr "Modifiez <0>src/App.tsx</0> et enregistrez pour recharger."

msgid "Learn React"
msgstr "Apprendre React"

Update package.json file, now this command with compile your translation on each start:

"scripts": {
"start": "yarn compile && react-scripts start",
"extract": "lingui extract",
"compile": "lingui compile"
},

🟢 Let’s test our updates:

yarn start

Results

Open browser and check results:

So what will be if we got a huge text component:

Run extract and compile:

message.js after compile:

As you can see object key for LoremIpsum text is huge (duplicates full-text content). To avoid it you can pass an id value to the <Trans/> component:

Add id prop with lorem-ipsum value:

Run “extract”:

As you can see old values ​​remains, we can fix it by adding --clean flag to the “extract” script that will:

Remove obsolete messages from catalogs. Message becomes obsolete when it’s missing in the source code.

"scripts": {

"extract": "lingui extract --clean",
...
},

🟢 Run “extract” script again:

yarn extract

Excellent, now we’re talking. Now our JS object key value will be small and readable after yarn compile command.

Note: LinguiJS will use msgid value if you will not provide translations so be careful with --clean flag. We will cover a way how you can manage it in the next section.

Let’s take a deep dive into the Macros world.

The advantages of using macros are:

  • You don’t need to learn ICU MessageFormat syntax. You always use familiar JS and JSX code;
  • Components and functions are type-checked;
  • Additional validation of plural rules is performed during transformation;
  • Non-essentials data are removed from production build (eg comments and default messages) to shave a few bytes;

👉 Here are other <Trans/> props and its useful features instead of the already mentioned id:

  • comment: very useful for translators, provides meaning or a text context;
  • render: uses render props pattern, with it you can add a wrapper for your translation — very useful for adding additional logic or analytics events;
  • component: same as render prop, but it will not pass id, message or transation props (TransRenderProps type from @lingui/react library);

Let’s look at the above with an example:

.po file result:

#. This message in an example without any context
#: src/components/TransPropsExample.tsx:35
msgid "no-sense-message"
msgstr "A message that makes no sense. Value: {priceForProduct}"

Update App.tsx file:

🟢 Run extract script and restart the app:

yarn extract && yarn start

Results (English locale):

Results (French locale):

⚠️ NOTE: Try not to use anonymous values ​​in Trans:

<Trans>
A message that makes no sense. Value: {getSomeValue()}
</
Trans>
// .po file result
msgid "A message that makes no sense. Value: {0}"

Try to declare a variable with some meaning that translators will understand:

const priceForProduct = getSomeValue();<Trans>
A message that makes no sense. Value: {priceForProduct}
</
Trans>
// .po file result
msgid "A message that makes no sense. Value: {priceForProduct}"

These macros can be used in any context (eg outside JSX). All JS macros are transformed into a Message Descriptor wrapped inside of i18n._ call.

import { t } from "@lingui/macro"
const message = t({
id: 'msg.hello',
comment: 'Greetings at the homepage',
message: `Hello ${name}`
})

// ↓ ↓ ↓ ↓ ↓ ↓

import { i18n } from "@lingui/core"
const message = i18n._(/*i18n*/{
id: 'msg.hello',
comment: 'Greetings at the homepage',
message: 'Hello {name}',
values: { name }
})

Example:

This will lead to the exact same result as before but without wrappers, checkers, and additional JSX features, but JS macros are very useful for simple cases like an array of strings.

Plurals are essential when dealing with internationalization. LinguiJS uses CLDR Plural Rules. In general, there are 6 plural forms (taken from CLDR Plurals page):

  • zero;
  • one (singular);
  • two (dual);
  • few (paucal);
  • many (also used for fractions if they have a separate class);
  • other (required — general plural form — also used if the language only has a single form).

Only the last one, the otheris required because it’s the only common plural form used in all languages.

JSX Example:

Run extract and run app. Result diff locales:

Note: Most of the time one and other values ​​are enough but be careful, you need to check what Language Plural Rules are used for your locales (en and fr in our case, so we can easily exclude zero and few from our example as redundant).

JS Macros example with the same result:

We will use @lingui/detect-locale package with some helper functions that will help you detect the locale of the user:

  • fromCookie(key: string) – Accepts a key as param will recover from navigator cookies the value
  • fromHtmlTag(tag: string) – Will find on HtmlDocument the attribute passed in params (normally it’s used lang or xml:lang)
  • fromNavigator() – Recovers the navigator language, it’s also compatible with old browsers like IE11
  • fromPath(localePathIndex: number) – Splits the location.pathname in an array so you have to specify the index of the array where’s locale is set
  • fromStorage(key: string, { useSessionStorage: boolean } – Will search on localStorage by default the item that has that key, if **useSessionStorage** is passed, will search on sessionStorage
  • fromSubdomain(localeSubdomainIndex: number) – Like fromPath, splits the location.href on segments you must specify the index of that segment
  • fromUrl(parameter: string) – Uses a query-string parser to recover the correct parameter

@lingui/detect-locale exports methods:

  • detect – Will return the first occurrence of detectors;
  • multipleDetect – Will return an array with all the locales detected by each detector

🟢 Add package:

yarn add @lingui/detect-locale

We will update our <I18nProvider/> component with detect function only. This way we will get a meaningful result:

const result = {
url,
storage,
navigator,
cookie,
tag,
fromPath,
fromSubdomain,
}

Plus, we changed simple setLocale to handleChangeLocale → now locale will be saved to the localStorage and cookies on locale change and will be retrieved on app refresh.

Result: http://localhost:3000/fr/?lang=jp

Happy coding! And don’t hesitate to ask questions!

Link to the GitHub repository:

Leave a Comment