Skip to main content
kld.dev

Me, myself, and I love Intl.ListFormat

I was building a component for this site to show summaries of webmention activity. To better explain, here are some examples of the sort of output I wanted:

Aziz Nestan and David Fauna reposted this.

Jennifer, jordan ali, and Dana Fihr mentioned this.

Nelly Kumar, Christopher, dragonstorm19, Astro, and 8 others liked this.

Building the list

After a bit of trial and error, I came up with some code that would create the list portion of this. It looked something like this:

// allNames = ['Al', 'Bill', 'Cindy', 'Dana', 'Evan', 'Frank']
const MAX_NAMES_TO_SHOW = 4;
const namesToShow = allNames.slice(0, MAX_NAMES_TO_SHOW);
const remaining = allNames.length - namesToShow.length;
if (remaining > 0) {
  namesToShow.push(`${remaining} others`);
}
// namesToShow = ['Al', 'Bill', Cindy', 'Dana', '2 others']
let list = namesToShow[0];
for (let i = 1; i < namesToShow.length; i++) {
  if (namesToShow.length > 2) list += ', ';
  if (i === namesToShow.length - 1) {
    list += 'and ';
  }
  list += namesToShow[i];
}
// list = 'Al, Bill, Cindy, Dana, and 2 others'

After getting it to work, it dawned on me that browsers have an API for this exact purpose. It’s called Intl.ListFormat, and it is part of the Internationalization API.

Intl.ListFormat()

The Intl.ListFormat() constructor takes two arguments:

  • a locale string, such as 'en', 'de', 'fr', etc. You can also pass 'default' to use the browser’s locale.
  • an options object with three possible properties:
    • style - This is either 'long', 'short', or 'narrow'. Passing 'short', for example, will return ’&’ instead of ‘and’.
    • type - One of 'conjunction', 'disjunction', or 'unit'. The first two output ‘and’ or ‘or’ respectively, while 'unit' is for a list like ‘7 pounds, 6 ounces’.
    • localeMatcher - This is either 'lookup', which enforces an exact locale, or 'best fit', which uses the the most appropriate match.

The constructor returns an object with a format method that takes an array of strings and returns a formatted string just like the one we created above.

const longConjunction = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
longConjunction.format(['Bill', 'Ted']);
// 'Bill and Ted'

const shortConjunction = new Intl.ListFormat('en', { style: 'short', type: 'conjunction' });
shortConjunction.format(['Athos', 'Porthos', 'Aramis']);
// 'Athos, Porthos & Aramis'

const disjunction = new Intl.ListFormat('en', { style: 'long', type: 'disjunction' });
disjunction.format(['to be', 'not to be']);
// 'to be or not to be'

const unit = new Intl.ListFormat('en', { type: 'unit' });
unit.format(['7 pounds', '6 ounces']);
// '7 pounds, 6 ounces'

const narrowUnit = new Intl.ListFormat('en', { style: 'narrow', type: 'unit' });
narrowUnit.format(['7 pounds', '6 ounces']);
// '7 pounds 6 ounces'

const frenchConjunction = new Intl.ListFormat('fr', { style: 'long', type: 'conjunction' });
frenchConjunction.format(['La belle', 'la bete']);
// 'La belle et la bete'

Take two

With Intl.ListFormat, the last eight lines of my previous implementation can be replaced with this:

const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
const list = formatter.format(namesToShow);

Nice and clean.

The final component

For any curious parties, here is the current state of my WebMentionSummary.astro component:

---
const { mentions } = Astro.props;
const verbs = new Map([
  ['like-of', 'liked'],
  ['repost-of', 'reposted'],
  ['bookmark-of', 'bookmarked'],
  ['mention-of', 'mentioned'],
]);
const verb = verbs.get(mentions[0]['wm-property']);
let maxNames = 4;
if (mentions.length === maxNames + 1) maxNames += 1; // Do not show "and 1 other"
const visibleMentions = mentions
  .slice(0, maxNames)
  .map((m) => `<a href="${m.author.url}">${m.author.name}</a>`);
const remaining = mentions.length - visibleMentions.length;
if (remaining > 0) visibleMentions.push(`${remaining} others`);
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
const summary = `${formatter.format(visibleMentions)} ${verb} this.`;
---

<p class="font-mono text-sm" set:html={summary} />

Ultimately I am using Intl.ListFormat to build not just a string, but a string of HTML. I then use Astro’s set:html directive to set the innerHTML of the paragraph element. One other thing to note is that I didn’t want to ever output “and 1 other”. I’d rather show that last person’s name, so if the number of mentions is only one more than the max, then I increase the max by one.

new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format([
  'Cheers',
  'thanks for reading',
  'have a great day',
]);

Webmentions

Ryan reposted this.