First published July 7, 2019 in ITNext

JavaScript International Methods

Because date formatting is pain & international formatting is impossible

Completely stumped developer

Why is this so hard??

Introduction

I'm lucky in that the company I work for is based mainly in the US, with a few additional locations in Mexico and Canada. This makes my job so much easier because I don't have to worry much about internationalization: not about converting to different date formats, not about the idiosyncrasies resulting from language translations, and certainly not about the oddities regarding lists and currencies in other countries (really — who puts commas after periods when it comes to money??).

Just because these aren't my problems though, doesn't mean they aren't incredibly painful for other engineers. As an example, a lack of international standardizations (and the inability to convert numbers) resulted in a mismatch of metric units versus English units, causing NASA's Mars Orbiter to completely miss entering orbit around Mars in 1999, to the tune of $125 million.

Capt Picard face palm

Oof...

While the sums of money my software development team works with aren't quite so large, and our errors result in bugs in the code we can usually fix in a matter of hours or days, I can definitely empathize with the NASA engineers, and I cringe thinking about how one simple mistake like not double checking everyone was using the same measurements in Orbiter's code caused such a loss.

Which is why, when I heard the ECMAScript Internationalization API had recently been released, I was excited to see what kinds of improvements were about to make JavaScript around the world better and more standardized for everyone using it.

Today, let's check out some of the best parts of Intl: the ECMAScript Internationalization API, which provides language sensitive string comparison, number formatting, and date and time formatting.

International date time highlights

As per usual, Mozilla's MDN Web Docs, give the best summation of the new international API, and the properties that come with it:

The INTL object provides access to several constructors as well as functionality common to the internationalization constructors and other language sensitive functions. — MDN Web Docs

There's six properties that come along with the new Intl object, but in my opinion, only five of them deserve their own place in this blog post.

  • Intl.DateTimeFormat
  • Intl.RelativeTimeFormat
  • Intl.ListFormat
  • Intl.NumberFormat
  • Intl.PluralRules

So without further preamble, I'll dive into the first property: the international date time format.

International date time format

Wrong date, wrong date, finally correctly formatted date

Yes, we've all been there. Yes, it's really this much of a nightmare with dates and times.

The Intl.DateTimeFormat object is a constructor for objects enabling language-sensitive date and time formatting.

At its most basic, the date time format takes in locales and options objects and outputs formatted time stamps based on those options.

Here's an example using locales from around the globe to format this date variable.

Intl.DateTimeFormat locales example

var date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

// using US date time formatting
console.log(new Intl.DateTimeFormat('en-US').format(date));
// expected output: "12/19/2012" // using Great Britain date time formatting

console.log(new Intl.DateTimeFormat('en-GB').format(date));
// expected output: "19/12/2012"

// Include a fallback language, in this case US English with the Buddhist calendar
console.log(new Intl.DateTimeFormat(['en-US-u-ca-buddhist']).format(date));
// expected output: "12/19/2555"

In addition to being able to pass in locations, users can provide additional parameters strung together with the location, like:

  • nu — the number systems used in different countries,
  • ca — the calendar to use,
  • hc — hour cycle ( h11, h24, etc.).

After the additions to locales, comes the optional options object which can include properties like:

  • timezone — this can be either "UTC" or something like "America/New_York",
  • hourCycle — once again, specs like "h12" or "h23",
  • and my favorite one: formatMatcher — this algorithm can take in properties used to describe date-times and format the outputs to match.

Subsets for formatMatcher include things like:

  • weekday, year, month, day, hour, minute, second
  • hour, minute, second

And even then, the subset properties have different options. Here are the options for weekday.

  • "long" (e.g., Thursday),
  • "short" (e.g., Thu),
  • "narrow" (e.g., T).

Check out this example using DateTimeFormat's options arguments to format the outputs.

Intl.DateTimeFormat options arguments example

var date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

// request a weekday along with a long date in Japanese
var options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
console.log(new Intl.DateTimeFormat('ja-JP', options).format(date));
// expected output: "2012年12月19日水曜日"

