DEV Community

Vadim Namniak for Jam3

Posted on

10 3

Creating a localized experience for visitors from other countries using React Redux

Getting Started

It is assumed you are already familiar with both React and Redux and looking to add internalization to your application. If you are not, there is a number of boilerplate options out there that can help you get started.
Feel free to check out our implementation of it that we use at Jam3.

Prerequisites

You are highly advised to read the i18next internationalization framework documentation to get an understanding of the main concepts and benefits of using it.

List of required extra dependencies:

Take a sneak peek at these libraries before we proceed.

👉 Consider the overall additional cost of roughly 20kB (minified and gzipped) added to the production build.

Installation

Run this command in your terminal to install the above modules in one batch:
$ npm i --save i18next react-i18next@9.0.10 i18next-fetch-backend i18next-browser-languagedetector i18next-redux-languagedetector i18next-chained-backend

Configuration

The example we’ll be referring to is bootstrapped with Create React App with added Redux on top.
Here’s what our application structure will look like:

App structure

See the CodeSandbox example or check this GitHub repo.

Step 1: Creating translation files

We are going to use English and Russian translations as an example.
Let’s create two JSON files with identical structure and keep them in their respective folders:

{
"homePage": {
"title": "Home Page"
},
"notFoundPage": {
"title": "404: Not Found"
}
}
view raw common.json hosted with ❤ by GitHub
/public/locales/en-US/common.json

 
{
"homePage": {
"title": "Главная Страница"
},
"notFoundPage": {
"title": "404: Страница не найдена"
}
}
view raw common.json hosted with ❤ by GitHub
/public/locales/ru/common.json

These files will serve as our translation resources that are automatically loaded based on the detected browser language.

Step 2: Creating the i18n config file

Make sure to check the complete list of available i18next config options.
This is our main localization config file:

import i18n from 'i18next';
import { reactI18nextModule } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import ReduxDetector from 'i18next-redux-languagedetector';
import Backend from 'i18next-chained-backend';
import Fetch from 'i18next-fetch-backend';
const Detector = new LanguageDetector();
Detector.addDetector(ReduxDetector);
export default function configureI18n({ i18nextConfig, redux }) {
i18n
.use(Backend)
.use(Detector)
.use(reactI18nextModule)
.init({
backend: {
backends: [Fetch],
backendOptions: [
{
loadPath: '/locales/{{lng}}/{{ns}}.json'
}
]
},
detection: {
order: ['navigator'],
lookupRedux: redux.lookupRedux,
cacheUserLanguageRedux: redux.cacheUserLanguageRedux,
caches: ['redux'],
excludeCacheFor: ['cimode']
},
whitelist: i18nextConfig.whitelist,
fallbackLng: i18nextConfig.fallbackLng,
ns: i18nextConfig.ns,
defaultNS: i18nextConfig.defaultNS,
debug: process.env.NODE_ENV !== 'production',
interpolation: {
escapeValue: false
},
react: {
wait: false
},
nonExplicitWhitelist: true,
load: 'currentOnly'
});
return i18n;
}
view raw index.js hosted with ❤ by GitHub
/src/i18n/index.js
  • First off, we need to add the i18next-chained-backend plugin which allows chaining multiple backends. There are several backend types available for different purposes. We are using fetch to load our translation resources.
  • Then we are adding Browser Language Detector (connected with Redux store through Redux Language Detector) for automatic user language detection in the browser. Read more about the approach.
  • Next up, we use reactI18nextModule to pass i18n instance down to react-i18next.
  • Finally, we initialize i18next with basic config options.

Step 3: Adding i18next reducer to the store

Redux Language Detector provides i18nextReducer so you don’t need to implement your own reducers or actions for it — simply include it in your store:

import { createStore, combineReducers } from 'redux';
import { applyMiddleware } from 'redux';
import createHistory from 'history/createBrowserHistory';
import { composeWithDevTools } from 'redux-devtools-extension';
import { i18nextReducer } from 'i18next-redux-languagedetector';
const middleware = [];
const enhancers = [];
const reducers = {
i18next: i18nextReducer
// ... other app reducers
};
export const history = createHistory();
export default function configureStore(initialState) {
const store = createStore(
combineReducers(reducers),
initialState,
composeWithDevTools(
applyMiddleware(...middleware),
...enhancers
)
);
return store;
}
view raw index.js hosted with ❤ by GitHub
/src/redux/index.js

