JavascriptReactLocalization

React.JS Localization v0.2

Remember my last post about React.js localization? Not 2 years passed yet and I’m happy to share with you the “proper” way to do localization in React.js.

Globalize.js

Yes, I still use globalize. As I said in my previous post, the Common Locale Data Repository is maintained by Unicode Consortium and have support for every possible locale. Please don’t re-invent the wheel. Languages are hard (believe me, I know Russian) and I’ve seen a lot of people trying to do their own localization and plurals. Kudos to them, its a valuable experience to get, but when you are doing a professional web site, you want to use proper plurals.

But while CLDR is only data (usually JSON, but if you are old and nostalgic you can get it in XML format), globalize is the tool. You feed globalize with data and it gives you way to produce shiny localized numbers or dates. Globalize API is very extensive and modular, the core library is only 1KB minified + gzipped and you can add other modules depending on your needs.

Getting CLDR

The way you get CLDR did not change from my previous post. I still think it should be part of your build step. The only difference is that I no longer use cldr-data-downloader since it is limited in the way it downloads the CLDR. Instead I use the combination of gulp-download and gulp-decompress as well as a custom mapping JSON file to map the different packages and versions of CLDR.

What I do recommend you pay attention to, is that CLDR package is split into two “modules”: Locale files and Supplemental files. It is important that you do not ship everything as one json file. Supplemental data is shared across all locales, so if a user switches between different locales, he will use the same supplemental file, but different locale file. This will allow the browser to cache the supplemental.json file if it was already requested once. So while you package your cldr, split it into at least 2 files: {locale}.json and supplemental.json, where {locale} is being the locale you are shipping.

Here are the 2 gulp tasks that I use to create supplemental.json and {locale}.json for the locales I support:

gulp.task('cldr:bundle:supplemental', ['cldr:bundle:clean'], function () {
    return gulp
        .src(CLDRConfig.files.supplemental.map(function (f) {
            return path.join(CLDR_SOURCE_PATH, 'supplemental', f);
        }))
        .pipe(extend('supplemental.json'))
        .pipe(gulp.dest(CLDR_DEST_PATH));
});

gulp.task('cldr:bundle:locale', ['cldr:bundle:clean'], function () {
    const streams = Config.i18n.locales.map(function (locale) {
        return gulp
            .src(CLDRConfig.files.locale.map(function (f) {
                return path.join(CLDR_SOURCE_PATH, 'main', locale, f);
            }))
            .pipe(extend(locale + '.json'))
            .pipe(gulp.dest(CLDR_DEST_PATH));
    });

    return merge(streams);
});

Localization

Now that you have your CLDR ready, you can start with the actual localization.

The main and the most important file is the GlobalizeProvider:

import React from "react";
import PropTypes from "prop-types";
import Globalize from "globalize";

export default function provide(locale, supplemental, data) {

    Globalize.load(supplemental);
    Globalize.load(data);
    const globalize = new Globalize(locale);

    const GlobalizeProvider = class extends React.Component {

        static childContextTypes = {
            globalize: PropTypes.object
        };

        getChildContext() {
            return {
                globalize: globalize
            };
        }

        render() {
            return this.props.children;
        }

    };

    return GlobalizeProvider;

}

locale — is the actual locale name (e.g. “en”, “ru” etc), supplemental — is the content of supplemental.json and data is the content of {locale}.json.

You can theoretically load ALL locale data you will ever support, but its a huge huge huge waste. Locale is not something that users change every 5 minutes, so if I prefer an en_US version of you website, most likely Ill never switch to ru or en_GB, and even if I will switch, I’m ok with you reloading the website and loading the locale data again.

So we have the GlobalizeProvider. It uses context to pass the globalize instance (you can read about context in one of my posts). We will need one more Container component that handles the locale loading based on any strategy you want (guess it from the browser, headers, make the user select; whatever, I prefer to start with en_US and let the user select). Here is a snippet of my RootContainer component that handles the locale loading (I’m using redux here):

import React from "react";
import {connect} from "react-redux";
import {BrowserRouter as Router, Route, Switch} from "react-router-dom";

import {Actions as LocaleActions} from "../actions/locale";

