Menu Search
Jump to the content X X
Smashing Conf Barcelona

You know, we use ad-blockers as well. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. our upcoming SmashingConf Barcelona, dedicated to smart front-end techniques and design patterns.

React Internationalization – How To

First of all, let’s define some vocabulary. “Internationalization” is a long word, and there are at least two widely used abbreviations: “intl,” “i18n”. “Localization” can be shortened to “l10n”.

Internationalizing React Apps

Internationalization can be generally broken down into the following challenges:

  • detecting the user’s locale;
  • translating UI elements, titles and hints;
  • serving locale-specific content such as dates, currencies and numbers.

Note: In this article, I am going to focus only on front-end part. We’ll develop a simple universal React application with full internationalization support.

Further Reading on SmashingMag: Link1

Let’s use my boilerplate repository5 as a starting point. Here we have the Express web server for server-side rendering, webpack for building client-side JavaScript, Babel for translating modern JavaScript to ES5, and React for the UI implementation. We’ll use better-npm-run to write OS-agnostic scripts, nodemon to run a web server in the development environment and webpack-dev-server to serve assets.

Our entry point to the server application is server.js. Here, we are loading Babel and babel-polyfill to write the rest of the server code in modern JavaScript. Server-side business logic is implemented in src/server.jsx. Here, we are setting up an Express web server, which is listening to port 3001. For rendering, we are using a very simple component from components/App.jsx, which is also a universal application part entry point.

Our entry point to the client-side JavaScript is src/client.jsx. Here, we mount the root component component/App.jsx to the placeholder react-view in the HTML markup provided by the Express web server.

So, clone the repository, run npm install and execute nodemon and webpack-dev-server in two console tabs simultaneously.

In the first console tab:

 git clone https://github.com/yury-dymov/smashing-react-i18n.git cd smashing-react-i18n  npm install npm run nodemon

And in the second console tab:

 cd smashing-react-i18n  npm run webpack-devserver

A website should become available at localhost:30016. Open your favorite browser and try it out.

We are ready to roll!

1. Detecting The User’s Locale Link

There are two possible solutions to this requirement. For some reason, most popular websites, including Skype’s and the NBA’s, use Geo IP to find the user’s location and, based on that, to guess the user’s language. This approach is not only expensive in terms of implementation, but also not really accurate. Nowadays, people travel a lot, which means that a location doesn’t necessarily represent the user’s desired locale. Instead, we’ll use the second solution and process the HTTP header Accept-Language on the server side and extract the user’s language preferences based on their system’s language settings. This header is sent by every modern browser within a page request.

Accept-Language Request Header Link

The Accept-Language request header provides the set of natural languages that are preferred as a response to the request. Each language range may be given an associated “quality” value, which represents an estimate of the user’s preference for the languages specified by that range. The quality value defaults to q=1. For example, Accept-Language: da, en-gb;q=0.8, en;q=0.7 would mean, “I prefer Danish, but will accept British English and other types of English.” A language range matches a language tag if it exactly equals the tag or if it exactly equals a prefix of the tag such that the first tag character following the prefix is -.

(It is worth mentioning that this method is still imperfect. For example, a user might visit your website from an Internet cafe or a public computer. To resolve this, always implement a widget with which the user can change the language intuitively and that they can easily locate within a few seconds.)

Implementing Detection of User’s Locale Link

Here is a code example for a Node.js Express web server. We are using the accept-language package, which extracts locales from HTTP headers and finds the most relevant among the ones supported by your website. If none are found, then you’d fall back to the website’s default locale. For returning users, we will check the cookie’s value instead.

Let’s start by installing the packages:

 npm install --save accept-language  npm install --save cookie-parser js-cookie

And in src/server.jsx, we’d have this:

import cookieParser from 'cookie-parser';
import acceptLanguage from 'accept-language';

acceptLanguage.languages(['en', 'ru']);

const app = express();

app.use(cookieParser());


function detectLocale(req) {
  const cookieLocale = req.cookies.locale;

  return acceptLanguage.get(cookieLocale || req.headers['accept-language']) || 'en';
}
…

app.use((req, res) => {
  const locale = detectLocale(req);
  const componentHTML = ReactDom.renderToString(<App />);

  res.cookie('locale', locale, { maxAge: (new Date() * 0.001) + (365 * 24 * 3600) });
  return res.end(renderHTML(componentHTML));
});

Here, we are importing the accept-language package and setting up English and Russian locales as supported. We are also implementing the detectLocale function, which fetches a locale value from a cookie; if none is found, then the HTTP Accept-Language header is processed. Finally, we are falling back to the default locale (en in our example). After the request is processed, we add the HTTP header Set-Cookie for the locale detected in the response. This value will be used for all subsequent requests.

