125

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?

Antoine Jaussoin
  • 5,002
  • 4
  • 28
  • 39
  • 2
    I prefer the idea of an object of keys with translation strings which is passed down as a prop, you don't have to pass each string as a prop individually. Changing this at a top level should trigger a re-render. I don't think using context is a good idea for this, and each component having access to the translation file makes them less "dumb" and portable actually imo (and harder to get the app to re-render on language change). – Dominic Oct 29 '15 at 12:52
  • what about a central store that has what type of language stored on it. and a mixin on your components that pulls that in and looks at the spcified language? – John Ruddell Oct 29 '15 at 15:28
  • @DominicTobias: Yes, that was my proposition number 2 above, but let say you have a big app: do you pass the entire object with all translations to each component (which then are responsible for cherry picking which one applies), or more likely, do you build that object from the entire set of translation strings, but in which case, you need to put that logic somewhere (and for each component). And if it has to be somewhere, it should be within the component itself, which means no needs for props... bit of a catch 22. – Antoine Jaussoin Oct 29 '15 at 16:36
  • @JohnRuddell : I'm indeed using a store to store the language (well, I'm using Redux, so using a reducer), and dealing with that is simple. The issue is more how to pass that state to each component without becoming too repetitive :) As for the mixins, I'm using ES6 syntax, which means no mixins (and they are on the way out anyway). Could use an Higher Order Component though (trying that right now) – Antoine Jaussoin Oct 29 '15 at 16:38
  • 1
    Actually according to https://facebook.github.io/react/docs/context.html, using context for sharing the current language is one of the legitimate use case. The approach I'm trying now is to use this plus a Higher Order Component to deal with the logic of extracting the strings for that particular component (probably based on some key) – Antoine Jaussoin Oct 29 '15 at 16:40
  • I see, I wonder how you would re-render the tree on a change of context? – Dominic Oct 29 '15 at 16:44
  • if all your components have a store why not just have them inherit from a base store that has your language preference on it. the state for each will have the correct language preference from there – John Ruddell Oct 29 '15 at 17:09
  • I think I found a non disruptive way that works well, will post an answer – Antoine Jaussoin Oct 29 '15 at 17:38
  • 1
    Maybe you can also take a look at [Instant](https://instant.cm/en/landing/react). They deal with this problem in a completely different way by tackling it in the frontend ala Optimizely (aka altering the DOM while loading). – Marcel Panse Oct 24 '16 at 08:35
  • 1
    Not bad at all! It's indeed a completely different beast (which ties you up to a service that you might need to pay if your website grows), but I like the idea and it's indeed probably worth it for a small website that you need to get running quickly! – Antoine Jaussoin Oct 24 '16 at 09:59
  • 4
    Also, you might want to mention that you are a co founder of Instant, instead of saying "They" as if you didn't have anything to do with them :) – Antoine Jaussoin Oct 24 '16 at 10:00

7 Answers7

111

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

Antoine Jaussoin
  • 5,002
  • 4
  • 28
  • 39
  • This is a cool solution.. wondering if you're still on board with this after a few months? I haven't found much advice in the way of advice on patterns for this online – Damon Jan 07 '16 at 16:17
  • 2
    I am actually, I found that to work perfectly (for my needs). It makes the component work without translation by default, and translation just comes on top of it without the component being aware of it – Antoine Jaussoin Jan 08 '16 at 11:11
  • @AntoineJaussoin How would this approach go regarding value interpolation (numbers, dates) and related issues like pluralization and date formats? Your example code doesn't handle such situations... – Teodor Sandu Apr 13 '16 at 12:53
  • @Mtz : as I wrote in my original question, "I'm not too worried about formatting numbers, dates etc. at the moment, I want a simple solution to just translate strings". If you want to format date, you can use the builtin functionality of MomentJS for instance – Antoine Jaussoin Apr 15 '16 at 07:52
  • @AntoineJaussoin ok, nice stuff :) i actually started using this method and i like it! For interpolation i'm currently using functions instead of strings (in the `strings` prop) because my cases are extremely simple now. For more complex cases i was thinking about using Jed https://slexaxton.github.io/Jed/ in the language files. It would be a 2-step process basically but each step pretty simple and manageable. Only using Jed would introduce a dependency on it in the components, which i don't want. What do you think? – Teodor Sandu Apr 21 '16 at 08:48
  • @AntoineJaussoin also, in your repo i see that components depend on the `Translate` module which provides the HOC. I'm trying to remove that dependency and `translate` in the routes config, e.g. `` - this way the component itself is totally oblivious of the fact that it's being translated (thus reusable via simple file copy/paste). How does this sound? And btw thank you for the well-written answer :) – Teodor Sandu Apr 21 '16 at 08:58
  • My issue with this approach is that it localizes messages based on component rather than a simple message name. This means if you have multiple components that use the same string you need multiple translations of the same text. You could get around this a variety of ways, but in general, I tend towards the Java-/Rails-style key-language-string approach. Does this fit into that approach somehow? If so, an example would be great. – Dave Newton Apr 26 '16 at 19:17
  • @DaveNewton: you can actually use the translate decorator multiple times, merging the translations into one object, as seen here: https://github.com/antoinejaussoin/retro-board/blob/master/app/components/SessionName.jsx (the Translate HOC is a bit different to allow this, look here for the source: https://github.com/antoinejaussoin/retro-board/blob/master/app/i18n/Translate.jsx) – Antoine Jaussoin Apr 27 '16 at 15:04
  • @Mtz: the problem with this approach is that you are only translating top-level (container) components, not sub-components, is that on purpose? If you wanted to separate the translation from the components "physically", you could have your (untranslated) component in one file, and another file which reference translate, your component, and returns the translated component – Antoine Jaussoin Apr 27 '16 at 15:06
  • @AntoineJaussoin not really, i was just exploring available options to weigh pros/cons and learn more about how this works. Interesting suggestion, thank you! :) – Teodor Sandu May 18 '16 at 14:32
  • How would you handle the case where you want to surround some of the word with a tag ? React escapes it if you put it in the string. Spliting the string to translate can be a solution, but the word may not be at the same place in the sentence depending on the language – lcetinsoy May 22 '16 at 14:14
  • 1
    @l.cetinsoy you can use the `dangerouslySetInnerHTML` prop, just be mindful of the implications (manually sanitize the input). See https://facebook.github.io/react/tips/dangerously-set-inner-html.html – Teodor Sandu May 25 '16 at 10:17
  • I'm reading the strings dynamically from an API, and storing them on the Redux store, so as a TranslationComponent I just use a redux-react `connect` call with strings returned by `mapStateToProps`. The other issue I found was a weakness when you pass an incomplete strings prop to the component, it will overwrite the default strings prop and cause an issue. So instead of using the strings directly, I store the defaults in a defaultStrings var, pass it to defaultProps, but also inside the component: `var strings = Object.assign({}, defaultStrings, props.strings);` Great method, thanks! – Pablo Barría Urenda Aug 02 '16 at 13:55
  • 7
    Is there a reason as to why you haven't tried react-intl ? – SureshCS Aug 03 '16 at 05:59
  • 1
    Really like this solution. One thing I'd add which we found very useful for consistency and time saving is that if you have lots of components with common strings you could take advantage of variables and spreading over objects e.g. `const formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }` – Huw Davies Oct 28 '16 at 13:50
  • @SitharaSuresh this solution loads all translation files in one time so you can switch languages without reloading page like in react-intl – Dmitry Feb 15 '17 at 10:22
