Back

Formatting Dates and Numbers with the Intl API

Formatting Dates and Numbers with the Intl API

You’ve used Intl.DateTimeFormat and Intl.NumberFormat before. But if your mental model dates back a few years, you’re likely missing significant capabilities—particularly around Intl.NumberFormat rounding controls and how Temporal and Intl work together in late-2025 runtimes.

This article provides an updated technical overview of JavaScript internationalization through the Intl API, focusing on what’s changed and what experienced developers often get wrong.

Key Takeaways

  • The Intl API formats values into locale-aware strings but doesn’t parse, calculate, or store data—keep presentation separate from application logic.
  • Intl.NumberFormat now supports advanced rounding controls including roundingMode, roundingIncrement, and trailingZeroDisplay.
  • Temporal types handle their own toLocaleString() formatting while Intl.DateTimeFormat continues to format Date objects.
  • Always reuse formatter instances for performance and avoid hardcoding expected output strings in tests.

What Intl Actually Does

The Intl namespace handles locale-aware formatting of values. It doesn’t parse, calculate, or manipulate data—it transforms values into human-readable strings according to cultural conventions.

Two critical points:

  1. Intl formats; it doesn’t store or compute. Your application logic stays separate from presentation.
  2. Output varies by runtime. The underlying locale data comes from ICU libraries bundled with browsers and Node.js. Exact strings may differ slightly between Chrome and Safari, or between Node versions.

Never hardcode expected output strings in tests. Use formatToParts() or structural assertions instead.

Intl.DateTimeFormat: Beyond Basic Options

Formatting Date Objects

Intl.DateTimeFormat remains the standard way to format JavaScript Date objects:

const formatter = new Intl.DateTimeFormat('de-DE', {
  dateStyle: 'long',
  timeStyle: 'short',
  timeZone: 'Europe/Berlin'
});

formatter.format(new Date()); // "27. Juni 2025 um 14:30"

The dateStyle and timeStyle options provide preset configurations. When you need granular control, use individual options like weekday, month, hour, and timeZoneName.

Temporal Types and Locale-Aware Formatting

Temporal is actively being implemented in modern JavaScript engines, but it is not yet broadly supported across all browsers and environments. Where available, Temporal.PlainDate, Temporal.ZonedDateTime, and other Temporal types format themselves via toLocaleString(), rather than being passed directly to Intl.DateTimeFormat.prototype.format().

const date = Temporal.PlainDate.from('2025-06-27');
date.toLocaleString('ja-JP', { dateStyle: 'full' });
// "2025年6月27日金曜日"

Intl.DateTimeFormat accepts Date objects. Temporal types handle their own formatting logic and may delegate locale-sensitive behavior internally, but they are not formatted by Intl.DateTimeFormat itself. This distinction matters when designing APIs that accept date inputs.

Date Ranges with formatRange

For displaying date ranges, use formatRange():

const start = new Date(2025, 5, 27);
const end = new Date(2025, 6, 3);

new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })
  .formatRange(start, end);
// "Jun 27 – Jul 3, 2025"

The formatter intelligently collapses redundant parts based on locale conventions.

Intl.NumberFormat Rounding and Display Controls

Modern Rounding Options

Intl.NumberFormat rounding behavior has expanded significantly in recent specifications, with support varying by runtime. Beyond minimumFractionDigits and maximumFractionDigits, you now have:

  • roundingMode: Controls how values round (ceil, floor, halfExpand, halfEven, etc.)
  • roundingIncrement: Rounds to specific increments (5, 10, 50, etc.)
  • trailingZeroDisplay: Controls whether trailing zeros appear (auto or stripIfInteger)
const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  roundingMode: 'halfEven',
  maximumFractionDigits: 2
});

formatter.format(2.225); // "$2.22" (banker's rounding)
formatter.format(2.235); // "$2.24"

Locale-Aware Number Formatting Patterns

For compact notation and unit formatting:

// Compact notation
new Intl.NumberFormat('en-US', {
  notation: 'compact',
  compactDisplay: 'short'
}).format(1234567); // "1.2M"

// Unit formatting
new Intl.NumberFormat('de-DE', {
  style: 'unit',
  unit: 'kilometer-per-hour',
  unitDisplay: 'short'
}).format(120); // "120 km/h"

Practical Considerations

Reuse Formatter Instances

Creating formatters has overhead. Cache them when formatting multiple values:

// Do this
const priceFormatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});
prices.map(p => priceFormatter.format(p));

// Not this
prices.map(p => new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).format(p));

Feature Detection

Check for newer options programmatically:

try {
  new Intl.NumberFormat('en', { roundingMode: 'halfEven' });
  // Feature supported
} catch (e) {
  // Fall back to default rounding
}

Locale vs. Timezone

These are independent concepts. A locale (en-GB) determines formatting conventions. A timezone (Europe/London) determines the clock time displayed. You can format a date in German conventions while showing Tokyo time.

Conclusion

The Intl API for date formatting and locale-aware number formatting has matured substantially. The specification now includes rounding modes, display controls, and range formatting that eliminate most reasons for external libraries.

Temporal types exist alongside Intl—they handle their own toLocaleString() calls while Intl.DateTimeFormat continues to format Date objects. Build your mental model around this separation, test without hardcoded string expectations, and reuse formatter instances for performance.

FAQs

No. Intl.DateTimeFormat.prototype.format() accepts Date objects only. Temporal types like PlainDate and ZonedDateTime have their own toLocaleString() methods. They are not formatted by Intl.DateTimeFormat itself, even though they may use similar locale data internally.

Intl relies on ICU locale data bundled with each runtime. Chrome, Safari, Firefox, and Node.js may ship different ICU versions with slight variations in output. Avoid hardcoding expected strings in tests. Use formatToParts() or structural assertions to verify formatting behavior reliably.

Banker's rounding (halfEven) rounds midpoint values toward the nearest even digit, reducing cumulative rounding bias. It is commonly used in financial and accounting contexts, but exact results can still be affected by binary floating-point representation.

Attempt to construct a formatter with the option enabled. If the runtime does not support it, it may throw a RangeError or silently ignore the option, depending on the engine. Always include a fallback strategy.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay