Software localization

JS I18n with Globalize.js

This tutorial will guide you through JS i18n with Globalize.js, a simple yet powerful i18n and l10n library that leverages the official Unicode CLDR JSON.
Software localization blog category featured image | Phrase

If you want to use Globalize.js in your JS i18n project, there are a few things you need to do to make it work. First, you need to load the necessary resources to configure it. Subsequently, we only need to provide translations. Let's discuss the steps one by one.

🔗 Resource » Make sure to stop by our Ultimate Guide on JavaScript Localization and learn everything you need to go multilingual with your JS apps.

First Steps towards JS I18n with Globalize.js

Let's start our journey with a simple HTML5 Boilerplate. If you want to access the source code of this project, it's hosted on GitHub. First, create a project folder and download the sources from here.

wget https://github.com/h5bp/html5-boilerplate/releases/download/v7.1.0/html5-boilerplate_v7.1.0.zip -O temp.zip

unzip temp.zip -d phrase-app-globalize

rm temp.zip

ce phrase-app-globalize

Install the dependencies. Once the installation is completed, we can start building the application:

npm install && npm run build

This will create a dist folder where all the static assets of your website are compiled. To run the application, just start a static webserver on that location. For example, using python3:

cd dist

python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Navigate to http://0.0.0.0:8000/ on your browser and you will see the default message:

HTML boilerplate | Phrase

Now that we have created our boilerplate, let's see how we can install Globalize.js into our application...

Installing Globalize.js

First, install it together with its dependencies using npm:

npm i cldr cldr-data --save

npm i globalize --save

You'll also want to install some gulp plugins needed to move forward:

npm i gulp-concat --save-dev

Add some tasks to load the necessary libraries for Globalize.js and include them in the copy sequence:

gulp.task('copy:globalize:deps', () =>

  gulp.src([

    'node_modules/cldrjs/dist/cldr.js',

    'node_modules/cldrjs/dist/cldr/event.js',

    'node_modules/cldrjs/dist/cldr/supplemental.js',

  ]).pipe(plugins().concat('cldr.js'))

    .pipe(gulp.dest(`${dirs.dist}/js/vendor`))

);

gulp.task('copy:globalize', () =>

  gulp.src([

    'node_modules/globalize/dist/globalize.js',

    'node_modules/globalize/dist/globalize/message.js',

    'node_modules/globalize/dist/globalize/number.js',

    'node_modules/globalize/dist/globalize/plural.js',

    'node_modules/globalize/dist/globalize/date.js',

    'node_modules/globalize/dist/globalize/currency.js',

    'node_modules/globalize/dist/globalize/relative-time.js',

    'node_modules/globalize/dist/globalize/unit.js',

  ]).pipe(plugins().concat('globalize.js'))

    .pipe(gulp.dest(`${dirs.dist}/js/vendor`))

);

...

gulp.task('copy', [

  'copy:.htaccess',

  'copy:index.html',

  'copy:jquery',

  'copy:globalize:deps',

  'copy:globalize',

  'copy:license',

  'copy:main.css',

  'copy:misc',

  'copy:normalize'

]);

Finally, reference the dependencies in the index.html file just below the jquery script tags:

<script src="js/vendor/cldr.js"></script>

<script src="js/vendor/globalize.js"></script>

Now we need to bootstrap the library with the right CLDR data. Because the cldr-data package downloads the whole list of locale data from a lot of languages, we may want to load all of them now Only the ones that we support, that is! Let's say we want English and Greek.

We'll need the following files:

  • cldr/main/en/ca-gregorian.json and cldr/main/el/ca-gregorian.json for the Calendar data
  • cldr/main/en/currencies.json and cldr/main/el/currenciens.json for currencies
  • cldr/main/en/numbers.json and cldr/main/el/numbers.json for numbers
  • cldr/main/en/units.json and cldr/main/el/units.json for units of measure
  • cldr/supplemental/timeData.json for time data
  • cldr/supplemental/weekData.json for week data
  • cldr/supplemental/plurals.json for plural rules
  • cldr/supplemental/likelySubtags.json for language tag rules

We can do the same thing as previously. Add a gulp task to copy those files into a cldr folder so we can retrieve them when we load the application.

Add the following gulp tasks:

gulp.task('copy:cldr:data', ['copy:cldr:en', 'copy:cldr:el'], () =>

  gulp.src([

    'node_modules/cldr-data/supplemental/timeData.json',

    'node_modules/cldr-data/supplemental/weekData.json',

    'node_modules/cldr-data/supplemental/plurals.json',

    'node_modules/cldr-data/supplemental/likelySubtags.json',

  ]).pipe(gulp.dest(`${dirs.dist}/js/vendor/cldr/supplemental`))

);

gulp.task('copy:cldr:el', () =>

  gulp.src([

    'node_modules/cldr-data/main/el/ca-gregorian.json',

    'node_modules/cldr-data/main/el/currencies.json',

    'node_modules/cldr-data/main/el/numbers.json',

    'node_modules/cldr-data/main/el/units.json',

  ]).pipe(gulp.dest(`${dirs.dist}/js/vendor/cldr/el`))

);

gulp.task('copy:cldr:en', () =>

  gulp.src([

    'node_modules/cldr-data/main/en/ca-gregorian.json',

    'node_modules/cldr-data/main/en/currencies.json',

    'node_modules/cldr-data/main/en/numbers.json',

    'node_modules/cldr-data/main/en/units.json',

  ]).pipe(gulp.dest(`${dirs.dist}/js/vendor/cldr/en`))

);

If you run gulp build, you should be able to see the following file structure:

➜ tree dist/js/vendor

dist/js/vendor

├── cldr

│   ├── el

│   │   ├── ca-gregorian.json

│   │   ├── currencies.json

│   │   ├── numbers.json

│   │   └── units.json

│   ├── en

│   │   ├── ca-gregorian.json

│   │   ├── currencies.json

│   │   ├── numbers.json

│   │   └── units.json

│   └── supplemental

│       ├── plurals.json

│       ├── timeData.json

│       └── weekData.json

├── cldr.js

├── globalize.js

├── jquery-3.3.1.min.js

└── modernizr-3.7.1.min.js

4 directories, 15 files

Now we are ready to bootstrap the library.

Add the following code in the main.js located inside the src folder:

(function($) {

  $.when(

    $.getJSON( 'js/vendor/cldr/en/ca-gregorian.json' ),

    $.getJSON( 'js/vendor/cldr/en/currencies.json' ),

    $.getJSON( 'js/vendor/cldr/en/numbers.json' ),

    $.getJSON( 'js/vendor/cldr/en/units.json' ),

    $.getJSON( 'js/vendor/cldr/supplemental/plurals.json' ),

    $.getJSON( 'js/vendor/cldr/supplemental/timeData.json' ),

    $.getJSON( 'js/vendor/cldr/supplemental/weekData.json' ),

    $.getJSON( 'js/vendor/cldr/supplemental/likelySubtags.json' )

  ).then(function() {

    // Normalize $.get results, we only need the JSON, not the request statuses.

    return [].slice.apply( arguments, [ 0 ] ).map(function( result ) {

      return result[ 0 ];

    });

    // eslint-disable-next-line no-undef

  }).then( Globalize.load ).then(function() {

  });

// eslint-disable-next-line no-undef

}(jQuery));

Notice that we loaded only the English CLDR data. This is because we have the option to load the Greek CLDR data on demand when we switch the locale, thus saving resources. We can do that with the help of a small toolkit that we're going to use to switch languages.

Let's explore the Globalize.js API to see how it can help us with our needs:

Core API

  • Loading Messages: The most basic task is to load the messages for each particular locale. To do this, we need to use the loadMessages method that accepts an object with language tags as keys, for example:
Globalize.loadMessages({

      'en': {

        'intro': "Hello"

      },

      'el': {

        'intro': "Γιά σας"

      }

    });

After loading the messages, we can use the formatMessage method to print a particular key:

Globalize( "el" ).formatMessage( "intro" ); // Γιά σας

  • Variable Replacement: We can loadMessages that accept parameters. We can then use the messageFormatter method to return a formatter function that we can invoke with the parameters we want to provide:
Globalize.loadMessages({

  en: {

    hello: "Hello, {0} {1}",

    hey: "Hey, {prefix} {last}"

  },

  'el': {

    hello: "Γιά σας, {0} {1}",

    hey: "Γιά σου, {prefix} {last}"

  }

});

formatter = Globalize( "el" ).messageFormatter( "hello" );

formatter([¨κυρία¨, "Ανδρέου"]);

// > "Γιά σας, κυρία Ανδρέου"

// Named variables using Object key-value pairs.

formatter = Globalize( "en" ).messageFormatter( "hey" );

formatter({

  prefix : "Mr",

  last: "Andrew"

});

// > "Hey, Mr Andrew"
  • Plural Rules: We can also specify plural forms for the messages. We need to describe our message values in the following format:
// Note you can define multiple lines message using an Array of Strings.

Globalize.loadMessages({

  en: {

    email: [

      "You have {count, plural,",

      "    one {one new email}",

      "  other {{formattedCount} new emails}",

    ]

  }

});

var en = new Globalize( "en" );

var numberFormatter = en.numberFormatter();

var taskFormatter = en.messageFormatter( "email" );

taskFormatter({

  count: 10,

  formattedCount: numberFormatter( 10 )

});

// > "You have 10 new emails"
  • Gender Rules: We can also format messages based on the specified gender whenever it makes sense. For example:
var formatter;

// Note you can define multiple lines message using an Array of Strings.

Globalize.loadMessages({

  en: {

    party: [

      "{hostGender, select,",

      "  female {{from} joined {to}} at her excursion}",

      "    male {{from} joined {to} at his excursion}",

      "   other {{from} joined {to} to their excursion}",

      "}"

    ]

  }

});

formatter = Globalize( "en" ).messageFormatter( "propose" );

formatter({

  from: "Maria",

  to: "Alex",

  hostGender: "female"

});

// > "Maria invited Alex at her excursion"

In addition to the main API, there are some other useful methods available:

  • relativeTimeFormatter – prints locale-specific relative time messages
  • currencyFormatter – formats currency according to the specified locale
  • dateFormatter – formats dates according to the specified locale

Detecting User Locale

Globalize.js does not offer a way to detect the current user locale so we have to manually add some checks. We can easily do that with a simple service.

First, we are going to use Javascript modules; create a new file called utils.mjs and add the following code:

export function detectLocale() {

  const languageString = navigator.language || '';

  const language = languageString.split(/[_-]/)[0].toLowerCase();

  switch (language) {

  case 'en':

    return 'en';

  case 'el':

    return 'en';

  default:

    return 'en';

  }

}

Then, in our main.js file, we can import it as:

import {detectLocale} from './utils.mjs';

...

console.log('Detected user locale is ', detectLocale()); // 'en'

Using that approach, we can build our own little i18n toolkit that handles the current locale settings for us. For example:

import EventEmitter from 'https://unpkg.com/EventEmitter@1.0.0/src/index.js?module';

export class LocaleProvider {

  constructor(currentLocale = 'en', availableLocales = ['en', 'el'], defaultLocale = 'en') {

    this.availableLocales = availableLocales;

    this.defaultLocale = defaultLocale;

    this.emiter = new EventEmitter();

    this.setCurrent(currentLocale);

  }

  getDefault() {

    return this.defaultLocale;

  }

  getCurrent() {

    return this.currentLocale;

  }

  setCurrent(tag) {

    if (!this.availableLocales.includes(tag)) {

      console.warn('Sorry ', tag, 'is not supported right now. Setting default');

      this.currentLocale = this.defaultLocale;

    }

    this.currentLocale = tag;

    this.emiter.emit('locale:changed', tag);

  }

  onChangeLocale(cb) {

    this.emiter.on('locale:changed', cb);

  }

}

Then, in our main app:

import {LocaleProvider} from './localeProvider.mjs';

...

const locale = new LocaleProvider(detectLocale());

locale.onChangeLocale((tag)=> console.log('Locale Changed to', tag));

console.log('Detected user locale is', detectLocale());

locale.setCurrent('el');

console.log('Current user locale is', locale.getCurrent());

If you look at the console log output:

Detected user locale is en

main.js:21 Locale Changed to el

main.js:36 Current user locale is el

Using an EventEmitter, we can trigger now new events such as loading the Greek translations from the store.