2. Translating UI Elements, Titles And Hints Link

I am going to use the React Intl7 package for this task. It is the most popular and battle-tested i18n implementation of React apps. However, all libraries use the same approach: They provide “higher-order components” (from the functional programming design pattern8, widely used in React), which injects internationalization functions for handling messages, dates, numbers and currencies via React’s context features.

First, we have to set up the internationalization provider. To do so, we will slightly change the src/server.jsx and src/client.jsx files.

 npm install --save react-intl

Here is src/server.jsx:

import { IntlProvider } from 'react-intl';

…
--- const componentHTML = ReactDom.renderToString(<App />);
const componentHTML = ReactDom.renderToString(
  <IntlProvider locale={locale}>
    <App />
  </IntlProvider>
);
…

And here is src/client.jsx:

import { IntlProvider } from 'react-intl';
import Cookie from 'js-cookie';

const locale = Cookie.get('locale') || 'en';
…
---  ReactDOM.render(<App />, document.getElementById('react-view'));
ReactDOM.render(
  <IntlProvider locale={locale}>
    <App />
  </IntlProvider>,
  document.getElementById('react-view')
);

So, now all IntlProvider child components will have access to internationalization functions. Let’s add some translated text to our application and a button to change the locale (for testing purposes). We have two options: either the FormattedMessage component or the formatMessage function. The difference is that the component will be wrapped in a span tag, which is fine for text but not suitable for HTML attribute values such as alt and title. Let’s try them both!

Here is our src/components/App.jsx file:

import { FormattedMessage } from 'react-intl';
…
--- <h1>Hello World!</h1>
<h1><FormattedMessage id="app.hello_world" defaultMessage="Hello World!" description="Hello world header greeting" /></h1>

Please note that the id attribute should be unique for the whole application, so it makes sense to develop some rules for naming your messages. I prefer to follow the format componentName.someUniqueIdWithInComponent. The defaultMessage value will be used for your application’s default locale, and the description attribute gives some context to the translator.

Restart nodemon and refresh the page in your browser. You should still see the “Hello World” message. But if you open the page in the developer tools, you will see that text is now inside the span tags. In this case, it isn’t an issue, but sometimes we would prefer to get just the text, without any additional tags. To do so, we need direct access to the internationalization object provided by React Intl.

Let’s go back to src/components/App.jsx:


--- import { FormattedMessage } from 'react-intl';
import { FormattedMessage, intlShape, injectIntl, defineMessages } from 'react-intl';

const propTypes = {
  intl: intlShape.isRequired,
};

const messages = defineMessages({
  helloWorld2: {
    id: 'app.hello_world2',
    defaultMessage: 'Hello World 2!',
  },
});

--- export default class extends Component {
class App extends Component {
  render() {
    return (
      <div className="App">
        <h1>
          <FormattedMessage
            id="app.hello_world"
            defaultMessage="Hello World!"
            description="Hello world header greeting"
          />
        </h1>
        <h1>{this.props.intl.formatMessage(messages.helloWorld2)}</h1>
      </div>
    );
  }
}

App.propTypes = propTypes;

export default injectIntl(App);

We’ve had to write a lot more code. First, we had to use injectIntl, which wraps our app component and injects the intl object. To get the translated message, we had to call the formatMessage method and pass a message object as a parameter. This message object must have unique id and defaultValue attributes. We use defineMessages from React Intl to define such objects.

The best thing about React Intl is its ecosystem. Let’s add babel-plugin-react-intl to our project, which will extract FormattedMessages from our components and build a translation dictionary. We will pass this dictionary to the translators, who won’t need any programming skills to do their job.

 npm install --save-dev babel-plugin-react-intl

Here is .babelrc:

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "env": {
    "development": {
      "plugins":[
        ["react-intl", {
          "messagesDir": "./build/messages/"
        }]
      ]
    }
  }
}

Restart nodemon and you should see that a build/messages folder has been created in the project’s root, with some folders and files inside that mirror your JavaScript project’s directory structure. We need to merge all of these files into one JSON. Feel free to use my script9. Save it as scripts/translate.js.

Now, we need to add a new script to package.json:

"scripts": {
  …
  "build:langs": "babel scripts/translate.js | node",
  …
}

Let’s try it out!

 npm run build:langs

You should see an en.json file in the build/lang folder with the following content:

{
  "app.hello_world": "Hello World!",
  "app.hello_world2": "Hello World 2!"
}

