There is a common misconception that internationalization (i18n) is simply text translation. While translation is very important, it is only one aspect. One of the most complex parts is adapting information to different cultural expectations: how should a date be displayed in Japan compared to Germany? What is the correct way to pluralize a noun in Arabic compared to English? How should a list of names be sorted across different languages?
Many developers have relied on heavy third-party libraries or, worse, custom-built formatting functions to solve these challenges. These solutions, while functional, often introduce significant overhead: increased bundle size, potential performance bottlenecks, and the constant struggle to keep up with changing linguistic rules and locale data.
Meet the ECMAScript Internationalization API, better known as the Intl object. This quiet powerhouse, built directly into modern JavaScript environments, is an often underestimated but incredibly powerful, built-in, performant, and standards-compliant solution for handling data internationalization. It is a testament to the web’s commitment to being global, providing a unified and efficient way to format numbers, dates, lists, and more according to specific locales.
Intl and locales: more than just language codes
At the heart of Intl is the concept of locales. A locale is much more than a two-letter language code, such as en for English or es for Spanish. It includes the full context needed to present information properly for a specific cultural group. This includes:
- Language: The primary linguistic medium, such as
en,es, orfr. - Script: The writing system, such as
Latnfor Latin orCyrlfor Cyrillic. For example,zh-Hansfor Simplified Chinese compared tozh-Hantfor Traditional Chinese. - Region: The geographic area, such as
USfor the United States,GBfor Great Britain, orDEfor Germany. This is crucial for variants of the same language, such asen-USanden-GB, which differ in date, time, and number formatting. - Preferences/variants: Other specific cultural or linguistic preferences. For more information, see the W3C guidelines “Choosing a Language Tag”.
Usually, you will want to choose the locale based on the language of the webpage. This can be determined from the lang attribute:
JavaScript
// Get the page language from the HTML lang attribute
const pageLocale = document.documentElement.lang || 'en-US'; // Fallback to 'en-US'
Sometimes you may want to override the page locale with a specific locale, for example when displaying content in multiple languages:
// Force a specific locale regardless of the page language
const tutorialFormatter = new Intl.NumberFormat('zh-CN', {
style: 'currency', currency: 'CNY' });
console.log(`Chinese example:
${tutorialFormatter.format(199.99)}`); // Result: ¥199.99
In some cases, you may want to use the user’s preferred language:
// Use the user’s preferred language
const browserLocale = navigator.language || 'ja-JP';
const formatter = new Intl.NumberFormat(browserLocale, { style:
'currency', currency: 'JPY' });
When you create an Intl formatter, you can optionally pass one or more locale strings. The API will then choose the most suitable locale based on availability and preference.
The main formatting powerhouses
The Intl object exposes several constructors, each dedicated to a specific formatting task. Let’s explore the most commonly used ones, along with a few powerful, often overlooked gems.
1. Intl.DateTimeFormat: dates and times, globally
Date and time formatting is a classic i18n problem. Should it be MM/DD/YYYY or DD.MM.YYYY? Should the month be a number or a full word? Intl.DateTimeFormat handles all of this easily.
const date = new Date(2025, 6, 27, 14, 30, 0); // June 27, 2025, 14:30
// Specific locale and options (for example, long date, short time)
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
timeZoneName: 'shortOffset' // e.g., "GMT+3"
};
console.log(new Intl.DateTimeFormat('en-US',
options).format(date));
// "Friday, June 27, 2025 at 2:30 PM GMT+3"
console.log(new Intl.DateTimeFormat('de-DE',
options).format(date));
// "Freitag, 27. Juni 2025 um 14:30 GMT+3"
// 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"
The flexibility of DateTimeFormat options is enormous, allowing you to control year, month, day, weekday, hour, minute, second, time zone, and much more.
2. Intl.NumberFormat: numbers with cultural nuance
Beyond simple decimals, numbers require careful handling: thousands separators, decimal marks, currency symbols, and percentage signs vary widely by locale.
const price = 123456.789;
// Currency formatting
console.log(new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price));
// "$123,456.79" (automatically rounds)
console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price));
// "123.456,79 €"
// Measurement 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"
Options such as minimumFractionDigits, maximumFractionDigits, and notation — for example, scientific or compact — provide even more precise control.
3. Intl.ListFormat: natural-language lists
Rendering lists is surprisingly complex. English uses “and” for conjunction and “or” for disjunction. Many languages use different conjunctions, and some require specific punctuation. This API simplifies a task that would otherwise require complex 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));
// "pommes, oranges ou bananes"
4. Intl.RelativeTimeFormat: human-readable timestamps
Displaying “2 days ago” or “in 3 months” is common in user interfaces, but accurately localizing these phrases requires large amounts of data. Intl.RelativeTimeFormat automates this.
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"
The option numeric: 'always' would force “1 day ago” instead of “yesterday.”
5. Intl.PluralRules: mastering pluralization
This is arguably one of the most critical aspects of i18n. Different languages have very different plural rules. For example, English has singular/plural, while Arabic has zero, one, two, few, many, and other forms. Intl.PluralRules lets you determine the “plural category” for a given number in a specific locale.
const prEn = new Intl.PluralRules('en-US');
console.log(prEn.select(0)); // "other" (e.g., "0 items")
console.log(prEn.select(1)); // "one" (e.g., "1 item")
console.log(prEn.select(2)); // "other" (e.g., "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"
This API does not directly pluralize text, but it provides the essential classification needed to choose the correct translation string from your message sets. For example, if you have message keys such as item.one and item.other, you would use pr.select(count) to select the right one.
6. Intl.DisplayNames: localized names for everything
Need to display the name of a language, region, or script in the user’s preferred language? Intl.DisplayNames is your complete solution.
// 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"
With Intl.DisplayNames, you avoid hard-coding endless language, region, or script names, keeping your application robust and lightweight.
Browser support
You may be wondering about browser compatibility. The good news is that Intl has excellent support across modern browsers. All major browsers — Chrome, Firefox, Safari, and Edge — fully support the core functionality discussed here: DateTimeFormat, NumberFormat, ListFormat, RelativeTimeFormat, PluralRules, and DisplayNames. You can confidently use these APIs without polyfills for the vast majority of your users.
Conclusion: embrace the global web with Intl
The Intl API is a cornerstone of modern web development for global audiences. It gives front-end developers the ability to deliver highly localized user experiences with minimal effort, using optimized capabilities built into the browser.
By starting to use Intl, you will reduce dependencies, decrease bundle sizes, and improve performance — all while ensuring that your application respects and adapts to the diverse linguistic and cultural expectations of users around the world. Stop struggling with custom formatting logic and embrace this standards-compliant tool!
It is important to remember that Intl handles data formatting. While incredibly powerful, it does not solve every aspect of internationalization. Content translation, bidirectional text (RTL/LTR), locale-specific typography, and deep cultural nuances unrelated to data formatting still require careful consideration. Perhaps we will write about those in the future! But when it comes to presenting dynamic data accurately and intuitively, Intl is the built-in browser answer.
Further reading and resources
- MDN Web Docs:
- ECMAScript Internationalization API specification: Official ECMA-402 standard
- Choosing a Language Tag