20

From my experience the best approach is to create an i18n redux state and use it, for many reasons:

1- This will allow you to pass the initial value from the database, local file or even from a template engine such as EJS or jade

2- When the user changes the language you can change the whole application language without even refreshing the UI.

3- When the user changes the language this will also allow you to retrieve the new language from API, local file or even from constants

4- You can also save other important things with the strings such as timezone, currency, direction (RTL/LTR) and list of available languages

5- You can define the change language as a normal redux action

6- You can have your backend and front end strings in one place, for example in my case I use i18n-node for localization and when the user changes the UI language I just do a normal API call and in the backend, I just return i18n.getCatalog(req) this will return all the user strings only for the current language

My suggestion for the i18n initial state is:

{
  "language":"ar",
  "availableLanguages":[
    {"code":"en","name": "English"},
    {"code":"ar","name":"عربي"}
  ],
  "catalog":[
     "Hello":"مرحباً",
     "Thank You":"شكراً",
     "You have {count} new messages":"لديك {count} رسائل جديدة"
   ],
  "timezone":"",
  "currency":"",
  "direction":"rtl",
}

Extra useful modules for i18n:

1- string-template this will allow you to inject values in between your catalog strings for example:

import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة

2- human-format this module will allow you to converts a number to/from a human readable string, for example:

import humanFormat from "human-format";
//...
humanFormat(1337); // => '1.34 k'
// you can pass your own translated scale, e.g: humanFormat(1337,MyScale)

3- momentjs the most famous dates and times npm library, you can translate moment but it already has a built-in translation just you need to pass the current state language for example:

import moment from "moment";