👉 For your convenience, use Redux dev tools in dev environment and make sure you import composeWithDevTools from redux-devtools-extension/developmentOnly.

Step 4: Creating the main app file

There’s nothing specifically related to the internalization in this file.
We simply set the routes for our pages in a standard way.

import React, { lazy, Suspense } from 'react';
import { Route, Switch, withRouter } from 'react-router-dom';
const Home = lazy(() => import('../pages/Home'));
const NotFound = lazy(() => import('../pages/NotFound'));
const App = React.memo(({ location, ...props }) => {
return (
<main className="App" role="main">
<Suspense fallback={<div>Loading...</div>}>
<Switch location={location}>
<Route exact path="/" render={() => <Home />} />
<Route render={() => <NotFound />} />
</Switch>
</Suspense>
</main>
);
});
export default withRouter(App);
view raw index.js hosted with ❤ by GitHub
/src/app/index.js

Step 5: Initializing the app and adding I18nextProvider

The provider is responsible for passing the i18next instance down to withNamespaces HOC or NamespacesConsumer render prop.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import { I18nextProvider } from 'react-i18next';
import { languageChange } from 'i18next-redux-languagedetector';
import configureStore, { history } from './redux';
import configureI18n from './i18n';
import App from './app';
import './styles.scss';
const i18nextConfig = {
language: null,
whitelist: ['en', 'ru'],
ns: ['common'],
defaultNS: 'common'
};
const store = configureStore({
i18next: i18nextConfig
});
const i18n = configureI18n({
i18nextConfig,
redux: {
lookupRedux: function() {
return store.getState().i18next;
},
cacheUserLanguageRedux: function(language) {
store.dispatch(languageChange(language));
}
}
});
const root = document.getElementById('root');
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>
</ConnectedRouter>
</Provider>,
root
);
view raw index.js hosted with ❤ by GitHub
/src/index.js

We initialized our store and i18n config file with the same options to keep both in sync.

Step 6: Using translation keys

We‘ll use withNamespaces HOC that passes the t function as a prop down to the component. We need to specify the namespace(s), and the copy is now accessible via object properties using t function: t(‘homePage.title’).
Note, it is required to prepend the namespace when accessing the copy from multiple namespaces within one component e.g. t('shared:banner.title').

import React from 'react';
import { withNamespaces } from 'react-i18next';
const Home = React.memo(props => {
const { t, i18n } = props;
const data = i18n.getDataByLanguage(i18n.language);
return (
<section className={`Home page ${data ? 'ready' : ''}`}>
<h1>{t('homePage.title')}</h1>
</section>
);
});
export default withNamespaces('common')(Home);
view raw Home.js hosted with ❤ by GitHub
/src/pages/Home.js

Alternatively, we could use NamespacesConsumer component which would also give us access to the t function. We’ll cover it in the next step.

👉 You can test language detection by changing your default browser language. When using Chrome, go to chrome://settings/languages and move the languages up and down in the list.

Step 7 (Bonus part): Creating language switcher

Ok, we’ve implemented language auto-detection and dynamic translation resources loading. Now it’s time to take it up a notch and create a component that allows users switching the language through user interface.
Make sure to include this component in your app.

import React from 'react';
import { connect } from 'react-redux';
import { NamespacesConsumer } from 'react-i18next';
const LanguageSwitcher = React.memo(props => {
function onChange(lng, i18n) {
i18n.changeLanguage(lng);
}
return (
<NamespacesConsumer ns={['common']} wait={true}>
{(t, { i18n, ready }) => (
<select
className="LanguageSwitcher"
defaultValue={props.lang}
onChange={e => onChange(e.target.value, i18n)}
>
<option value="en-US">English</option>
<option value="ru">Русский</option>
</select>
)}
</NamespacesConsumer>
);
});
const mapStateToProps = state => {
return {
lang: state.i18next.language
};
};
export default connect(mapStateToProps)(LanguageSwitcher);
/src/components/LanguageSwitcher.js

NamespacesConsumer render prop provides access to the i18n instance. Its changeLanguage method can be used to change language globally. This will force the app to re-render and update the site with the translated content.

🎉That’s a wrap!

Code examples

Related documentation

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay