i18n with React, react-intl and POEditor

Sebastián Rajo
Worldsensing TechBlog
5 min readSep 30, 2019
Photo by Conor Luddy on Unsplash

For the past couple of months, we have been struggling with the translation process of our platform, OneMind. Working with external translators using a JSON file is not the best way to go (dah!). There were several pain points to tackle. I will try to explain here what is now the complete flow we are following to enjoy handling literals again (no matter the language!) in our web platform.

TL; DR;

Check out the final repository here: https://github.com/worldsensing/ws-intl-poeditor

Building from scratch

As I mentioned before, we use React, so it’s pretty natural that we have chosen Yahoo’s react-intl for the internalisation. We are going to create an application from scratch using yarn create (I will asume you have Yarn (0.25+) and Node (6+) installed locally).

$ yarn create react-app ws-intl-poeditor
$ cd ws-intl-poeditor

i18n

First of all, we must add some dependencies:

$ yarn add -D @babel/cli @babel/core babel-plugin-react-intl babel-preset-react-app

react-intl: “This library provides React components and an API to format dates, numbers, and strings, including pluralisation and handling translations.”

babel-plugin-react-intl: “Extracts string messages from React components that use React Intl.”

Extracting literal’s ids

Now that we have our app with the dependencies installed, we should create a file called .babelrc with the following data:

{
"presets": ["react-app"],
"plugins": [
[
"react-intl",
{
"messagesDir": "./messages/"
}
]
]
}

This is to let Babel know that we want to use babel-plugin-react-intl.

Also, we have to add a new script to our package.json:

"scripts": {
...
"extract:messages": "NODE_ENV=production babel ./src >/dev/null"
}

Finally, edit App.js:

import React, { Component } from "react";
import { FormattedMessage } from "react-intl";
import "./App.css";
class App extends Component {
render() {
return <FormattedMessage id="hello_world" defaultMessage="Hello world!" />;
}
}
export default App;

The most important change is the component <FormattedMessage /> (notice the necessary import). We are saying that we want a literal with an id called hello_world which default text is Hello world!.

Now, we can execute the extract:messages script:

$ yarn extract:messages

You will find a new folder called messages/. The complete path to the messages is messages/src/App.json. messages/ came from the path defined in .babelrc. The rest of the path, as you may already notice, is the path to the component with the specific literal.

Gettext

Once we have the JSON with the literals, we can send them to POEditor. POEditor is a localization management platform. In a few words: it would make your life easier. Of course, the first thing we are going to do is to create a new account.

From the repository mentioned before, you can get all the javascript files needed to interact with POEditor API.

With a new account created, we will get a new API token: just click UNLOCK and copy the API token to poeditor.settings.js.

Next, we have to install a dependency (react-intl-po) that will help us to handle .po and .pot files.

$ yarn add -D react-intl-po

After that, we will add yet another script to our package.json:

"scripts": {
...
"extract:pot": "react-intl-po json2pot 'messages/**/*.json' -o 'output/messages.pot'"
}

As you can see, the process will iterate over all the folders under messages/ and get all the JSON files to build just one file called messages.pot in the output folder (we must create this folder first).

$ mkdir output
$ yarn extract:pot

Now we have a file that looks like this:

msgid ""
msgstr ""
"POT-Creation-Date: 2019–04–16T15:14:35.372Z\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"MIME-Version: 1.0\n"
"X-Generator: react-intl-po\n"
#: messages/src/App.json
#. [hello_world]
#. defaultMessage is:
#. Hello world!
msgid "Hello world!"
msgstr ""

We are ready to let POEditor know about the new literals to be translated. You can create a new project and a new language in POEditor using the Rest API. To keep it simple, we’re going to create both manually (more info about this).

I have just created a project called WS React-intl POEditor with a new language: Spanish.

Creating a new project in POEditor

Grab the project’s id form the URL and put it into poeditor.settings.js.

Adding terms

Now we have to add the terms.

A new dependency is needed in order to simplify our Node scripts interaction with POEditor API:

$ npm install request

Then, we have to create a folder where the translations will go:

$ mkdir translations

Add two new scripts: poeditor:update:terms and poeditor:download:translations. You can see the script’s code in the repository. The first one will push the terms to POEditor and the second one will download the translated files.

$ yarn poeditor:update:terms
Terms list

Once the terms are uploaded, we must let our translator know that has work to do. Let’s say that our translator is super fast (yay!), and the translation is already completed.

Translations view

Getting the translations

Now we can proceed to execute the next script:

$ yarn poeditor:download:translations

This script will download all the translated files (one for each language defined). Under translations/ you will find a file called es.po (because Spanish is the only language defined by me). Looks like this:

...#. [hello_world]
#. defaultMessage is:
#. Hello world!
#: messages/src/App.json
msgid "Hello world!"
msgstr "¡Hola mundo!"

Great! So, we are ready for the last step, which is converting all .po files into a unique JSON file with all the languages integrated. For that, we will use react-intl-po once again. Create a new script like this:

scripts: {
...
"extract:po": "react-intl-po po2json 'translations/*.po' -m 'messages/**/*.json' -o 'public/translations.json'"
}

…and execute it:

$ yarn extract:po

A file called translations.json under public/ folder should appear:

{ "es": { "hello_world": "¡Hola mundo!" } }

OK. Now we're going to translate our app. Pretty straightforward. You can see an example here: https://github.com/yahoo/react-intl

Just edit index.js like this:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { IntlProvider, addLocaleData } from "react-intl";
import es from "react-intl/locale-data/es";
const loadJSON = (file, callback) => {
...
};
loadJSON('translations.json', data => {
addLocaleData([...es]);
ReactDOM.render(
<IntlProvider locale="es" messages={data["es"]}>
<App />
</IntlProvider>,
document.getElementById("root")
);
});

Start the server:

$ yarn start

check it at http://localhost:3000/ and we are done!

You can integrate this scripts in the way that better suit you. In our Github repository you can see that I’ve adjusted the scripts slightly. Executing just two of them, translations:push:terms and build will do all the job.

And remember, if you want to help us with this and other wonderful problems, don’t hesitate to let us know! Check out our open positions at https://worldsensing.wpengine.com/engineering/

Happy translations!

--

--