Localizing a React app

tl;dr: Following on from a rush of success at conquering React contexts, Rik experiments further to see if he can add a ‘reasonably sane’ localization strategy to his app

Localization - why do important things hurt?

I’m not a great fan of inflicting pain on myself. Yet sometimes I have to face up to my fear of pain and take the hits. This is why I believe websites and apps should have localization built into them from the beginning because - in my (limited) experience - taking the hits at the start of a project is much more fun that being mauled to Hell and back when the client asks you to ‘internationalize’ their marvellous app a few months (or years) down the line.

Truth: localization is hard! It’s not just about translating a few strings here and there - when you internationalize a website or app you need to start not from the developer’s point of view, but rather that of the app’s user in China, or Russia, or Argentina. Because User Experience is cultural, not just linguistic.

Now the React community and various businesses have come up with some interesting ways to localize a React app over the years. I expect some of them work really well - I’ve not explored them in any depth because I’ve not (yet) had an opportunity to work on a big international React project.

Some unexpected benefits of localization

There are a couple of benefits to localizing an app or website early, beyond impressing the client with my strategic, long-term view and potential additional profits:

  • It stops me littering the codebase with hard-coded strings, which are always a bugger to hunt out and fix when the client asks for copy updates.

  • It helps me order my dictionary files in a way that (possibly) makes the codebase easier to navigate for my imaginary co-workers - because site maintenance is important and lucrative stuff.

I expect there’s other benefits as well - for instance opportunities to practice some fancy React code-splitting to bring site load times down; nobody likes a site that takes 30 seconds to present its landing page innit. Whatever.

Anyway. Doing some localization work with a React app is a learning opportunity for me. And when I’m learning I prefer to code up stuff in Vanilla JavaScript where possible - even if there’s already a library which does this stuff perfectly. When I use a library, what I learn is how to use that library; I miss out on all the fundamental stuff about how something needs to be done and why it needs to be done this way, not that way.

As before, I’ll present my thoughts on the how and the why as comments in the code. I shall use the codebase I developed in my Coming to terms with React context post, where I did combat with React’s new context things (and came out ahead on points). It turns out that the solution I found only requires me to add 3 new files to the codebase, and an update to just one existing Component.

Rik’s code for adding localization to his demo Reace app

The first new file is a React Higher Order Component which I shall call Copy.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import React, { Component } from 'react';

// I can use the UserConsumer component to keep track of the user's current locale
import { UserConsumer } from './UserContext';

// I like to keep my locale files in their own folder
// - every different language/dialect will have its own locale dictionary file
// - filenames should really be ISO language codes - this is just for fun
// - https://www.andiamo.co.uk/resources/iso-language-codes
// - only importing the default dictionary for now
import England from '../locales/England';

// This wrapper function takes a component as an argument
function addCopy(WrappedComponent) {

return class extends Component {

constructor(props) {
super(props);

this.state = {
dictionary: England,
locale: 'England'
}
}

// getCopy() updates the locale dictionary whenever user's locale changes
// - would be a lot simpler if we imported all the locale dictionaries at the start
// - but dictionary files can get quite big; this approach offers future adaptability
getCopy = (locale) => {

// I didn't know React ships with an inbuilt dynamic import() function ... until today
// - https://reactjs.org/docs/code-splitting.html

// If at some point I decide the dictionary files are getting too big to include client-side
// I should be able to replace this Promise chain with a fetch-based Promise chain
// - that way all (except the default) dictionary files could stay on the server
import(`../locales/${locale}`)
.then((res) => {
if(res && res.default){
this.setState({
dictionary: res.default,
locale: locale
});
return true;
}
else throw new Error('unknown dictionary requested');
})
.catch((err) => {
this.setState({
dictionary: England,
locale: 'England'
});
return false;
});
};

// Wrapping the WrappedCompoent in UserConsumer gives us access to the User's locale value
render() {
return (
<UserConsumer>
{( {locale} ) => (

// returning the wrapped component
<WrappedComponent

// 'copy' linked to Copy component's dictionary state value
copy={this.state.dictionary}

// only want to fire a change if user has updated their locale
// - I use a 'nonce' attribute for any nonsense return
// - getCopy() is asynchronous but the render function isn't
// - meaning this attribute's value will always be meaningless
// - ... it's a hack, but it seems to work ok
nonce={locale !== this.state.locale ? this.getCopy(locale) : false}

// - pass in all the wrapped component's other props
{...this.props} />
)}
</UserConsumer>
);
}
};
}

// Export the wrapper function, not a component
export {
addCopy
}

Now I need a locale dictionary file to act as the default. This file I shall put in a sister folder to my Components folder, calling it England.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const England = {

HelloRik: {

// simple text value
button: 'Change locale',

// text with an interpolated variable
paragraph1: (locale) => `Hello, Rik. Your locale is: ${locale.toUpperCase()}`,

// text that includes HTML markup (and is thus considered dangerous by React)
paragraph2: (token, expires) => `Your token "${token}" expires at: <i>${expires}</i>`,
},
};

export default England;

It’s not much of an experiment if I don’t have a second locale to swap between. I shall call this locale Abroad.js, where everyone speaks Pirate.

1
2
3
4
5
6
7
8
9
10
const Abroad = {

HelloRik: {
button: 'Change yer Flag o\' Convenience',
paragraph1: (locale) => `Ahoy, Rik. Yer locale be: ${locale.toUpperCase()}`,
paragraph2: (token, expires) => `Yer token &laquo;${token}&raquo; walks the plank at: <i>${expires}</i>`,
},
};

export default Abroad;

The existing Component which needs updating is HelloRik.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import React, { Component } from 'react';
import moment from 'moment';

import { UserConsumer } from './UserContext';

// Get the 'Copy' higher order componenet wrapper function
import { addCopy } from './Copy';

class HelloRik extends Component {

render() {

// It makes sense to me to separate dictionary file object into sections
// - each Component gets its own section in the dictionary
let copy = this.props.copy['HelloRik'];

return (
<div className="HelloRik">

{/* First paragraph uses a function to interpolate a variable */}
<UserConsumer>
{( { locale } ) => (
<p>{copy.paragraph1(locale)}</p>
)}
</UserConsumer>

{/* Second paragraph returns copy with dangerous HTML markup */}
<UserConsumer>
{( { token, tokenExpires } ) => (
<p dangerouslySetInnerHTML={{ __html: copy.paragraph2(token, tokenExpires) }}></p>
)}
</UserConsumer>

{/* Pass the button copy as a prop to our helper component */}
{/* - this copy is just a string, not a function */}
<UserConsumer>
{( { locale, updateUser } ) => (
<HelloRikUpdateButton
buttonText={copy.button}
currentLocale={locale}
updateUser={updateUser} />
)}
</UserConsumer>
</div>
);
}
}

// helper component
class HelloRikUpdateButton extends Component {

render() {

let p = this.props;

let changeLocale = (e) => {

// only have two locales to care about in this exercise
let loc = (p.currentLocale === 'England') ? 'Abroad' : 'England';
p.updateUser({
locale: loc,

// spot the hard-coded copy
// - ought to be catching all copy in the locale files
tokenExpires: moment().format('DD MMM YYYY - HH:mm:ss') + 'hrs'
});
};

return (
<p>
<button onClick={changeLocale}>
{p.buttonText}
</button>
</p>
);
}
}

// this is where we wrap our component in the 'Copy' higher order component
export default addCopy(HelloRik);

So does it work?

It works for me!

As ever, I am not a React expert. If people spot obvious flaws in my code then please don’t hesitate to let me know in the comments.