
Global business
Software localization
If you've been looking around for a good JavaScript / React i18n library, you might have come across some juggernauts like react-intl or i18next. These are popular, well established React and JavaScript i18n libraries with large communities. That's nothing to shake a stick at, but the maturity that these libraries offer often comes with bloat and overgeneralized configuration and syntax that can make them a bit messy to work with. Enter LinguiJS, a small, robust JS / React i18n library that feels lean and elegant compared to its competitors.
📖 Go Deeper » We've covered both react-intl and i18next in detail in our article Best Libraries for React I18n. Check out that article for deeper dive into these libraries with source code.
One of the most important pros of LinguiJS is its elegant and intuitive syntax. Compare a React component written with react-intl and (functionally) the same component written with Lingui.
function Home() { return ( <div className="Home"> <div className="Home-header"> <img src={logo} className="Home-logo" alt="logo"/> <h2><FormattedMessage id="home.welcome" values={{name: 'React.js'}}/></h2> </div> <div className="Home-container"> <h3 className="focus"><FormattedMessage id="home.declarative"/></h3> <div> <p><FormattedMessage id="home.declarative.p1" values={{name: <i>React</i>}}/></p> <p><FormattedMessage id="home.declarative.p2"/></p> </div> </div> </div> ); }
function Home() { return ( <div className="Home"> <div className="Home-header"> <img src={logo} className="Home-logo" alt="logo"/> <h2><Trans>Welcome to {React.js}</Trans></h2> </div> <div className="Home-container"> <h3 className="focus"><Trans>Declarative</Trans></h3> <div> <p><Trans> {React} makes it painless to create interactive UIs. Design simple views for each state in your application, and {React} will efficiently update and render just the right components when your data changes. </Trans></p> <p><Trans>Declarative views make your code more predictable and easier to debug.</Trans></p> </div> </div> </div> ); }
While more verbose, the second component, written using Lingui, is much more readable: we can tell exactly where the markup is and where the content is. Since the translation messages are embedded right in your views—in the source language, of course—we can understand the context of our views without digging through translations files. Lingui will render the text between the <Trans>
tags as a translated message based on the currently active locale. The library achieves this with Lingui macros, which transform <Trans>
tags into the common ICU MessageFormat (the same format used by react-intl), and places them in translation files for our translators to work with. We can actually have rich text, including HTML and JSX, within our translation messages as well. Lingui will convert that text into ICU messages that remember where markup tags are, without bogging down our translators with the tag meanings.
<p> <Trans> See all <Link to="/unread">unread messages</Link>{" or "} <a onClick={markAsRead}>mark them</a> as read. </Trans> </p> // When extracted, the text between the <Trans> tags above appears // like this in your translation files: // See all <0>unread messages</0> or <1>mark them</1> as read.
If you've worked on internationalized apps before, you know that rich text with tags within translation messages can be a source of headaches for developers and translators alike. Lingui macros just make these messages a breeze to work with. We'll dive into this in more detail when we build a demo app with Lingui a bit later.
We developers know how our third-party libraries can add up to megabytes going down the pipe to our users' browsers if we're not careful. So the cost-benefit analysis of each library we choose has to include the library's size. Now, consider the following size comparison, with package versions at time of writing.
react-intl (v3.2.0) 12.7 kB gzipped i18next (v17.0.13) 10.2 kB gzipped react-i18next (v10.12.2) 5.2 kB gzipped --------------- 15.4 kB gzipped @linguijs/core (v2.3.3) 1.4 kB gzipped @linguijs/react (v2.8.3) 2.5 kB gzipped --------------- 3.9 kB gzipped
It's obvious that LinguiJS gives us massive savings in kilobytes.
🔗 Resource » All kB sizes provided above are from Bundlephobia.
Worth mentioning is that, according to the Lingui documentation, the library is compatible with both JSON and the PO translation file formats.
✋🏽 Heads Up » While I have worked with the JSON format in a LinguiJS project, I haven't tried the PO format with the library myself. PO files are very common for localization, of course. If you do try the PO format, let us know how that worked out for you in the comments below.
Extracting messages from JavaScript source code and placing them into translation files is done via the LinguiJS CLI; so is compiling those messages into performant functions for production. When using other libraries, this kind of functionality might need extra configuration and wiring up to get working. However, Lingui makes extraction and compilation really straightforward with its @lingui/cli
development package. Just install the package and start running the CLI commands to work with translation files. We'll look at how to do that in a minute.
Unlike i18next, LinguiJS doesn't have a first-party HTTP translation file loader. So if you want to use Lingui and load your all translation files dynamically, you will have to write your own translation loader. Generally, this is pretty easy, and, of course it's a non-issue if you're just bundling your translations into your main app bundle. However, you should be aware of the lack of first-party loader here. We'll write our own little translation file loader for our Lingui demo app shortly.
react-intl and i18next have established communities, and you can usually quickly find solutions for most of the common hurdles you'll encounter as you work with them. Lingui is a newer library with a smaller community. This may mean that at times you won't find answers to problems you encounter with the library right away. You may need to interact with the Lingui community to get some help. From my cursory viewing, the community does look quite active and helpful. And it can actually be nice to be part of an up-and-coming library community like Lingui's. So it's up to you to decide if "new and small" is a pro or a con for you. It should also be mentioned that the core functions of the library and its documentation seemed robust to me as I worked with the library.
OK, let's get our hands dirty with some code and see what working with LinguiJS is really like. We'll build a little demo React app that shows a list of users belonging to an imaginary forum. This will allow us to test out Lingui's translation workflow, working with messages, the CLI, plurals, and date formatting. Let's get started.
I've already built a starter project that we can use to add Lingui to. It's been bootstrapped with Create React App, and uses the Bulma CSS framework for basic styling. You can get the starter project by cloning the Git repo for this demo app and checking out the commit with the start
tag.
Using that as our starting point, we can install LinguiJS through NPM.
# With NPM npm install --save-dev @babel/core@7.6.0 @lingui/cli@2.8.3 @lingui/macro@2.8.3 babel-core@7.0.0-bridge.0 npm install --save @lingui/react@2.8.3 # With Yarn yarn add --dev @babel/core@7.6.0 @lingui/cli@2.8.3 @lingui/macro@2.8.3 babel-core@7.0.0-bridge.0 yarn add @lingui/react@2.8.3
This installs the necessary packages, with latest stable versions at time of writing, to develop with Lingui.
@lingui/core
, which you would need to install explicitly on a non-React project)✋🏽 Heads Up » While installing the NPM packages you might get a warning about
babel-plugin-macros
being an unmet peer dependency of@lingui/cli
and@lingui/macro
. I've found Lingui to work just fine withoutbabel-plugin-macros
, but you can choose to install the package as a dev dependency if you want to get rid of that warning.
With these in place, let's add a .linguirc
file to our project root to configure Lingui.
{ "localeDir": "src/locales/", "srcPathDirs": ["src/"], "sourceLocale": "en" }
We've kept our configuration as simple as possible here. Lingui will store our translation message files in the src/locales
directory. Our source/development locale will be English. Lingui will scan everything under our src
directory for translation messages to extract into translation files.
To make our lives easier as we develop, let's add three NPM scripts to our package.json
file.
{ "name": "linguijs-demo-app", "version": "0.1.0", // ... "scripts": { //... "add-locale": "lingui add-locale", "extract": "lingui extract", "compile": "lingui compile" }, // ... }
add-locale
, extract
, and compile
will be used to manage our translation messages through the command-line.
In fact, let's use the add-locale
command to tell Lingui which locales our app will initially support.
# With NPM npm run add-locale en fr ## With Yarn yarn add-locale en fr
This will get Lingui to create our src/locales
directory and put empty message files in it for English and French.
Now let's wire up our React app to use Lingui. Here's the most basic setup in index.js
.
import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@lingui/react'; import App from './App'; // ... function LocalizedApp() { return ( <I18nProvider language="en"> <App /> </I18nProvider> ); } ReactDOM.render(<LocalizedApp />, document.getElementById('root')); // ...
This provides our root App
component, and all its children, with an underlying context that allows access to the active locale's translation messages. We generally don't have to worry about the details under the hood here, but we do need to make sure that our root component is wrapped with the library's I18nProvider
, like we've done above.
✋🏽 Heads Up » We've hard-coded English as the active locale here, but we'll make that dynamic a bit later.
That's it for installation. With LinguiJS now in place, let's start using it to localize our app.
Our UI text is currently hard-coded into your components.
// ... const USER_COUNT = 5; function App() { // ... return ( // ... <h2 className="is-size-4 has-mb-1"> Top {USER_COUNT} Active Users </h2> // ... ); }
Let's start using Lingui's React macros to make that text dynamic and translatable.
// ... import { Trans } from '@lingui/macro'; // ... const USER_COUNT = 5; function App() { // ... return ( // ... <h2 className="is-size-4 has-mb-1"> <Trans>Top {USER_COUNT} Active Users</Trans> </h2> // ... ); }
We just wrap our text with Lingui's Trans
macro. Notice that if we save and reload our app in the browser after this change, everything looks the same. Let's add the French translation so we can see how our app will start looking to our French users. We'll run the extraction CLI command to have Lingui scan our src
directory and pull out any Trans
ed text.
# With NPM npm run extract # With Yarn yarn extract
If everything went well, we'll see a message showing that 1 message is in our translation catalog, and we're missing 1 French translation.
✋🏽 Heads Up » If you get an error that says that no locales have been defined when running
extract
, make sure you've added your locales usingadd-locales
first (see above).
Let's add the translation. When we open src/locales/fr/messages.json
, we'll see our untranslated message.
{ "Top {USER_COUNT} Active Users": { "translation": "", "origin": [ [ "src/App.js", 42 ] ] } }
The "translation"
value is the one we need to fill in.
{ "Top {USER_COUNT} Active Users": { "translation": "Top {USER_COUNT} des utilisateurs actifs", "origin": [ [ "src/App.js", 42 ] ] } }
(Sorry about the translation if it's off: I'm using Google Translate). Notice that we keep our dynamic, interpolated value, USER_COUNT
, exactly as it is in the translated text. Because Lingui macros transpile our messages into appropriate JavaScript, they take care of the interpolation for us. We now need to compile our message files.
# With NPM npm run compile # With Yarn yarn compile
This will create a messages.js
file in each of our src/locales/*
directories. These minified files contain JavaScript functions that provide our messages to our app in a performant way. Now all we need to do is to pull in our French messages catalog and make the language our active locale. Let's update our index.js
file to do just that.
// ... import catalogFr from './locales/fr/messages.js' const catalogs = { fr: catalogFr }; function LocalizedApp() { return ( <I18nProvider language="fr" catalogs={catalogs}> <App /> </I18nProvider> ); } // ...
If we save and reload our app, we'll see our header appear in French.
So the basic Lingui translation workflow is:
<Trans>
tagsextract
to update translation filesmessages.json
compile
to generate performant runtime catalogsNotice that our active locale remains hard-coded. Another issue in our current setup is that we would have to front-load all catalogs for all translations in our main app bundle. This may be fine if our translation files are small. However, we'll add dynamic catalog loading and locale-switching, making our app more flexible and performant, when we build a language switcher in a few minutes. First, let's take a look at how we work with plurals when using Lingui.
You may have noticed that we're not exactly handling plurals in our current UI.
import React from 'react'; function User({ user }) { return ( // ... <div className="tags"> <span className="tag is-light"> {user.postCount} posts </span> <span className="tag is-light"> {user.commentCount} comments </span> </div> // ... ); } export default User;
Our count strings are hard-coded. We need to set up plurals for our source language, English, as well as the other locales our app supports. Of course, different languages have different rules for pluralization. Thankfully, the ICU Message format that Lingui supports handles these cases gracefully. So let's get to the code. We'll first update our UI to use Lingui's Plural
macro.
import React from 'react'; import { Plural } from '@lingui/macro'; function User({ user }) { return ( // ... <div className="tags"> <span className="tag is-light"> <Plural value={user.postCount} _0="No posts yet" one="# post" other="# posts" /> </span> <span className="tag is-light"> <Plural value={user.commentCount} _0="No comments yet" one="# comment" other="# comments" /> </span> </div> // ... ); } export default User;
The macro takes props with values in our source locale, English. Other languages may have plural forms that differ from our source language; Lingui's Plural macro supports all of the standard plural forms, and we can have any plural form we like in each of our locales' translation files. Plural
has a nice additional feature: it will support exact count messages that override general plural forms. We're using that in our code to show a message for our zero count. We want to show a special message for zero, so we use the non-standard _0
prop to override standard language rules here. This saves a conditional and makes our code a bit cleaner. Note that we can use any number after the underscore, so if we wanted a special message for 42 we could use the _42
prop. Alright, with our Plural
macro in place let's extract our messages again with the extract
CLI command. Once we do, we'll find that our /src/locales/fr/messages.json
file has a couple of new entries for us to translate.
{ // ... "{0, plural, =0 {No comments yet} one {# comment} other {# comments}}": { "translation": "", "origin": [ [ "src/components/User.js", 32 ] ] }, // ... }
Our messages are in the ICU format, and to translate them we simply copy the English into our French translation
s, making any changes to the plural forms we want to.
{ // ... "{0, plural, =0 {No comments yet} one {# comment} other {# comments}}": { "translation": "{0, plural, =0 {Aucun commentaire pour l'instant} one {# commentaire} other {# commentaires}}", "origin": [ [ "src/components/User.js", 32 ] ] }, // ... }
After we compile
, we should see the French versions of our plurals appear in our app. (Our source English text will, of course, also show the correct plural forms dynamically when our active locale is English).
Let's take a look at dates and numbers in Lingui before building a language-switcher to round out our demo. We'll deal with the "Last seen" date that we're showing for our users, which is currently just outputting the date string as it is in our data.
import React from 'react'; function User({ user }) { return ( // ... <p> <small>Last seen {user.lastSeen}</small> </p> // ... ); } export default User;
All we have to do is wrap our entire string in a Trans
component, and pass the date value to a DateFormat
macro.
// ... import { Trans, DateFormat, Plural } from '@lingui/macro'; function User({ user }) { return ( // ... <p> <small> <Trans> Last seen{' '} <DateFormat value={user.lastSeen} format={{ weekday: 'short', hour: 'numeric', minute: 'numeric' }} /> </Trans> </small> </p> // ... ); } export default User;
DateFormat
takes a string or Date
object as a value
and uses the standard JavaScript Intl.DateTimeFormat API under the hood to format its given date. We can pass a format
prop to DateFormat
that will get relayed to Intl.DateFormat
as its options parameter. This allows us to easily format our dates so that they show the weekday, hour, and minute our given user was last seen.
To translate our date, we go through our familiar workflow: after extract
ing our messages we open our src/locales/fr/messages.json
file and fill in the French translation.
{ // ... "Last seen {0,date,date0}": { "translation": "Vu pour la dernière fois {0,date,date0}", "origin": [ [ "src/components/User.js", 19 ] ] }, // ... }
We place the {0,date,date0}
placeholder, which DateFormat
will handle, in the appropriate position in our translation. After we compile and switch our language to French, we can see our underlying libraries, Lingui and Intl, doing the heavy lifting of localizing our date format for us.
And notice how we can simply nest our DateFormat
macro within our Trans
macro. This feels like working with any other React component and is incredibly intuitive.
🔗 Resource » Check out the Lingui documentation on SelectOrdinal, DateFormat, and NumberFormat for more details on date and number formatting with Lingui.
As promised, it's time to build a simple language switcher so that our locale is not hard-coded into our root component's source code. When our user switches the app's language, we'll give Lingui's I18nProvider
the newly selected language, as well as dynamically load in that locale's message catalog. Before we start coding, we need to install the Lingui Webpack loader, which will load in catalogs dynamically for us.
# Using NPM npm install --save-dev @lingui/loader@2.8.3 # Using Yarn yarn add --dev @lingui/loader@2.8.3
With the loader in place, let's update our index.js
file to manage language
and catalogs
bits of state that we pass back and forth to our App
component.
import React, { useState } from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@lingui/react'; import '../node_modules/bulma/css/bulma.css'; import App from './App'; async function loadMessages(language) { return await import(`@lingui/loader!./locales/${language}/messages.json`); } function LocalizedApp() { const [catalogs, setCatalogs] = useState({}); const [language, setLanguage] = useState('en'); async function handleLanguageChange(language) { const newCatalog = await loadMessages(language); const newCatalogs = { ...catalogs, [language]: newCatalog }; setCatalogs(newCatalogs); setLanguage(language); } return ( <I18nProvider language={language} catalogs={catalogs}> <App language={language} onLanguageChange={handleLanguageChange} /> </I18nProvider> ); } ReactDOM.render(<LocalizedApp />, document.getElementById('root'));
We use Lingui's Webpack loader to dynamically import the catalog for the current locale.
✋🏽 Heads Up » You may need to install the Babel dynamic import plugins to enable support for
import()
called dynamically like we have in ourloadMessages
function. If you're working along with our demo app and started with our starter app provided earlier, there's no need to install anything: our demo is built with Create React App, which includes the dynamic import plugins. Check out the Lingui documentation for more details.
Now let's update our App
component to work with the language
and onLanguageChange
props, and to pass those to our new LanguageSelector
component.
import React, { useState, useEffect } from 'react'; import { Trans } from '@lingui/macro'; // ... import LanguageSelector from './components/LanguageSelector'; function App({ language, onLanguageChange }) { // ... return ( <section className="section"> <div className="container"> <div className="columns is-vcentered"> <div className="column"> <Header /> <div className="column is-narrow"> <LanguageSelector language={language} onChangeLangage={onLanguageChange} /> </div> </div> </div> // ... </div> </section> ); } export default App;
Finally, let's build our simple LanguageSelector
itself, which is just a glorified <select>
control.
import React from 'react'; function LanguageSelector({ language, onChangeLangage }) { function handleChange(event) { event.preventDefault(); onChangeLangage(event.target.value); } return ( <div className="select"> <select onChange={handleChange} value={language}> <option value="en">English</option> <option value="fr">French</option> </select> </div> ); } export default LanguageSelector;
That's it. Now when our user selects a language, our app will respond by loading in the language's message catalog and updating its translated messages.
📖 Go Deeper » This is just a basic look at dynamic catalog loading. If you want to explore this feature with regards to saving the compilation step when developing with Lingui, take a look at Lingui's Webpack loader documentation. If you want an in-depth look at dynamic loading of catalogs in a React app, check out the Lingui guide, Dynamic loading of message catalogs. 🔗 Resource » You can get the code for the complete demo app we built in this article on Github.
If you want to dive a bit deeper into JavaScript localization, check out our excellent guides and round-ups on the subject.
That's all for our little foray into the world of the up-and-coming LinguiJS library. We hope you've seen how intuitive and clean this library is, and how it's built with developer happiness and simplicity in mind. Speaking of developer happiness, if you're looking for a pro localization platform for your websites and apps, check out Phrase. Phrase is built for developers, product managers, and translators, and has a flexible API, powerful web console, and many advanced i18n features: OTA translations, branching, machine translations, and more. Take a look at all of Phrase's products, and sign up for a free 14-day trial.
Last updated on January 27, 2023.