const umoment = moment().locale(i18n.language);
umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م

Update (14/06/2019)

Currently, there are many frameworks implement the same concept using react context API (without redux), I personally recommended I18next

Fareed Alnamrouti
  • 30,771
  • 4
  • 85
  • 76
6

Antoine's solution works fine, but have some caveats :

  • It uses the React context directly, which I tend to avoid when already using Redux
  • It imports directly phrases from a file, which can be problematic if you want to fetch needed language at runtime, client-side
  • It does not use any i18n library, which is lightweight, but doesn't give you access to handy translation functionalities like pluralization and interpolation

That's why we built redux-polyglot on top of both Redux and AirBNB's Polyglot.
(I'm one of the authors)

It provides :

  • a reducer to store language and corresponding messages in your Redux store. You can supply both by either :
    • a middleware that you can configure to catch specific action, deduct current language and get/fetch associated messages.
    • direct dispatch of setLanguage(lang, messages)
  • a getP(state) selector that retrieves a P object that exposes 4 methods :
    • t(key): original polyglot T function
    • tc(key): capitalized translation
    • tu(key): upper-cased translation
    • tm(morphism)(key): custom morphed translation
  • a getLocale(state)selector to get current language
  • a translate higher order component to enhance your React components by injecting the p object in props

Simple usage example :

dispatch new language :

import setLanguage from 'redux-polyglot/setLanguage';

store.dispatch(setLanguage('en', {
    common: { hello_world: 'Hello world' } } }
}));

in component :

import React, { PropTypes } from 'react';
import translate from 'redux-polyglot/translate';

const MyComponent = props => (
  <div className='someId'>
    {props.p.t('common.hello_world')}
  </div>
);
MyComponent.propTypes = {
  p: PropTypes.shape({t: PropTypes.func.isRequired}).isRequired,
}
export default translate(MyComponent);

Please tell me if you have any question/suggestion !

