Using React’s context API to provide a localization toolbox for your components

A brief overview of how to format time and numbers, how to make it relevant to a specific locale, and how to make it all easy with React’s context API.

Localization is hard

WooRank’s services are available in 6 languages : English, Spanish, French, German, Dutch and Portuguese. It gives us international reach, but it also means some headaches to keep everything consistent across the board.

Our services are based on data, on numbers and dates and values and ratios. And the funny thing with those is that every language has a different way of formatting these seemingly self-evident pieces of information.

Take a simple date/time in English, for example : June 5, 2018 3:56 PM .

Date and time of the review generation.

Here is what it translates to in the other languages :

Spanish : 5 junio de 2018 15:56

: French : 5 juin 2018 15:56

: German : 5. Juni 2018 15:56

: Dutch : 5 juni 2018 15:56

: Portuguese : 5 de junho de 2018 15:56

Trust me, you do not want to design your own solution to automatically generate those from Unix timestamps.

But there is more! How about when you need to display percentages ? Or round down a value? Sure, you could do write a host of different utility functions like this :

const formatPercent = number => Math.round(number * 10000) / 100;

But do you really want to ?

And how about the simple task of displaying a number ? 1,000,000 is correct in English, but nonsensical in French, German or Dutch, where such a number would be noted 1 000 000 . They use the comma to mark decimal points. In those language, 1,5 is correct, where it would be 1.5 in English.

This is a whole new world of linguistics and scientific tradition trickiness you also do not want to get into at the lowest level.

A solved problem … ?

Luckily, the problem of formatting dates and numbers is as old as the Internet, or even computer science, and very smart people have solved it for us. In the JavaScript world, it mostly means using Moment to handle dates, and Numeral to handle numbers.

import moment from 'moment';

import numeral from 'numeral'; moment(1528206960000).format('LLL');

// June 5, 2018 5:56 pm numeral(1000000).format();

// 1,000,000

numeral(1.5).format('0.0');

// 1.5

But wait! We also want this nicely formatted data to be adequately localized according to the language chosen by the user. That's where the locales provided by both libraries come into play. These hold all the necessary data to get everything right (here is the ‘fr’ locale from numeral , for example).

import moment from 'moment';

import numeral from 'numeral';

import 'numeral/locales';

// yes, you have to import the locales from numeral manually moment.locale('es');

moment(1528206960000).format('LLL');

// 5 de junio de 2018 15:56 numeral.locales('fr');

numeral(1000000).format();

// 1 000 000

numeral(1.5).format('0.0');

// 1,5

Alright then! I’ll just import these libraries everywhere I need to localize data. And the locales too. And then I’ll fetch the current language value. Here’s to hoping the server and the client don’t disagree on this one. But where do I set the locale value ? Once per component? Or at the top level of the app ? Hmm…

…

There must be a better way.

React’s context API to the rescue

React 16.3 got a lot of hype. Like, a lot. And particularly because of the stable implementation of the context API. Basically, it’s a way to pass data around in your React app without needing to drill it from the top down via props until you need it. The perfect tool for our purpose : building an localizationContext which will give access to properly localized instances of numeral and moment anywhere in our React app.

Edit (Saturday, 30 June 2018) : as observed in a response, and as answered by WooRank’s lead dev, this is a solution that is especially useful when dealing with an isomorphic app (i.e. concerned with rendering the same code on the client and the server). It may be overkill and/or inefficient for purely client-side SPAs.

The pieces we need are :

a root App where we will create localized versions of moment and numeral . For moment , we can create a localized instance by calling .locale(lang) on it. Unfortunately, numeral doesn’t support this yet, so we have to set the locale globally.

const lang = "en"; const localizedMoment = (...args) => {

return moment(...args).locale(lang);

}; const localizedNumeral = (...args) => {

numeral.locale(lang);

return numeral(...args);

} return numeral(...args);

} localizedMoment(1528206960000).format('LLL');

// June 5, 2018 5:56 pm localizedNumeral(1000000).format();

// 1.000.000

a React Context giving us access to the Provider (which will receive our localizedMoment and localizedNumeral on its value prop) …

const Context = React.createContext(); const ContextProvider = props => {

return (

<Context.Provider

value={{

moment: props.moment,

numeral: props.numeral

}}

>

{props.children}

</Context.Provider>

);

};

… and giving us access to the Consumer , which will give us the ability to retreive localizedMoment and localizedNumeral in our components. Because we anticipate we will need these often and we are too lazy to wrap every single div that needs it in a <Context.Consumer> , we're going to create a withLocalization higher-order component which take a component and returns the same component, but with our localization toolbox merged into its props!

const withLocalization = Component => {

return function LocalizedComponent (props) {

return (

<Context.Consumer>

{context => {

return <Component

{...props}

moment={context.moment}

numeral={context.numeral}

/>;

}}

</Context.Consumer>

);

};

}

Plug it in

Time to put it all together! The root of your app could look like this :

const time = 1528206960000;

const data = { amount: 19879989776, fraction: 0.22 }; const App = () => (

<ContextProvider

moment={localizedMoment}

numeral={localizedNumeral}

>

<Localized data={data} time={time} />

</ContextProvider>

); render(<App />, document.getElementById("root"));

While your Localized component could look like this (notice how we gain access to moment and numeral on its props by wrapping it in our withLocalization higher-order component) :

import { withLocalization } from "./localizationContext"; const Localized = ({ data, time, moment, numeral }) => {

const localizedDate = moment(time).format("LLL");

const localizedAmount = numeral(data.amount).format();

const formattedFraction = numeral(data.fraction).format("0%"); return (

<React.Fragment>

<h1>Date : {localizedDate}</h1>

<h1>Amount : {localizedAmount}</h1>

<h1>Percent : {formattedFraction}</h1>

</React.Fragment>

);

}; export default withLocalization(Localized);

And here is the result you should be able to get!

Changing the locale value at the root of the app

From there on, you can add all sorts of other tools to your localization context, like a translate function, or timezones handling!

I hope you have learned a thing or two, and please, have a play with the Sandbox for this little project~~