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:
Parameter | Opt. | Description |
---|---|---|
id | optional | Identifier used to reference the translation. E.g. login.form.button |
description | optional | A description for the message - to give the translator some context for the work |
defaultMessage | optional | A default message that is displayed if no translation is found. This can already be the text in your primary language |
values | optional | Object 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 essential
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 doesKC4q+6
tell you? Nothing!- The manual ID stays the same when the text changes. E.g. from
Learn React
toLearn 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>!"
}
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 with BabelEdit
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:
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!
Drag and drop the extracted/en.json onto BabelEdit:
Drag and drop the other language files (src/translations/de.json and src/translations/fr.json) onto BabelEdit:
Select English (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.
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.