Jalil
  • 3,150
  • 3
  • 30
  • 39
  • 1
    A lot better original phrases to be translated. And to make a tool that parses all components for `_()` functions for example to get all of those strings. So you can in language file translate it more easy and don`t mess with crazy variables. At some cases landing pages need specific part of layout to be displayed differently. So some smart function of how to choose default vs other possible choices should be available also. – Roman M. Koss Dec 07 '16 at 01:46
  • Hi @Jalil, is there anywhere a complete example with middleware? – ArkadyB Feb 07 '17 at 22:21
  • Hi @ArkadyB, We use it on production on several projects that are not open-sourced. You can find more information on the module README : https://www.npmjs.com/package/redux-polyglot Do you have any question/difficulty using it ? – Jalil Feb 11 '17 at 12:26
  • My major issue with this and polyglot.js is that it is completely re-inventing the wheel rather than building on top of PO files. This alternate library looks promising https://www.npmjs.com/package/redux-i18n. I don't think that is doing much different - it is just providing an extra layer to convert to and from PO files. – icc97 Feb 15 '17 at 17:39
3

Yet another (light) proposal implemented in Typescript and based on ES6 & Redux & Hooks & JSON with no 3rd party dependencies.

Since the selected language is loaded in the redux state, changing the language becomes very fast without the need of rendering all pages, but just the affected texts.

Part 1: Redux setup:

/src/shared/Types.tsx

export type Language = 'EN' | 'CA';

/src/store/actions/actionTypes.tsx

export const SET_LANGUAGE = 'SET_LANGUAGE';

/src/store/actions/language.tsx:

import * as actionTypes from './actionTypes';
import { Language } from '../../shared/Types';

export const setLanguage = (language: Language) => ({
   type: actionTypes.SET_LANGUAGE,
   language: language,
});

/src/store/reducers/language.tsx:

import * as actionTypes from '../action/actionTypes';
import { Language } from '../../shared/Types';
import { RootState } from './reducer';
import dataEN from '../../locales/en/translation.json';
import dataCA from '../../locales/ca/translation.json';

type rootState = RootState['language'];

interface State extends rootState { }
interface Action extends rootState {
    type: string,
}

const initialState = {
    language: 'EN' as Language,
    data: dataEN,
};

const setLanguage = (state: State, action: Action) => {
    let data;
    switch (action.language) {
        case 'EN':
            data = dataEN;
            break;
        case 'CA':
            data = dataCA;
            break;
        default:
            break;
    }
    return {
        ...state,
        ...{ language: action.language,
             data: data,
            }
    };
};

const reducer = (state = initialState, action: Action) => {
    switch (action.type) {
        case actionTypes.SET_LANGUAGE: return setLanguage(state, action);
        default: return state;
    }
};

export default reducer;

/src/store/reducers/reducer.tsx

import { useSelector, TypedUseSelectorHook } from 'react-redux';
import { Language } from '../../shared/Types';

export interface RootState {
    language: {
        language: Language,
        data: any,
    }
};

export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;

/src/App.tsx

import React from 'react';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import languageReducer from './store/reducers/language';
import styles from './App.module.css';

// Set global state variables through Redux
const rootReducer = combineReducers({
    language: languageReducer,
});
const store = createStore(rootReducer);

const App = () => {

    return (
        <Provider store={store}>
            <div className={styles.App}>
                // Your components
            </div>
        </Provider>
    );
}

export default App;

Part 2: Dropdown menu with languages. In my case, I put this component within the navigation bar to be able to change the language from any screen:

/src/components/Navigation/Language.tsx

import React from 'react';
import { useDispatch } from 'react-redux';
import { setLanguage } from '../../store/action/language';
import { useTypedSelector } from '../../store/reducers/reducer';
import { Language as Lang } from '../../shared/Types';
import styles from './Language.module.css';

const Language = () => {
    const dispatch = useDispatch();
    const language = useTypedSelector(state => state.language.language);
    
    return (
        <div>
            <select
                className={styles.Select}
                value={language}
                onChange={e => dispatch(setLanguage(e.currentTarget.value as Lang))}>
                <option value="EN">EN</option>
                <option value="CA">CA</option>
            </select>
        </div>
    );
};

export default Language;

Part 3: JSON files. In this example, just a test value with a couple of languages:

/src/locales/en/translation.json

{
    "message": "Welcome"
}

/src/locales/ca/translation.json

{
    "message": "Benvinguts"
}

Part 4: Now, at any screen, you can show the text in the selected language from the redux setup:

import React from 'react';
import { useTypedSelector } from '../../store/reducers/reducer';

const Test = () => {
    const t = useTypedSelector(state => state.language.data);

    return (
        <div> {t.message} </div>
    )
}

export default Test;

Sorry for the post extension, but I tried to show the complete setup to clarify all doubts. Once this is done, it is very quick and flexible to add languages and use descriptions anywhere.

Sergi Juanati
  • 1,230
  • 1
  • 9
  • 17
  • 1
    Why didn't anyone comment on this approach? For a roll it yourself approach this seems to be the best most simple solution here. Love it @Sergi Juanati – BradStell Jan 06 '21 at 17:46
  • @AntoineJaussoin did you try this approach and if so what pitfalls did you come across? Was it simply to make your components more reusable and not rely on redux? – BradStell Jan 06 '21 at 17:53
2

From my research into this there appears to be two main approaches being used to i18n in JavaScript, ICU and gettext.

I've only ever used gettext, so I'm biased.

What amazes me is how poor the support is. I come from the PHP world, either CakePHP or WordPress. In both of those situations, it's a basic standard that all strings are simply surrounded by __(''), then further down the line you get translations using PO files very easily.

gettext

You get the familiarity of sprintf for formatting strings and PO files will be translated easily by thousands of different agencies.

There's two popular options:

  1. i18next, with usage described by this arkency.com blog post
  2. Jed, with usage described by the sentry.io post and this React+Redux post,

Both have gettext style support, sprintf style formatting of strings and import / export to PO files.

i18next has a React extension developed by themselves. Jed doesn't. Sentry.io appear to use a custom integration of Jed with React. The React+Redux post, suggests using

Tools: jed + po2json + jsxgettext

However Jed seems like a more gettext focussed implementation - that is it's expressed intention, where as i18next just has it as an option.

ICU

This has more support for the edge cases around translations, e.g. for dealing with gender. I think you will see the benefits from this if you have more complex languages to translate into.

A popular option for this is messageformat.js. Discussed briefly in this sentry.io blog tutorial. messageformat.js is actually developed by the same person that wrote Jed. He makes quite stong claims for using ICU:

Jed is feature complete in my opinion. I am happy to fix bugs, but generally am not interested in adding more to the library.

I also maintain messageformat.js. If you don't specifically need a gettext implementation, I might suggest using MessageFormat instead, as it has better support for plurals/gender and has built-in locale data.

Rough comparison

gettext with sprintf:

i18next.t('Hello world!');
i18next.t(
    'The first 4 letters of the english alphabet are: %s, %s, %s and %s', 
    { postProcess: 'sprintf', sprintf: ['a', 'b', 'c', 'd'] }
);

messageformat.js (my best guess from reading the guide):

mf.compile('Hello world!')();
mf.compile(
    'The first 4 letters of the english alphabet are: {s1}, {s2}, {s3} and {s4}'
)({ s1: 'a', s2: 'b', s3: 'c', s4: 'd' });
Community
  • 1
  • 1
icc97
  • 11,395
  • 8
  • 76
  • 90
  • Down voted. This does not answer the question. OP asked for an architecture idea, not a suggestion or comparison any i18n library. – Tony Dinh Nov 24 '17 at 03:41
  • @TrungDQ This is what the OP asked: *"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."*. These are two patterns that are being used in production. – icc97 Nov 27 '17 at 14:25
  • In my opinion this answer does not provide the information I'm (and others are) looking for. The information you provided is helpful, but maybe for another question. I just want to contribute my downvote to make the right answer pop up to the top (I hope). – Tony Dinh Nov 28 '17 at 01:53
  • 1
    @TrungDQ If it's not what you're looking for, then just upvote the one you did use and ignore the others rather than downvoting pefectly valid answers that don't match the specific part of the question you're interested in. – icc97 Nov 28 '17 at 11:35
1

If not yet done having a look at https://react.i18next.com/ might be a good advice. It is based on i18next: learn once - translate everywhere.

Your code will look something like:

<div>{t('simpleContent')}</div>
<Trans i18nKey="userMessagesUnread" count={count}>
  Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>

Comes with samples for:

  • webpack
  • cra
  • expo.js
  • next.js
  • storybook integration
  • razzle
  • dat
  • ...

https://github.com/i18next/react-i18next/tree/master/example

Beside that you should also consider workflow during development and later for your translators -> https://www.youtube.com/watch?v=9NOzJhgmyQE

jamuhl
  • 4,352
  • 2
  • 25
  • 31
  • This does not answer the question. OP asked for an architecture idea, not a suggestion or comparison any i18n library. – Tony Dinh Nov 24 '17 at 03:39
  • @TrungDQ as with your comment on my answer that you downvoted - the OP asked for current solutions used in production. However I had suggested i18next in [my answer](https://stackoverflow.com/a/42261765/327074) from back in Feb. – icc97 Nov 27 '17 at 14:29
0

I would like to propose a simple solution using create-react-app.

The application will be built for every language separately, therefore whole translation logic will be moved out of the application.

The web server will serve the correct language automatically, depending on Accept-Language header, or manually by setting a cookie.

Mostly, we do not change language more than once, if ever at all)

Translation data put inside same component file, that uses it, along styles, html and code.

And here we have fully independent component that responsible for its own state, view, translation:

import React from 'react';
import {withStyles} from 'material-ui/styles';
import {languageForm} from './common-language';
const {REACT_APP_LANGUAGE: LANGUAGE} = process.env;
export let language; // define and export language if you wish
class Component extends React.Component {
    render() {
        return (
            <div className={this.props.classes.someStyle}>
                <h2>{language.title}</h2>
                <p>{language.description}</p>
                <p>{language.amount}</p>
                <button>{languageForm.save}</button>
            </div>
        );
    }
}
const styles = theme => ({
    someStyle: {padding: 10},
});
export default withStyles(styles)(Component);
// sets laguage at build time
language = (
    LANGUAGE === 'ru' ? { // Russian
        title: 'Транзакции',
        description: 'Описание',
        amount: 'Сумма',
    } :
    LANGUAGE === 'ee' ? { // Estonian
        title: 'Tehingud',
        description: 'Kirjeldus',
        amount: 'Summa',
    } :
    { // default language // English
        title: 'Transactions',
        description: 'Description',
        amount: 'Sum',
    }
);

Add language environment variable to your package.json

"start": "REACT_APP_LANGUAGE=ru npm-run-all -p watch-css start-js",
"build": "REACT_APP_LANGUAGE=ru npm-run-all build-css build-js",

That is it!

Also my original answer included more monolithic approach with single json file for each translation:

lang/ru.json

{"hello": "Привет"}

lib/lang.js

export default require(`../lang/${process.env.REACT_APP_LANGUAGE}.json`);

src/App.jsx

import lang from '../lib/lang.js';
console.log(lang.hello);
Igor Sukharev
  • 2,467
  • 24
  • 21
  • Wouldn't it only work at compile time? Without the ability for the user to change the language on the fly? That would then be a different use-case. – Antoine Jaussoin May 19 '18 at 10:31
  • The app will be compiled for every language needed. The web server will serve the correct version automatically, depending on "Accept-Language" header, or by a cookie set by the user on the fly. By doing this, whole translation logic could be moved out of the app. – Igor Sukharev May 21 '18 at 15:09