i18n with FormatJS for your React Native application

Step by step guide on how to get up and running with internationalization (i18n) via FormatJS on your React Native project.

June 17, 2020

According to its creators:

FormatJS is a modular collection of JavaScript libraries for internationalization that are focused on formatting numbers, dates, and strings for displaying to people. It includes a set of core libraries that build on the JavaScript Intl built-ins and industry-wide i18n standards, plus a set of integrations for common template and component libraries.

I used this library packaged as react-intl for previous React and React Native projects and I found it to respond perfectly to those applications needs, supporting very reliably interfaces for users spread across the Globe and speaking 6 different languages. The workflow centered on Babel was a joy during those months and I’ll try to share with you guys what I’ve learnt.

What is covered

Code repository

Primers

Installing React Intl & configuring Babel

Beside react-intl package we also need react-native-localize under dependencies and babel-plugin-react-intl under devDependencies. Take a look now at /babel.config.js and especially at lines 6-14. That configuration tells Babel how to use react-intl plugin. messagesDir is where Babel will mirror, according to extractSourceLocation option, the files and folders’ structure of messages.js containing the definition of translated strings in realtime and hot reloaded. Do not bother that absolute path at line 10. It will be rewritten by the postinstall npm script to reflect your project’s location.

Let’s see how /src/i18n/ folder looks like. We only need to have in place an empty folder called translation and a translation.json file containing only this { “en”: {} }. This file will be populated by the Node.js script which we’ll discuss next, based on the mirrored translation definitions folder structure under the translation folder.

Node.js script to cumulate the translated definitions

The purpose of /scripts/translator.js Node.js script is to sift through the above mentioned translation folder, to gather all translations definitions keys and their values, to cumulate and to copy them inside of that object from translation.json file. That file is what the application will use at build time. Keep this in mind because we’ll need it later. You can execute this script manually or you can use npm run dev and forget about it. This is the script recommended by react-intl documentation.

const glob = require('glob');
const fs = require('fs');
const mkdirp = require('mkdirp');

const filePattern = `${__dirname}/../src/i18n/translation/**/*.json`;
const outputLanguageDataDir = `${__dirname}/../src/i18n/`;

// 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.
const defaultMessages = glob
  .sync(filePattern)
  .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;
  }, {});

console.log(defaultMessages);

mkdirp.sync(outputLanguageDataDir);

fs.writeFileSync(
  `${outputLanguageDataDir}translation.json`,
  `{ "en": ${JSON.stringify(defaultMessages, null, 2)} }`,

As you may have noticed that comment about the need of unique definition ids, we need to have in place a naming convention for those translation keys to avoid collisions. I propose one prefixed by the component's name using that translation definition followed by the definition key like this Activation.activateTitle. If you find yourself duplicating translation definitions between different components, do not worry. Human translators need enough flexibility from language to language, from context to context. They may decide to translate them differently from eachother in other language than English.

Configuring the IntlProvider

The IntlProvider will wrap any other component of the app like so /src/App.js. locale prop is provided via react-native-localize languageCode property. This is the key that will be used to select all the translation definitions under the language of the system. We also make sure we fallback to English on line 16. The messages prop is coming from the translation.json file that I've previously told you to keep it in mind.

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { enableScreens } from 'react-native-screens';
import { Text } from 'react-native';
import { IntlProvider } from 'react-intl';
import localeData from './i18n/translation.json';
import get from 'lodash.get';

import deviceLocale from './constants/deviceLocale';
import StoreProvider from './store/reducer/StoreProvider';
import ApolloProvider from './store/apollo/Provider';
import Navigator from './navigator/StackedNavigator';
import { ignored } from './constants/yellowBox';

const language = get(deviceLocale, '[0].languageCode', 'en');
const messages = localeData[language] || localeData.en;

console.ignoredYellowBox = ignored;
console.disableYellowBox = true;

enableScreens();

const App = () => {
  return (
    <IntlProvider locale={language} messages={messages} textComponent={Text}>
      <StoreProvider>
        <ApolloProvider>
          <NavigationContainer>
            <Navigator />
          </NavigationContainer>
        </ApolloProvider>
      </StoreProvider>
    </IntlProvider>
  );
};

export default App;

Using defineMessages, injectIntl, formatMessage

The messages for translation definitions are used like this /src/screens/Activation/Activation.js: we use the higher order component to injectIntl as a prop that provides us with the handy formatMessage function.

import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import Container from '../../components/Container';
import Heading from '../../components/Heading';
import PreloaderScreen from '../../components/PreloaderScreen';
import messages from './messages';

const Activation = ({ activationCode, intl: { formatMessage } }) => {
  if (!activationCode) {
    return <PreloaderScreen />;
  }

  return (
    <Container centered>
      <Heading>{formatMessage(messages.activateTitle)}</Heading>
      <Heading>{activationCode}</Heading>
    </Container>
  );
};

Activation.propTypes = {
  activationCode: PropTypes.oneOfType([PropTypes.string, PropTypes.bool])
    .isRequired,
  intl: PropTypes.object.isRequired,
};

export default injectIntl(Activation);

We also need to take a look at the messages.js file we import from at line 7 of the script from above:

import { defineMessages } from 'react-intl';

const messages = defineMessages({
  activateTitle: {
    id: 'Activation.activateTitle',
    defaultMessage: 'Activate your device using this code',
  },
});

export default messages;

That’s it from now on: just create these messages.js files containing translation definitions or assign them inside the component file. Babel will take care of the rest. It will sense that you are using somewhere a react-intl translation definition, it will mirror the folder structure and copy the translation definitions inside of their respective .json files.

Please keep in mind that FormatJS is much more powerful than what I showed you here: formatDate, formatTime, formatRelativeTime, formatNumber, formatPlural, formatList are but a few tools at your disposal. You can read about all of that in the official documentation.

Adding more languages to translation.json file

Well, everything is fine and dandy with English, but what about the case I also want Japanese as a supported language? All you need is to send the cumulated /src/i18n/translation.json file to a professional human translator and to ask her / him to mirror the exact object structure under the jp top-level key in this case and to only change the values as translations per se.