Why the Intl API Deserves a Place in Every Frontend Toolbox

Internationalization is often mistaken for translation alone. In practice, it also governs how dates are arranged in Germany versus Japan, how Arabic pluralizes quantities, and how names are sorted across scripts. Developers have historically reached for bulky third-party libraries—or hand-rolled formatting helpers—to handle these tasks, paying for it in bundle size and maintenance burden. The ECMAScript Internationalization API, exposed as the built-in Intl object, offers a native, standards-compliant alternative that runs directly in modern JavaScript environments.

Understanding Locales: Beyond Language Codes

A locale is not just a language tag like en or es. It bundles the full set of cultural conventions needed to present data correctly:

  • Language: The base linguistic medium, e.g., en, fr.
  • Script: Writing system, e.g., zh-Hans (Simplified) versus zh-Hant (Traditional Chinese).
  • Region: Geographic context, which explains why en-US and en-GB format the same numbers and dates differently.
  • Variants and preferences: Additional cultural specifics covered by the W3C language tag guidance.

In practice, you usually derive the locale from the page’s declared language:

// Get the page's language from the HTML lang attribute
const pageLocale = document.documentElement.lang || 'en-US'; // Fallback to 'en-US'

You might override this when serving content in multiple languages on one page, or defer to the user’s own preference settings:

// Force a specific locale regardless of page language
const tutorialFormatter = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' });

console.log(`Chinese example: ${tutorialFormatter.format(199.99)}`); // Output: ¥199.99
// Use the user's preferred language
const browserLocale = navigator.language || 'ja-JP';

const formatter = new Intl.NumberFormat(browserLocale, { style: 'currency', currency: 'JPY' });

When constructing a formatter, you can pass one or more locale strings; the API picks the best match from what the runtime supports.

The Core Intl Constructors

Each Intl constructor targets a distinct formatting problem. These are the ones you will reach for most often.

Dates and Numbers with Cultural Context

Intl.DateTimeFormat removes the guesswork from locale-specific date displays—whether the expected output is MM/DD/YYYY, DD.MM.YYYY, or a fully spelled-out month. Its options argument gives granular control over every component from weekday to time zone.

const date = new Date(2025, 6, 27, 14, 30, 0); // June 27, 2025, 2:30 PM

// Specific locale and options (e.g., long date, short time)
const options = {
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
  timeZoneName: 'shortOffset' // e.g., "GMT+8"
};

console.log(new Intl.DateTimeFormat('en-US', options).format(date));

// "Friday, June 27, 2025 at 2:30 PM GMT+8"
console.log(new Intl.DateTimeFormat('de-DE', options).format(date));

// "Freitag, 27. Juni 2025 um 14:30 GMT+8"

// Using dateStyle and timeStyle for common patterns
console.log(new Intl.DateTimeFormat('en-GB', { dateStyle: 'full', timeStyle: 'short' }).format(date));

// "Friday 27 June 2025 at 14:30"

console.log(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long', timeStyle: 'short' }).format(date));

// "2025年6月27日 14:30"

Likewise, Intl.NumberFormat goes beyond decimal rounding. It automatically applies the correct thousands separators, decimal markers, currency symbols, and percentage signs for the target locale. Finer control is available through options such as minimumFractionDigits, maximumFractionDigits, and notation for compact or scientific display.

const price = 123456.789;

// Currency formatting
console.log(new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price));

// "$123,456.79" (auto-rounds)

console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price));

// "123.456,79 €"

// Units
console.log(new Intl.NumberFormat('en-US', { style: 'unit', unit: 'meter', unitDisplay: 'long' }).format(100));

// "100 meters"

console.log(new Intl.NumberFormat('fr-FR', { style: 'unit', unit: 'kilogram', unitDisplay: 'short' }).format(5.5));

// "5,5 kg"

Lists, Relative Time, and Plurals

Formatting a list of items in natural language is deceptively difficult: English inserts “and” or “or,” while other languages change conjunctions or punctuation entirely. Intl.ListFormat handles this without bespoke conditional logic.

const items = ['apples', 'oranges', 'bananas'];

// Conjunction ("and") list
console.log(new Intl.ListFormat('en-US', { type: 'conjunction' }).format(items));

