React / Redux Multilingual Isomorphic Apps (i18n)
Inspired by React / Redux and Multilingual (Internationalization) Apps – Architecture, I’m about to represent Antoine Jaussoin‘s idea and some modification to fit my project requirement.
Checkout Antoine’s repo here.
In short, he wraps the component with another component called Translate component. This Translate component would get the currentLanguage from this.context, which is passed down to every child components from the TranslationProvider, used in top class components (in his project, are the pages and layout components which hold other components without rendering a single word of text, since they could not translate themselves).
The top class components don’t know about the currentLanguage, the TranslationProvider does. TranslationProvider connects to the store (is this case, the user store) to get the currentLanguage, and passes it to all of its child components via getChildContext(), so every child component can access currentLanguage by this.context.currentLanguage.
Translate component is a function which receives a key (the component name) and the Component class reference. It get the currentLanguage and the correct translation file, then return the same Component injected “strings” into its props. “Strings” is an array with correct translated strings.
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; }; }
After that, the real component just get the strings by calling:
this.props.strings.hello_world
More carefully, he puts a default Strings array in every component’s props, so that if somehow the Translate component couldn’t inject the translated strings, the component would still render anyway, using the default strings.
MyComponent.propTypes = { strings: PropTypes.object }; MyComponent.defaultProps = { strings: { someTranslatedText: 'Hello World' } };
I think Antoine’s idea is really great, as it solves his needs:
- Each component deal with translation in isolation. The component is standalone, can be reuse anywhere, optionally being wrapped by a Translate component, without crashing if the currentLanguage could not be reached.
- Translated strings are centralized in a file, divided into Component sections. Edit once, use anywhere.
What’s confusing me is the default strings array in every component, does it necessary? Why don’t we just pick a default language (e.g. English) to ensure the currentLanguage always exists? He uses the TranslationProvider to help wrapping any component wants to have translation. In my opinion, I would just use the App component as the TranslationProvider, since its the highest level component in the app.
Another thing, I’m working on an isomorphic web app (isomorphic application is one whose code can run both in the server and the client), which has server-side rendering. How do I know which language to render for the first time the user request to my server? And the URL will not change when the language changes. Definitely I can not just pick one as default. The answer is: cookies.
Before thinking of using cookies on server side, I was desperate watching my website to render in English on the server and then re-render in Vietnamese on the client, as the user’s language setting is Vietnamese. There was always a flash of language-changing frames when viewing the page, such a nightmare for a web app!
On the first time a user visit the website, we have no choice but showing the default language, but then if they change the language, we must make sure that next time they visit it should render the same language they chose. So here is how I do:
The action: Switching language
export function select(params) { const { lang } = params; if (cookies.enabled) { cookies.set('language', slug); } return { type: 'LANGUAGE_SELECT', field: 'currentLanguage', data: lang }; }
When the user clicks on a language button on the language switcher, this action will be dispatched, to update the language setting in browser’s cookie and also the store.
The reducer: Update language state
export default function LanguageReducer(state = defaultState, action) { if (typeof state === 'string') { let cookieArr = state.split(';'); let languageState; for (let i = 0; i < cookieArr.length; i++) { if (cookieArr[i].indexOf('language') > -1) { languageState = defaultState.set('currentLanguage', cookieArr[i].replace('language=', '')); i = cookieArr.length; } } return languageState; } const { type, data, field } = action; switch(type) { case 'LANGUAGE_SELECT': return state.set(field, data); default: return state; } }
This looks quite normal, but it would have some update when we get to server-side rendering problem.
The App component
In my case, App is the highest level component (defined in route file for React Router):
<Route path="/" component={App}> // Child routes </Route>
App will be the first component to be rendered, so it’s the most suitable to hold the currentLanguage. The App component connects to the language node in the state tree to get the currentLanguage, then passes it down to its children via child context.
class App extends Component { getChildContext() { const { language } = this.props; return { language: language.get('currentLanguage') || 'vi' // DEFAULT LANGUAGE: vi }; } } App.childContextTypes = { language: React.PropTypes.string } function mapStateToProps(state, props) { return { language: state.language }; }; export default connect(mapStateToProps)(App);
Cookies FTW: get, set and be cool.
What would happen when the user changes the language?
Re-render some components or simply reload the whole page.
Conclusion
That’s it. Well, now its just a flow frame, I’m actually writing this before try doing any of these ideas. Okay, write time’s over, it’s programming time!
~to be continued~