We have recently integrated react-i18next (which is basically a wrapper around i18next) into one of our big projects. The process was almost straightforward and went smoothly with just few nuances which I’m going to explain in details in this article.

Dates

We use moment.js for dealing with all dates and times. The lib itself comes with numerous locales and can do translations on its own, thus we only need to tight together react-i18next and moment.js, so that changing language in react-i18n will switch locale in moment.js.

For this purpose we use React Context API which by the way was updated not that long ago and no longer considered to be experimental feature.

Technically our solution look as follow:

create new context with provider which takes current language value from props and creates new prop with moment.js bound to locale corresponding to current language of i18n instance:

moment.js bound to current locale to children

apply new provider to the root of the app. Important — moment.js provider must be placed under the i18n provider in the tree in order to have access to props.i18n.language :

Apply provider to the app

and the final piece of the puzzle — consume the property created by provider in child components. We will create the HoC which will facilitate wrapping of a component with consumer:

Pass moment.js bound to locale to child component prop

Now let’s apply the HoC to a component which needs date function:

Apply HoC which takes moment.js bound to current locale from context consumer

This is it! Now any component with HoC gets momentTranslated which is instance of moment.js bound to current locale.

Reactive translations and performance optimisation

In some rare cases we render components from js structures, though we normally try to avoid generating jsx from js. E.g. we render tables headers based on array of objects representing each column with properties like column title and column width. Columns titles used as is, which means they are not going to be translated by the Table component which renders columns. They are passed to Table component and are already translated. Like this:

Table columns array returned with columns titles properly translated at the moment of function call

What was tricky with this approach is optimisation of components with columns() calls — making them to call columns() only when language changes. Changing language doesn’t change i18n property passed to component from translate() HoC. It means that when change of language is requested, i18n instance changes its .language property but i18n property itself stays intact, instance is always the same. And it turned out that real language change can’t be tracked within standard React methods or recompose functions like shouldUpdate or withPropsOnChange in a simple way (please, correct me someone if I’m wrong). So, this doesn’t work:

This doesn’t work, withPropsOnChange never calls its second param

This looks ugly but it works:

translate() hoc ensures that i18n.language property is updated on every language change

This is less ugly but not optimised, translation will happen on any property change:

withProps() will call columns() function on any change of any property

Huge dictionaries should stay huge

Let’s talk about size and structure. Locale file volume correlates to app size — while app is growing up you will add all sorts of labels/notifications/errors/whatever. Your locale file can easily grow well over 100kb. If you have multilingual app with numerous locales a very good idea will be to load them only on demand. Fortunately i18next provides a very convenient way of loading locales:

What is left to you is to catch the moment when i18next is loading a locale and to turn on some sort of spinner to make locales transition smoother:

Next let me share my thoughts about the structure of locale files. i18next allows you to create namespaces. The whole page of docs for Namespaces is devoted to explaining reasons why you need them.

They are fine for improving of performance. But spreading locales between files will inevitably lead to duplication of phrases. Phrases with the same meaning will be written differently in different namespaces. E.g. if you split locale file by app sections/pages then there are good chances that one of your app pages will say There is no data yet when it is empty and another one will say No data yet. Not a big deal, if you are not perfectionist.

Huge locale files are totally fine to maintain and you will have no duplications. But as I said loading just necessary translations will produce positive performance impact in huge apps. You can achieve this even with single locale file — build app section specific locale file on backend based on big single locale file and list of phrases which particular app section requires: