JavaScript Intl API: Complete Guide
The Intl API formats dates, numbers, currencies, and lists for any locale without external libraries. Learn DateTimeFormat, NumberFormat, RelativeTimeFormat, and ListFormat with practical examples.
The JavaScript Intl API is the built-in internationalization library for the language. It formats dates, numbers, currencies, relative times, and lists according to the conventions of any locale, without external dependencies.
// Dates for different locales
console.log(new Intl.DateTimeFormat("en-US").format(new Date())); // "7/15/2026"
console.log(new Intl.DateTimeFormat("de-DE").format(new Date())); // "15.7.2026"
console.log(new Intl.DateTimeFormat("ja-JP").format(new Date())); // "2026/7/15"
// Numbers with locale-aware grouping
console.log(new Intl.NumberFormat("en-US").format(1234567)); // "1,234,567"
console.log(new Intl.NumberFormat("de-DE").format(1234567)); // "1.234.567"
console.log(new Intl.NumberFormat("hi-IN").format(1234567)); // "12,34,567"Every Intl constructor takes a locale string and an options object. The locale controls conventions like digit grouping, date order, and list punctuation. The options control what information appears and how.
Before this API existed, teams pulled in libraries like Moment.js or Numeral.js just to get correct comma placement or a translated month name. Those libraries still have their place for date math, but for pure display formatting, the browser and Node.js already ship everything you need.
Intl.DateTimeFormat -- Dates and Times
DateTimeFormat is the most-used constructor in the API. It replaces manual date formatting and most library date display code, and it comes with a set of shorthand styles for the most common date layouts:
const date = new Date("2026-07-15T14:30:00");
console.log(new Intl.DateTimeFormat("en-US", { dateStyle: "full" }).format(date));
// "Wednesday, July 15, 2026"
console.log(new Intl.DateTimeFormat("en-US", { dateStyle: "short" }).format(date));
// "7/15/26"The dateStyle option accepts four preset values, each showing a different amount of detail for the same date:
| dateStyle | Example output (en-US) |
|---|---|
| full | Wednesday, July 15, 2026 |
| long | July 15, 2026 |
| medium | Jul 15, 2026 |
| short | 7/15/26 |
dateStyle and timeStyle are shorthand for these common patterns, but they do not let you mix and match individual pieces. For fine-grained control over exactly which parts of the date appear, specify individual components instead:
// Custom: weekday, month, day only
const fmt = new Intl.DateTimeFormat("en-US", {
weekday: "long",
month: "long",
day: "numeric"
});
console.log(fmt.format(date)); // "Wednesday, July 15"
// Time with time zone name
const timeFmt = new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "2-digit",
timeZoneName: "short"
});
console.log(timeFmt.format(date)); // "2:30 PM GMT+5:30" (varies by runtime TZ)The available components are weekday, era, year, month, day, hour, minute, second, and time zone name. Combine them to produce exactly the format your UI needs, including locale-specific defaults like 24-hour time in German instead of 12-hour time in English.
Formatting Relative to a Specific Time Zone
By default, DateTimeFormat uses the runtime's time zone. Pass a timeZone option to format for a specific zone instead:
const meeting = new Date("2026-07-15T14:30:00Z"); // UTC
const nyc = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short"
});
console.log(nyc.format(meeting)); // "10:30 AM EDT"This is essential for apps that display times for users in different time zones, such as a scheduling tool showing a meeting time to attendees in three countries at once. No date math is needed; the formatter does the conversion.
Intl.NumberFormat -- Numbers, Currencies, and Percentages
NumberFormat handles locale-aware number display:
const price = 1234567.89;
// Plain number, grouped per locale convention
console.log(new Intl.NumberFormat("en-US").format(price)); // "1,234,567.89"
console.log(new Intl.NumberFormat("en-IN").format(price)); // "12,34,567.89"
// Currency, symbol and placement decided by NumberFormat
console.log(
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(price)
); // "$1,234,567.89"Currency formatting handles symbol placement (prefix vs suffix), decimal separators, and grouping automatically per locale, so you never concatenate a symbol onto a number yourself. Getting this wrong by hand is an easy way to ship a price that reads correctly to you but looks broken to a user in another country:
| Locale | Currency | Output |
|---|---|---|
| de-DE | EUR | 1.234.567,89 € |
| ja-JP | JPY | ¥1,234,568 (no decimal places) |
| en-US | percent style, 0.1234 | 12% |
Units and Compact Notation
ES2020 added unit formatting and compact notation:
// Units
console.log(
new Intl.NumberFormat("en-US", { style: "unit", unit: "kilometer-per-hour" }).format(88)
); // "88 km/h"
// Compact notation
console.log(
new Intl.NumberFormat("en-US", { notation: "compact" }).format(1500000)
); // "1.5M"Available units include liter, kilogram, megabyte, hour, and many more. Use NumberFormat with the unit style instead of hardcoding unit labels. Add the long compact display option if you want "1.5 million" instead of "1.5M".
Significant Digits and Precision
Control how many digits appear:
const value = 0.0123456;
console.log(
new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 4 }).format(value)
); // "0.0123"
console.log(
new Intl.NumberFormat("en-US", { minimumSignificantDigits: 3, maximumSignificantDigits: 3 }).format(value)
); // "0.0123"Use the minimum and maximum fraction digit options for fixed decimal places, like currency. Use the minimum and maximum significant digit options for scientific or engineering displays, where the number of meaningful digits matters more than decimal position.
Intl.RelativeTimeFormat -- Human-Friendly Time Differences
RelativeTimeFormat produces phrases like "3 days ago" or "in 5 minutes":
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
console.log(rtf.format(-3, "day")); // "3 days ago"
console.log(rtf.format(1, "day")); // "tomorrow"The auto numeric option produces natural language like "tomorrow" instead of "in 1 day" when possible. Use the always numeric option for the numeric form every time. The supported units range from year and quarter down to second.
This API does not compute the difference for you. You need to decide which unit to use and pass the appropriate value:
function relativeTime(date) {
const now = new Date();
const diffMs = date - now;
const diffSecs = Math.round(diffMs / 1000);
const diffMins = Math.round(diffSecs / 60);
const diffHours = Math.round(diffMins / 60);
const diffDays = Math.round(diffHours / 24);
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
if (Math.abs(diffDays) >= 1) return rtf.format(diffDays, "day");
if (Math.abs(diffHours) >= 1) return rtf.format(diffHours, "hour");
if (Math.abs(diffMins) >= 1) return rtf.format(diffMins, "minute");
return rtf.format(diffSecs, "second");
}
const yesterday = new Date(Date.now() - 86400000);
console.log(relativeTime(yesterday)); // "yesterday"Intl.ListFormat -- Natural Language Lists
ListFormat joins array items with locale-aware conjunctions and punctuation, so you no longer have to hand-write the comma and "and" logic for turning a list into a sentence:
const items = ["apples", "bananas", "oranges"];
console.log(
new Intl.ListFormat("en", { style: "long", type: "conjunction" }).format(items)
); // "apples, bananas, and oranges"
console.log(
new Intl.ListFormat("en", { style: "long", type: "disjunction" }).format(items)
); // "apples, bananas, or oranges"
console.log(
new Intl.ListFormat("en", { style: "narrow", type: "unit" }).format(items)
); // "apples bananas oranges"The type option controls the connector word: conjunction for "and", disjunction for "or", and unit for no connector, used with units like "2 hours 30 minutes". The style option controls length: long, short, or narrow.
This replaces brittle code like joining an array with a comma, with a function that correctly handles Oxford commas, two-item lists, and locale-specific conventions that vary from language to language.
Other Intl Constructors
The API includes a few more constructors for specialized use. Most projects only reach for these occasionally, but each one solves a formatting problem that would otherwise require a dedicated library or a pile of locale-specific conditionals:
| Constructor | Purpose |
|---|---|
Intl.Collator | Locale-aware string comparison and sorting |
Intl.PluralRules | Determine which plural form to use for a given number and locale |
Intl.DisplayNames | Translate language, region, currency, and script codes into display names |
Intl.DurationFormat | Format time durations (newly available in modern browsers as of 2025) |
Intl.Segmenter | Split text into words, sentences, or grapheme clusters per locale rules |
// PluralRules: which plural form for a number?
const pr = new Intl.PluralRules("en");
console.log(pr.select(1)); // "one"
console.log(pr.select(2)); // "other"
// Russian has more plural categories than English
const ru = new Intl.PluralRules("ru");
console.log(ru.select(1)); // "one"
console.log(ru.select(2)); // "few"
console.log(ru.select(5)); // "many"PluralRules is especially useful for i18n work. Instead of hardcoding a check for whether a number equals one, use it to get the correct plural category for any locale, then look up the right translation string. DisplayNames follows the same pattern for translating codes like "ja" into a readable name such as "Japanese".
Caching Formatter Instances
Creating an Intl formatter is cheap, but if you format hundreds of values, cache the instance:
// Bad: creates a new formatter on every call
function formatPriceBad(amount) {
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
}
// Good: cache the formatter
const usdFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
function formatPrice(amount) {
return usdFormatter.format(amount);
}Formatter instances are immutable. You can reuse the same instance across your entire application, including passing it down through props or a shared module rather than recreating it in every component.
Constructing a formatter is more expensive than calling format on one, because the constructor has to look up locale data such as digit grouping rules, currency symbols, and calendar systems. In a hot code path, like formatting every row of a large table, creating a fresh formatter per row adds measurable overhead compared to building it once outside the loop.
Locale Fallbacks
Always specify a locale. The default is the system locale, which varies across machines and makes your output unpredictable. This matters even more in server-rendered apps: if the server runs in a different locale than the browser, a component that formats a date without an explicit locale can render one string on the server and a different one on the client, causing a hydration mismatch:
// Unpredictable: depends on where the code runs
new Intl.DateTimeFormat().format(new Date());
// Predictable: you control the output
new Intl.DateTimeFormat("en-US").format(new Date());If you support multiple locales, use the supportedLocalesOf method to check which ones are available, and fall back gracefully to a default when a visitor's preferred locale is not one you support:
function getFormatter(requestedLocale) {
const supported = Intl.NumberFormat.supportedLocalesOf([requestedLocale, "en"]);
const locale = supported[0]; // requested locale if available, otherwise "en"
return new Intl.NumberFormat(locale, { style: "currency", currency: "USD" });
}
console.log(getFormatter("fr-FR").format(100)); // French formatting
console.log(getFormatter("xx-YY").format(100)); // Falls back to EnglishThe "xx-YY" locale is not real, so the supportedLocalesOf check filters it out and only "en" survives in the returned array. This pattern keeps a broken or missing Accept-Language header from crashing your formatting logic, since the formatter always receives a locale it actually understands.
For date formatting basics without locale considerations, see /javascript/formatting-dates-in-javascript-complete-guide. For locale-aware number display beyond what this article covers, see /javascript/javascript-number-type-complete-guide.
Rune AI
Key Insights
- Intl.DateTimeFormat formats dates and times for any locale with customizable date/time components.
- Intl.NumberFormat formats numbers, currencies, percentages, and units with locale-aware grouping and symbols.
- Intl.RelativeTimeFormat produces human-readable relative times like '3 days ago' or 'in 5 minutes'.
- Intl.ListFormat joins arrays into natural lists: 'apples, bananas, and oranges'.
- Always specify a locale explicitly. The default is the system locale, which varies unpredictably.
Frequently Asked Questions
Do I need Moment.js or date-fns with the Intl API?
How do I know which locales are supported?
Conclusion
The Intl API brings locale-aware formatting into JavaScript without any external libraries. DateTimeFormat handles every date display pattern you need. NumberFormat covers currencies, percentages, and units. RelativeTimeFormat gives you '3 days ago' style output. And ListFormat joins arrays into natural-language lists. Together they replace the formatting half of every date and i18n library you used to install.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.