// an application may want to use UTC and make that visible in German
options.timeZone = 'UTC';
options.timeZoneName = 'short';
console.log(new Intl.DateTimeFormat('de-DE', options).format(date));
// expected output: "Donnerstag, 20. Dezember 2012, UTC"

// sometimes you want to be more precise in Australian English
options = {
  hour: 'numeric', minute: 'numeric', second: 'numeric', 
  timeZone: 'Australia/Sydney',
  timeZoneName: 'short'
};
console.log(new Intl.DateTimeFormat('en-AU', options).format(date));
// expected output: "2:00:00 pm AEDT"

// even the US needs 24-hour time for some occasions
options = {
  year: 'numeric', month: 'numeric', day: 'numeric',
  hour: 'numeric', minute: 'numeric', second: 'numeric',
  hour12: false,
  timeZone: 'America/Los_Angeles' 
};
console.log(new Intl.DateTimeFormat('en-US', options).format(date));
// expected output: "12/19/2012, 19:00:00"

// to specify options but use the browser's default locale, use 'default'
console.log(new Intl.DateTimeFormat('default', options).format(date));
// expected output: "12/19/2012, 19:00:00"

This is such a cool feature. It's going to make it so much easier to translate dates and times properly and format them in so many specific ways that packages like moment.js, made specifically for parsing, validating and manipulating JavaScript dates and times, will become unnecessary.

The next property I want to segue into, since I'm already on the subject of dates, is international relative time format.

International relative time format

The Intl.RelativeTimeFormat object is a constructor for objects that enables language-sensitive relative time formatting.

Similar to the Intl.DateTimeFormat, this object takes in a locales parameter and an options object.

The locales parameter functions exactly the same in this relative time format object as it does in the date time format, so I'll skip any further explanation here.

As for options, the Intl.RelativeTimeFormat has a variety of additional properties it accepts:

  • localeMatcher — a locale matching algorithm, with the options "best fit" or "lookup",
  • numeric — the format of the output message,
  • style — the length of the internationalized message.

style follows the same lines as weekday, in that it has three different values:

  • "long" (default, e.g., in 2 months),
  • "short" (e.g., in 2 mo.),
  • or "narrow" (e.g., in 2 mo.).

narrow and short could be the same depending on the country.

Below are some samples using Intl.RelativeTimeFormat in various locales and with various options passed to them.

Intl.RelativeTimeFormat examples

var rtf1 = new Intl.RelativeTimeFormat('en', { style: 'narrow' });
// relative time formats in English

console.log(rtf1.format(3, 'quarter'));
//expected output: "in 3 qtrs."

console.log(rtf1.format(-4, 'hour'));
//expected output: "4 hr. ago"

var rtf2 = new Intl.RelativeTimeFormat('de', { numeric: 'auto' });
// relative time formats in German

console.log(rtf2.format(2, 'day'));
//expected output: "übermorgen"

// Create a relative time formatter in your locale
// with default values explicitly passed in.
const rtf = new Intl.RelativeTimeFormat("en", {
    localeMatcher: "best fit", // other values: "lookup"
    numeric: "always", // other values: "auto"
    style: "long", // other values: "short" or "narrow"
});

// Format relative time in English using negative value (-1).
console.log(rtf.format(-1, "day"));
// > "1 day ago"

// Format relative time in English using positive value (1).
console.log(rtf.format(2, "day"));
// > "in 2 days"

Once again, super handy that we, as developers, don't have to deal with the logic of formatting days or hours or even some words, in multiple languages no less, according to the number passed in. No complicated if/else logic blocks or even ternary expressions.

With the help of Intl.RelativeTimeFormat just specify the language to use, the numeric output and the style and it's good to go. Nice! 😄

On that note of the Internationalization API, let's look next at another thing that varies quite a bit from language to language: how lists are formatted.

International list format

It's tough enough transforming an array in one language into something that resembles a human-readable list with ands or ors added in, try doing that in multiple languages.

The Intl.ListFormat object is a constructor for objects to enable language-sensitive list formatting.

As is becoming the recurring theme of this article, the first optional parameter ListFormat accepts is a locales specification.

