loke.dev
Header image for Localized Labels Without the 2MB Dictionary: A Deep Dive into Intl.DisplayNames

Localized Labels Without the 2MB Dictionary: A Deep Dive into Intl.DisplayNames

Stop shipping massive JSON dictionaries for country and language names and leverage the browser's built-in internationalization data instead.

· 4 min read

You’ve seen that countries.json file. It’s sitting in your /assets folder, a 400KB behemoth (or 2MB if you’re supporting multiple languages) that exists solely to map "US" to "United States" or "fr" into "French." Every time a user loads your settings page, they’re downloading a dictionary that the browser already has baked into its soul.

Stop shipping the atlas. The browser already knows where France is.

The Intl.DisplayNames object is a hidden gem of the ECMAScript Internationalization API. It provides a standardized way to get localized names for languages, regions, currencies, and even scripts without importing a single line of external data.

The "Hello World" of Built-in Labels

Instead of importing a massive object and doing a lookup like myJson['en']['US'], you can let the JavaScript engine do the heavy lifting.

// We want English names for regions
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' });

console.log(regionNames.of('US')); // "United States"
console.log(regionNames.of('JP')); // "Japan"
console.log(regionNames.of('KR')); // "South Korea"

If you want to switch to Spanish? Just change the locale tag. No extra JSON fetch required.

const regionNamesEs = new Intl.DisplayNames(['es'], { type: 'region' });
console.log(regionNamesEs.of('US')); // "Estados Unidos"

It’s Not Just for Countries

Most people find this API when they’re tired of managing country lists, but it handles a lot more. You can grab localized names for languages, currencies, and even scripts (like "Latin" vs "Cyrillic").

Here’s how you handle a currency selector without a massive metadata library:

const currencyNames = new Intl.DisplayNames(['de'], { type: 'currency' });

// Get the German name for various currency codes
console.log(currencyNames.of('USD')); // "US-Dollar"
console.log(currencyNames.of('EUR')); // "Euro"
console.log(currencyNames.of('JPY')); // "Japanischer Yen"

The type option accepts several values:
- language: e.g., "British English"
- region: e.g., "United States"
- script: e.g., "Traditional Chinese"
- currency: e.g., "US Dollar"
- calendar: e.g., "Gregorian"
- dateTimeField: e.g., "era", "year", "month"

The Performance "Gotcha"

I’ve seen people use this inside a .map() call in a React component or a loop. Don't do that.

Creating an Intl object is significantly more expensive than a simple object lookup. If you’re rendering a list of 200 countries, don't instantiate the constructor 200 times. Instantiate it once and reuse it.

// ❌ Bad: Creating a new instance every iteration
const list = codes.map(code => {
  return new Intl.DisplayNames(['en'], { type: 'region' }).of(code);
});

// ✅ Good: One instance, many lookups
const formatter = new Intl.DisplayNames(['en'], { type: 'region' });
const list = codes.map(code => formatter.of(code));

If your app switches languages frequently, you might even want to memoize these instances in a simple cache object.

Handling the Unknown (Fallbacks)

The world is a messy place. Sometimes you might pass a code that the browser doesn't recognize, or perhaps the data for a specific locale hasn't been loaded in that particular browser version.

By default, Intl.DisplayNames will return the code itself if it can't find a match. But you can control this behavior using the fallback option.

const scriptNames = new Intl.DisplayNames(['en'], { 
  type: 'script',
  fallback: 'none' // Instead of returning the code, return undefined
});

console.log(scriptNames.of('Zzzz')); // undefined (Unknown script)

Dialing in the Style

Sometimes you don't want "United States." You want "US." Or you want "U.K." instead of "United Kingdom." The style option allows for long, short, or narrow.

const longNames = new Intl.DisplayNames(['en'], { type: 'region', style: 'long' });
const shortNames = new Intl.DisplayNames(['en'], { type: 'region', style: 'short' });

console.log(longNames.of('US'));  // "United States"
console.log(shortNames.of('US')); // "US"

Why go this route?

1. Bundle Size: This is the obvious one. You’re moving logic from your bundle to the browser runtime. This is a massive win for mobile users on slow connections.
2. Accuracy: The browser's Intl data is updated by the OS/Browser vendor. When a country changes its name (like Turkey to Türkiye), your app updates "for free" once the browser engine updates.
3. Consistency: Your app’s labels will match the labels used by the user's operating system and other websites, leading to a more native feel.

The Real World: Browser Support

Support is actually excellent. Intl.DisplayNames has been supported in all major browsers (Chrome, Edge, Firefox, Safari) since roughly 2020. Unless you are specifically targeting legacy corporate environments stuck on Internet Explorer, you are good to go.

If you’re paranoid, a simple check like if (window.Intl && Intl.DisplayNames) is all you need to decide whether to use the native API or fall back to a tiny "emergency" JSON file.

But honestly? In 2024, it's time to delete that 2MB dictionary. Your bundle size—and your users—will thank you.