Localization and Internationalization Tutorial With a Real E-Commerce Website Example

Using Yahoo’s react-intl library to internationalize .us and .eu domains for Siren Apparel, and locale detection for localization: attempting to minimize what could be a complex codebase as a solo developer.

Written by Chris on August 19th, 2018.

Background & Motivation

As I promised in my very first Siren Apparel post, I would, in addition to discussing art, design, and charity, be also going through interesting tech-sided learning points of building a company in 2018. So here’s the very first in what will be a series of that type of post.

As I’ve been expanding Siren Apparel from the US to the EU, I of course have been considering language components of our EU homepage. This is when Yahoo’s react-intl npm module came to my attention.

Even after taking Egghead’s react-intl course by Damon Bauer (***Note — I think this course is only for pro members — but I anyway strongly encourage a subscription to Egghead if you’re interested in improving your front end skills!) and learning the basics of react-intl, I still had some internal design questions and decisions I needed to make, according to 1. The user’s location, and 2. the user’s preferred browser language.

Two Levels of Rendering: Location and Language

Render Level I: Localization (Location)

Location is important to consider for Siren Apparel, since we have different product lines and suppliers based on our three locations: the United States, the European Union, and Asia.

Brainstorming how to render based on location, I realized there were a few paradigms in implementing location-based rendering:

Hipster notes.

As is probably not clear from my ‘hipster notes’ 😂, the ultimate question boiled down to a key choice from a few options:

  1. Make totally separate react apps for the EU, US, and Asia sites? (i.e. three Nodejs create-react-app web apps, two git repositories, etc., etc.) → probably maintenance nightmare a few years out into development in terms of keeping all changes to both sites consistent in their look and feel.
  2. Make each component of the site dynamically rendered based on the URL of the site? (i.e. we are at ‘sirenapparel.eu’ → show EU products, ‘sirenapparel.us’ → show US products) → probably a good idea for very location specific parts of the site, for example, the store or the contact section.
  3. When small modifications in the UI are required, do inline rendering based on the location, still keeping a global component for all sites

I decided with a mix of options 2 and 3, because the styling and thus overall site feel would remain, and only small components would change dynamically. Indeed, concept 2. requires more maintenance, but nowhere near as much as concept 1. So far I have only needed to leverage concept 2 for the store, having separate components for our US store, EU store, and Asia store, but this could increase if we introduce news, updates, and blog posts that are country specific. Concept 3 is most widely used so far.

Render Level II: Internationalization (Language)

I think it makes sense that regardless of a users location (United States, Europe or Asia), I should render the site in the user’s preferred language. This notion didn’t require any extra work on my part; react-intl does exactly this: detecting the browser language and rendering the text in that language. The fallback when the requested language is not found is English.

Wrapping It Up

To me, a unified experience across locations and languages is most important when starting a digital brand. I figured options 2 and 3 made that most achievable, for now, and in the longer term as I build my brand. I realize if I were to continue building, option 1 could be a better option, but building and maintaining threeseparate sites is only feasible if I had a giant team of software developers.

As for now, I believe the path I have chosen is the best option for what I am trying to achieve. Additionally, with this style of building, the language and location-based rendering are totally adjacent: I can build out one or the other, or both, at different rates and the app will continue to work.

Localization Implementation

Again, the location of the site has the largest affects to the business itself (products avaliable, shipping methods, etc), and in my mind is like above, the ‘primary’ level of rendering, so I’ll go through it’s implementation first. To determine the location of the site itself (whether the U.S., EU, or Asia) I test the window.location.href with the help of this function on the client side at runtime:

export function determineLocation() {
let sLocation = "";
if (window.location.href.includes("sirenapparel.us")) {
sLocation = CONSTANTS.US;
} else if (window.location.href.includes("sirenapparel.eu")) {
sLocation = CONSTANTS.EU;
} else if (window.location.href.includes("sirenapparel.asia")) {
sLocation = CONSTANTS.ASIA;
} else { // default to US site, the good ol' original
sLocation = CONSTANTS.US;
}
return sLocation;
}

(Let me know if this is wrong, or if there is a better / more reliable option. I realize window.location.href can be spoofed, but probably the kind of people doing are not in the same Venn Diagram as customers to my store!)

I call this function only once in App.js , and then pass it down through the entire app through the sLocationprop:

import React, { Component } from 'react';
import './styles/App.css';
import { determineLocation } from './utils';
// custom components
import Nav from './components/Nav';
import HomePage from './components/HomePage/HomePage';
import Footer from './components/Footer';
class App extends Component {
render() {
// determine which site to render
let sLocation = determineLocation();
return (
<div>
<Nav locale={this.props.locale} sLocation={sLocation}/>
<HomePage sLocation={sLocation}/>
<Footer/>
</div>
);
}
}
export default App;

As you can see, so far, only the navigation bar Nav , and the Homepage components need to inherit the location — the Footer has no reliance on location (yet).

Internationalization Implementation

Again, the internationalization is covered 100% by the npm package react-intl. While internationalization may at first sound simple, in that it “just renders a site in a user’s preferred language”, react-intl requres a few intricate steps in order to get up and running.

First off, we’ll need of course the react-intl npm module:

npm install --save react-intl

I implemented react-intl as follows:

First, create a messages.js file in your root src/ that will hold all the translated texts for all the languages you wish your app to support. We’ll just fill it with an empty en value for English for now:

export default {
'en': {...} // English
}

Add this flattenMessages function to a utils.js file or similar:

export function flattenMessages(nestedMessages, prefix = '') {
return Object.keys(nestedMessages).reduce((messages, key) => {
let value = nestedMessages[key];
let prefixedKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'string') {
messages[prefixedKey] = value;
} else {
Object.assign(messages, flattenMessages(value, prefixedKey));
}
return messages;
}, {});
}

