Intl.ListFormat and Intl.DisplayNames: Localized Lists and Names Without Translation Tables
AI generated
JS
() =>
JavaScript · Internationalization · Browser APIs
Intl.ListFormat and Intl.DisplayNames
Localized lists and names without your own translation tables

Two underrated Intl APIs take over work teams usually handle with hand-rolled translation tables: grammatically correct list phrasing and localized names for languages, regions, and currencies, built right into the browser engine.

14 min read Intl.ListFormat Intl.DisplayNames i18n without a library

1. Why Native Localization Matters

Anyone joining a list of names into a sentence usually reaches reflexively for array.join(', '). The result works in English but breaks in German and many other languages, because a plain comma before the last item is not how those languages phrase conjunctions. Every language has its own rules for the separator before the final item, for long and short forms, and for the distinction between 'and' lists and 'or' lists. Rebuilding that by hand ends up as a small hand-maintained grammar table per locale that goes stale the moment a new language pair is added.

The Intl namespace has covered numbers and dates for years through NumberFormat and DateTimeFormat. Intl.ListFormat and Intl.DisplayNames close two gaps that were previously solved almost exclusively through libraries or hand-written switch statements: grammatically correct list phrasing, and translating language, region, and currency codes into readable names. Both APIs draw on the same locale database as the rest of Intl, run entirely in the browser or in Node, and require no additional bundle.

2. Intl.ListFormat: Basics and Syntax

The constructor new Intl.ListFormat(locale, options) takes a locale string or array of locales plus an options object, just like the other Intl classes. The important part is the format() method: it takes an array of strings and returns a fully formatted sentence, complete with correct separators and conjunction. The style option controls verbosity (long, short, narrow), the type option controls the kind of connection.

For German, format(['Rot', 'Gruen', 'Blau']) returns 'Rot, Gruen und Blau'; for English with the same input but a different locale it returns 'Red, Green, and Blue', including the Oxford comma per that language's rules. The developer does not need to know a single grammar rule; switching the locale string alone produces correctly formatted text.


const colorsDe = new Intl.ListFormat('de', { style: 'long', type: 'conjunction' });
console.log(colorsDe.format(['Rot', 'Gruen', 'Blau']));
// "Rot, Gruen und Blau"

const colorsEn = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
console.log(colorsEn.format(['Red', 'Green', 'Blue']));
// "Red, Green, and Blue"

3. Different Types: Conjunction, Disjunction, Unit

The type parameter distinguishes three scenarios. conjunction produces 'A, B, and C' for enumerations where all items apply together. disjunction produces 'A, B, or C' for choices, such as available payment methods. unit connects values without any semantic conjunction, for example compound measurements like '3 hours, 15 minutes', where neither 'and' nor 'or' fits.

For cases where individual list items should be visually highlighted, for example as links or bold text, formatToParts() returns an array of part objects with type ('element' or 'literal') and value instead of a finished string. That lets you render the list precisely in JSX or template strings without rebuilding the formatting logic yourself.


const paymentOptions = new Intl.ListFormat('en', { type: 'disjunction' });
console.log(paymentOptions.format(['PayPal', 'Credit card', 'Bank transfer']));
// "PayPal, Credit card, or Bank transfer"

const parts = new Intl.ListFormat('en', { type: 'conjunction' })
  .formatToParts(['Anna', 'Ben', 'Clara']);
// [{type:'element',value:'Anna'}, {type:'literal',value:', '}, ...]

4. Intl.DisplayNames: Translating Languages, Regions, Currencies

Many applications store language, country, or currency codes (en, DE, EUR) and need to turn them into readable names in the current UI language, for example in a language switcher or a price overview. Before Intl.DisplayNames that meant either a custom translation table with hundreds of entries per supported UI language, or pulling in a heavy locale data library.

new Intl.DisplayNames(locales, { type }).of(code) handles exactly that job. The type parameter accepts language, region, currency, script, calendar, and dateTimeField. That turns the code 'fr' into 'French' in English, 'DE' into 'Germany' in English, or 'EUR' into 'Euro' in German, all without a single line of self-maintained translation data.


const regionNamesEn = new Intl.DisplayNames(['en'], { type: 'region' });
console.log(regionNamesEn.of('FR')); // "France"

const languageNamesEn = new Intl.DisplayNames(['en'], { type: 'language' });
console.log(languageNamesEn.of('de')); // "German"

const currencyNamesEn = new Intl.DisplayNames(['en'], { type: 'currency' });
console.log(currencyNamesEn.of('EUR')); // "Euro"

5. Fallback Behavior and Error Handling

Not every string is a valid code. Passing an invalid language or region code to of() throws a RangeError, while valid but unknown codes are handled differently depending on the fallback option. fallback can be 'code' (default, returns the raw code when no name is known) or 'none' (returns undefined).

In practice it pays to validate user input or external data before the call and additionally wrap the call in try/catch, especially when codes come from an API or third party. That prevents a single malformed code from crashing the entire language selection UI instead of just omitting one entry.


function safeRegionName(locale, code) {
  const dn = new Intl.DisplayNames([locale], { type: 'region', fallback: 'code' });
  try {
    return dn.of(code);
  } catch (error) {
    return code; // invalid format, raw code as fallback
  }
}

6. Practical Example: Combining a Language Selector UI

Both APIs really show their value combined. A typical task: from an array of available locale codes, build a sentence like 'This page is available in German, English, and French', where the names themselves should display in the currently active UI language, not in their own target language.

First, each code is translated into a readable name via Intl.DisplayNames, then the resulting array is joined into a grammatically correct sentence via Intl.ListFormat. Both steps consistently respect the current UI locale, so the entire logic adapts to a language switch just by swapping a single locale string.


