Software localization

Localizing JavaScript & React Apps with LinguiJS

LinguiJS is a small, robust JavaScript and React i18n library that feels lean and elegant compared to its well-established competitors. A deep dive.
Software localization blog category featured image | Phrase

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.

An Intuitive Syntax

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.

With react-intl

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>

  );

}

With Lingui

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.

A Small Footprint

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.

Universality

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.

Powerful CLI

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.

No Built-in Translation File Loading

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.

Newer, Smaller Community

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.

Working with Lingui: A Little Demo App

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.

Demo app | Phrase Our app in its finished form

Installation

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.

Installing the NPM Packages

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/cli — command-line tools used to work with translation messages
  • @lingui/macro — transpilation functions that allow us to use intuitive translation messages in our JSX
  • @lingui/react — components that hook Lingui into our React app (this package pulls in @lingui/core, which you would need to install explicitly on a non-React project)
  • @babel/core and babel-core@bridge — requirements for backwards-compatibility for projects using Babel 6

✋🏽 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 without babel-plugin-macros, but you can choose to install the package as a dev dependency if you want to get rid of that warning.

Configuring LinguiJS

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.

Adding Our Lingui CLI Scripts

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.

Adding Our Initial Locales

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.

Connecting Lingui to React

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.

Message Translation

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 Transed 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 using add-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.

Translated header | Phrase Et voilà!

So the basic Lingui translation workflow is:

  1. Wrap translated text in <Trans> tags
  2. extract to update translation files
  3. Add translations to messages.json
  4. compile to generate performant runtime catalogs

Notice 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.

Plurals

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;

Demo app with wrong pluralization | Phrase Our posts counter is always showing the many form, even when we have one

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 translations, 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).

French version with proper pluralization | Phrase The one, the many

Date Formatting

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;

Demo app with unformated date format | Phrase That is ugly, dawg

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.

Formated dates in English locale | Phrase Our dates formatted in the English locale

To translate our date, we go through our familiar workflow: after extracting 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.

Formated dates in English locale | Phrase En français

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.

A Simple Language Switcher & Dynamic Loading of Message Catalogs

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 our loadMessages 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.

Demo app with language switcher | Phrase Take your pick

📖 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.

Further Reading

If you want to dive a bit deeper into JavaScript localization, check out our excellent guides and round-ups on the subject.

And That's a Wrap

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.