Why?

Recently I had to add a localization to my React Mobx web application. What I wanted to avoid is coding the mechanism of locale resolving from scratch (once again in my life).

I was really tired from:

long .json files

files checking navigator.language

transforming en-US to en

to and doing similar tedious stuff

I was looking for a solution, that would allow me to focus on my code — but seems i have found one, that actually over-exceeded my expectations.

Say hello to i18next!

i18next

It’s not just a library that does translations and pluralization of values, but much more. It is a framework for doing all this work that provides you with complete localization solution.

During reading its docs, I was really impressed by the capabilities. From other side docs seemed quite complex to me and it happened that I had to check what some setting means only on real application. So I decided to write this article as a small knowledge base.

Before we start, I would like to mention that i18next is pure JavaScript solution. So you can use it with any framework you like (React, Angular, Vue).

React and i18next

i18next has a powerful system of plugins and connectors to frameworks that you are working with. As we are talking about React app we will have to work with 2 main libraries:

i18next actually localization framework

react-i18next set of bindings for your React application

Its almost the same like mobx and mobx-react, or redux and react-redux.

Lets start

First of all, you will have to add both dependencies to your application.

npm install i18next react-i18next

I would recommend to create a separate file called i18n.js where we will keep configuration for your localization process. It will be small at the beginning but will grow as you add more and more config options and plugins. So we have something like this at that to start with.

/

- i18n.js

- index.jsx

and content of files will look like:

// i18n.js import i18n from 'i18next'; i18n.init({

debug: true,



lng: 'en-US', resources: {

'en-US':{

'translation': {

intro: 'Hello my name is'

}

}

}, react: {

wait: false,

bindI18n: 'languageChanged loaded',

bindStore: 'added removed',

nsMode: 'default'

}

}); export default i18n; // index.jsx import React from 'react';

import ReactDOM from 'react-dom';

import { I18nextProvider, withNamespaces } from 'react-i18next'; import i18n from './i18n.js'; @withNamespaces()

class App extends React.Component {

render() {

const { t } = this.props; return (

<span>{t('intro')} Viktor</span>

);

}

} ReactDOM.render((

<I18nextProvider i18n={i18n}>

<App />

</I18nextProvider>

), document.getElementById('mount')

);

After execution we will see a web page saying “Hello my name is Viktor”

Let me explain what just happened 🤷🏻‍♂️

In order to use i18next in your application it needs to be initialized. i18n.init() is used for this. Method accepts quite huge amount of different properties. But we will focus here on more standard one that we are good to start with.

debug: true — very useful during development, It logs to console all the information on i18next state. Like when it was initialized, what language was set, notifies if some translation is missing. Consider switching it to false in production. If using webpack set NODE_ENV variable and use it like debug: process.env.NODE_ENV === 'development' .

— very useful during development, It logs to console all the information on i18next state. Like when it was initialized, what language was set, notifies if some translation is missing. Consider switching it to in production. If using webpack set variable and use it like . lng: 'en-US' — initial language to use. Later we will see how it is possible to identify language dynamically, avoiding hardcode on init.

— initial language to use. Later we will see how it is possible to identify language dynamically, avoiding hardcode on init. resources — bundle of translation is called resource. It is possible to define them inside init method, load them in runtime with i18n.addResourceBundle method, or have them (I like that part) been fetched asynchronously based on selected language. You should pass an object of the next structure {localeName: {namespaceName: {key: 'value'}} Default namespace is translation so I have passed it in example.

Namespace is a really cool feature of i18next. It allows you to move translations into separate files. As an outcome — you load translations per page basis instead of downloading huge .json file at once and block appearing of your web application content. As the goal of this article is to give just a bird eye view on localization — we will use default namespace( translation ) only.

react — is an object with configuration properties for react-18next addon. I believe no one can explain better than official docs. In example above it uses default values. If you are not planning to change them— just omit react property for i18next init.

In index.jsx you may see also few differences:

I18nextProvider — this is component exposed by react-i18next library. It accepts one attribute i18n with your instance of i18n . Check that we have imported it at the beginning. I18nextProvider is a context provider. Allowing context consumers receive some additional parameters through React Context.

— this is component exposed by library. It accepts one attribute with your instance of . Check that we have imported it at the beginning. is a context provider. Allowing context consumers receive some additional parameters through React Context. @withNamespaces() — My lovely part. Decorator withNamespaces is also imported from react-i18next library. Basically this is a React High Order Component. That passes to our component 2 additional properties. t function and i18n instance.

— My lovely part. Decorator is also imported from library. Basically this is a React High Order Component. That passes to our component 2 additional properties. function and instance. t — calling function t('intro') will check if translation for intro key is presented for selected language ( en-US in our case), and default namespace( translation ). This will output a translation if resource was found, or will return a key. Note: if you are in debug mode — you will see a message in browser console, saying that translation was not found.

Dynamically fetching user language

Of course we don’t want to hardcode user locale in init method of i18n . And ever more, we don’t want to write code for resolving it. And i18next-browser-languagedetector library can help us. So…

npm install i18next-browser-languagedetector

And lets do some modification to i18n.js file

// i18n.js import i18n from 'i18next';

import LanguageDetector from 'i18next-browser-languagedetector'; i18n.use(LanguageDetector)

.init({

debug: true, resources:

'en-US':{

'translation': {

intro: 'Hello my name is'

}

}

}

}); export default i18n;

