Internationalizing React Apps with FormatJS
We'll explore the internationalization of a React application using the powerful FormatJS library, and I'll show you some tricks to make it work well with TypeScript.
i18n
react
react-intl
format.js
frontend
Internationalization, or i18n for short (because there are 18 letters between the first i and the last n), is the process of preparing your website or app to support local languages and cultural preferences. So, whereas an English (British) visitor of your website would get to see:
The perfect temperature for a cup of tea is 71.29 degrees Celsius
A Spanish visitor would get to see:
La temperatura perfecta para una taza de té es de 71,29 grados Celsius
Mind you that internationalization is not just about translating words, though. E.g.: for Brits, it's common to use a dot as the decimal separator (71.29), whereas for Spaniards it's common to use a comma (7,29).
By using language and region-specific
- words,
- measurement units (metric system vs. imperial system),
- decimal separators, and
- writing style,
internationalization allows you to offer a local experience to visitors or users from different cultural backgrounds.
So, in this article we'll have a look at setting up internationalization in a React application,
using the powerful FormatJS react-intl
library.
You should think of FormatJS as a general internationalization framework, with official bindings for a number of client-side frameworks, like the react-intl binding for the React framework.
We'll have a look at how to set up react-intl
in such a way that translation files are dynamically loaded,
depending on whether your visitor actually needs them;
we'll introduce some type-safety into the project,
so it's much harder to accidentally reference non-existing translation snippets,
and we'll finish with a useful overview of the powerful FormatJS message syntax.
Table of Contents
- Create React App
- Minimal i18n Example
- Dynamically Loaded Translation Files
- Statically Typed Translation Keys
- FormatJS Syntax Overview
Create React App
You can find the full source code for this demo project on GitHub.
To follow along, please make sure to have the standard suite of frontend tooling installed (node
, npm
, npx
and yarn
).
The easiest way to set up a new React application, is via the Create React App project. To generate a Typescript-based boilerplate application, simply execute the following command.
To keep things lean, I'll immediately get rid of the following auto-generated files which we won't be using.
README.md
src/App.test.tsx
src/serviceWorker.ts
(remember to also remove theserviceWorker
references fromsrc/index.tsx
)src/setupTests.ts
Minimal i18n Example
Let's start out by including the react-intl
library into our project.
Next, we need to set up an internationalization context via the IntlProvider
component,
because this will allow us to use components like <FormattedMessage id="websiteGreeting" />
which react-intl
will resolve to something like Welcome to my website!
,
depending on the currently loaded locale and translation file.
The code snippet below is a minimal example of this,
where the App.tsx
main component will simply render Welcome to my website!
onto the page.
In order to support an additional language,
we'll just have to make sure that we're using a different set of IntlProvider
properties for Spanish visitors.
So let's convert the functional App.tsx
component into a class-based stateful one,
so we'll be able to easily switch between locales and translation objects via state variables.
There are some obvious downsides to this approach. First of all, there's some unnecessary duplication going on with the English translation object.
Second; notice how we're loading both the English and Spanish translation objects,
directly when the App.tsx
component loads
(because everything's inlined in the same module).
It would be much better in terms of page load optimization if English visitors wouldn't have to load Spanish translation data at all, since these translation objects can grow pretty large for bigger applications.
So that's why, in the next section,
we'll set up this i18n
directory in our project,
which will house a Promise-returning loadTranslation
function,
allowing us to update our App.tsx
component to this:
Here, I've chosen to show the visitor a Loading...
message until the default (English) translation object has finished loading.
I've found this approach useful for scenarios where I already have the logged-in user's preferences in-memory, as it allows me to decide whether to only load the English or the Spanish translation file.
If this doesn't apply to your situation,
consider loading your default (English) translation file as an ordinary direct import at the top of App.tsx
.
Dynamically Loaded Translation Files
In this section, we'll be implementing the aforementioned loadTranslation
function,
which is expected to return a promise containing the English or Spanish translation object:
Being able to dynamically load translations is useful because these translation objects can grow pretty big, as the application gets larger. And we wouldn't want to force our English (British) visitors to unnecessarily load the Spanish translation object.
The implementation of loadTranslation
is fairly straightforward:
Where the above snippet refers to the following translation files.
We're using dynamic imports like import('./es')
to let Webpack know we want to create
splitting points
in our final JavaScript bundle.
Simply put, this means that the Spanish es.ts
translation file will only be loaded
if the loadTranslation
function is explicitly called with 'es'
as its argument.
We can put this to the test as follows:
- Create a Webpack production build via
yarn build
- Serve the production files from the
/build
folder vianpx serve build
- Visit the URL from
npx serve
(usually localhost:5000) - Open up the developer tools, and inspect the JS network requests
- On the page itself, click the button which says Spanish
You should now see the browser make a request for the Spanish translation file,
which would be called something like 4.055e63d3.chunk.js
.
If you click on the request, you should be able to find the string ¡Bienvenidos a mi sitio web!
somewhere in this JavaScript chunk.
We've now successfully reached a point where Webpack no longer combines all of the translations into a single (potentially huge) JavaScript bundle, but instead loads each translation's JavaScript chunk on-demand, depending on whether it was explicitly requested for the visitor.
Statically Typed Translation Keys
With our current implementation, we're at risk for two types of errors:
- referencing a non-existing translation id from our
<FormattedMessage id="..." />
components - translation files getting out-of-sync
With the help of TypeScript, we can protect ourselves against these potential problems by introducing some kind of TranslationKey
enum type.
The next step would be to update our App.tsx
file (and every other existing + future component) to only use this enum, instead of using string literals.
And the final step would be to add a static type definition to both of our en.ts
and es.ts
translation files.
This should prevent the English and Spanish translations from getting out-of-sync,
since you can't just add or remove entries from either translation file,
because that would break the Record<TranslationKey, string>
contract.
FormatJS Syntax Overview
Now that we've successfully set up a React application with the react-intl
library,
we'll have a closer look at the FormatJS message syntax,
because there's much more to it than what we've seen so far.
FormatJS describes an abstract message syntax
which can be used for creating expressive translation templates,
but requires some extra knowledge about the FormatJS React binding
(react-intl
and its various components)
to deploy in a React application.
Because we've only looked at simple substitutions of translation snippets, I will end this article with an overview of some neat FormatJS syntax features.
Argument
This formatting option is used for inserting placeholders into your translation snippets.
Argument (Number Type)
This one is used for formatting numerical placeholders, like numbers and percentages.
Argument (Date Type)
This one is used for formatting date-like placeholders in a locale-specific manner.
Argument (Time Type)
This one is used for formatting time-like placeholders in a locale-specific manner.
Select
This control-flow formatting option can be used as some kind of switch statement inside your translation snippet.
Plural
This formatting option can be used for pluralization by switching on a numeric placeholder variable.
Rich Text Formatting
FormatJS supports rich text formatting by allowing you to specify custom XML tags in your translation snippets, and then resolving these tags against your own resolver functions.