I18N in React with Typescript & React-Intl

Earlier this week I went in search of a React-friendly i18n library and I spent some time experimenting with react-intl. React-intl is based on FormatJS, which is a library for localizing numbers, dates, and strings. My impression is that react-intl is a fairly small, practical library that is written to make i18n unobtrusive for developers.

Edit, May 1, 2017

Today I’m wondering whether it may have been a mistake to adopt react-intl over the alternatives. At the moment it appears to be in limbo after Yahoo’s collapse.

But although React-intl has type definitions, it does not play particularly well with TypeScript. It assumes there is a single default language which a developer will put directly into the source code, and those strings will later be auto-extracted from the JavaScript code and handed off to translators. Unfortunately, it’s a babel plugin that does the extraction, so TypeScripters will either have to use a separate library to extract strings, or else they’ll need to create a parallel TypeScript-to-ES2015 compilation step to use the native libraries. I went with the latter, inspired by this github thread, because I wanted to use react-intl-translations-manager, and the typescript-only library didn’t extract the language strings in a format that the translation manager could use.

TL;DR The code for this example is at https://github.com/mikebridge/react-intl-ts-demo/.

Setup

Create a new project with the TypeScript fork of create-react-app:

$ create-react-app react-intl-ts-demo --scripts-version react-scripts-ts

Then add babel with the react plugins, react-intl and the react-intl-translations-manager:

$ npm install --save-dev  babel-cli babel-plugin-react-intl babel-preset-es2015 babel-preset-react
$ npm install --save react-intl
$ npm install --save-dev react-intl-translations-manager  

So at this point the package.json file should look something like this:

Add a .babelrc file to compile JSX files:

Usage

Open up the index.tsx file and add in the IntlProvider. The IntlProvider makes the react-intl functionality available to components via the React Context:

Here we’re hardcoding the locale to “en” for the time being:

Now we can localize the strings in App.tsx file. Import FormattedMessage and wrap the text:

Translation

I added these three scripts to package.json. In production I’ll probably write a bash script to clean up the artifacts, but in the meantime:

    "trans:compile": "tsc -p .  --target ES6 --module es6 --jsx preserve --outDir extracted",
    "trans:extract": "babel  \"extracted/**/*.jsx\"",
    "trans:manage": "node scripts/translationRunner.js"

1) The first script compiles the tsx files to ES2015 in a temporary directory called extracted. (Make sure you also add this to “exclude” in your package.json and to .gitignore)

    $ npm run trans:compile

2) The next one extracts all the default strings into the src/translations/extracted directory.

    $ npm run trans:extract

You should now see a file App.json that contains your extracted resource metadata:

3) Lastly, react-intl-translations-manager generates the stub files. I copied their scripts/translationRunner.js script:

Then you can run node scripts/translationRunner.js directly, or via the npm script:

$ npm run trans:manage

Duplicate ids:

  No duplicate ids found, great!

Maintaining fr.json:

Added keys:
  app.to_get_started: To get started, edit {filename} and save to reload.
  app.welcome: Welcome to React

Maintaining zh.json:

Added keys:
  app.to_get_started: To get started, edit {filename} and save to reload.
  app.welcome: Welcome to React

This creates two .json files per language—one for the translations, and one for whitelisting messages that are causing invalid warnings.

Modify the French file to look like this (disclaimer: these come from Google Translate):

For this demo, I just hardcoded a different locale to see it working. But to change the locale in real code, you’ll need to set the key on IntlProvider. (See this for more information—redux would be a good solution.)

The French locale data will come from the react-intl package.

To hardcode the French locale, modify index.tsx to look like this:

Before we check our page, let’s add in a date component to index.tsx:

   

Here’s how it looks:

create-react-app-ts in French.

create-react-app-ts in French.

Injecting react-intl into Components

Although you can use one of react-intl’s “Formatted*” controls much of the time, some text will occur inside TypeScript code or in JSX attributes. Then you’ll need to access the API directly. For this you can inject the intl object into the props of your control:

Those strings that don’t appear in Formatted* controls still need to exist somewhere so that they can be located during resource extraction. There’s a call defineMessages for this purpose—I wasn’t sure what to do with them so I put all these in a separate .tsx file:

(I’m not sure what the object key here is for—I don’t see it being used anywhere.)

Conclusion

I am going to use react-intl for a smaller project and see how it goes. I’ll add more info here as I get more practical experience with it.