It works! Now comes interesting part. On the server side, we can load all translations into memory and serve each request accordingly. However, for the client side, this approach is not applicable. Instead, we will send the JSON file with translations once, and a client will automatically apply the provided text for all of our components, so the client gets only what it needs.

Let’s copy the output to the public/assets folder and also provide some translation.

 ln -s ../../build/lang/en.json public/assets/en.json

Note: If you are a Windows user, symlinks are not available to you, which means you have to manually copy the command below every time you rebuild your translations:

cp ../../build/lang/en.json public/assets/en.json

In public/assets/ru.json, we need the following:

{
  "app.hello_world": "Привет мир!",
  "app.hello_world2": "Привет мир 2!"
}

Now we need to adjust the server and client code.

For the server side, our src/server.jsx file should look like this:

--- import { IntlProvider } from 'react-intl';
import { addLocaleData, IntlProvider } from 'react-intl';
import fs from 'fs';
import path from 'path';

import en from 'react-intl/locale-data/en';
import ru from 'react-intl/locale-data/ru';

addLocaleData([…ru, …en]);

const messages = {};
const localeData = {};

['en', 'ru'].forEach((locale) => {
  localeData[locale] = fs.readFileSync(path.join(__dirname, `../node_modules/react-intl/locale-data/${locale}.js`)).toString();
  messages[locale] = require(`../public/assets/${locale}.json`);
});