The second object is options with parameters like:

  • localeMatcher — the locale matching algorithm, same options as with Intl.RelativeTimeFormat,
  • type — the format of the output message. Possible values are "conjunction", which stands for "and"-based lists (default, e.g., 1, 2, and 3), or "disjunction" that stands for "or"-based lists (e.g., 1, 2, or 3). "unit" stands for lists of values with units (e.g., 7 pounds, 11 ounces).
  • style — the length of the formatted message. Values include "long", "short", or "narrow".

Here's some examples of how you could use the Intl.ListFormat object in practice.

Intl.ListFormat examples

const vehicles = ['Cat', 'Dog', 'Fish'];

// list formats in English
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });

console.log(formatter.format(vehicles));
// expected output: "Cat, Dog, and Fish"

// list formats in French
const formatter2 = new Intl.ListFormat('fr', { style: 'short', type: 'disjunction' });
console.log(formatter2.format(vehicles));
// expected output: "Cat, Dog ou Fish"

// list formats in Spanish
const formatter3 = new Intl.ListFormat('es', { style: 'narrow', type: 'unit' });
console.log(formatter3.format(vehicles));
// expected output: "Cat Dog Fish"

const list = ['BMW', 'Volvo', 'Audi'];

// list formats in British English
console.log(new Intl.ListFormat('en-GB', { style: 'long', type: 'conjunction' }).format(list));
// expected output: "BMW, Volvo and Audi"

console.log(new Intl.ListFormat('en-GB', { style: 'short', type: 'disjunction' }).format(list));
// expected output: "BMW, Volvo or Audi"

console.log(new Intl.ListFormat('en-GB', { style: 'narrow', type: 'unit' }).format(list));
// expected output: "BMW Volvo Audi"

How awesome is it that finally, finally there's a built-in API that can make lists look good? Heck, I'd take this even if it was only available for English-speaking countries or English language-only applications, this is so useful.

But wait! I've got another incredibly useful constructor to introduce: the number formatter! 🤑

International number format 💰

Make it rain

Make it rain, everybody.

Although the Intl.NumberFormat definition doesn't sound exciting at first, the implications are big. This object is a constructor for objects that enables language sensitive number formatting. And you know what that means? The formatting for international currencies is a breeze.

In addition to locales and nu — the numbering system to be used, the Intl.NumberFormat object can accept the following options parameters:

  • localeMatcher,
  • style — the formatting style to use. Possible values are "decimal" for plain number formatting, "currency" for currency formatting, and "percent" for percent formatting; "decimal" is the default style.
  • currency — the currency to use in currency formatting,
  • currencyDisplay — how to display the currency formatted. The options are "symbol" to use a localized currency symbol such as €, "code" to use the ISO currency code, or to use a localized currency name such as "dollar".
  • useGrouping — Whether to use grouping separators, such as thousands separators or thousand/lakh/crore separators. Possible values are true and false.

There's a few more options around digits: minimumIntegerDigits, maximumSignificantDigits, but I think their usefulness is limited, so I'll skip defining them all here.

Intl.NumberFormat examples

var number = 123456.789;

// currency formatting with euros in German
console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(number));
// expected output: "123.456,79 €"

// currency formatting in Japanese where the Japanese yen doesn't use a minor unit
console.log(new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(number));
// expected output: "¥123,457"

// limiting an Indian currency format to three significant digits
console.log(new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(number));
// expected output: "1,23,000"

// Arabic in most Arabic speaking countries uses real Arabic digits
console.log(new Intl.NumberFormat('ar-EG').format(number));
// expected output: ١٢٣٤٥٦٫٧٨٩

// India uses thousands/lakh/crore separators
console.log(new Intl.NumberFormat('en-IN').format(number));
// expected output: 1,23,456.789

// the nu extension key requests a numbering system, e.g. Chinese decimal
console.log(new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(number));
// expected output: 一二三,四五六.七八九

// when requesting a language that may not be supported, such as
// Balinese, include a fallback language, in this case Indonesian
console.log(new Intl.NumberFormat(['ban', 'id']).format(number));
// expected output: 123.456,789

In my mind, for an international company or website, this has got to be a huge, huge selling point for the Internationalization API. Instead of hand-coding for all the varieties of currency differences that exist, we can rely on a single API to handle it for us — what a logic and time-saver!

