Comparing react i18n: react-intl (part 3)

The biggest guy on the block

2019/01/28 2019/02/20    react javascript

Installing and configuring

We’ll start out with the same code as we did when we tried the previous library (a funny app to count how many puppies we want), bootstrapped by Create React App, and then we’ll install the library with yarn add react-intl . React-intl seems to come without the need for extensive configuration before we can add it do our app, so it’s already time for next step.

Connecting to the app

So, in our App.js we need to add the provider provided (see what I did there!) by the library, so let’s do that by importing the IntlProviderand wrap it around our App:

//index.js
import React from "react";
import ReactDOM from "react-dom";
import { IntlProvider } from "react-intl";

import "./index.css";
import App from "./App";

ReactDOM.render(
  <IntlProvider locale="en">
    <App />
  </IntlProvider>,
  document.getElementById("root")
);

Adding translations

And, although one desperately wishes this was not the case, we need to change our markup to tell the translation engine where to do it’s magic. The syntax that react-intl uses leaves, let’s say, some things to be desired. It uses the component FormattedMessage(because why not use a name that is 16 characters long). Okay, let’s be fair: it’s atrocious. We’re not wrapping the messages in a component, but sending them as props.

    <FormattedMessage id="unique.key"
                      defaultMessage="Translation of {what}"
                      description="Description of the message"
                      values={{ what: 'an app' }}/>

We can, however, destructure the function (because components are mere functions) out of the Intl-object provided by the wrapper like thus:

const {intl: {formatMessage}} = this.props;

And then use it in a way akin to the t(...);, but, of course, a bit more verbose:

<div>
{formateMessage(messages.something)}
</div>

But, we still need to write the messages somewhere, and that is by using defineMessages(), a utility function supplied by the library, and giving it an object of all our messages.

const messages = defineMessages({
  something: {
    id: "unique.key",
    defaultMessage: This be the default message,
  },
});

The most common, and idiomatically react way to do this, seems to be using the component, so let’s do that. We’ll import the component from the library and use it to render our messages:

import { FormattedMessage } from "react-intl";
	<>
        <header>
          <h1>
            <FormattedMessage
              id="app.title"
              defaultMessage="Testing react-intl"
            />
          </h1>
          <button>Other language</button>
        </header>
        <main>
          <p>
            <FormattedMessage
              id="app.puppies"
              defaultMessage="I want {count, plural,
                                        one {1 puppy}
                                        other { {count} puppies}}"
              values={{ count: count }}
            />
          </p>
          <button onClick={this.addNumber}>
            <FormattedMessage
              id="button.label"
              defaultMessage="I want more puppies!"
            />
          </button>
        </main>
      </>

Here we send the value, upon with the plural is dependent, in along with the id and the default message. The syntax for the actual messages is a lot of curly braces, within even more braces. But, alas, it works.

Now, let’s add the actual translations as in messages in another language! There are two options, or well, actually three, but let’s not consider adding another instance of babel on top of the one already provided by CRA. Other we download react-intl-cra, a little utility that find babel for us and use the plugin react-intl provides for this - or we use a bable-macro, a recent feature of babel that CRA2 already have support for. I chose to use the little utility, because that seems like the easiest solution to me. Go yarn add react-intl-cra -D !

Small note! react-intl-cra uses Babel 6, and can thus not understand <>..</>, we must thus change this to <React.Fragment>...</React.Fragment> or it will trow an error.

Now, return to the terminal and run the command npx react-intl-cra './src/**/*.js' -o messages/messages.json. It will generate files that look like this:

[
  {
    "id": "app.title",
    "defaultMessage": "Testing react-intl",
    "filepath": "./src/App.js"
  },
  {
    "id": "app.puppies",
    "defaultMessage": "I want {count, plural, one {1 puppy} other {{count} puppies}}",
    "filepath": "./src/App.js"
  },
  {
    "id": "button.label",
    "defaultMessage": "I want more puppies!",
    "filepath": "./src/App.js"
  }
]

