Intl

There are a lot of complex problems that needs solving when writing frontend code, many of which are best solved by using someone else's code. Tried and true libraries can be absolute life savers when it comes to saving time and sanity in your projects, but sometimes you might be reaching for a dependency when you already have all the tools you need right in front of you.

Localization is one problem that is not very easy to solve. For example, you might have to involve a translation service of some kind for your texts. But there are other things too, like how you format numbers, or how you print a list. The translator won't help you there, but there exists a library of functions that can be very useful in these situations. And it's not an external dependency either, it is baked right into every users web browser.

It is called Intl, and in this article I will walk through a bunch of different scenarios where it can be useful.

Scenario 1: You want nice formatting on your dates

Different countries write dates differently. As silly as this is, it is something we must consider. Let's use Intl.DateTimeFormat to print nice-looking and area-appropriate dates and times.

A plain Date object looks like this when printed in different ways:

console.log(new Date());
// Fri Feb 25 2022 14:46:05 GMT+0100 (Central European Standard Time)

console.log(new Date().toISOString());
// 2022-02-25T13:49:29.782Z

console.log(new Date().toDateString());
// Fri Feb 25 2022

Ranging from "unreadable" to "quite alright" we might still want more granular configuration. Let's try it the Intl way.

console.log(new Intl.DateTimeFormat("en-US").format(date));
// 2/25/2022

console.log(new Intl.DateTimeFormat("sv").format(date));
// 2022-02-25

We instantly get something that is more readable, but we can further customize the output with on options object.

const options = { dateStyle: "medium", timeStyle: "short" };
console.log(new Intl.DateTimeFormat("en-US", options).format(new Date()));
// Feb 25, 2022, 3:41 PM

console.log(new Intl.DateTimeFormat("sv", options).format(new Date()));
// 25 feb. 2022 15:41

This allows us granular access to the formatting of both the time and the date, and they are nicely put together in the correct way for the locale provided.

Using Intl.DateTimeFormat you might not need other time related libraries like Moment or date-fns.

Note: similar effects can be achieved with the Date objects toLocaleString and toLocaleDateString functions.

The JavaScript Date object is hardly perfect though, and so a library might still be relevant if you run into problems with it. You should also keep an eye on the proposal for Temporal which will probably be coming to browsers sooner or later.

Scenario 2: Your international strings are sorting weird

So you have a bunch of strings, but they're all out of order and you want to sort them:

let unsortedArray = ["groda", "and", "älg", "murveldjur", "orre", "örn"];

Sounds like the easiest thing in the world, right? You just do:

let sortedArray = unsortedArray.sort();

And the result of this operation is:

["and", "groda", "murveldjur", "orre", "älg", "örn"];

Which is fine, if you're Swedish, but what if you're German? Then you want these strings to sort in a different order. Let's use Intl to fix this! The sort method on arrays can take a comparison method that is used to determine wether one thing is greater than another when sorting, and Intl.Collator has one that we can use, called compare. It also keeps track of what order things should be in depending on the language used. So, lets make use of it, and tell it to use the German locale ("de"):

let sortedArray = unsortedArray.sort(new Intl.Collator("de").compare);

Now we get:

["älg", "and", "groda", "murveldjur", "örn", "orre"];

And everyone in Germany is happy!

Scenario 3: Your numbers are sorting weird

So you have a bunch of numbers, but they're all out of order and you want to sort them:

let unsortedNumbers = [2, 8, 12, 16, 4, 20, 5, 5, 7, 9, 22];

Sounds like the easiest thing in the world, right? You just do:

let sortedArray = unsortedNumbers.sort();

But lo and behold, the result of this operation is:

[12, 16, 2, 20, 22, 4, 5, 5, 7, 8, 9];

Because the sort method by default converts everything in the array to a string and then makes the comparisons, it will look at the first digit of each number, and so everything that starts with a one ends up before everything that starts with a two, and so on. Let's again use Intl.Collator to solve this. When creating a new collator we can give it an options object, and setting the property numeric to true takes the actual values of the numbers into consideration.

sortedArray = unsortedNumbers.sort(
Intl.Collator("en", { numeric: true }).compare
);

The result:

[2, 4, 5, 5, 7, 8, 9, 12, 16, 20, 22];

This doesn't have much to to with internationalization, but is pretty neat regardless!

Scenario 4: You want a list of languages, all in their own language

Imagine your British user has somehow managed to change their app language to chinese. Not an ideal situation. How will they find their way back? Maybe they can find their settings page by recognizing the cog wheel icon. And then hopefully find some list of languages to use. But if all of the options in the list are in chinese, that doesn't exactly help. It is best to have each item in such a list be in the respective language that the user might want to switch to.