Let's create a folder that will load the locale messages:

➜ tree src/locale

src/locale

├── el

│   └── messages.json

└── en

    └── messages.json

2 directories, 2 files

Put some messages here for each locale:

{

  "el": {

    "like": [

      "{0, plural, offset:1",

      "     =0 {Αγάπησε το}",

      "     =1 {Το αγαπάς ήδη}",

      "    one {Εσύ και καποιος άλλος το αγαπάτε}",

      "  other {Εσύ και # άλλοι το αγαπάτε}",

      "}"

    ]

  }

}

{

  "en": {

    "like": [

      "{0, plural, offset:1",

      "     =0 {Be the first to like this}",

      "     =1 {You liked this}",

      "    one {You and someone else liked this}",

      "  other {You and # others liked this}",

      "}"

    ]

  }

Modify the initial loading bootstrapping of the app

(function($) {

  $.when(

    $.get( 'js/vendor/cldr/en/ca-gregorian.json' ),

    $.get( 'js/vendor/cldr/en/currencies.json' ),

    $.get( 'js/vendor/cldr/en/numbers.json' ),

    $.get( 'js/vendor/cldr/en/units.json' ),

    $.get( 'js/vendor/cldr/supplemental/plurals.json' ),

    $.get( 'js/vendor/cldr/supplemental/timeData.json' ),

    $.get( 'js/vendor/cldr/supplemental/weekData.json' ),

    $.get( 'js/vendor/cldr/supplemental/likelySubtags.json' )

  ).then(function() {

    // Normalize $.get results, we only need the JSON, not the request statuses.

    return [].slice.apply( arguments, [ 0 ] ).map(function( result ) {

      return result[ 0 ];

    });

  }).then(Globalize.load).then(function() {

    const locale = new LocaleProvider(detectLocale());

    locale.onChangeLocale((tag)=> {

      console.log('Locale Changed to', tag);

      loadTranslationsFor(tag).then(() => {

        const current = Globalize(tag);

        console.log(current.messageFormatter( 'like' )(10));

      });

    });

Now, let's see what the loadTranslationsFor function does:

function loadCldrData(languageTag = 'en', $ = jQuery) {

  return $.when(

    $.get( `js/vendor/cldr/${languageTag}/ca-gregorian.json` ),

    $.get( `js/vendor/cldr/${languageTag}/currencies.json` ),

    $.get( `js/vendor/cldr/${languageTag}/numbers.json`),

    $.get( `js/vendor/cldr/${languageTag}/units.json` ),

  ).then(function() {

    // Normalize $.get results, we only need the JSON, not the request statuses.

    return [].slice.apply( arguments, [ 0 ] ).map(function( result ) {

      return result[ 0 ];

    });

  }).then(Globalize.load)

}

export function loadTranslationsFor(languageTag = 'en', $ = jQuery) {

  return loadCldrData(languageTag).then(function () {

    return $.get( `locale/${languageTag}/messages.json` ).then(function (data) {

      return Globalize.loadMessages(data);

    })

  })

}

So we need a language tag to load the relative CLDR data and messages for that. If we run the main.js we can see that it logs the correct message in the console:

Εσύ και 9 άλλοι το αγαπάτε

Generally, when we change the locale, we also want to re-render the HTML that displays the old messages so as to reflect the current selection. Since we're not using a framework, we can only rely on having multiple document.getElementById selectors to update the messages. However, you can do better if you have a template engine such as Mustache.js or Handlebars.js.

Alternatively, we can use a microframework such as Hyperapp that can trigger updates whenever the model changes. The fact that we can use Globalize.js in Node and refresh the page when we change the locale is also to the benefit of your project. Of course, this is a topic for another article, but for simple cases, we can have a tool that works without adding a lot of extra dependencies.

Conclusion

In this tutorial, we introduced Globalize.js to a startup HTML5 boilerplate code and explored some of the ways we can use it in practice. In general, we can say that this library has good support for different translation rules and good interoperability. By leveraging the utilities that we described in this post, we can make our lives so much easier and also provide some value to our users. We can also do better and integrate a fully fledged translation management system such as Phrase that will take care of a lot of the nitty details for us. Until then, stay put for more articles regarding i18n and l10n best practices!