This is a file with all the messages of your app. But, we’re not done yet, because it it would have been that easy, life would be quite nice. The library wants be fed data in the format {"key": "translation"}, and here we get something else. So, we need to either edit this by hand, or do it programatically. Well, time for yet another little utility with the fitting name react-intl-translations-manager! Let’s add that yarn add react-intl-translations-manager -D, and then, of course, do some configuration!

// translationRunner.js
const manageTranslations = require('react-intl-translations-manager').default;

manageTranslations({
  messagesDirectory: 'src/messages/',
  translationsDirectory: 'src/locales/',
  languages: ['en', 'se', 'es', 'zh'] // any language you need
});

Hint! Using echo "any kind of text" > filename.fileending in the terminal immediately creates a file, containing the supplied text. When following guides, like this, it saves a lot of time when copying configuration content!

Then, head over to package.jsonand add this to your scripts:

{
  "scripts": {
    "manage:translations": "node ./translationRunner.js"
  }
}

Now, we can run the utility and generate files for our locales (defined in an array in the translationRunner.js) with yarn manage:translations and it will spit out files into src/locale/. They will look like this:

{
  "app.puppies": "I want {count, plural, one {1 puppy} other {{count} puppies}}",
  "app.title": "Testing react-intl",
  "button.label": "I want more puppies!"
}

Here’s Swedish:

{
  "app.puppies": "Jag vill ha {count, plural, one {1 valp} other {{count} valpar}}",
  "app.title": "Testar react-intl",
  "button.label": "Jag vill ha fler valpar!"
}

Etc.

Toggle languages

React-intl does not provide a built in solution as to how you change the language settings, but merely says that it uses whatever locale and messages that are sent through the provider as props. This is… a bit annoying, as we have to get a wrapper of some sorts, around the wrapper, to be able to supply it with the language locale. It can be done trough Redux (which I do not want to add), or by using a route through React Router, or by downloading some nice fellows wrapper component that utilises the new React Context API. I will do the later, because it seems like the smallest and most convenient solution for our minimal working example.

It’s not an npm pakage, so it has to be installed directly from from GitHub. yarn add https://github.com/Tomekmularczyk/react-intl-context-provider. Then we replace the React-intl provider, with that from the library, import the message translations for our default language, and send it as an object as the initialProps on the IntlProvider, and our index.js will look like this:

//index.js
import React from "react";
import ReactDOM from "react-dom";
import { IntlProvider, loadLocaleData } from "react-intl-context-provider";

import "./index.css";
import App from "./App";

import en from "./locales/en.json";

loadLocaleData(["en", "se", "es", "zh"]);

ReactDOM.render(
  <IntlProvider
    initialProps={{ locale: "se", defaultLocale: "se", messages: en }}>
    <App />
  </IntlProvider>,
  document.getElementById("root")
);

To toggle the language, we make use of another component provided by the context provider, the IntlProvider. For the sake of similarity, I’ve made a file that export all the locales as an object that we can map over:

//locales.json
import en from "./locales/en.json";
import se from "./locales/se.json";
import es from "./locales/es.json";
import zh from "./locales/zh.json";
export const locales = [
  { locale: "en", value: en, label: "English" },
  { locale: "se", value: se, label: "Svenska" },
  { locale: "es", value: es, label: "Español" },
  { locale: "zh", value: zh, label: "中文" },
];

This time, we import the message translation here, and send them as the value, and add another property to the object, that is the language code as a string. This is because this time, we will need both to loop over the object to make the toggle buttons. Import the IntlConsumer and the locales.js from above, and use the updateProps function to update the props. We’ll map over locales, and set the local as the string in locale, the messages as the value imported from the message translations, and the label of the button as label.

//App.js
 ...
import { IntlConsumer } from "react-intl-context-provider";
import { locales } from "./locales";
 ...

          <IntlConsumer>
            {({ updateProps, ...providerProps }) =>
              locales.map(l => ( /* That's a small letter L, not numer one*/
                <button
                  onClick={() =>
                    updateProps({ locale: l.locale, messages: l.value })
                  }>
                  {l.label}
                </button>
              ))
            }
          </IntlConsumer>

And, voilà, we’re done. There is a difference however, whenever you change language, the local state of App will be erased. This was not the case of i18next.