This could be solved by having some hard coded meta list of languages, but there is a faster and easier way: Intl.DisplayNames! This fancy API can write out things like currencies and regions, but it is the languages we are interested in. As long as we know what locales we want to support the rest can be handled programmatically.

Each locale string can be used to simultaneously tell Intl.DisplayNames which language it should output, and what language it should do so in. Like this:

const locales = ["de", "en-GB", "uk", "pl", "sv", "zh-Hant"];

const languageList = locales.map((locale) => {
const displayName = new Intl.DisplayNames([locale], {
type: "language",
languageDisplay: "standard",
});

return displayName.of(locale);
});

console.log(languageList);

This will log the following to the console:

[
"Deutsch",
"English (United Kingdom)",
"українська",
"polski",
"svenska",
"中文(繁體)",
];

Put this in a select element and you're done!

Scenario 5: You want a list of countries, in a single language

In a related case to the one above, you might want a list of languages, but you want the entire list to be in the same language. Maybe you're an admin, setting the language for a user, or maybe you're working with languages in an entirely different context. Either way, the solution is quite similar to the previous one, but instead of selecting the output language dynamically, we just choose a single one:

const locales = ["de", "en-GB", "uk", "pl", "sv", "zh-Hant"];

const languageList = locales.map((locale) => {
const displayName = new Intl.DisplayNames(["sv"], {
type: "language",
languageDisplay: "standard",
});

return displayName.of(locale);
});

console.log(languageList);

The result:

[
"tyska",
"engelska (Storbritannien)",
"ukrainska",
"polska",
"svenska",
"kinesiska (traditionell)",
];

Scenario 6: You want to display a list of things, but not sound like a computer

As programmers we have a lot of lists. They usually look something like this:

const gods = ["Odin", "Thor", "Freya", "Idun"];

If we want to write this out, we can achieve a reasonably nice looking list by putting commas between them.

const niceList = gods.join(", ");

The result:

"Odin, Thor, Freya, Idun";

It looks ok, but is hardly how you would type it out if you want more natural language. What I'm looking for here is something like this: "Odin, Thor, Freya and Idun". Manually putting an "or" or and "and" between the last two items is certainly possible, but taking a bunch of languages into account suddenly makes it daunting. Especially since different languages do lists like this differently.

Using Intl.ListFormat it's a breeze! Check it out:

const formatter = Intl.ListFormat("is");
const formattedList = formatter.format(gods);
// Result: "Odin, Thor, Freya og Idun"

You can also give it an option to make it an or-based list (Odin OR Thor):

const formatter = Intl.ListFormat("de", {type: 'disjunction});
const formattedList = formatter.format(gods);
// Result: "Odin, Thor, Freya oder Idun"

Scenario 7: You want a want to describe a quantity of a unit

Maybe your application is almost all numbers and you need very little in ways of translating strings. Good for you. With Intl.NumberFormat you'll need even less! No longer must you request translations of tiny little strings that denotes units, and gone is the worry of different translators doing things slightly different. Render your units the following way:

new Intl.NumberFormat("sv", {
style: "unit",
unit: "inch",
unitDisplay: "long",
}).format(26);
// Result: "26 tum"

new Intl.NumberFormat("it", {
style: "unit",
unit: "mile",
unitDisplay: "long",
}).format(10000);
// Result: "10.000 miglia"

Scenario 8: You want to display a relative time

Finally, a fairly common pattern in interfaces are timestamps. When did something happen? While you could certainly just print the exact time, it is a lot more readable and relatable to describe that time in relation to the current time. Like "an hour ago" or "tomorrow". This is not too hard to code yourself, but again we run into the problem of localization. Intl.RelativeTimeFormat to the rescue!

const formatter = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
formatter.format(-1, "day");
// Result: "yesterday"
const formatter = new Intl.RelativeTimeFormat("fi", { numeric: "auto" });
formatter.format(25, "minute");
// Result: "25 minuutin päästä"

This sort of only takes you half way though, Intl doesn't help you with determining which unit to use. If you give it 4000 seconds it will give you "4000 seconds" back and not "an hour", which might be what you want from a design standpoint. If you're looking for something that does all of this, take a look at GitHubs excellent time-elements web component.

Limitations

The Intl library generally has very high support and I would deem it suitable for production. Not all browsers (especially Internet Explorer) support all features though, so take a look at these compatibility tables when you decide to use it.

Kangax compatibility table

MDN