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.
Table of Contents
- 1. Why Native Localization Matters
- 2. Intl.ListFormat: Basics and Syntax
- 3. Different Types: Conjunction, Disjunction, Unit
- 4. Intl.DisplayNames: Translating Languages, Regions, Currencies
- 5. Fallback Behavior and Error Handling
- 6. Practical Example: Combining a Language Selector UI
- 7. Performance: Reusing and Caching Instances
- 8. Browser Support and Feature Detection
- 9. Best Practices and Summary
- 10. Summary
- 11. FAQ
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.