Internationalization (i18n) prepares your apps for a global audience in today’s interconnected world. If you’re a JavaScript or React developer looking for a lightweight, powerful i18n tool, you might like LinguiJS.
What is Lingui?
Lingui is an i18n framework for JavaScript, React.js, Vue, Next.js, Node.js, Angular, Svelte, and others. Its simple syntax makes it easy to get started with, and should be familiar for users of other React i18n libraries. Lingui uses the popular ICU MessageFormat for representing translations, and it uses the PO (gettext) and JSON formats for storing translations.
Lingui has a CLI tool with helpful commands for extracting messages for translation, tracking the number of translated messages, and compiling messages.
To demonstrate Lingui’s capabilities, we’ll localize the Statistics page of a fictional game. You can find the complete source code on GitHub.
Let’s install Lingui in the app.
Installing Lingui for React
I built the sample app with the Create React App starter template. If you want to install Lingui on any other React project that doesn’t use Create React App, follow this guide on Lingui’s website.
Set up LinguiJS with the following steps:
First, install @lingui/cli
, @lingui/macro
, and @lingui/react
:
Then, create the LinguiJS configuration file .linguirc
in the root folder of your project with this content:
We’ll use English and French as the locales in this project. English is the source locale, so we’ll translate from English to French.
The path
field tells Lingui where to store message catalogs for each locale. It will store the message catalogs for French in src/locales/fr/messages
and the message catalogs for English in src/locales/en/messages
.
Finally, we’ll use the PO format to store our translations. Add these command line scripts to package.json
:
These commands form an important part of Lingui’s internationalization and localization workflow. They’re responsible for extracting messages from the source code and compiling them so they’re ready for use in the app. Let’s start with a simple translation to see how they work.
Translating Simple Messages
Before we start translating, we’ll have to import the libraries we need in the index.js
file:
The i18n
object contains the methods we’ll use for managing locales, message catalogs and formatting dates. The I18nProvider
takes the i18n
object as a prop which will allow <App/>
’s child components to access to the i18n
instance.
Now for the actual translation, we’ll translate the words “Quiz” and “Statistics” at the top of the page. To make the strings translatable, we’ll have to wrap them in a Trans
macro:
The <Trans>
macro transforms the messages into components compatible with the ICU MessageFormat syntax, and it makes them messages available to the CLI tool for extraction. We’ll handle that in the next section.
Extracting Messages for Translation
The extract
command will check our code for messages wrapped in <Trans>
and put them in the message catalogs so we can translate them.
Open your CLI and run this command:
You should see this printed to the screen:
After extracting the messages to the catalog, the extract
command displays a table showing how many messages each locale has and how many messages we haven’t translated. The table shows there are two translations missing for the French locale and none for English. Since English is the source locale, there’s nothing to translate, and that’s why there’s a dash (-) in the “Missing” column.
Let’s add the missing translations to the message catalog.
Open src/locales/fr/messages.po
and you should see the messages at the bottom of the file.
You should see the line numbers of the messages we extracted, and the message ids of the content <Trans>
wrapped. We’ll add the translations for each message to the msgstr
field:
Next, we’ll have to compile the message catalogs to runtime catalogs. These runtime catalogs are minified .js files that allow us to use the messages in the app.
Open your CLI and run this command:
You should now see a messages.js
file in each locale’s folder.
We can now import the compiled messages into the app and load them:
Great! We’ve imported the messages and set English as the current locale. Change the locale to French to see the translations:
Setting up a message for translation in Lingui may seem like a lot, but it’s actually a 4-step process:
- Wrap the message you want to translate in a
<Trans>
macro. - Extract the message to a translation file
- Translate the message
- Compile the translated message to a runtime catalog
Let’s translate another simple message: the “High score” message in the Card
component:
Run extract
, and take a look at the French message catalog:
Lingui allows us to wrap variables and expressions in <Trans>
so we can easily use them in our translations. Add the translation in front of the {highScore}
variable:
Compile the message and see how it looks.
One more thing before we move on: We can give our messages a custom id with the id prop for <Trans>
.
For example: When we compile <Trans id="header.text">Statistics</Trans>
, we’ll get this in the catalog:
That’s all for simple messages, let’s look at plurals next.
Working with Plurals
Looking at the cards in the app, we can see “5 levels played” for the first player and “1 levels played” for the second player. They appear this way because we’re not using any logic to handle plural forms. Of course, we could use conditional logic to solve this problem, but it won’t work well across different locales. Let’s see how Lingui handles this issue.
In Card.js
, we’ll import the Plural macro:
The <Plural>
macro takes a value and plural forms as props so it can render the correct plurals based on the value and locale. ”#” is a placeholder for levelsPlayed
, so the code above will render “1 level played” when levelsPlayed
equals 1. Numbers greater than 1 will take the form of the other
prop, so when levelsPlayed
equals 2, <Plural>
will render “2 levels played”.
The <Plural>
component also accepts exact forms so you can render specific messages for any number you want. For example, the _0
prop above will make <Plural>
render “No levels played” when levelsPlayed
equals 0.
Back to our code: Run extract
, then open the French message catalog.
The code here is similar to what we’ve seen so far. Since we’re dealing with plurals, we’ll have to fill in the translation for each plural form. Copy over the content of msgid
to msgstr
and replace the English messages with the French translations:
Compile the messages and the plurals should be working fine for both locales.
Formatting Dates
I used the new Date()
constructor to store the dates each player last played the game. The string new Date()
returns is quite long though, so a little formatting will make it look better.
We’ll use the i18n.date()
method to format the dates. To access the i18n.date()
method, we’ll have to import the useLingui()
hook, which allows children of the root component to access the context we passed to <I18nProvider>
:
The i18n.date()
object takes two arguments: the message to be translated, and options for formatting. i18n.date()
uses the Intl object under the hood, so the options parameter uses the same values that can be passed to the Intl.DateTimeFormat()
method. You can check out all the date formatting options on MDN.
Run extract and open the French message catalog:
{0}
is a placeholder for lastPlayed
. How come we don’t see {lastPlayed}
itself here instead? It’s because only simple values will appear with their names when compiled. Since we called i18n.date()
on lastPlayed
, it’s no longer a simple variable; that’s why Lingui compiled it to the positional argument, {0}
.
Since Lingui has done most of the work for us, we only need to translate the text before the date:
Compile and voila! You should see the date properly formatted for both locales.
Creating a Locale Switcher
After translating messages in your app, the last step is to give users the option to change the language. We’re also going to make this locale switcher load messages dynamically. We only use one locale at a time so there’s no need to load more than one when our app starts. Let’s add that functionality first.
Adding Dynamic Message Loading
We’ll start by creating a new file to handle the loading logic.
Create localeLoader.js
in the root of the /src
folder and fill it with this code:
Since we’re handling all the loading in this file, we can safely remove the code from index.js
.
We’ll use the useEffect
hook to load the default locale from the loader:
Check out the app, and it should still work as before.
Adding the locale switcher
Let’s create a function to handle locale change and a state variable to hold the current locale:
Define the props in App.js
and create a select box with the locales as options:
And finally, pass the current locale and the handler function to <App>
:
Done! You can now change locales and the app will load the messages dynamically.
Conclusion
We’ve come to the end of this tutorial. Through it, we’ve seen how Lingui’s syntax makes it straightforward to localize your app. With the CLI commands, developers have a simple workflow that allows them to extract messages and keep track of translations.
If you want to learn more about Lingui, here are a few resources you’ll find helpful:
- Lingui: Official Tutorial for React
- Lingui Macros API reference
- Lingui Core API Reference
- Lingui React API Reference
- Lingui usage examples
You can also check out Lingui’s official repository on GitHub.