loke.dev
Header image for Intl.ListFormat Is a Localization Powerhouse

Intl.ListFormat Is a Localization Powerhouse

Stop writing fragile array-to-string logic and let the browser handle your Oxford commas and localized separators natively.

· 4 min read

Intl.ListFormat Is a Localization Powerhouse

I spent way too much of my early career writing messy if (i === items.length - 1) logic just to make a list of names look like a human actually wrote it. You know the drill: you have an array like ['Apples', 'Oranges', 'Bananas'] and you want it to display as "Apples, Oranges, and Bananas."

The naive names.join(', ') gives you "Apples, Oranges, Bananas," which feels robotic. Then you start writing a helper function with .slice() and .pop() to manually inject that "and" before the last item. It feels clever until you realize you've completely ignored the Oxford comma debate—and heaven help you if your app needs to support Spanish, where "and" becomes "y" (or "e" depending on the next word).

Stop doing that. The browser already solved this for us with Intl.ListFormat.

The "It Just Works" Example

The Intl.ListFormat object is part of the Internationalization API. It takes a locale and an options object, then provides a format() method that handles the heavy lifting.

const fruits = ['Apples', 'Oranges', 'Bananas'];

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

console.log(formatter.format(fruits)); 
// Output: "Apples, Oranges, and Bananas"

Look at that beautiful Oxford comma. It’s right there. No weird string concatenation, no off-by-one errors in your loops.

Changing the "And" to an "Or"

Sometimes you aren't listing items together; you're offering a choice. In the old days, you'd have to write a second helper function for "disjunctions." With Intl.ListFormat, you just change the type.

const options = ['Email', 'SMS', 'Carrier Pigeon'];

const disjunctionFormatter = new Intl.ListFormat('en-GB', { 
  style: 'long', 
  type: 'disjunction' 
});

console.log(disjunctionFormatter.format(options));
// Output: "Email, SMS, or Carrier Pigeon"

The Real Power: Instant Localization

This is where the "Internationalization" part of the name really earns its keep. If your user switches their language to Spanish, you don't want to hunt through your codebase to find every hard-coded "and" or "or."

const users = ['Alice', 'Bob', 'Irene'];

// English
const en = new Intl.ListFormat('en', { type: 'conjunction' });
console.log(en.format(users)); // "Alice, Bob, and Irene"

// Spanish
const es = new Intl.ListFormat('es', { type: 'conjunction' });
console.log(es.format(users)); // "Alice, Bob e Irene"

Notice what happened in the Spanish example? The formatter is smart enough to know that because "Irene" starts with an "I" sound, the conjunction "y" should change to "e" to avoid a phonological train wreck. Good luck writing a regex for that in five minutes.

Dealing with Units

If you're building a dashboard or a weather app, you might want a list that doesn't use "and" at all, but still follows local spacing rules. That’s what the unit type is for.

const measurements = ['50 miles', '20 gallons', '10 pounds'];

const unitFormatter = new Intl.ListFormat('en-US', { 
  style: 'narrow', 
  type: 'unit' 
});

console.log(unitFormatter.format(measurements));
// Output: "50 miles 20 gallons 10 pounds"

const longUnitFormatter = new Intl.ListFormat('en-US', { 
  style: 'long', 
  type: 'unit' 
});

console.log(longUnitFormatter.format(measurements));
// Output: "50 miles, 20 gallons, 10 pounds"

The style option (long, short, or narrow) lets you control the verbosity. narrow is especially great for tight UI spaces like sidebars or mobile headers.

Why use this over a custom helper?

1. Correctness: It handles linguistic edge cases you haven't thought of (like the Spanish "y" vs "e" example).
2. Bundle Size: You aren't shipping a heavy localization library for simple list formatting. This is native code running in the browser.
3. Consistency: Your lists will match the conventions of the user's OS and browser, making the app feel more native.

A Quick Gotcha: Browser Support

Intl.ListFormat is supported in all modern browsers (Chrome 72+, Firefox 71+, Safari 14.1+). If you're still supporting ancient versions of Internet Explorer... well, first of all, I'm sorry. Second, you’ll need a polyfill. But for most modern web apps, this is ready for prime time.

One thing to remember: Intl.ListFormat only works with strings. If you have an array of objects, you'll need to .map() them to a flat array of strings before passing them to the formatter.

const team = [
  { name: 'Sarah', role: 'Dev' },
  { name: 'Mike', role: 'Design' },
  { name: 'Phoebe', role: 'PM' }
];

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

// Map the objects to names first
const list = formatter.format(team.map(member => member.name));

console.log(`The squad: ${list}`); 
// "The squad: Sarah, Mike, and Phoebe"

Localization is often one of those "I'll do it later" tasks because it feels hard. But when the browser gives you tools this clean, there's really no excuse to keep writing fragile string logic. Give your join() calls a rest and let Intl handle the grammar.