How to translate your React app with react-intl / FormatJS

Andreas Löw, Joachim Grill
How to translate your React app with react-intl / FormatJS

Set up your first React app

We're setting up a small React application to learn how localization works. Of course, you can skip this section if you want to use your own application for that.

With the following lines you create an empty react app and start it:

npx create-react-app react-intl-demo
cd react-intl-demo
npm start

The last line automatically opens the URL http://localhost:3000 and displays the welcome message rendered by the created app.

Prepare your app: Add react-intl to your project

As we want to use react-intl which is now part of FormatJS to localize our application, add it to you project:

npm install react-intl

Wrap your app with IntlProvider

The file src/index.js renders the App react element into your DOM:

ReactDOM.render(<App />, document.getElementById('root'));

To make the internationalization functions visible in all our components we have to wrap the App component with IntlProvider. It expects the current locale as property. For the moment we're setting it to a fixed language, later we will determine the user's locale by evaluating the language request sent by the browser.

import {IntlProvider} from "react-intl";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <React.StrictMode>
        <IntlProvider locale='en' defaultLocale="en">
            <App/>
        </IntlProvider>
    </React.StrictMode>
);

Prepare your app with FormattedMessage

Now we have to find all language-specific string in our app. In the simple application generated by create-react-app there are two strings in App.js:

A text paragraph with included HTML formatting, and a link with text:

<p>
  Edit <code>src/App.js</code> and save to reload.
</p>
<a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
  Learn React
</a>

You have to wrap the text parts with a <FormattedMessage> component to translate it. It supports the following parameters:

ParameterOpt.Description
idoptionalIdentifier used to reference the translation. E.g. login.form.button
descriptionoptionalA description for the message - to give the translator some context for the work
defaultMessageoptionalA default message that is displayed if no translation is found. This can already be the text in your primary language
valuesoptionalObject containing parameters for the message. It can also contain function to render rich text

First import FormattedMessage at the top of App.js

import {FormattedMessage} from 'react-intl';

Replace the string in the <p> tag with a <FormattedMessage> and the content of the <a> with a FormattedMessage. Copy the original text to the defaultMessage attribute.

I've also added a parameter to show you how parameters work in react-intl:

import logo from './logo.svg';
import './App.css';
import React from "react";
import {FormattedMessage} from "react-intl";

function App() {
    return (
        <div className="App">
            <header className="App-header">
                <img src={logo} className="App-logo" alt="logo"/>
                <p>
                    <FormattedMessage id="app.text"
                                      defaultMessage="Edit <code>src/App.js</code> and save to reload. Now with {what}!"
                                      description="Welcome header on app main page"
                                      values={{
                                          what: 'react-intl',
                                          code: chunks => <code>{chunks}</code>
                                      }}
                    />
                </p>
                <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
                    <FormattedMessage id="app.learn-react-link"
                                      defaultMessage="Learn React"
                                      description="Link on react page"/>
                </a>
            </header>
        </div>
    );
}

export default App;

If you refresh your browser window the welcome string changes from "Edit src/App.js and save to reload." to "Edit src/App.js and save to reload. Now with react-intl".

Why setting the id attribute is better

The id attributes is optional and the Format.JS documentation proposes not to manually assign IDs. Their argument is, that you might create conflicting entries, and they prevent this from happening by using auto-generated IDs like JkyjEs or KC4q+6.

Here's why I really want to encourage you to set manual IDs:

  • When using the extraction tool, it warns you if you use the same ID with different translation texts. So conflicts are not a real problem.
  • app.learn-react-link is something you can understand: It's a link inside the application. What does KC4q+6 tell you? Nothing!
  • The manual ID stays the same when the text changes. E.g. from Learn React to Learn React! or if I fix a typo. The auto-generated ID changes and my translations are lost.
  • app.main-screen.title gives you a nice hierarchy in translation tools like BabelEdit where translations in the same context are grouped together (e.g. app.main-screen)

Rich text formatting

The first <FormattedMessage> uses rich text formatting using a <code> tag. If you want this to render correctly, you have to add code: chunks => <code>{chunks}</code> to your values.

If you are using rich text formatting in multiple locations you can of course define a set of supported tags:

const richText = {
    code: chunks => <code>{chunks}</code>,
    b: chunks => <b>{chunks}</b>,
    it: chunks => <it>{chunks}</it>,
    em: chunks => <em>{chunks}</em>,
    strong: chunks => <strong>{chunks}</strong>
}

and use it with a spread operator:

<FormattedMessage id="app.text"
                  defaultMessage="Edit <code>src/App.js</code> and save to reload. Now with <b>{what}</b>!"
                  description="Welcome header on app main page"
                  values={{
                      what: 'react-intl',
                      ...richText
                  }}
/>

<FormatMessage> also supports injecting values and components into your text.

In the example {what} is replaced with the value react-intl. It also supports the ICU Message Syntax that allows you to create variants of the translation message e.g. depending on a count ('You have no items!', 'You have one item.', 'You have 10 items').

As we haven't defined a translation for the ID app.text, the string defined by defaultMessage is used. If neither a translation nor a default message is defined, the ID would be displayed. The description property will be displayed to the translator to give him some context information.

Add polyfills for older browsers (optional)