--- function renderHTML(componentHTML) {
function renderHTML(componentHTML, locale) {
…
      <script type="application/javascript" src="${assetUrl}/public/assets/bundle.js"></script>
      <script type="application/javascript">${localeData[locale]}</script>
    
…

--- <IntlProvider locale={locale}>
<IntlProvider locale={locale} messages={messages[locale]}>
…
---  return res.end(renderHTML(componentHTML));
return res.end(renderHTML(componentHTML, locale));
```

Here we are doing the following:

  • caching messages and locale-specific JavaScript for the currency, DateTime and Number formatting during startup (to ensure good performance);
  • extending the renderHTML method so that we can insert locale-specific JavaScript into the generated HTML markup;
  • providing the translated messages to IntlProvider (all of those messages are now available to child components).

For the client side, first we need to install a library to perform AJAX requests. I prefer to use isomorphic-fetch because we will very likely also need to request data from third-party APIs, and isomorphic-fetch can do that very well in both client and server environments.

 npm install --save isomorphic-fetch

Here is src/client.jsx:

--- import { IntlProvider } from 'react-intl';
import { addLocaleData, IntlProvider } from 'react-intl';
import fetch from 'isomorphic-fetch';

const locale = Cookie.get('locale') || 'en';

fetch(`/public/assets/${locale}.json`)
  .then((res) => {
    if (res.status >= 400) {
      throw new Error('Bad response from server');
    }

    return res.json();
  })
  .then((localeData) => {
    addLocaleData(window.ReactIntlLocaleData[locale]);

    ReactDOM.render(
---        <IntlProvider locale={locale}>
      <IntlProvider locale={locale} messages={localeData}>
…
    );
}).catch((error) => {
  console.error(error);
});

We also need to tweak src/server.jsx, so that Express serves the translation JSON files for us. Note that in production, you would use something like nginx instead.

app.use(cookieParser());
app.use('/public/assets', express.static('public/assets'));

After the JavaScript is initialized, client.jsx will grab the locale from the cookie and request the JSON file with the translations. Afterwards, our single-page application will work as before.

Time to check that everything works fine in the browser. Open the “Network” tab in the developer tools, and check that JSON has been successfully fetched by our client.

react internationalization10

AJAX request (View large version11)

To finish this part, let’s add a simple widget to change the locale, in src/components/LocaleButton.jsx:

import React, { Component, PropTypes } from 'react';
import Cookie from 'js-cookie';

const propTypes = {
  locale: PropTypes.string.isRequired,
};

class LocaleButton extends Component {
  constructor() {
    super();

    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    Cookie.set('locale', this.props.locale === 'en' ? 'ru' : 'en');
    window.location.reload();
  }

  render() {
    return <button onClick={this.handleClick}>{this.props.locale === 'en' ? 'Russian' : 'English'};
  }
}

LocaleButton.propTypes = propTypes;

export default LocaleButton;

Add the following to src/components/App.jsx:

import LocaleButton from './LocaleButton';

…

    <h1>{this.props.intl.formatMessage(messages.helloWorld2)}</h1>
    <LocaleButton locale={this.props.intl.locale} />

Note that once the user changes their locale, we’ll reload the page to ensure that the new JSON file with the translations is fetched.

High time to test! OK, so we’ve learned how to detect the user’s locale and how to show translated messages. Before moving to the last part, let’s discuss two other important topics.

Pluralization And Templates Link

In English, most words take one of two possible forms: “one apple,” “many apples.” In other languages, things are a lot more complicated. For example, Russian has four different forms. Hopefully, React Intl will help us to handle pluralization accordingly. It also supports templates, so you can provide variables that will be inserted into the template during rendering. Here’s how it works.

In src/components/App.jsx, we have the following:

const messages = defineMessages({
  counting: {
    id: 'app.counting',
    defaultMessage: 'I need to buy {count, number} {count, plural, one {apple} other {apples}}'
  },

…

    <LocaleButton locale={this.props.intl.locale} />
    <div>{this.props.intl.formatMessage(messages.counting, { count: 1 })}</div>
    <div>{this.props.intl.formatMessage(messages.counting, { count: 2 })}</div>
    <div>{this.props.intl.formatMessage(messages.counting, { count: 5 })}</div>

Here, we are defining a template with the variable count. We will print either “1 apple” if count is equal to 1, 21, etc. or “2 apples” otherwise. We have to pass all variables within formatMessage‘s values option.

Let’s rebuild our translation file and add the Russian translations to check that we can provide more than two variants for languages other than English.

npm run build:langs

Here is our public/assets/ru.json file:

{
  …
  "app.counting": "Мне нужно купить {count, number} {count, plural, one {яблоко} few {яблока} many {яблок}}"
}

All use cases are covered now. Let’s move forward!

3. Serving Locale-Specific Content Such As Dates, Currencies And Numbers Link

Your data will be represented differently depending on the locale. For example, Russian would show 500,00 $ and 10.12.2016, whereas US English would show $500.00 and 12/10/2016.

React Intl provides React components for such kinds of data and also for the relative rendering of time, which will automatically be updated each 10 seconds if you do not override the default value.

Add this to src/components/App.jsx:

--- import { FormattedMessage, intlShape, injectIntl, defineMessages } from 'react-intl';
import {
  FormattedDate,
  FormattedRelative,
  FormattedNumber,
  FormattedMessage,
  intlShape,
  injectIntl,
  defineMessages,
} from 'react-intl';

…

<div>{this.props.intl.formatMessage(messages.counting, { count: 5 })}</div>
<div><FormattedDate value={Date.now()} /></div>
<div><FormattedNumber value="1000" currency="USD" currencyDisplay="symbol" style="currency" /></div>
<div><FormattedRelative value={Date.now()} /></div>

Refresh the browser and check the page. You’ll need to wait for 10 seconds to see that the FormattedRelative component has been updated.

You’ll find a lot more examples in the official wiki12.

Cool, right? Well, now we might face another problem, which affects universal rendering.

Universal rendering is broken13

Universal rendering is broken. (View large version14)

On average, two seconds will elapse between when the server provides markup to the client and the client initializes client-side JavaScript. This means that all DateTimes rendered on the page might have different values on the server and client sides, which, by definition, breaks universal rendering. To resolve this, React Intl provides a special attribute, initialNow. This provides a server timestamp that will initially be used by client-side JavaScript as a timestamp; this way, the server and client checksums will be equal. After all components have been mounted, they will use the browser’s current timestamp, and everything will work properly. So, this trick is used only to initialize client-side JavaScript, in order to preserve universal rendering.

Here is src/server.jsx:

--- function renderHTML(componentHTML, locale) {
function renderHTML(componentHTML, locale, initialNow) {
  return `
    <!DOCTYPE html>
      <html>
      <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Hello React</title>
      </head>
      <body>
        <div id="react-view">${componentHTML}</div>
        <script type="application/javascript" src="${assetUrl}/public/assets/bundle.js"></script>
        <script type="application/javascript">${localeData[locale]}</script>
        <script type="application/javascript">window.INITIAL_NOW=${JSON.stringify(initialNow)}</script>
      </body>
    </html>
  `;
}

    const initialNow = Date.now();
    const componentHTML = ReactDom.renderToString(
---   <IntlProvider locale={locale} messages={messages[locale]}>
      <IntlProvider initialNow={initialNow} locale={locale} messages={messages[locale]}>
        <App />
      </IntlProvider>
    );

    res.cookie('locale', locale, { maxAge: (new Date() * 0.001) + (365 * 24 * 3600) });
---   return res.end(renderHTML(componentHTML, locale));
    return res.end(renderHTML(componentHTML, locale, initialNow));

And here is src/client.jsx:

--- <IntlProvider locale={locale} messages={localeData}>
<IntlProvider initialNow={parseInt(window.INITIAL_NOW, 10)} locale={locale} messages={localeData}>

Restart nodemon, and the issue will almost be gone! It might persist because we are using Date.now(), instead of some timestamp provided by the database. To make the example more realistic, in app.jsx replace Date.now() with a recent timestamp, like 1480187019228.

(You might face another issue when the server is not able to render the DateTime in the proper format, which will also break universal rendering. This is because version 4 of Node.js is not built with Intl support by default. To resolve this, follow one of the solutions described in the official wiki15.)

4. A Problem Link

It sounds too good to be true so far, doesn’t it? We as front-end developers always have to be very cautious about anything, given the variety of browsers and platforms. React Intl uses the native Intl browser API for handling the DateTime and Number formats. Despite the fact that it was introduced in 2012, it is still not supported by all modern browsers. Even Safari supports it partially only since iOS 10. Here is the whole table from CanIUse for reference.

Internationalization browser support16

Intl browser support (View large version17)

This means that if you are willing to cover a minority of browsers that don’t support the Intl API natively, then you’ll need a polyfill. Thankfully, there is one, Intl.js18. It might sound like a perfect solution once again, but from my experience, it has its own drawbacks. First of all, you’ll need to add it to the JavaScript bundle, and it is quite heavy. You’ll also want to deliver the polyfill only to browsers that don’t support the Intl API natively, to reduce your bundle size. All of these techniques are well known, and you might find them, along with how to do it with webpack, in Intl.js’ documentation19. However, the biggest issue is that Intl.js is not 100% accurate, which means that the DataTime and Number representations might differ between the server and client, which will break server-side rendering once again. Please refer to the relevant GitHub issue20 for more details.

I’ve come up with another solution, which certainly has its own drawbacks, but it works fine for me. I implemented a very shallow polyfill21, which has only one piece of functionality. While it is certainly unusable for many cases, it adds only 2 KB to the bundle’s size, so there is not even any need to implement dynamic code-loading for outdated browsers, which makes the overall solution simpler. Feel free to fork and extend it if you think this approach would work for you.

Conclusion Link

Well, now you might feel that things are becoming too complicated, and you might be tempted to implement everything yourself. I did that once; I wouldn’t recommend it. Eventually, you will arrive at the same ideas behind React Intl’s implementation, or, worse, you might think there are not many options to make certain things better or to do things differently. You might think you can solve the Intl API support issue by relying on Moment.js22 instead (I won’t mention other libraries with the same functionality because they are either unsupported or unusable). Fortunately, I tried that, so I can save you a lot of time. I’ve learned that Moment.js is a monolith and very heavy, so while it might work for some folks, I wouldn’t recommend it. Developing your own polyfill doesn’t sound great because you will surely have to fight with bugs and support the solution for quite some time. The bottom line is that there is no perfect solution at the moment, so choose the one that suits you best.

(If you feel lost at some point or something doesn’t work as expected, check the “solution” branch of my repository23.)

Hopefully, this article has given you all of the knowledge needed to build an internationalized React front-end application. You should now know how to detect the user’s locale, save it in the cookie, let the user change their locale, translate the user interface, and render currencies, DateTimes and Numbers in the appropriate formats! You should also now be aware of some traps and issues you might face, so choose the option that fits your requirements, bundle-size budget and number of languages to support.

(rb, al, il, vf)

Footnotes Link

  1. 1 https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/#further-reading-on-smashingmag
  2. 2 https://www.smashingmagazine.com/2016/04/consider-react-native-mobile-app/
  3. 3 https://www.smashingmagazine.com/2016/09/how-to-scale-react-applications/
  4. 4 https://www.smashingmagazine.com/2016/04/the-beauty-of-react-native-building-your-first-ios-app-with-javascript-part-1/
  5. 5 https://github.com/yury-dymov/smashing-react-i18n
  6. 6 http://localhost:3001
  7. 7 https://github.com/yahoo/react-intl
  8. 8 https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.zb3t19yyx
  9. 9 https://github.com/yury-dymov/smashing-react-i18n/blob/solution/scripts/translate.js
  10. 10 https://www.smashingmagazine.com/wp-content/uploads/2017/01/AJAX-request-large-opt.png
  11. 11 https://www.smashingmagazine.com/wp-content/uploads/2017/01/AJAX-request-large-opt.png
  12. 12 https://github.com/yahoo/react-intl/wiki/Components
  13. 13 https://www.smashingmagazine.com/wp-content/uploads/2017/01/universal-rendering-is-broken-large-opt.png
  14. 14 https://www.smashingmagazine.com/wp-content/uploads/2017/01/universal-rendering-is-broken-large-opt.png
  15. 15 https://github.com/nodejs/node/wiki/Intl
  16. 16 https://www.smashingmagazine.com/wp-content/uploads/2017/01/intl-browser-support-large-opt.png
  17. 17 https://www.smashingmagazine.com/wp-content/uploads/2017/01/intl-browser-support-large-opt.png
  18. 18 https://github.com/andyearnshaw/Intl.js
  19. 19 https://github.com/andyearnshaw/Intl.js/
  20. 20 https://github.com/andyearnshaw/Intl.js/issues/124
  21. 21 https://github.com/yury-dymov/intl-polyfill
  22. 22 http://momentjs.com/
  23. 23 https://github.com/yury-dymov/smashing-react-i18n/tree/solution

↑ Back to top Tweet itShare on Facebook

Yury Dymov is currently working as a Solution Architect at SAP. He has over ten years of web and mobile application development experience and many projects in his portfolio delivered for the biggest enterprises of the world.

  1. 1

    What about tying it in with a URL to help Google and general SEO out.
    https://support.google.com/webmasters/answer/189077?hl=en

    For example: domain.com/en, domain.com/ru

    2
    • 2

      Certainly it is a very good practice. I focused more on front-end side, and route management topic is beyond this article as it is very long already.

      3
  2. 3

    Typo: localization is l10n, not i10n

    1
  3. 4

    l10n and i18n does NOT mean the same.

    “Internationalization is the design and development of a product, application or document content that enables easy localization for target audiences that vary in culture, region, or language.

    Localization refers to the adaptation of a product, application or document content to meet the language, cultural and other requirements of a specific target market (a locale).”

    Source: https://blog.mozilla.org/l10n/2011/12/14/i18n-vs-l10n-whats-the-diff/

    3
  4. 6

    Alexey Ivanov

    January 19, 2017 8:18 pm

    Just finished removing react-intl from one of our projects few days ago and replacing it with counterpart. Results:

    1. Bundle size reduced from 1052kB to 907kB.
    2. Removed a lot of boilerplate code from jest/enzyme tests. Before that I need function that wrapped each tested component with IntlProvider and loaded locale for it, now I don’t need it.
    3. Component code become a lot clearer: No more injectIntl, intlShapes, getting intl from props and intl components that returns additional spans. Just {t(‘name’)} to insert string.
    4. I can now use localized strings in my actions and other places (to set loading errors for example). With react-intl it was impossible.
    5. And now I can place locale files inside component folder and have all component dependencies in one place. It’s a lot easier to maintain that way.

    All in all can’t recommend using react-intl or other component-based localisation systems, you will have a lot more problems than profit with them. Just use old style string-based i18l systems instead.

    0
    • 7

      Alexey Ivanov

      January 19, 2017 8:33 pm

      Oh, and I’m sorry. I was to preoccupied with react-intl and only write about it. But other pars of article like locale detection and server-side rendering was very interesting and really useful for me. Also I learned some tricks for react-intl with using babel to build resulting locale file. Thank you for sharing them with us!

      2
    • 8

      Well, first of all, thank you for the reading and your feedback. Let me briefly answer to your points:

      1. Valid point but after gzipping, difference is a lot smaller, which still might be critical in some cases but on average it is not
      2. You might check following URL: https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl. If understood correctly, this should resolve mentioned issue. At least, we are happy with it
      3. I don’t mind writing boilerplate code. Explicit is a good thing, especially, if you are working in a big team. You can also avoid having spans in a way I mentioned in the article
      4. I don’t know your use case but sounds like an anti-pattern to me. We pass error in a format “error.form.sign_in.username.short” to the component with some additional payload.
      5. I do the same with react-intl. I put “.jsl” files in the folder and define my messages there.

      I used both approaches you mentioned in different projects, so in general, I would prefer to stick with react-intl. In case your project is relatively small and you need to support three or fewer locales, “old approach” might be a better choice.

      2
      • 9

        Alexey Ivanov

        January 19, 2017 9:46 pm

        Thank you for your answer!

        Can you show example how you load locales from “.jsl” files in components? I tried to do something like it but was not able to do it. I’m very interest how you do it.

        I also agree that 150kB gzipped is not a big deal, but if we can have smaller bundle without additional problems, then why not?

        But about items 2-4 I had to disagree. Let me write why in more details:

        2. We used pretty much code from examples there. Basically you need to create components with refs then you use injectIntl and then either wrap components with IntlProvider with locales by hand, or write a wrapper function that will do it for you.

        Also if you want to test multiple locales you need update example wrapper to accept locale name as param. It is not too hard, but it’s still few lines of additional boilerplate in each test that we now can easily remove.

        Also our jest snapshots are now a lot cleaner without injectIntl and multiple spans.

        3. Yes. We used injectIntl and this.props.intl.formatMessage in the parts of the app where we can’t use strings wrapped in span (for example for titles). But the thing is – we didn’t actually need spans in other places too. We have more or less localised 100 components with around 500 strings in them overall. And there is zero places there spans was actually useful.

        But if you don’t need spans, then you don’t really need components from react-intl and things like injectIntl, IntlProvider, etc. You can just load strings yourself.

        Also without this elements, code in React devtools and in jest snapshots are a lot easier to read and understand.

        4. Yes, we also used string ids in our actions before we moved from react-intl. But with this approach we had to write a lot more code that was harder to maintain and test. Let me give an example:

        We have action that sends data to server that have 2 possible types of errors: 1) server errors – for example that user with this login/password was not found, and 2) connections errors – we was not able to connect to server and we need to tell user about it now.

        For first type of errors we want to use exact strings, because we are not sure beforehand what the problem is and we don’t want to store all possible errors for all possible forms on client. But for the second type of errors we need to add error ourself in proper language. And now we have 2 types of errors, string and id, with different logic that we need to show in one place on page. Also we need to either save them to the store separately or add additional error data to differentiate between them. And then we still have to add logic to render 2 types of errors in component itself.

        But without react-intl we can just always send strings to store and we didn’t need to think about any of this things. It’s a lot easier this way and there is a lot less places that can possibly break.

        I understand, that it is possible that for your usecases react-intl can have same advantages, but for our usecases it was mostly additional problems without any profit.

        1
        • 10

          Andreas Allgaier

          January 22, 2017 10:16 am

          Just my 2 cents :)

          to 2.
          The IntlProvider is designed as the react-redux Provider (if you know it). It is enough to have only one of them at the top most level, and all subsequent components can use it’s functionality. No need to wrap each component in its own IntlProvider.
          For your testing issues, assuming you use es6 syntax: Use the default export of your component as the wrapped one with all with all the stuff like redux connect, injectInlt … and the named export of your component without all that. This means if you use the named export in tests, you can easily mock out all the external libs.

          to 3.
          You don’t have to use spans. The output tag is customizable (it can even be another component). So you could even get rid of the container element by specifiying that as output tag.
          The component based translations are better that function based, because react can diff and see if it even should render/do something. So component based gives more performance than just function based because functions are always evaluated.

          to 4.
          We do dynamically load the translations only for the components/route just used and add them to the react-intl store. So when you load the page/route with your form, we would load the error messages for that page, including the forms. Now you are right, you have the errors for the forms on the client, but only as much as could really be used. That is even true, not only for errors. We only load any message only when it is used (webpack code splitting etc.) on route change etc.
          Then it is always ok to use only ids for your translations and have no different behavior for any translation.

          3
          • 11

            Alexey Ivanov

            January 24, 2017 5:43 pm

            Hello Adreas, thank you for you answer!

            to 2. Yes, we use IntlProvider only once in our app (plus one per test suite for translated components) and it is not a big deal. But we have to use injectIntl a lot more often because there is a lot of place there you can’t use component and need to use strings, for example for element titles. I think that around 1/3 of our components had injectIntl and it was not very convenient to write and debug.

            It’s possible to export components without injectIntl as you suggest and mock it after. But right now we didn’t need to, it just works out of the box without any of this problems and need for mocks and separate exports. :)

            to 3. Oh, I didn’t know that you can remove spans through settings, it sure helps with having more clear snapshots. It’s a good news! But you still need to use injectIntl if you want to use resulting strings as html props, am I right?

            About React rerenders. Fair point. I think you are right and rerender of the components with intl, whose props was changed will be faster. But we also need to consider this (I don’t have speed tests on my hands, so maybe I’m wrong):

            1. Components with changed props are a small subset of components on page, and not all of them have translations. So only small part of components will be affected on rerender.
            2. On the first render component will probably render longer that functions that just return strings.
            3. Virtual dom snapshot will probably need more memory for components than for strings.
            4. DOM diffs will need more time to pass for components than for strings.

            So all in all I think we still win more on using strings, and plus we will also have smaller bundle size, cleaner code, easier tests, cleaner devtools, etc.

            to 4. If I am understand correctly you are using code splitting on routes and load locales with components themselves. Am I right?

            It’s a good thing and it will help you reduce bundle size on first load. But from that I understand you are still load all possible errors for each component that may need them. Even if you will never use them.

            It’s ok if you only have couple of errors for each server request. But unfortunately we have very complex forms which can have from 20 to even 100 possible error messages. While it’s possible to load them all from the start it will make our forms harder to support: We need either share error codes between our server and our locale files by hand and hope that there will be no divergences, or we need to write automatic locale file generator for server-side errors that will do this for us.

            But you need all this only as a workaround for the situation then you can’t use locales in actions or other non-react parts of application and we can. And there is a lot of other places there you need to work with translated strings too: Changing title or page metadata, generating code for Facebook and Twitter share buttons, confirmation dialogs, etc. HTML is not the only part of the user interface so I do’t think that using locales outside React is an antipattern.

            0
        • 12

          Hey Alexey,

          as other points were covered by Andreas, let me share, how I store translations in jsl files.

          1. Add “jsl” to your webpack config like you do for “jsx” extenstion.
          2. My “SampleComponent.jsl”

          
          import { defineMessages } from 'react-intl';
          
          export default defineMessages({
            list: {
              id: 'navbar.friends.list',
              defaultMessage: 'Subscription Management',
            },
            suggested: {
              id: 'navbar.friends.suggested',
              defaultMessage: 'Marmot suggestions',
            },
            yours: {
              id: 'navbar.friends.yours',
              defaultMessage: 'Your friends',
            },
          });
          

          3. My “SampleComponent.jsx”

          
          import messages from './SampleComponent.jsl';
          ...
          {intl.formatMessage(messages.list)}
          ...
          
          0
  5. 14

    Andreas Allgaier

    January 22, 2017 10:16 am

    Just my 2 cents :)

    to 2.
    The IntlProvider is designed as the react-redux Provider (if you know it). It is enough to have only one of them at the top most level, and all subsequent components can use it’s functionality. No need to wrap each component in its own IntlProvider.
    For your testing issues, assuming you use es6 syntax: Use the default export of your component as the wrapped one with all with all the stuff like redux connect, injectInlt … and the named export of your component without all that. This means if you use the named export in tests, you can easily mock out all the external libs.

    to 3.
    You don’t have to use spans. The output tag is customizable (it can even be another component). So you could even get rid of the container element by specifiying that as output tag.
    The component based translations are better that function based, because react can diff and see if it even should render/do something. So component based gives more performance than just function based because functions are always evaluated.

    to 4.
    We do dynamically load the translations only for the components/route just used and add them to the react-intl store (we have a REST api for translations). So when you load the page/route with your form, we would load the error messages for that page, including the forms. Now you are right, you have the errors for the forms on the client, but only the ones that could really be used. That is even used not only for errors. We only load any message only when it is used (webpack code splitting etc.) on route change etc.
    Then it is always ok to use only ids for your translations and have no different behavior for any translation.

    1
  6. 15

    Always ask the client what language he/she wants to use.

    -4
  7. 16

    Guillermo Grau

    January 25, 2017 7:31 am

    For completeness, have a look at Mady (https://github.com/guigrpa/mady), a translator tool that integrates bidirectionally with React Intl (among other environments): extracting the user’s locale (babel-plugin-react-intl is included), as well as generating React Intl-compatible json files. Oh, and it’s designed with translator UX in mind. [disclaimer: I’m its author]

    1
  8. 17

    Tomáš Ehrlich

    February 1, 2017 7:40 pm

    Hey,
    nice and comprehensive article!

    I used react-intl in one project and found it a bit difficult to use. So I released react-trans package along with `babel-plugin-transform-react-trans`. It makes writing multilingual text with variables and inline elements much easier. It basically adds React components which are compiled into ICU message format:

    Hello, my name is {name}
    // becomes "Hello, my name is {name}"
    
    Read more
    // becomes "Read more", so inline element props doesn't affect message ID
    

    Pluralization, gender/polite form, variable substitution and inline elements — in all can be mixed together, so developer works with React as they is used to and translator still gets standardised ICU messages.

    Feel free to check it out. Comments appreciated

    https://medium.com/lingui-engineering-blog/translation-and-localisation-in-react-406869673efa#.fdn3cf4me

    https://github.com/lingui/js-lingui

    0
    • 18

      Tomáš Ehrlich

      February 1, 2017 7:43 pm

      Ugh, I forgot about HTML. The examples above should be:

      <Trans>Hi, my name is {name}</Trans>
      // becomes "Hi, my name is {name}"

      <Trans>Read <a href="/more">more</a></Trans>
      // becomes "Read <0>more</0>"

      0
  9. 19

    How would you approach the l10n-Process? I’ll pass everything in build/messages to my translator, because there lies the description and then i’ll parse the translated files to json with the provided script? Right?
    What if I add more functionality to my app later, how can i keep track whats new and merge it with the existing files?

    0

↑ Back to top