Ok, time for the last property I'll cover today: Intl.PluralRules.

International plural rules

When you think about rules for plurals in English, the rules are hard: "one dog" becomes "two dogs" but "one goose" becomes "two geese", "one" becomes "two" but beyond "two" becomes "a few" or "many" or "other" when describing some vague, larger numbers of items, and so on and so forth— are you starting to see where I'm going with this?

Now imagine needing to know unwritten rules like this in more than one language...ugh. 😑

Well, this is where the International Plural Rules becomes a life saver. The Intl.PluralRules object is a constructor for objects enabling plural sensitive formatting and plural language rules.

This constructor accepts locales (of course), and its options object takes in the following parameters:

  • localeMatcher — The locale matching algorithm to use. Possible values are "lookup" and "best fit"; the default is "best fit".
  • type — the type to use. Possible values are: "cardinal" for cardinal numbers (referring to the quantity of things). This is the default value. Or "ordinal" for ordinal number (referring to the ordering or ranking of things, e.g. "1st", "2nd", "3rd" in English).

Uses for Intl.PluralRules

There's a few different ways to use Intl.PluralRules in your code.

Basic usage — In basic use without specifying a locale, a formatted string in the default locale and with default options is returned. This is useful to distinguish between singular and plural forms such as "cat" and "cats".

Locales usage — When using locales, the example code below shows some of the variations in localized plural rules. In order to get the format of the language used in the user interface of your application, make sure to specify that language (and possibly some fallback languages) using the locales argument.

Options usage — The plural results can be customized using the options argument, which has one property called type which you can set to ordinal. This is useful to figure out the ordinal indicator, e.g. "1st", "2nd", "3rd", "4th", "42nd" and so forth.

The code snippets below outline all the usages I just described.

Intl.PluralRules examples

// basic usage examples
var pr = new Intl.PluralRules();
// examples of plural rules if in US English locale

console.log(pr.select(0));
// expected output: 'other'

console.log(pr.select(1)); 
// expected output: 'one'

console.log(pr.select(2));
// expected output: 'other'

// examples using `locale`
// for example: Arabic has different plural rules
console.log(new Intl.PluralRules('ar-EG').select(0));
// expected output: 'zero'

console.log(new Intl.PluralRules('ar-EG').select(1)); 
// expected output: 'one'

console.log(new Intl.PluralRules('ar-EG').select(2));
// expected output: 'two'

console.log(new Intl.PluralRules('ar-EG').select(6));
// expected output: 'few'

console.log(new Intl.PluralRules('ar-EG').select(18));
// expected output: 'many'

// examples using `options`
var pr = new Intl.PluralRules('en-US', { type: 'ordinal' });
// examples using US English and `options type` specification

console.log(pr.select(0));
// expected output: 'other'

console.log(pr.select(1));
// expected output: 'one'

console.log(pr.select(2));
// expected output: 'two'

console.log(pr.select(3));
// expected output: 'few'

console.log(pr.select(4));
// expected output: 'other'

Pretty nice, huh? I'll admit, of all the Intl methods, this is less widely applicable than some of the other methods included, but I'm sure it will still have its moments where it's just the ticket.

It is also worth noting that this method is still in "Draft" status, with the comment "initial definition" in MDN's docs, so I would hold off for a while longer before attempting to rely on it completely in production.

Conclusion

Translating websites and applications for use in different countries and languages is tough enough, but being able to correctly handle things like date time formats, currencies and lists of items with their many language-specific rules is a real challenge.

The newest version of the ECMAScript Internationalization API aims to make some of these arduous tasks easier by letting its methods do the formatting for you, based on locales and whatever other options you might want to specify.

For JavaScript developers working with international requirements, this stands to be a huge boon, and I for one, am looking forward to incorporating these new features into my own applications just for the total convenience these properties provide.

Check back in a few weeks, I’ll be writing about JavaScript, ES6 or something else related to web development.

Thanks for reading, I hope you see the value the ECMAScript Internationalization API can provide to your applications, and are able to incorporate some of the properties into your web apps in the future. Please share this with your friends if you found it helpful!

References & Further Resources

Want to be notified first when I publish new content? Subscribe to my newsletter.