← All writing
Craft · · 6 min

Intl

On the one everybody half-knows, the one nobody knows and is definitely getting wrong, and the mistake that makes it slow.

JavaScript

I needed to render a date as “November 15, 2022”. So I installed a date library, because that’s what you do, and it’s about 20 KB, and I’ve done it on every project since roughly 2014.

Then I deleted it, because this does the same thing:

dates.js
date.toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' });
// "November 15, 2022"

date.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
// "15 November 2022"   ← note the comma disappears entirely
JavaScript

That second line is the argument for the whole thing. The British format isn’t ours with the words moved, it has different punctuation, and there is no chance whatsoever that I’d have remembered that. There are hundreds of those facts and the browser already has all of them.

So I went and read the rest of it, and there’s more in here than I knew.

Numbers, which are the same argument

numbers.js
const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
usd.format(1234.5);        // "$1,234.50"

new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.5);
// "1.234,50 €"   ← separators swapped, symbol on the other side, with a space

new Intl.NumberFormat('en-US', { notation: 'compact' }).format(1234567);
// "1.2M"
JavaScript

Currency symbol placement, thousands separator, decimal separator, spacing, and whether the negative sign goes before or after, all of which vary and none of which I want to hold in my head. The compact notation is the one I’d have written by hand about six times.

Plurals, which you are getting wrong

This is the one I’d most like people to know about, because the naive version isn’t merely verbose, it’s incorrect, and it’s incorrect in a way that’s invisible if you only speak English.

Everyone writes this:

the wrong one
`${n} item${n === 1 ? '' : 's'}`
JavaScript

That encodes an assumption: there are exactly two plural forms and the boundary is at one. English happens to work that way. Welsh has six categories. Arabic has six. Russian has three, and which one you need depends on the last digit and the last two digits. Even in English it’s not quite right, because zero and fractions have their own conventions in some contexts.

There’s a whole API for this and nobody uses it:

plurals.js
const pr = new Intl.PluralRules('en-US');
pr.select(0);   // "other"
pr.select(1);   // "one"
pr.select(2);   // "other"

const forms = { one: 'item', other: 'items' };
`${n} ${forms[pr.select(n)]}`;
JavaScript

That looks like more code to achieve the same English output, and it is, and the point is the shape: the strings are now a lookup keyed by a category the language decides, rather than a ternary that hardcodes English grammar into your templating. Add a locale and you add keys to the object. With the ternary you’d add a rewrite.

There’s an ordinal mode as well, { type: 'ordinal' }, which gives you the categories for 1st, 2nd, 3rd, 4th, and which I have written by hand with a modulo and got wrong for 11, 12 and 13, twice.

The four smaller ones

Relative time. 3 days ago, in 2 hours. This is the single most common reason people install a date library.

relative.js
const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });
rtf.format(-1, 'day');     // "yesterday"  ← not "1 day ago", because numeric: auto
rtf.format(3, 'week');     // "in 3 weeks"
JavaScript

Lists. Joining things with the right conjunction and comma placement.

lists.js
new Intl.ListFormat('en-US', { type: 'conjunction' }).format(['a', 'b', 'c']);
// "a, b, and c"   ← note the serial comma, which en-GB omits
new Intl.ListFormat('en-US', { type: 'disjunction' }).format(['a', 'b', 'c']);
// "a, b, or c"
JavaScript

Sorting. The default sort() on strings compares code units, which is wrong in essentially every language including this one.

sorting.js
names.sort(new Intl.Collator('en-US', { sensitivity: 'base' }).compare);
// case- and accent-insensitive, and puts accented characters where a human expects

files.sort(new Intl.Collator('en-US', { numeric: true }).compare);
// "file2" before "file10", which is the thing everybody writes a regex for
JavaScript

That numeric: true option is worth the price of admission on its own. Every project I’ve worked on has a hand-rolled natural sort in it somewhere.

Segmentation, which counts characters the way a person would rather than the way a code unit does, so an emoji with a skin tone modifier is one thing rather than four. It’s in two of the three engines and not the third yet, so it needs a check for now.

The gotcha that makes it slow

Constructing these objects is expensive. Actually expensive, not theoretically: the constructor resolves a locale and builds formatting data.

Which is fine once and terrible in a loop.

perf.js
// slow: builds a formatter per row
rows.map(r => r.total.toLocaleString('en-US', { style: 'currency', currency: 'USD' }));

// fast: one formatter, reused
const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
rows.map(r => usd.format(r.total));
JavaScript

The convenient toLocaleString methods construct a formatter each time they’re called. On a table with 500 rows that’s measurable and it’s the reason people conclude this stuff is slow.

What it isn’t

It’s not a translation system. It formats values, it doesn’t hold your strings.

It doesn’t parse. There’s no Intl for reading “11/15/2022” and working out what it means, which is genuinely hard and ambiguous and is one of the things a library is still for.

And it doesn’t do arithmetic: adding a month, finding the start of a week, working with time zones properly. There’s a whole new date and time API in the standards pipeline for that, and it’s a few years from being something I’d use on client work, and it will be worth the wait.

The same post, four times

I’ve now written four posts that are the same post. The library that made jQuery unnecessary. The form validation already in the browser. The scroll handler that became an observer. And now the date library that was three method calls.

The pattern is that the platform quietly absorbs the common case, several years after everybody has installed a solution for it, and nobody announces it because there’s no release event for “this has been fine for a while now.”

So it’s worth occasionally auditing your dependencies against what the browser can do, in the same spirit as reading your own bank statement. I got 20 KB back and learned that I’ve been pluralizing incorrectly since 2014 🙃

Read similar posts
6 min

View transitions on a multi-page site

There's one argument for building a client-side app I've never had a good answer to: a real page navigation looks worse. The answer turns out to be a browser feature rather than a rebuttal.

6 min

AbortController

Everybody meets this as the way to cancel a fetch. The more interesting thing it does has nothing to do with the network at all.