This function allows you to have nested .json style language texts. If this function is not called in your codebase, you would need to write your messages.js file in a totally flat key: value format. This function will have to be imported in your index.js:

import { flattenMessages } from './utils';

While we’re at it, also import the function addLocaleData and the component IntlProvider — we’ll need these later:

import { addLocaleData, IntlProvider } from 'react-intl';

Also add to your index.js all planned languages from the react-intl library:

// US / fallback / europe
import en from 'react-intl/locale-data/en';
// europe
import de from 'react-intl/locale-data/de'; // German
import fr from 'react-intl/locale-data/fr'; // French
import it from 'react-intl/locale-data/it'; // Italian
import es from 'react-intl/locale-data/es'; // Spanish
// asia
import zh from 'react-intl/locale-data/zh'; // Chinese
import hi from 'react-intl/locale-data/hi'; // Hindi
import ja from 'react-intl/locale-data/ja'; // Japanese
import messages from './messages';
addLocaleData([...en, ...de, ...fr, ...it, ...es, ...zh, ...hi, ...ja]);

Good. We’re done with the importing housekeeping.

Then, flattenMessages will be called right in the root index.js file, in the <IntlProvider> component that wraps the <App> component:

// WAIT! DON'T copy me just yet! Read on!
ReactDOM.render(
<IntlProvider locale={locale} messages={flattenMessages(messages[locale])}>
<App locale={locale} client={client}/>
</IntlProvider>,
document.getElementById('root')
);

***But wait! Don’t copy and paste that code snippet yet! We’re not done! I noted that there was a bit of an issue with this method of implementation, namely two reasons:

  1. Handling two-part locales. For example, if you have some users visiting with an en-US locale, and some with en-UK, and you’ve only implemented en-US, etc. vs. Your app with throw a TypeError in the flattenMessage function and your entire app won’t work!
  2. Handling one-part locales. (!?) Sometimes, the browser will pass only a language locale, for example, simply an en locale without any -US, -UK, or similar info. (This is possible, at least with Google Chrome — you can select a generic English vs. a locale with both English and locations. I’m not sure about other browsers.) If you’ve only defined en-US or en-UK or en-AU, a simple en locale wouldn’t match any of those!

This is not good! We should at least be making a few checks on the locale, and if we can’t find anything at all, defaulting to a given language.

So after a bit of fiddling, I implemented a more robust implementation of the locale determination and subsequent messages parsing could be done as such:

  1. Trying for the full two-part locale, and
  2. If that key doesn’t exist, just parsing out the first two letters (the language part of the locale, via .substring(0,2) )
  3. Finally, if in the case if the key is undefined for all message locales, I fall back to the English "en" messages. You can do that right under the locale determination step. It looks like this:
// copy me
let oMessages;
if (messages[locale]) { // optimum case: full locale is found in messages object, ex: en-US, en-UK, de-DE, de-AT etc.
oMessages = messages[locale];
}
else if (messages[locale.substring(0, 2)]) { // second-best case, the language by itself is found as a key "en" or "de"
oMessages = messages[locale.substring(0, 2)]; // key found for locale of language
} else {
oMessages = messages["en"]; // key not found; use English: "en"
}

With this safety check, then you can pass the oMessages object into your <IntlProvider> component:

// copy me
ReactDOM.render(
<IntlProvider locale={locale} messages={flattenMessages(oMessages)}>
<App locale={locale} client={client}/>
</IntlProvider>,
document.getElementById('root')
);

Finally, you may be wondering where the locale variable is determined. You can place this just above that ReactDOM.render() call like so:

// copy me
let locale =
(navigator.languages && navigator.languages[0])
|| navigator.language
|| navigator.userLanguage
|| 'en'; // English as fallback if locale cannot be determined

These are the two code snippets you should copy!

Then it’s just a matter of adding your languages to your messages.jsfile (for me, this is a work in progress — luckily, I have friends from all of these countries, so I can get a cheap, if not free, translation of all the text I need — though it may take some time! 😄

export default {
'en': {...}, // English... fallback primary language :)
'de': {...}, // German - EU language no. 2
'fr': {...}, // French - EU language no. 3
'it': {...}, // Italian - EU language no. 4
'es': {...}, // Spanish - EU language no. 5
'zh': {...}, // Mandarin
'hi': {...}, // Hindi
'ja': {...} // Japanese
}

Let’s say in en you had the following structure:

Header: {
title: 'We are Siren Apparel.',
...
}

To use that text in a page of your react app, you would first import FormattedMessage from react-intl:

import { FormattedMessage } from 'react-intl'

You can then reference the Header and title by concatenating them as an id with a . :

<h2><FormattedMessage id="Header.title"/></h2>

Repeat this process for any text in your app! (I know, this is probably tedious work, but your international users will thank you!)

And Violá! You’ve done it! You’ve internationalized your app!

To Do and Future Work

Though I have a basic implementation for internationalization and localization, I’ve brainstormed a handful of ideas that I think would be good to implement:

  • internationalization: detect if the location of the user’s browser is different from their respective .us, .eu, .asia url, and show a small suggestion in the Nav if they want to spring to another, hopefully more relevant domain assuming the location lookup is correct
  • internationalization: also create in the nav a small request function that popups when we are falling back to the English standard
  • localization: show designs and products most relevant to a user’s location. For example, visitors from Germany should see products with German fire departments / volunteer groups, then EU, then Asia / rest of world products

So that’s how you can internationalization and localize a react app! I hope I’ve written it in a generic enough fashion such that you can use it for your own work and projects!

Cheers!