function describeAvailableLanguages(uiLocale, availableCodes) {
  const languageNames = new Intl.DisplayNames([uiLocale], { type: 'language' });
  const names = availableCodes.map((code) => languageNames.of(code));
  const list = new Intl.ListFormat(uiLocale, { style: 'long', type: 'conjunction' });
  return list.format(names);
}

describeAvailableLanguages('en', ['de', 'en', 'fr']);
// "German, English, and French"

7. Performance: Reusing and Caching Instances

Constructing an Intl.ListFormat or Intl.DisplayNames object is comparatively expensive next to a plain string join, because the engine has to load locale data and compile rules. If an instance is created for a single format() call and discarded afterward, for example inside a render function that runs on every state update, unnecessary overhead builds up.

Best practice is creating instances once per locale and option combination and caching them in a Map instead of instantiating on every call. The cache key combines the locale with the relevant options. On a language switch, only a new cache entry is created; old entries can be discarded as needed.


const listFormatCache = new Map();

function getListFormat(locale, options) {
  const key = `${locale}:${options.type ?? 'conjunction'}:${options.style ?? 'long'}`;
  if (!listFormatCache.has(key)) {
    listFormatCache.set(key, new Intl.ListFormat(locale, options));
  }
  return listFormatCache.get(key);
}

8. Browser Support and Feature Detection

Intl.ListFormat and Intl.DisplayNames are available in all current evergreen browsers (Chrome, Edge, Firefox, Safari) and in Node.js from version 14 onward, making them standard equipment in modern JavaScript environments by now. For projects that still need to support older browsers or restricted Node builds without full ICU data, the formatjs project offers polyfills with an identical API.

A simple feature detection checks whether the class exists before use, and otherwise falls back to a plain join implementation that is not perfectly localized but at least does not throw a runtime error. That keeps the application functional even in rare legacy environments, while modern browsers get the fully localized output.

9. Best Practices and Summary

Intl.ListFormat and Intl.DisplayNames replace two categories of code teams have often maintained themselves: grammar rules for enumerations and translation tables for language, region, and currency codes. Both APIs are free, run natively in the engine, and produce output formatted according to the official CLDR locale data, which updates automatically with every browser update.

In practice it pays to combine them with other Intl APIs like NumberFormat and DateTimeFormat into a unified localization layer, supplemented with instance caching for performance and validation or try/catch for robust error handling on unknown codes. Teams that consistently use these building blocks no longer need an external library for many i18n tasks.

API Purpose Key Option Sample Output (en)
Intl.ListFormat Join enumerations grammatically type: conjunction/disjunction/unit 'Red, Green, and Blue'
Intl.DisplayNames Translate codes into readable names type: language/region/currency 'Germany' for 'DE'
formatToParts() Render list parts individually (links, bold) - [{type:'element',...}]
Instance cache Avoid repeated construction Map keyed by locale+options -

Mironsoft

Modern browser APIs, performance, and maintainable JavaScript

JavaScript that holds up in the real browser, not just in the tutorial?

We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.

Code Review

Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.

Performance Optimization

Improving bundle size, load time, and runtime performance with modern APIs.

Modernization

Deliberately introducing native browser APIs instead of heavy libraries.

10. Summary

Intl.ListFormat & DisplayNames: The Essentials at a Glance

ListFormat

Joins arrays into grammatically correct enumerations, with the right conjunction per language.

DisplayNames

Translates language, region, and currency codes into readable names without a custom translation table.

Performance

Intl instances are expensive to construct, so cache them per locale and option instead of recreating them.

Support

Available in all evergreen browsers and Node from version 14, formatjs provides polyfills for legacy cases.

11. FAQ: Intl.ListFormat & DisplayNames: The Essentials at a Glance

1What is Intl.ListFormat concretely used for?
For localized joining of arrays into sentences, such as tag lists, ingredient lists, or available payment methods, with linguistically correct separators and conjunctions instead of a plain comma.
2What is the difference between type conjunction and disjunction?
Conjunction produces 'and' lists like 'A, B, and C', disjunction produces 'or' lists like 'A, B, or C'. Unit connects values without semantic linkage, for example compound measurements.
3Can Intl.DisplayNames also translate scripts and calendars?
Yes, the type parameter accepts script, calendar, and dateTimeField besides language, region, and currency, letting you turn 'gregory' into 'Gregorian Calendar', for example.
4What happens with an unknown but syntactically valid code?
Depending on the fallback option, of() either returns the raw code (fallback: 'code', default) or undefined (fallback: 'none'). A syntactically invalid code throws a RangeError instead.
5Are these APIs expensive at runtime?
Constructing an instance is comparatively expensive, the actual format() call afterward is fast. Cache instances per locale and option combination instead of recreating them on every render.
6Does this also work in Node.js?
Yes, from Node.js 14 onward with full ICU data (the default for official Node builds since version 14). Minimal ICU builds may be missing individual locales.
7Do I still need an i18n library alongside this?
For translating UI text itself, yes, but for list formatting and code-to-name translation, ListFormat and DisplayNames fully replace typical library functions with no added bundle weight.
8How does formatToParts compare to format?
format() returns a finished string, formatToParts() returns an array of part objects with type and value, allowing targeted formatting of individual list items in JSX or templates.
9Is there a polyfill for older browsers?
Yes, the formatjs project offers API-compatible polyfills via @formatjs/intl-listformat and @formatjs/intl-displaynames for environments without native support.
10Can DisplayNames also produce flag emoji?
No, DisplayNames only returns text names, no flag emoji. Flags would have to be assembled separately from Unicode regional indicator symbols based on the region code.