Localization with React Hooks and Redux Toolkit
By: Tamás Polgár
Tags: React Hook, Localization, Vuejs, Typescript With React, React

I recently tried to implement my Simple dynamic localization in VueX pattern in React, and it turned out to be a true shitshow. It beats me why people “love” React, but these days we celebrate weirdos, so I guess it’s OK.

Vue is lovely…

Let’s do a quick recap what was achieved there. In VueX I defined a store module called i18n which simply imported a JSON file with all the strings used in the app. This file was generated with Localise.biz, then exported and used as an asset. There was a VueX value to store the currently selected language’s code, like en-US , fr-FR or hu-HU and of course there was an action to change it. Then I used a parameterized selector called translate to get the correct translation for placeholder strings called Asset ID-s, like this:

<p>{{ translate('page.title') }}</p>

from a JSON tree like this:

{

"en-EN": {

"page": {

"title": "My great website"

}

}, "fr-FR": {

"page": {

"title": "Mon grand site Web"

}

}

}

… but React isn’t nice

The same pattern simply can’t be implemented with React Hooks and Redux Toolkit. Redux selectors don’t accept arguments like in VueX on the first place. OK, they actually do, but in a very different and much more restrictive fashion. So if you defined one called translate and used it like this…

const translate = (assetId: string) => useSelector(translate(assetId)); return (

<div>

{translate('page.title')}

<div>

)

…it will cause a firework of exceptions. The above example might work, but it’ll crash as soon as you try to use the selector for anything else than static DOM content. For example if that div is rendered conditionally…

{ condition && <div>

{translate('page.title')}

<div>}

… it’ll crash as soon as the condition changes. React Hooks expects to have the exact same number of hooks as when the page was first rendered, and a new one isn’t welcome.

The solution: Redux GTFO!

As I was growing increasingly frustrated about the meaningless restrictions of React, I began to think why on earth am I using Redux for this on the first place? At the end I began to build a plain Typescript class.

import i18n from '../assets/jsondata/i18n.json'; export default class I18n {

private static instance: I18n; constructor() {

if (I18n.instance)

return I18n.instance;

I18n.instance = this;

} private i18n: any = i18n as any;

private language: string = 'en-EN'; public get currentLanguage(): string {

return this.language;

} public set currentLanguage(newLanguage: string) {

this.language = newLanguage;

}

}

It’s a singleton. It imports the localization file, then sets a private language variable to store the currently selected language’s code. Then there’s a getter and a setter to get and set this value.

Why not make the variable public? You’ll see it soon. In my actual project the setter is way more complicated, and so is the class. For example my real life i18n.json also stores locales for each language (currency unit, date format, etc) which has to be read and put into another private variable, but I didn’t want to complicate this article with such details. My real life code also supports language fallbacks. A language may use identical strings to another, but a different locale. For example European and Canadian French language are (mostly) the same, but they use different currencies and date formats.

Now we can add the class to a React Hooks module.

import I18n from './utils/i18n'; export const ReactHooksComponent: React = () => {



const i18n = new I18n(); return (

<a onClick={() => i18n.currentLanguage = 'en-US'}>en-EN</div>

<a onClick={() => i18n.currentLanguage = 'fr-FR'}>fr-FR</div> <div>{i18n.currentLanguage}</div>

); }

Now we can switch languages by clicking the links. Unfortunately nothing will happen on the screen. The DOM doesn’t update when the class changes, and really, why would it? (Because this is neither Vue nor Angular, that’s why…) We’ll address this problem right away.

Since our class is a singleton, we can add it anywhere in the app. This is to ensure that all our components will use the same language. If we set the language on one page, it will change everywhere.

If you’re using ESLint, it may scream at you for various things. Mine freaked out about the singleton and complained that static is an invalid token. If you need to adjust your .eslintrc file, here’s a good article: Config ESLint, Prettier in Typescript React App.

Adding translation functionality

Next step is to add a method called translate() which will, as you guessed it, perform the translation of a string. This might be familiar from the VueX localization article. I’m giving you a simplified version (no placeholder values):

public translate(assetId: string): string { let translated: any = this.i18n[this.currentLanguage];

assetId.split('.').forEach(key =>

translated = (!!translated && !!translated[key])

?

translated[key]

:

null

); return translated || assetId;

}

It looks up a deeply nested Asset ID in the JSON file and returns its content. Usage:

<h1>{ i18n.translate('page.title') }</h1> <a onClick={() => switchLanguage('en-US')}>en-EN</div>

<a onClick={() => switchLanguage('fr-FR')}>fr-FR</div>

But it will still only display the string once, and it won’t change when we switch language. Our string will only be updated when the DOM changes for any reason.

Component state to the rescue!

There is a frequently used React trick to force the DOM to update: changing the component state. It might not be the most elegant way, but it’s safe. But then, React isn’t elegant at all, so why bother.

How could our class signal the parent React component that the language was changed? Easy: with an EventEmitter . Now you’ll see why I used a setter instead of a public variable to store the language code.

import i18n from '../assets/jsondata/i18n.json';

import EventEmitter from 'events'; export default class I18n {

private static instance: I18n; constructor() {

if (I18n.instance)

return I18n.instance;

I18n.instance = this;

} public events = new EventEmitter(); private i18n: any = i18n as any;

private language: string = 'en-EN'; public get currentLanguage(): string {

return this.language;

} public set currentLanguage(languageCode: string) {

this.language = languageCode;

this.events.emit('switchLanguage', languageCode);

}

}

Now we just add this little trick to our React component:

const [counter, setCounter] = useState(0); const i18n = new I18n();

i18n.events.on('switchLanguage', () => setCounter(counter++));

Now when the language is changed, our class emits the switchLanguage event, which in turn sets a component state value, causing the DOM to be updated.

Wait a minute, you might ask. Do I have to add these silly lines to every single component I have? Must I now use setState in components which don’t even have a state?

Not at all! Here’s the trick: a component state change in a React Hooks component trickles down to all child components, causing them to refresh too. So actually you’ll only have to put the above lines into your App.tsx (or App.jsx for you non-Typescripters) and the event will update everything on the screen, as long as every component in your app is a child of App.tsx . In the child components you won’t need state setter and the event detector.

At the end there’s just one very important thing to remember: Jeffrey Epstein didn’t kill himself. Happy coding!