A React app usually consists of a large number of components. Each component may display messages to the user. When we internationalise a component with react-intl
we typically remove these strings from the innards of a render method and define them separately – but nearby – as react-intl
message descriptors: objects with a unique id, a default message (the string), and an optional description (important context for translators).
(If you use a plugin like babel-plugin-react-intl-auto
, some of the bureaucracy of message descriptors is taken care of automatically. With this plugin, in your source code you define simple objects mapping from keys to strings, and the plugin transforms these internally to full react-intl
message descriptors. It's important to understand that the message descriptors haven't gone away: they're just hidden from those developing the components. They're still visible for those worrying about internationalisation as a whole.)
Default message strings within message descriptors typically have three purposes:
- As fallbacks, for use if
react-intl
can't find a string with the correct message descriptor id from the currently defined id/string mappings plugged intoIntlProvider
. - As strings in the default locale (AKA base locale, master locale, source locale). If your developers or UX engineers use
en-GB
for these strings, consciously or unconsciously, your default locale isen-GB
. - As the translation source, together with any associated descriptions, supplied to a translation service.
With react-intl
, to translate a component you need to:
- Extract each "default locale" string for that component
- Find a translation for that string in each locale you want your app to support
- Store the translation in a file your app can load at the right time and then plug in to
IntlProvider
.
riw helps you do all three. It can't perform the translation itself, but it can look up translations already performed (it maintains a database), and it outputs the strings that still need to be translated.
riw also gives you housekeeping tools: for checking how complete your translations are, for modifying its database, and so on.
Importantly, riw fits into your current development process and does not constrain it. Used at appropriate times, riw can ensure you ship only the translations you need, and never ship any out-of-date translations.
-
App: The software being translated. Usually equivalent to an npm module with a
package.json
file. Each app has its own riw configuration settings, and several apps may share a translations database. -
Translations database: The JSON file containing the master list of default messages and any associated descriptions, plus the available translations of those messages into one or more locales. riw reads and writes to this database. One database may be used by more than one app.
-
Default locale: The locale of your app's
react-intl
default messages (thedefaultMessage
strings of message descriptors). This is usually but not necessarily a user-selectable locale for your app. -
Target locale: A locale, other than the default locale, you want your app to support.
-
Locale id: This document uses
ll-TT
format for locale ids: lower case for locale and upper case for territory/region, with a dash as separator. You can use any string format you want, eglll_ttt
, BUT you must be consistent. In particular, you must use the same format in your app and for the translations database.
The riw-example repository shows a simple React app, internationalised with react-intl
and babel-plugin-react-intl-auto
, set up for use with riw.
Meanwhile, here's the view from 10,000 feet:
- Install riw
- Define your riw configuration settings – primarily the default and target locales
- Initialise the riw translations database
- Initialise the riw output files
- Update your app to load strings from the output files
- Forever:
- Translate the untranslated strings, while continuing to develop the app normally
- Update the riw translations database
- Regenerate the riw output files
- Maybe ship
Let's look at each of these in turn.
You can install riw as a package dependency or globally, or both.
$ yarn add --dev riw # or yarn global add riw
or
$ npm install --save-dev riw # use npm -g for global
Installing riw gives you a script for use from the command line, and a library for use programmatically.
When installed as a package dependency, users of yarn
can run the script easily:
$ yarn riw help
When installed globally, you can run riw
directly:
$ riw help
If installed both globally and as a package dependency, running riw
directly will run the package dependency. These instructions assume you can run riw
directly.
riw has a small number of configuration settings, and you can store them in one of several locations:
Location | Description |
---|---|
Under the riw key of your package.json file |
Recommended |
As the default export of a module .riw-config.js in the same directory as your package.json file |
Use this if you need to derive any configuration settings programmatically |
As the default export of a module elsewhere | Use this if you can't use either method above. You'll need to run the riw command with the --config option to locate the file. |
riw doesn't combine these locations: if you specify --config
, riw won't look in package.json
or .riw-config.js
. If you omit --config
and there's a .riw-config.js
file, riw won't look in package.json
.
riw uses the configuration from one of these locations to override the default settings. See riw configuration for all the settings and their defaults.
Make sure you check your configuration settings into git, or whichever source control system you use.
Once riw is installed, you can see the current configuration settings in full using riw print-config
. For example:
$ riw print-config
{
"rootDir": "/Users/avaragado/my-react-app",
"defaultLocale": "en-US",
"targetLocales": [
"fr-FR",
"pt-BR",
],
"translationsDatabaseFile": "src/locale/riw-db.json",
"sourceDirs": [
"src/**/*.js"
],
"collateDir": "tmp/babel-plugin-react-intl",
"inputMode": "source",
"reactIntlAutoConfig": {
"removePrefix": "src",
"filebase": true
},
"translationsOutputFile": "src/locale/[locale].json",
"outputMode": "file-per-locale",
"todoFile": "src/locale/TODO-untranslated.json",
"configFile": "/Users/avaragado/my-react-app/package.json"
}
Relative paths in your configuration settings are treated as relative to the rootDir
output by riw print-config
(usually the directory containing package.json
).
The most likely settings you'll want to override are:
targetLocales
: The value is an array of locale ids. For example:["fr-FR", "pt-BR"]
for French in France, and Brazilian Portuguese.defaultLocale
: this defaults toen-US
but your app might use another locale.
Many apps can share a riw translations database, potentially reducing translation costs. If you already have a riw translations database, include the path in your riw configuration settings and ignore this section.
If you don't already have a riw translations database, set one up now:
$ riw db init
With the default configuration settings, you'll now have a small file src/locale/riw-db.json
. Check the file into source control.
Later, we'll explore what you can do with the database.
Now we have some configuration settings and a database, we can run riw's most interesting command:
$ riw app translate
Depending on the size of your app, this might take some time to complete. This command:
- Finds all
react-intl
message descriptors in your app. (By default, it usesbabel-plugin-react-intl-auto
andbabel-plugin-react-intl
to identify and extract them from your code. If you have a build system that already does this, perhaps using webpack, you can instead configure riw to pick up the JSON filesbabel-plugin-react-intl
outputs.) - Reports any duplicate message descriptor ids it finds. (
react-intl
leaves ids up to you, and it's easy to accidentally duplicate them, leading to confusing messages in your app.babel-plugin-react-intl-auto
generates ids for message descriptors automatically, but it's possible to configure this plugin so they're not guaranteed to be unique, so this step is still vital.) - Looks up every message descriptor's
defaultMessage
anddescription
(if present) in the riw translations database, for each of your app's target locales.- If a match is found, it's output in the appropriate locale file.
- If no match is found, it's added to the TODO file.
- Reports all the results.
(If you see an "unexpected token" error on running riw app translate
, you might need to add a .babelrc
file. See the FAQ.)
Here's some example output:
✔ Found 943 message descriptors from 711 files
✔ No duplicate message descriptor ids
✖ Missing translations: 4715
✔ Saved /Users/avaragado/my-react-app/src/locale/fr-FR.json
✔ Saved /Users/avaragado/my-react-app/src/locale/es-ES.json
✔ Saved /Users/avaragado/my-react-app/src/locale/de-DE.json
✔ Saved /Users/avaragado/my-react-app/src/locale/pt-BR.json
✔ Saved /Users/avaragado/my-react-app/src/locale/ja-JP.json
✔ Saved /Users/avaragado/my-react-app/src/locale/en-US.json
✔ Saved /Users/avaragado/my-react-app/src/locale/TODO-untranslated.json
With a newly initialised database (which is empty), running riw app translate
unsurprisingly reports lots of missing translations. But it creates a JSON file for each locale anyway, even if it contains no strings. If you look at the files in src/locale
now, you'll see that most of them are small: empty objects, {}
.
However, the file for your default locale is complete: it contains mappings from react-intl
id to strings for all the default messages currently defined in your code.
All the missing translations are in the TODO file, by default src/locale/TODO-untranslated.json
, as an array of objects. Each object includes the react-intl
id, default message and description (if defined), plus the file the message came from (relative to the configuration's rootDir
, which is almost certainly your package root) and the locale it needs to be translated into. Here's an example:
{
"id": "app.greeting",
"defaultMessage": "Welcome to my wonderful app",
"description": "Main body heading after user logs in",
"file": "src/components/Welcome.js",
"locale": "fr-FR"
},
Check all these files into your source control system.
... and to plug those strings and the current locale into the react-intl
IntlProvider
component.
How you do this depends on your app.
- You might load the strings for each locale dynamically using
require.ensure
orimport
promises or another mechanism. This is most appropriate for web apps, to reduce startup payload. - You might load all strings statically. This is most appropriate for Electron-based apps, where startup payload isn't as much of an issue. In this case, you might want to configure riw with
outputMode
set tosingle-file
: in this mode, riw puts all strings for all locales in a single JSON file.
If your app uses redux
to manage state, we recommend you store all (currently loaded) strings in state, indexed by locale, together with a record of the current locale. Use the connect
function from react-redux
with suitable state selectors to supply IntlProvider
with the strings for the current locale.
This connected component should render something like this:
<IntlProvider key={idLocale} locale={idLocale} messages={messages}>
...
</IntlProvider>
where idLocale
is the locale id, and messages
is the object of id/string mappings for that locale.
Setting the key
prop to idLocale
ensures that when the locale id changes, the entire subtree correctly rerenders. See react-intl
issue 243 for a discussion.
Depending on your translation service and the number of translations required, it might take hours or days to translate the strings. You can continue developing your app while translation takes place. Bear in mind this might obsolete some of the translations in progress, but it doesn't affect how you use riw.
Different translation services have different requirements. The objects in the TODO file should contain everything your translators need.
(For reasons of sanity, we recommend you don't overlap requests to your translation service: wait until one request is complete before sending another. Consider reducing each request to a smaller batch of strings if you start feeling the urge to overlap requests.)
Caution: Sanity check all results from translators. See the FAQ for tips.
Translation services return completed translations in different ways. For each individual translation, riw needs:
- The string in the default locale that was supplied to the translation service
- The description that was supplied to the translation service, if any
- The locale for the translation
- The result of the translation
The ideal format: a JSON file with an array of objects, where each object has at least the properties defaultMessage
, description
(if supplied to translators), locale
, and translation
. The TODO file, augmented with a translation
property for each object, works well. Here's an example of that (the properties id
and file
would be ignored here):
[
{
"id": "app.greeting",
"defaultMessage": "Welcome to my wonderful app",
"description": "Main body heading after user logs in",
"file": "src/components/Welcome.js",
"locale": "fr-FR",
"translation": "Bienvenue dans ma merveilleuse application",
}
]
If you have a file in the ideal format, say TODO-with-translations.json
, you can update the translations database with the command:
$ riw db import TODO-with-translations.json
If you can't produce a file in the suitable format for riw db import
, you can update the database string by string using riw db update
. For example:
$ riw db update --defaultMessage "Welcome to my wonderful app" --description "Main body heading after user logs in" --locale fr-FR --translation "Bienvenue dans ma merveilleuse application"
Running riw db update
multiple times is much slower than running riw db import
.
Check any changes to the translations database into source control.
Other useful riw db
commands:
riw db list
shows entries matching certain criteriariw db delete
deletes entries matching certain criteriariw db status
shows information about the database such as the locales it contains and how complete each locale is
Use riw db <command> --help
for usage information.
After updating the translations database, regenerate the output files to bring them up to date:
$ riw app translate
Take a look at the output files in src/locale/
to see how they've changed, and remember to check them into source control.
As this command extracts default messages from your source files, and those source files may have changed since you last ran the command, the TODO file generated this time may not be empty even if you fully translated the last TODO file.
At any time you can run riw app status
to see the current state of your output files. This command warns if it thinks you need to run riw app translate
– for example, if it looks like the database or the source files have changed since you last ran riw app translate
, or if your riw configuration has changed.
If you remove a target locale, riw app translate
does not remove that locale's output file, and riw app status
ignores it. You can remove it if you want.
The best time to ship a release is just after running riw app translate
. All the generated files correspond to the source files at the time you run the command, and so contain only those strings you need, and no out-of-date strings in any locale.
Conversely, it's unwise to ship without running riw app translate
: you may be shipping redundant and out-of-date strings.
After shipping (or not), the cycle begins again: translate untranslated strings and develop the app normally (back to ➏.➊).