Setting up i18n with react-intl in Mantra/React application

This article instructs how to setup i18n package `react-intl` from yahoo in the Mantra/React/Meteor application.

Installation

You need to install three sets of packages. First install the package itself:

npm install — save react-intl

Then, you need to install the supporting packages. First one extracts the i18n strings from your source files and stores them in separate files in the specified folder (usually `lib/i18n`).

npm install — save-dev babel-plugin-react-intl

Since this is a babel plugin, you need to use gadicc:hot-loader package to use it or wait for 1.3.2 which should be able to use babel plugins. This is my `.babelrc` from the root folder in which I specify the babel plugin and the path where messages are saved

// file: .babelrc
{
“presets”: [“es2015”, “stage-2”, “react”],
“plugins”: [
“react-require”,
[“react-intl”, {
“messagesDir”: “./lib/i18n/messages/”,
“enforceDescriptions”: false
}]]
}

Now, when you save your file, all the i18n strings are automatically extracted.

Next bit is a bit tricky as you need to setup a mechanism that collects all the extracted strings and builds your main language file. For this purpose, first, we need to install babel-cli. I prefer to install it globally in order to reuse in other projects.

npm install -g babel-cli

Now, we define three scripts (translate.js, printer.js and translator.js) that process the extracted strings (please setup your paths according to your configuration). Please place these files in the `.scripts` folder. These files were taken out of one of the examples in the package page.


// file: .scripts/translate.js
import * as fs from ‘fs’;
import {sync as globSync} from ‘glob’;
import {sync as mkdirpSync} from ‘mkdirp’;
import Translator from ‘./lib/translator’;
const MESSAGES_PATTERN = ‘./lib/i18n/messages/**/*.json’;
const LANG_DIR = ‘./lib/i18n/lang/’;
// Aggregates the default messages that were extracted from the example app’s
// React components via the React Intl Babel plugin. An error will be thrown if
// there are messages in different components that use the same `id`. The result
// is a flat collection of `id: message` pairs for the app’s default locale.
let defaultMessages = globSync(MESSAGES_PATTERN)
.map((filename) => fs.readFileSync(filename, ‘utf8’))
.map((file) => JSON.parse(file))
.reduce((collection, descriptors) => {
descriptors.forEach(({id, defaultMessage}) => {
if (collection.hasOwnProperty(id)) {
throw new Error(`Duplicate message id: ${id}`);
}
collection[id] = defaultMessage;
});
return collection;
}, {});
mkdirpSync(LANG_DIR);
fs.writeFileSync(LANG_DIR + ‘en-US.json’, JSON.stringify(defaultMessages, null, 2));

// file: .scripts/lib/printer.js

const ESCAPED_CHARS = {
‘\\’ : ‘\\\\’,
‘\\#’: ‘\\#’,
‘{‘ : ‘\\{‘,
‘}’ : ‘\\}’,
};
const ESAPE_CHARS_REGEXP = /\\#|[{}\\]/g;
export default function printICUMessage(ast) {
return ast.elements.reduce((message, el) => {
let {format, id, type, value} = el;
if (type === ‘messageTextElement’) {
return message + value.replace(ESAPE_CHARS_REGEXP, (char) => {
return ESCAPED_CHARS[char];
});
}
if (!format) {
return message + `{${id}}`;
}
let formatType = format.type.replace(/Format$/, ‘’);
let style, offset, options;
switch (formatType) {
case ‘number’:
case ‘date’:
case ‘time’:
style = format.style ? `, ${format.style}` : ‘’;
return message + `{${id}, ${formatType}${style}}`;
case ‘plural’:
case ‘selectOrdinal’:
case ‘select’:
offset = format.offset ? `, offset:${format.offset}` : ‘’;
options = format.options.reduce((str, option) => {
let optionValue = printICUMessage(option.value);
return str + ` ${option.selector} {${optionValue}}`;
}, ‘’);
return message + `{${id}, ${formatType}${offset},${options}}`;
}
}, ‘’);
}

// file: .scripts/lib/translator.js


import {parse} from ‘intl-messageformat-parser’;
import print from ‘./printer’;
export default class Translator {
constructor(translateText) {
this.translateText = translateText;
}
translate(message) {
let ast = parse(message);
let translated = this.transform(ast);
return print(translated);
}
transform(ast) {
ast.elements.forEach((el) => {
if (el.type === ‘messageTextElement’) {
el.value = this.translateText(el.value);
} else {
let options = el.format && el.format.options;
if (options) {
options.forEach((option) => this.transform(option.value));
}
}
});
return ast;
}
}

Last bit that is needed is to modify the package.json and add the command that processes the extracted string.

{
“scripts”: {
“translate”: “babel-node .scripts/translate/translate.js”
}
}

Now, we wrap our layout component into “react-intl”
 functionality in order to load the string:


import React from “react”;
import { FormattedMessage, addLocaleData, IntlProvider } from “react-intl”;
import enLocaleData from “react-intl/locale-data/en”;
// add info on the current locale
addLocaleData(enLocaleData);
const messages = require(“/lib/i18n/lang/en-US.json”);
export class Layout extends React.Component {
render() {
return (
<IntlProvider locale=”en” messages={messages}>
<main id=”home”>
<div id=”main”>
<FormattedMessage id=”info.unsupportedBrowser” />
{ this.props.content ? this.props.content() : null }</div>
</div>
</main>
</IntlProvider>
);
}
};
export default Layout;

Phew! Kind of lot of work to setup such a simple this right? Well, I guess it can be somehow automated. Now it’s time to define some intl strings. For example look at the following file:

// file: component.js
import React from “react”;
import { FormattedMessage } from “react-intl”;
export const Component = () => (
<main id=”home”> 
<h1 className=”ui header”>
<FormattedMessage id=”info.unsupportedBrowser” defaultMessage=”Your browser is not supported” />
</h1
</main>
);
export default Component;

In case you want to use i18n strings directly in your javascript files, you need to define them via messages and load them directly. First you need to setup your messages in the context:

// file: /client/config/context.js
// of cource you should load messages based on the currently selected locale
// you can even store them in the reactive variable in order to reactively re-render the page
const messages = require(“/lib/i18n/lang/en-US.json”);
export default {

Messages: messages
}

// finish


import { FormattedMessage } from “react-intl”;
import Context from “/client/config/context.js”;
const messages = defineMessages({
label: {
id: ‘myMessage’,
defaultMessage: ‘You the best!’,
}
});
let myMessage = Context.Messages.myMessage;

That’s all folks!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.