React / Redux and Multilingual (Internationalization) Apps - Architecture

Antoine Jaussoin picture Antoine Jaussoin · Oct 29, 2015 · Viewed 51k times · Source

I'm building an app that will need to be available in multiple languages and locales.

My question is not purely technical, but rather about the architecture, and the patterns that people are actually using in production to solve this problem. I couldn't find anywhere any "cookbook" for that, so I'm turning to my favourite Q/A website :)

Here are my requirements (they are really "standard"):

  • The user can choose the language (trivial)
  • Upon changing the language, the interface should translate automatically to the new selected language
  • I'm not too worried about formatting numbers, dates etc. at the moment, I want a simple solution to just translate strings

Here are the possible solutions I could think off:

Each component deal with translation in isolation

This means that each component have for example a set of en.json, fr.json etc. files alongside it with the translated strings. And a helper function to help reading the values from those depending on the selected language.

  • Pro: more respectful of the React philosophy, each component is "standalone"
  • Cons: you can't centralize all the translations in a file (to have someone else add a new language for example)
  • Cons: you still need to pass the current language as a prop, in every bloody component and their children

Each component receives the translations via the props

So they are not aware of the current language, they just take a list of strings as props which happen to match the current language

  • Pro: since those strings are coming "from the top", they can be centralized somewhere
  • Cons: Each component is now tied into the translation system, you can't just re-use one, you need to specify the correct strings every time

You bypass the props a bit and possibly use the context thingy to pass down the current language

  • Pro: it's mostly transparent, don't have to pass the current language and/or translations via props all the time
  • Cons: it looks cumbersome to use

If you have any other idea, please do say!

How do you do it?

Answer

Antoine Jaussoin picture Antoine Jaussoin · Oct 29, 2015

After trying quite a few solutions, I think I found one that works well and should be an idiomatic solution for React 0.14 (i.e. it doesn't use mixins, but Higher Order Components) (edit: also perfectly fine with React 15 of course!).

So here the solution, starting by the bottom (the individual components):

The Component

The only thing your component would need (by convention), is a strings props. It should be an object containing the various strings your Component needs, but really the shape of it is up to you.

It does contain the default translations, so you can use the component somewhere else without the need to provide any translation (it would work out of the box with the default language, english in this example)

import { default as React, PropTypes } from 'react';
import translate from './translate';

class MyComponent extends React.Component {
    render() {

        return (
             <div>
                { this.props.strings.someTranslatedText }
             </div>
        );
    }
}

MyComponent.propTypes = {
    strings: PropTypes.object
};

MyComponent.defaultProps = {
     strings: {
         someTranslatedText: 'Hello World'
    }
};

export default translate('MyComponent')(MyComponent);

The Higher Order Component

On the previous snippet, you might have noticed this on the last line: translate('MyComponent')(MyComponent)

translate in this case is a Higher Order Component that wraps your component, and provide some extra functionality (this construction replaces the mixins of previous versions of React).

The first argument is a key that will be used to lookup the translations in the translation file (I used the name of the component here, but it could be anything). The second one (notice that the function is curryed, to allow ES7 decorators) is the Component itself to wrap.

Here is the code for the translate component:

import { default as React } from 'react';
import en from '../i18n/en';
import fr from '../i18n/fr';

const languages = {
    en,
    fr
};

export default function translate(key) {
    return Component => {
        class TranslationComponent extends React.Component {
            render() {
                console.log('current language: ', this.context.currentLanguage);
                var strings = languages[this.context.currentLanguage][key];
                return <Component {...this.props} {...this.state} strings={strings} />;
            }
        }

        TranslationComponent.contextTypes = {
            currentLanguage: React.PropTypes.string
        };

        return TranslationComponent;
    };
}

It's not magic: it will just read the current language from the context (and that context doesn't bleed all over the code base, just used here in this wrapper), and then get the relevant strings object from loaded files. This piece of logic is quite naïve in this example, could be done the way you want really.

The important piece is that it takes the current language from the context and convert that into strings, given the key provided.

At the very top of the hierarchy

On the root component, you just need to set the current language from your current state. The following example is using Redux as the Flux-like implementation, but it can easily be converted using any other framework/pattern/library.

import { default as React, PropTypes } from 'react';
import Menu from '../components/Menu';
import { connect } from 'react-redux';
import { changeLanguage } from '../state/lang';

class App extends React.Component {
    render() {
        return (
            <div>
                <Menu onLanguageChange={this.props.changeLanguage}/>
                <div className="">
                    {this.props.children}
                </div>

            </div>

        );
    }

    getChildContext() {
        return {
            currentLanguage: this.props.currentLanguage
        };
    }
}

App.propTypes = {
    children: PropTypes.object.isRequired,
};

App.childContextTypes = {
    currentLanguage: PropTypes.string.isRequired
};

function select(state){
    return {user: state.auth.user, currentLanguage: state.lang.current};
}

function mapDispatchToProps(dispatch){
    return {
        changeLanguage: (lang) => dispatch(changeLanguage(lang))
    };
}

export default connect(select, mapDispatchToProps)(App);

And to finish, the translation files:

Translation Files

// en.js
export default {
    MyComponent: {
        someTranslatedText: 'Hello World'
    },
    SomeOtherComponent: {
        foo: 'bar'
    }
};

// fr.js
export default {
    MyComponent: {
        someTranslatedText: 'Salut le monde'
    },
    SomeOtherComponent: {
        foo: 'bar mais en français'
    }
};

What do you guys think?

I think is solves all the problem I was trying to avoid in my question: the translation logic doesn't bleed all over the source code, it is quite isolated and allows reusing the components without it.

For example, MyComponent doesn't need to be wrapped by translate() and could be separate, allowing it's reuse by anyone else wishing to provide the strings by their own mean.

[Edit: 31/03/2016]: I recently worked on a Retrospective Board (for Agile Retrospectives), built with React & Redux, and is multilingual. Since quite a lot of people asked for a real-life example in the comments, here it is:

You can find the code here: https://github.com/antoinejaussoin/retro-board/tree/master