Localizing your React-Redux Application with React-Intl
It’s not something that often comes up in tutorials and not always something you get to do when starting a green field project if indeed you’re lucky enough in your career as a developer to be there on the ground floor at the genesis of a new project. But localising an application so that the user can read the text in their native language is something I have been involved with twice.
The first time was localising a legacy winforms application which I will talk about in a future post. The second was localising the new kid on the block react with it’s favourite partner redux.
react-intl (https://github.com/yahoo/react-intl) is a library that helps you out and takes care of a lot of the heavy lifting of translation and number formatting, in this post I’m going to focus on translation and how to update the redux store so that the UI text language is then changed based upon user interaction.
I am assuming you know how to get yourself setup with react and redux and you know what reducers and action creators are.
The first thing to do is to install react-intl you do this with npm using:
?npm i react-intl
The extra pieces
There a couple of moving pieces from react-intl that help get all of this working:
IntlProvider – this sits at the root of your applications JSX above any in the component tree that will have translated text, essentially it can be thought of akin to the react-redux Provider or the React-Router Router object.
addLocaleData – A function that adds locale data which is set during the initialisation of the application.
A plain old Json object – More on this, but essentially this will hold keys based on the locale code for example “en†for English or “ja†for Japanese, that then holds values for the translations.
FormattedMessage – This object is what we will wrap our translations with, it takes two properties the first “id†being the key to the translation contained in the Json object, the second being “defaultMessage†should there be an issue in displaying the translation.
Show me the code
Lets start at the root of the application typically you will have something like this:
import React from 'react';
import { Provider } from 'react-redux';
import store from 'reducers/configureStore';
import AppRouter from './appRouter';
const App = ({ store }) => (
<Provider store={store}>
<ApplicationTree />
</Provider>
);
export default App;
ApplicationTree in this case would be the rest of your application whatever it maybe wrapped with the redux provider.
To carry on with the ApplicationTree, that would then look something like this:
import React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { IntlProvider, addLocaleData } from 'react-intl';
import { connect } from 'react-redux';
import languageObject from 'translations/messages';
import HomeComponent from './home/homeComponent';
import en from 'react-intl/locale-data/en';
import ja from 'react-intl/locale-data/ja';
addLocaleData(en);
addLocaleData(ja);
export const ApplicationTree = (props) => {
return (
<div>
<Router>
<IntlProvider locale={props.locale} messages={languageObject[props.locale]}>
<div>
<Switch>
<Route path="/" exact component={HomeComponent} />
</Switch>
</div>
</IntlProvider>
</Router>
</div>
);
};
ApplicationTree.defaultProps = {
locale: 'ja',
};
ApplicationTree.propTypes = {
locale: PropTypes.string,
};
const mapStateToProps = state => ({ locale: state.localeReducer.locale });
export default connect(mapStateToProps)(ApplicationTree);
There are a few things going on here, the first in that we are bringing in the components we want to use later in our JSX (IntlProvider and addLocalData) then we have imported our LanguageObject (our Json translation data) and brought in our HomeComponent which will be a page component that will be connected to react-redux and will contain some text that will be updated on the page in a language we choose.
Next we are bringing in our locales on lines 9-10 then adding that data to the context of IntlProvider on 12-13.
We also have mapStateToProps this sets the props.locale property on the component and updated the applications IntlProvider when a different locale is selected.
IntlProvider needs to know what the locale setting is, this is done with it’s “locale†property which we have set to the props.locale of the component, lastly it takes a messages object with it’s “messages†property which is again set depending on the props.locale.
The messages object as mentioned above is a Json object which contains the keys and translations which will be use throughout the application, this can be broken out into separate files as you wish, our example is in one file and looks like this:
const languageObject = {
en: {
'homeComponent.title': 'Home Component',
'homeComponent.english': 'English',
'homeComponent.japanese': 'Japanese',
},
ja: {
'homeComponent.title': 'ホームコンãƒãƒ¼ãƒãƒ³ãƒˆ',
'homeComponent.english': '英語',
'homeComponent.japanese': '日本人',
},
};
export default languageObject;
The props.locale will be a string, either ‘en’ or ‘ja’ and will be used as a key to select the inner objects containing the translations.
Our HomeComponent is connected to react-redux and contains the FormattedMessage component and looks like this:
import React from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import changeLocale from 'actions/locale/localeActionCreators';
const homeComponent = (props) => {
return (
<div>
<FormattedMessage id="homeComponent.title" defaultMessage="Default message" />
<button type="button" onClick?={ () => {props.dispatch(changeLocale('en')) }}><FormattedMessage id="homeComponent.english" defaultMessage="Default message" /></button>
<button type="button" onClick?={ () => {props.dispatch(changeLocale('ja')) }}><FormattedMessage id="homeComponent.japanese" defaultMessage="Default message" /></button>
</div>
);
}
export default connect()(homeComponent);
For simplicity I have directly imported the action creator and directly dispatching from props.dispatch. There are three FormattedMessage components, one that contains the title and two buttons that will take an action creator function that each take a locale string (‘en’ or ‘ja’) and dispatch them to the reducer.
As you can see if there is anything wrong in the update or an incorrect key is sent to a component then the default message will be displayed.
The action types and action creators are quite simple and look like this:
export const CHANGE_LOCALE_LANGUAGE_SUCCESS = 'CHANGE_LOCALE_LANGUAGE_SUCCESS';
// separate file
import * as localeActionTypes from './localeActionTypes';
export default function changeLocale(locale) {
return { type: localeActionTypes.CHANGE_LOCALE_LANGUAGE_SUCCESS, locale };
}
The final part of course is our locale Reducer, this will contain an action type and create a new state which when updated will update the ApplicationTree component which will then filter down and update other components which have Formatted Message components, the code for the reducer looks like this:
import * as localeActionTypes from 'actions/locale/localeActionTypes';
export default (state = { locale: 'ja' }, action = {}) => {
switch (action.type) {
case localeActionTypes.CHANGE_LOCALE_LANGUAGE_SUCCESS:
return { ...state,
locale: action.locale };
default:
return state;
}
};
Wrapping up
That is pretty much it for adding basic localisation to a react redux application, there are just the basic moving parts that you would expect to find within any new feature you may add to your application, reducers, action creators and components which all work with the flow of your application.
If you happen to have installed the redux chrome extension and installed that into your application, you can see the application change state and see what the locale settings are within the redux store.
I hope this guide provides a nice introduction and gets you on your way with react-intl.