Some functionality requires polyfills for older browsers.

For using plural rules on (IE11 and Safari 12-):

npm install @formatjs/intl-pluralrules

add this to your index.js:

import '@formatjs/intl-pluralrules/polyfill';
import '@formatjs/intl-pluralrules/locale-data/de'; // Add locale data for your supported languages

For relative time formats on IE11, Edge, Safari 13- add this:

npm install @formatjs/intl-relativetimeformat

add this to your index.js:

import '@formatjs/intl-relativetimeformat/polyfill';
import '@formatjs/intl-relativetimeformat/locale-data/de'; // Add locale data for your supported languages

Translate your app

Extract messages from source code with Format.JS cli

With Format.JS, you can automatically extract the messages and comments required for translation from your source code.

Extracting the messages is not a must — but it's highly recommended to keep your translations and code in sync.

Start installing the @formatjs/cli package:

npm i -D @formatjs/cli

Add the extraction command to your package.json:

"scripts": {
    "extract": "formatjs extract src/**/*.{ts,tsx,jsx,js} --ignore='**/*.d.ts' --out-file ./extracted/en.json"
}

Next run the following to update extracted/en.json:

npm run extract

The file looks as follows:

{
  "app.learn-react-link": {
    "defaultMessage": "Learn React",
    "description": "Link on react page"
  },
  "app.text": {
    "defaultMessage": "Edit <code>src/App.js</code> and save to reload. Now with <b>{what}</b>!",
    "description": "Welcome header on app main page"
  }
}

As you see, the file extracted the id, defaultMessage and description from the <FormattedMessage> components in your source code files.

It's important to note that this extracted JSON file differs from the translation files you'll create to translate your application: It contains more information required for the translator.

You also don't have to add this language to your translations since it's already the default language that is used, when no language is set.

Create language files

To translate your application, create the new folder src/translations and add JSON files in the following format:

src/translations/de.json:

{
	"app.text": "Bearbeite und speichere <code>src/App.js</code> um diese Seite neu zu laden. Nun mit <b>{what}</b>!",
	"app.learn-react-link": "Lerne React."
}

or src/translations/fr.json:

{
  "app.learn-react-link": "Apprenez React.",
  "app.text": "Modifiez <code>src/App.js</code> et enregistrez pour recharger. Nouveau avec <b>{what}</b>!"
}
Machine translated with DeepL...

These files in contrast to the en.json are inside the src folder because you have to add them to the project. This is not required for the en.json because these messages are already contained in the <FormattedMessage>.

Now we can load these JSON files and pass one of them to IntlProvider, depending on the language the user has configured in the browser:

Add these lines to src/index.js:

import messages_de from "./translations/de.json";
import messages_fr from "./translations/fr.json";

const messages = {
    'de': messages_de,
    'fr': messages_fr
};

// get browser language without the region code
const language = navigator.language.split(/[-_]/)[0];

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <React.StrictMode>
        <IntlProvider locale={navigator.language} messages={messages[language]}>
            <App/>
        </IntlProvider>
    </React.StrictMode>
);

Note that we've not added the en.json. This is not required because it's the default language that is used if no message is found.

You can test the other languages by changing the line setting the language:

const language = "fr";

Simplify your translation process

Keeping track of the different language files is easy at start but becomes a big burden over time:

  • Translation files get out of sync faster than you can imagine. There are some additional IDs in fr.json, some are missing in de.json.
  • You can't use a diff tool to compare the language files with the extracted file because they have a completely different format.

This is where BabelEdit comes into play. It's a translation software designed for developers!

BabelEdit setup for Format.JS with react-intl

On BabelEdits main screen select the React:

Select React as Framework
Click on React in BabelEdit's main screen.

Click the formatjs extract button in the next screen. It would also be possible to use react-intl without the extractor - but using it is much more convenient!

Select formatjs extract
Click on formatjs extract when using the source code extraction

Drag and drop the extracted/en.json onto BabelEdit:

Add the extracted translation file to BabelEdit
Drop the extracted en.json file onto BabelEdit

Drag and drop the other language files (src/translations/de.json and src/translations/fr.json) onto BabelEdit:

Add the translation file to BabelEdit
Drop the created translation files onto BabelEdit

Select English (en-US) as the primary language:

Set the primary language
Set en-US as the primary language

This is important because the primary language is the language used for the extraction. BabelEdit makes this language read-only. This is because changes to the primary language would be overwritten with the next npm run extract anyway. BabelEdit also reads the comments from that file.

The complete setup for BabelEdit with react-intl / FormatJS
Final setup for BabelEdit for react-intl and Format.JS

Finally, click Close.

Why BabelEdit?

BabelEdit is designed for the daily needs of developers. It's not a traditional translation software but an editor that speeds up managing your translations.

  • First of all, it keeps your JSON files in sync. No more old translation IDs that are no more used - or missing new translations in some files.
  • Preview your application in different languages using Machine Translation
  • See where a translation is used with Source code references
  • Export translation files to external translation agencies, and import them back into your project
  • Comes with a fair pricing model: No subscription. BabelEdit comes with a perpetual license.
Why use BabelEdit for react-intl
BabelEdit has many useful features: Keep your JSON files in sync, machine transation, source code references,...