yarn add --dev riw
Internationalisation! Or, if you prefer, internationalization. Better yet, i18n.
Similarly: localisation, localization, or l10n.
My first exposure to these terms was back in the early 90s, when giant modems stalked the earth and you could see each bit pulsing up and down the cables. I was a lowly tech writer with coding skillz, assigned to help the in-house French and German translators with terminology and glossary management. The product, X.desktop, was a UNIX desktop GUI used by many corporations worldwide. We shipped it to them with much hoo and ha on magnetic tape, with a set of release notes I maintained as penance for sins committed in a previous life.
That project taught me a lot about i18n and l10n, not least the distinction between them. Sadly they’re often seen as synonyms — like equating cars and petrol because they’re both a bit vroom vroom. Here’s how I describe them:
- Internationalisation: making something localisable
- Localisation: adapting something to a specific locale
You can localise without internationalising: swap one set of hard-coded strings etc for another. This is essentially forking.
It’s hard to internationalise without localising, as the act of internationalising an existing codebase by its nature gives you strings for one locale: whichever one you’d previously been hard-coding. You can certainly internationalise a codebase and then never ship more than this single locale.
It’s also worth emphasising that a locale is more than a language: it’s associated with a territory or region too. French in France is not the same as French in Canada, just as UK English is not the same as US English. Speakers of “English” can’t even agree — to pick an example entirely at random — on how to spell localisation. Nor can we agree on which side of the road to drive on, or how to write dates. A locale, at least in theory, encompasses all of that.
This is why we write locale ids like en-GB, en-US, fr-FR, de-DE, pt-BR, and so on: language, territory.
Today’s React apps
For modern-day websites built using React and not shipping on magnetic tape, there’s a great library you can use for i18n:
react-intl lets you move strings out of a React component’s render function, and replace them with a
react-intl component or function call that references the string by a unique id. The components and functions support formatting tailored to each locale — such as layout of numbers (thousands separators, decimal separators), currency (position of symbol), dates (month and day names, relative timestamps) and so on.
react-intl also supports ICU Message syntax for correct handling of the many hilarious categories of plural in human languages, and other great stuff.
As a developer, all you need to do is plug the locale id and its translated strings into
IntlProvider component at the top of your render tree, make sure you’ve loaded the rules for that locale (all based on standards), and off you go. It works amazingly well.
But there’s a gap here: and that gap is, fundamentally, localisation.
Consider the steps a dev team needs to take to ship an app localised to more than the default locale:
- Find all those strings that were moved out of the render function
- Make sure the id allocated to each string is never duplicated elsewhere
- Figure out which strings need to be translated to some or all of the locales you want to ship with, and which already have translations
- Herd translators as necessary
- Process the results of the translations into the right formats in the right files
- Wire the translations up to
- Test and (maybe) ship
- Goto 1 (because development never stops, and translations take time)
react-intl helps with the first of those, which solves a real problem. The rest, though, are left as exercises for the developers. This is not a criticism of
react-intl: it does exactly what it’s designed to do. Developers just need a bit more.
This is why I wrote riw.
What is riw?
riw began as
react-intl-workflow. Then I got fed up with typing all that, and saw that the name
riw was miraculously still available on npm. It seems to mean “slope” or “hill” in Welsh, and so it’s pronounced to rhyme with “drew”.
riw is a node-based command-line tool with two areas of functionality:
- The ability to read and write to a simple, dedicated translations database. This database maps from the default locale — the locale of the strings you’ve migrated out of your components’ render functions — to translations in any number of other locales.
- The ability to read strings from your source code (it uses a library written for
react-intl), look them up in the translations database, and write some output files.
The output files are:
- A JSON file for each locale, which your app can import and wire directly into
- A “todo” JSON file containing everything you don’t have a translation for
riw doesn’t replace
react-intl: you use both together. riw plugs easily into your repository and your processes with minimal configuration. It shouldn’t introduce any additional developer pain.
The translations database
The riw translations database is pretty simple. It’s a JSON file representing an object that maps default locale strings to descriptions to locales to translated strings. A slice might look like this:
Just like a dictionary, the translations database supports multiple entries for the same string: one for each description.
The descriptions correspond to the
description property in the
react-intl message descriptors you define in your source code. These are essential for adding context for translators, because translators shouldn’t have to guess which of the 42 meanings of “set” you intended.
To put it another way: if two semantically different labels happen to produce the same sequence of characters in one locale, the locale in which you happened to write the source code, then you just got lucky. It’s not true in general for all locales.
react-intl and in riw, descriptions in message descriptors are optional. You should use them wherever there’s the smallest chance a translator might lack enough context to choose the best translation.)
The translations database doesn’t store message ids: it’s only interested in translations. This means you can easily share a translations database between apps. You can fill it with translations for any locales you like, whether or not you use the strings or the locales in any particular app. For each app, you’ll only ship the strings you use.
There are command-line tools to update the translations database string by string (
riw db update) or in bulk (
riw db import). As it’s JSON under the hood you can use other tools to manipulate it if you need to.
riw db status to see the state of the database:
Generating translations for your app
When you want to generate some translations files to ship, you run one command:
riw app translate. This reads your app’s riw configuration (which can live in your
package.json file), inspects your source, identifies existing and missing translations, and writes the appropriate files. All you need to do is make sure your app imports strings from the appropriate files — and you only do that once.
It’s simple, and it brings a few extra benefits:
- You have an up-to-date list of missing translations corresponding to your current code.
- You discover any duplicate message descriptor ids in your code.
- You can easily change message descriptor ids in your code without having to update other files manually: just rerun the command.
- Translation files don’t contain unused translations: only translations for strings in your current code.
- Translation files don’t contain out-of-date translations: if a string/description has changed in the default locale, and there’s no match in the translations database for the changed string/description, it’s recorded as a missing translation.
You can run
riw app status to find out how complete your localisations are. This command will tell you if it thinks you need to run
riw app translate again.
OK, I’m sold
- Install riw:
yarn add --dev riwor
npm install --save-dev riw
- There’s a tutorial at https://github.com/avaragado/riw/blob/master/doc/tutorial.md
- See the repository at https://github.com/avaragado/riw