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.