import GlobalizeProvider from "../globalize/Provider";

import ErrorComponent from "../components/Display/Error";
import LoadingComponent from "../components/Display/Loading";

import "../../styles/style.sass"

class Root extends React.Component {

    componentWillMount() {
        const {dispatch, locale} = this.props;

        if (locale.supplemental === null) {
            dispatch(LocaleActions.loadSupplemental());
        }

        if (locale.data === null) {
            dispatch(LocaleActions.loadLocale(locale.locale));
        }
    }

    render() {
        const {locale} = this.props;

        if (locale.error) {
            return <ErrorComponent>Failed to load locale data</ErrorComponent>;
        }

        if ((locale.supplemental === null) || (locale.data === null)) {
            return <LoadingComponent/>;
        }

        let GlobalizeProviderContainer = GlobalizeProvider(locale.locale, locale.supplemental, locale.data);

        return (
            <GlobalizeProviderContainer>
                <Router>
                    <div>
                        <SignNDAtoViewThisComponent/>
                    </div>
                </Router>
            </GlobalizeProviderContainer>
        );
    }

}

const select = state => {
    return {
        locale: state.locale
    }
};

export default connect(select)(Root);

And now we have GlobalizeProviderContainer that wraps our whole application, and since it uses context we can pass initialized globalize instance to every leaf component.

Localized Leaf Components

The only thing left is to implement Localized leaf components.

Here is an example for DateTime component to display date & time in given format:

import React from "react";
import PropTypes from "prop-types";

export default class DateTime extends React.Component {

    static propTypes = {
        date: PropTypes.instanceOf(Date).isRequired,
        format: PropTypes.string.isRequired
    };

    static contextTypes = {globalize: PropTypes.object.isRequired};

    static _formatters = {};

    static getFormatter(format, globalize) {
        if (!(format in DateTime._formatters)) {
            DateTime._formatters[format] = globalize.dateFormatter({skeleton: format});
        }
        return DateTime._formatters[format];
    }


    render() {
        const formatter = DateTime.getFormatter(this.props.format, this.context.globalize),
            formattedDate = formatter(this.props.date);

        return <span className="component localized datetime">{formattedDate}</span>
    }

}

Notice the static contextTypes declaration. This is what gives us access to the globalize instance provided from GlobalizeProviderContainer (for more info read my context posts).

Inside the Localized component, you use pure globalize API to access the different formatting methods.

You can even query the CLDR itself for data. Take a look at Currency component below:

import React from "react";
import PropTypes from "prop-types";

import Currencies from "../../../../data/currencies.json";

export default class Currency extends React.Component {

    static contextTypes = {globalize: PropTypes.object.isRequired};
    static propTypes = {
        display: PropTypes.oneOf(['name', 'symbol']),
        currency: PropTypes.oneOf(Currencies).isRequired
    };
    static defaultProps = {display: 'name'};

    render() {
        switch (this.props.display) {
            case 'name':
                return <span
                    className="component currency name">{this.context.globalize.cldr.main(`numbers/currencies/${this.props.currency}/displayName`)}</span>;
                break;

            case 'symbol':
                return <span className="component currency symbol">{this.props.currency}</span>;
                break;

            default:
                return null;
        }
    }

}

This component, when rendered like this:

<Currency currency="USD" display="symbol"/>

Will produce:

<span class="component currency symbol">USD</span>

But when rendered like this

<Currency currency="USD" display="name"/>

Will produce

<span class="component currency name">US Dollar</span>

For en_US locale, and for the ru locale it will produce

<span class="component currency name">Доллар США</span>

You can now go on and implement as much Localized Leaf components as you wish, depending on your needs. I have Country and Currency Selects that are entirely localized, Date Picker that is based entirely on CLDR and Globalize (as opposed to a lot of date pickers that use internal localization files) and etc.

Hope this helped you to become a better React.js developer. Best of luck :)

Share this article

Published by

Dmitry Kudryavtsev

Dmitry Kudryavtsev

Senior Software Engineer / Tech Lead / Consultant

This post shares my opinions and is not meant to represent the opinions and views of my employer, my family, and or my colleagues.