// "apples, oranges, and bananas"

console.log(new Intl.ListFormat('de-DE', { type: 'conjunction' }).format(items));

// "Äpfel, Orangen und Bananen"

// Disjunction ("or") list
console.log(new Intl.ListFormat('en-US', { type: 'disjunction' }).format(items));

// "apples, oranges, or bananas"

console.log(new Intl.ListFormat('fr-FR', { type: 'disjunction' }).format(items));

// "apples, oranges ou bananas"

When you need timestamps like “2 days ago” rather than absolute dates, Intl.RelativeTimeFormat generates correctly localized phrasing for past and future offsets. Passing numeric: 'always' forces explicit values such as “1 day ago” instead of “yesterday.”

const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

console.log(rtf.format(-1, 'day'));    // "yesterday"
console.log(rtf.format(1, 'day'));     // "tomorrow"
console.log(rtf.format(-7, 'day'));    // "7 days ago"
console.log(rtf.format(3, 'month'));   // "in 3 months"
console.log(rtf.format(-2, 'year'));   // "2 years ago"

// French example:
const frRtf = new Intl.RelativeTimeFormat('fr-FR', { numeric: 'auto', style: 'long' });

console.log(frRtf.format(-1, 'day'));    // "hier"
console.log(frRtf.format(1, 'day'));     // "demain"
console.log(frRtf.format(-7, 'day'));    // "il y a 7 jours"
console.log(frRtf.format(3, 'month'));   // "dans 3 mois"

Pluralization is one of the trickiest i18n problems because the rules vary dramatically: English distinguishes singular from plural, while Arabic has separate categories for zero, one, two, and many. Intl.PluralRules does not translate text itself; it tells you the correct plural category for a given count and locale. You then use that label to select the appropriate string from your message bundle, e.g., an item.one key versus an item.other key.

const prEn = new Intl.PluralRules('en-US');

console.log(prEn.select(0));    // "other" (for "0 items")
console.log(prEn.select(1));    // "one"   (for "1 item")
console.log(prEn.select(2));    // "other" (for "2 items")

const prAr = new Intl.PluralRules('ar-EG');

console.log(prAr.select(0));    // "zero"
console.log(prAr.select(1));    // "one"
console.log(prAr.select(2));    // "two"
console.log(prAr.select(10));   // "few"
console.log(prAr.select(100));  // "other"

Displaying Names and Regions

Displaying a language or region name in a user’s own language typically requires a large lookup table. Intl.DisplayNames provides those localized mappings natively, whether you need the local name for a language, script, or country. This keeps your application smaller and removes hardcoded translation maps.

// Display language names in English
const langNamesEn = new Intl.DisplayNames(['en'], { type: 'language' });

console.log(langNamesEn.of('fr'));      // "French"
console.log(langNamesEn.of('es-MX'));   // "Mexican Spanish"

// Display language names in French
const langNamesFr = new Intl.DisplayNames(['fr'], { type: 'language' });

console.log(langNamesFr.of('en'));      // "anglais"
console.log(langNamesFr.of('zh-Hans')); // "chinois (simplifié)"

// Display region names
const regionNamesEn = new Intl.DisplayNames(['en'], { type: 'region' });

console.log(regionNamesEn.of('US'));    // "United States"
console.log(regionNamesEn.of('DE'));    // "Germany"

// Display script names
const scriptNamesEn = new Intl.DisplayNames(['en'], { type: 'script' });

console.log(scriptNamesEn.of('Latn'));  // "Latin"
console.log(scriptNamesEn.of('Arab'));  // "Arabic"
Hidden block? OK, ignoring.

Support and Scope

Adoption is a non-issue. Every major browser—Chrome, Firefox, Safari, and Edge—fully supports DateTimeFormat, NumberFormat, ListFormat, RelativeTimeFormat, PluralRules, and DisplayNames. For nearly all audiences you can use these APIs without polyfills.

That said, Intl is scoped to data formatting. It does not translate prose, handle right-to-left text flow, or account for typographic and cultural nuances beyond number, date, and list conventions. Those concerns remain separate, but for rendering dynamic data correctly, Intl is precise, performant, and dependency-free.

Smashing Editorial