Going International at the Front-End

Simple React i18n

Piotr Maniak
Daftcode Blog

--

Illustration by Michał Jałbrzykowski

Internationalization (i18n) is as old as the Internet, but in the fast changing world we are moving from the “web pages” generated by the server-side, to “single page applications”. This is great in so many ways, but often we find ourselves rediscovering the wheel. In some cases, server-side solutions are easily transferable to the client, but sometimes it’s not that simple and there are additional things to consider.

I18n is an interesting example of that. It seems easy, it will eventually move completely towards the front-end, and it’s already done on the server-side. However, inspiration from the good old server-side can be deceptive here. For instance, we need to consider the load times of huge translation files.

Angular JS has i18n already in place. Vanilla guys and the React’ers are left to their own resources. Or are they? Let’s see…🤔

“Imma write it myself!”

Please don’t. However easy it seems, it’s a deceptive ground. There are some serious pitfalls involved. When I actually wrote them down, this whole article turned into an epic rant. Then I counted to 7 000 000, calmed down and removed the whole massive paragraph. You can thank me for that later. Let’s do something more pleasant instead!

“There’s a plugin for that”

…and the first one that pops up when you google it is i18n-webpack-plugin. That’s exactly what we’re going to cover here because it's a scientific fact that the first google result is always true and perfect. On the other hand... if you ever came across something called i18next, don’t get too suspicious. I promise you we'll talk about it. But it’s a tale for another night.

What does this i18n-webpack-plugin offer?

It’s a webpack plugin so — you guessed — it’s a pre-processor. This means that the translations are done at build time (similar to Angular). Here’s how it works:

  1. You prepare translation files that have keys pointing to messages.
  2. You put a __() function call in your code and pass it the key to the desired message.
  3. Webpack bundles files separately for each language, and replaces __() with the actual messages.
  4. Depending on the language the user chooses, you serve appropriate bundles.

This solution comes with some obvious advantages and drawbacks.

Pros

  • Very good for performance: no redundant locales shipped, no libraries, no computation.
  • Basic config is really straightforward and allows you to build on it easily.
  • Perfect when you’re thinking of splitting your app into separate instances for different languages (you can even host them at separate domains).

Cons

  • Constructing keys dynamically in runtime is limited and has its drawbacks.
  • When preparing your translations you will have to restart/rebuild your app to see the changes.
  • The webpack config can become tricky (fortunately, I got you covered!).
  • Language change reloads a page (is that even relevant?).

Still interested? 😎

So let’s finally dive in…

Boilerplate

Everything we’ll be covering is available as a working boilerplate at https://github.com/m8ms/boilr

We’ll be using:

  • webpack 4.16
  • webpack-dev-server
  • html-webpack-plugin
  • react
  • babel
  • hereby discussed static i18n-webpack-plugin

I encourage you to check it out and play with it.

(it’s a fully fledged boilerplate so it also includes some stuff irrelevant to this tutorial like: sass & mini-css-extract-plugin, jest, alt (flux), react-router, eslint, stylelint, uglifyjs, optimize-css-assets; you can kick all that away, or just pay no attention to it)

Let’s go!

Install i18n-webpack-plugin by doing this to your console:

npm install i18n-webpack-plugin

Or if you happen to be more of a yarny person just go:

yarn add i18n-webpack-plugin

The most basic setup

For the basic setup and a single language to work, we need just three steps.

Include your locale file in your webpack.config.js. We're gonna use simple inline json, but in practice, you can import any file that you are able to parse with nodejs.

Include and initialize the plugin:

Put your keys in your code.

No need to import any libraries!

We’re done. Now, just build! If all you needed is to make sure that someday you will be able to translate your app, you can go back to things more interesting than languages 😉

Otherwise just read on 👇

Something more complete

Wait! How do we switch the language? How can we ship all languages?

Don’t fret, here it is!

Bundling for several languages

The i18n-webpack-plugin git page suggests us to create an array of webpack configs. Let’s do just that:

As you can see in the outputfield, we create bundles for each language in separate directories. html-webpack-plugin will generate index.html files in each directory, injecting references to particular languages. You can set up one of the languages as your default and put it in the root directory.

And the “change language” buttons will look like this:

What about that pesky global __()?

Cloggs your eslint, breaks your tests. The hell with that!?

Eslint is simple to fix, just add it to globals in your .eslintrc:

"rules": { /* ... */ },
"globals": {
"__": true
},

Tests are slightly more tricky, but doable 😏 If you use jest, here’s an example of a shim function you can add to your shim.js:

It mocks the behavior of the real __(). One benefit of this solution is that you actually get real translations in the tests (which you might find useful in the snapshot tests for example).

By the way — notice that I included a real life locale file: en.yml. This is to demonstrate how you can use any format, as long as you have a way of parsing it.

Interpolation

Interpolation stands for putting some dynamic values into your string. You might be familiar with it from the JS syntax:

`Mary stole ${count} apple(s).`

The standard approach in i18n is to create translations that look like this:

`Mary stole {0} apple(s).`

and then parse it with a format() or printf() function, that will replace the placeholders with data.

i18n-webpack-plugin does not come with such a utility. Luckily, there’s the internet. You can try this package 👉 https://github.com/alexei/sprintf.js

Pluralization

You probably noticed the annoying (s) parenthesis in the “thieving Mary” sentence. Not very elegant. Enter pluralization.

You can do it two ways using interpolation:

  1. Create two separate sentences: Mary stole 1 apple. and Mary stole {0} apples. And use them interchangeably depending on the value.
  2. Create one sentence: Mary stole {0} {1}. and two forms of the apple.

I prefer the 2nd approach because it’s more ascetic. Plus you probably know that some languages have even 6 plural forms. This is a complex topic and you should approach each language separately. What you need to be prepared for right away is that there will have to be a custom function included for each language that can calculate the proper form for you.

If you’d like to know more. You can check out this article: https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals

Quick note on dates, moment.js, and webpack

moment.js does a very good job with localizing your dates, you can freely use it. However, be aware that the default npm package of moment.js will include all the languages in your bundle. This is very undesirable. To amend this use the ignore plugin:

new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)

Real life scenarios

So should you use i18n-webpack-plugin in your app? I have implemented i18n this way in two projects. One turned out to be a great decision but the other... not really 😓

Success!

The success story is about a large portal that has already gathered massive amounts of locales. These translations are kept in huge messages_[lang].properties files and used by a Java Spring application to render HTML on the server side. What we had to do was to use the same files and inject translations into JS. i18n-webpack-pluginturned out to be the perfect solution, because it didn't require any tweaks in the JS app itself.

Our main task was to replace all the strings with the __() function calls. Minimum setup at webpack.config.js and we were ready to go. The app was designed to be deployed on separate domains for each language, so the duplicate bundles didn't matter in this case. We didn't have to worry about the numerous duplicate keys that the locales had, because we didn't include the files with the project.

P.S. the build time didn’t actually grow much with huge locale files 🙃

Fail

Another implementation was a typical react SPA. Knowing that implementing basic i18n-webpack-plugin is very easy I jumped right in.

It turned out that not everyone is so excited about the whole idea…

Dev mode only in webpack 4

The first thing that I learned was that webpack-dev-server for webpack < 4.0 wouldn't work with multiple configs. Therefore we had to forget about working with the locales in the dev mode. We had to set up just one language ad hoc. If you wanted to change the language you'd need to tweak the setup and reset the dev-server. In practice, this is not a real setback, because you don't really need to have all the languages in the dev mode. But still…

Some people find it tedious

Once the i18n is in place, the translation begins. Due to the fact that the locales are loaded at build time, it is necessary to reset the app each time you make changes in the locale files. This can be really annoying 😓

And here’s another thing. Static pre-processing limits the possibilities of concatenating keys. If you use __('some.group').someKey, everything will be peachy, as long as some.group.someKeys really exists. But if there is no someKey in the group, you will get undefined, which makes it hard to identify the missing translation. To make things worse - if some.group doesn't exist, you will get an error!

To be frank, I don’t recommend dynamically concatenating translation keys. It’s better to declare lists of messages as maps, and then use those maps explicitly in your code:

This is tedious. But safe.

What’s more, some people would like to manage their messages per component:

This is dangerous unless you are 100% sure that every locale file will contain all the keys. Otherwise, if you really need to go this way, you should consider some fully fledged libraries, like i18next.

To recap…

i18n-webpack-plugin is quite a simplistic approach with all the drawbacks and advantages that can come with it. It's easy to set up and doesn't involve any changes in the architecture. On top of that, it literally has no performance impact on your app. These reasons are more than enough for me. But if you're 100% sure that you must fiddle with the locale keys, plus if your translations are small, you should check out i18next. You can also wait a couple of weeks until I write another post about it 🙃

Thanks for your attention! Happy translating 😎

If you enjoyed this post, please hit the clap button below 👏👏👏

You can also follow us on Facebook, Twitter and LinkedIn.

--

--