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.
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
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>
);
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".
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:
app.learn-react-link
is something you can understand: It's a link inside the application. What does KC4q+6
tell you? Nothing!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
)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.
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
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.
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";
Keeping track of the different language files is easy at start but becomes a big burden over time:
This is where BabelEdit comes into play. It's a translation software designed for developers!
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.
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.