We have removed lng: 'en-US' and added a call for special i18n method use . This function is used to load additional plugins for i18n. So in this case, we benefit from LanguageDetector plugin. It checks multiple places and resolves user locale for you.

We also removed react property as relaying on default values

Loading translation files from external location

No one keeps long .json files with translations in project source. Mostly they are separate files (and may be hosted on CDN so not together with your source code). Lets see how can we upload a correct one.

Again we will benefit from i18next pluggable architecture.

npm install i18next-xhr-backend;

And do some code modification

// i18n.js import i18n from 'i18next';

import XHR from 'i18next-xhr-backend';

import LanguageDetector from 'i18next-browser-languagedetector'; i18n.use(XHR)

.use(LanguageDetector)

.init({

debug: true

}); export default i18n;

After applying XHR plugin. We can see 3 additional requests in Network tab of Dev Tools.

http://app.com/locales/en-US/translation.json

http://app.com/locales/en/translation.json

http://app.com/locales/dev/translation.json // pattern looks like

/locales/{language}/{namespace}.json

The first question I asked myself was “What is dev language?”. But after small reading it happened to be one more undiscovered cool features of i18next.

Save Missing — i18n can save missing translations to dev language, and later your translators can take it and write proper english phrases and other translations.

language, and later your translators can take it and write proper english phrases and other translations. Language(and Namespace) Fallback — in case your translation was not found in en-US language, it will be checked in en one. (And dev afterwards).

For production its better not to use dev language. You can control that by passing additional parameter to i18n.init({ fallbackLng: 'en' }) . That will also minimize amount of request for language files to 2.

There is even more potential for tuning. Many applications do operate few locales and use only language code like en skipping region code US . If that is applicable for your case. Use i18n.init({ load: 'languageOnly' }) . And you will see only one http request for locale file.

Of course you are able to change the path from where to load translations files

// i18n.js const backendOpts = {

loadPath: `myCusomPathToLocales/{{lng}}/{{ns}}.json`

} i18n.init({

backend: backendOpts

})

Tip: You can use publicPath from webpack to construct proper path to locale files. For example to CDNs.

Writing translation files

Your translation files are pure .json files. And the cool thing about that is that i18next supports nesting out of the box. So you can create meaningful structure inside translations files instead of pure 1 level depth list

// locales/en-US/translation.json {

"common": {

"confirm": "Confirm",

"cancel": "Cancel"

},

"question": "Use i18next?"

}

// App.jsx @withNamespaces()

class App extends React.Component {

render() {

const { t } = this.props; return (

<span>{t('question')}</span>

<button>{t('common.confirm')}</button>

<button>{t('common.cancel')}</button>

);

}

}

Using Mobx to change locale

Mobx provides a wonderful possibility on creating a reactions in order to make side effects in application after some observable property has changed. We will use them to switch language. Lets assume that we keep user selected locale in appStore.locale property.

Then reaction will look like:

// index.jsx reaction(

() => appStore.locale,

locale => {

i18n.changeLanguage(locale);

}

);

The awesome part about this one is that new translation will be asynchronously loaded. And all your React components will re-render to show text in new language. No more ugly full page reloads 🎉!

Real Example

Go to check out a real use case example here. Don’t forget to open console to track what is happening.

Ready for some Hooks?

Note: this functionality is experimental yet, so API may change, and you may face unexpected behavior. Before following this approach, verify your React version is 16.7.0-alpha and react-i18next 8.2.0 or above.

As you may know starting from v 16.7.0-alpha React team introduced a new concept called Hooks that is aimed to simplify a way of how you can reuse your stateful logic between components. And a blazingly fast feedback (less than 24 hours 😲) came form i18next team. So if you are ready to try something really cutting edge — you can localize your apps with react-i18next hooks:

// i18n.js import i18n from 'i18next';

import XHR from 'i18next-xhr-backend';

import LanguageDetector from 'i18next-browser-languagedetector';

import { initReactI18n } from 'react-i18next/hooks'; i18n.use(XHR)

.use(LanguageDetector)

.use(initReactI18n)

.init({

debug: true

}); export default i18n; // index.jsx import React from 'react';

import ReactDOM from 'react-dom';

import { useTranslation } from 'react-i18next/hooks'; import './i18n.js'; // you still need it in bundle function App() {

const [t, i18next] = useTranslation(); return <span>{t('intro')} Viktor</span>;

} ReactDOM.render((

<App />

), document.getElementById('mount')

);

use(initReactI18n) — by calling this plugin, during initialization stage — we make i18next instance available to useTranslation hook in any React Component

— by calling this plugin, during initialization stage — we make i18next instance available to hook in any React Component useTranslation() — i18next React Hook, a function that provides your Functional Components with t function (that we are already familiar with) and instance of i18next

You cannot disagree that with this approach code seems much cleaner. Beside it gives you even more benefits. At this point our App functional component is no more wrapped with @withNamespaces HOC — so when you will shallow render it for testing its structure can be easily observed. You may check it out.

Wrapping up

i18next is really interesting and powerful solution for localizing your application. It allows heavy customizations and tuning. Has saturated infrastructure around with set of Plugins and Tools.

This guide covers only small basic part from wide range of i18next features. So check out its docs for more in depth information.

Thanks for reading! If you like this article, I would sincerely appreciate you click that clap 👏 button or share this story. Your feedback is very important!