A polyfill for Intl.DateTimeFormat tested by the official ECMAScript Conformance test suite

npm Version size

ECMA-402 Spec Compliance

This package implements Intl.DateTimeFormat. See the conformance report for tested coverage and known limitations.

Implemented Features

✅ Core Methods

  • format(date) - Format a single date/time
  • formatToParts(date) - Format a date/time and return parts array
  • formatRange(startDate, endDate) - Format a date range (Proposal)
  • formatRangeToParts(startDate, endDate) - Format a date range and return parts array with source attribution
  • resolvedOptions() - Return resolved formatting options

Temporal inputs

When globalThis.Temporal is available before importing this polyfill, all four formatting methods accept Temporal.PlainDate, PlainDateTime, PlainTime, PlainYearMonth, PlainMonthDay, and Instant values. Plain values use their wall-clock fields; Instant uses the formatter's timezone. Range endpoints must have the same Temporal type. ZonedDateTime is rejected, as required by the spec.

For runtimes without native Temporal, install temporal-polyfill and load it first:

import 'temporal-polyfill/full/global'
import '@formatjs/intl-datetimeformat/polyfill-force'
import '@formatjs/intl-datetimeformat/locale-data/en'

Options select only fields relevant to the input type. Non-overlapping options throw TypeError; incompatible calendars throw RangeError. Supported calendars are gregory, iso8601, buddhist, coptic, ethiopic, ethioaa, roc, indian, islamic-civil, islamic-tbla, islamic-umalqura, persian, japanese, hebrew, chinese, and dangi. PlainYearMonth and PlainMonthDay require the formatter's calendar to match exactly, including iso8601. Calendar aliases are canonicalized; calendar: 'islamicc' selects islamic-civil.

✅ dateStyle & timeStyle Options

Global import

new Intl.DateTimeFormat('en', {dateStyle: 'long', timeStyle: 'short'})
// "January 1, 2024 at 12:00 PM"

ES Modules

import {DateTimeFormat} from '@formatjs/intl-datetimeformat'

new DateTimeFormat('en', {dateStyle: 'long', timeStyle: 'short'})
// "January 1, 2024 at 12:00 PM"

✅ Extended timeZoneName Options

All 6 timeZoneName values are supported:

  • 'short' - Short standard format (e.g., "EST", "PST")
  • 'long' - Long standard format (e.g., "Eastern Standard Time")
  • 'shortOffset' - Short format with UTC offset (e.g., "GMT-5")
  • 'longOffset' - Long format with UTC offset (e.g., "GMT-05:00")
  • 'shortGeneric' - Short generic non-location format (e.g., "ET", "PT")
  • 'longGeneric' - Long generic non-location format (e.g., "Eastern Time")

✅ Additional Features

  • dayPeriod - Format day periods ('narrow', 'short', 'long')
  • fractionalSecondDigits - Control decimal precision for seconds (1-3 digits)
  • era - Format era information ('narrow', 'short', 'long')
  • hour12 and hourCycle - 12/24 hour format control
  • Full IANA timezone support - 600+ timezones with accurate DST transitions
  • UTC offset timezones - Direct offset support (e.g., '+01:00', '-05:30')

Not Implemented (Stage 2.7 Proposals - Not Yet Finalized)

  • eraDisplay option - Control era visibility ('always', 'never', 'auto')
  • monthCode support - ISO 8601 month codes for non-Gregorian calendars

Features

Installation

npm i @formatjs/intl-datetimeformat

Requirements

This package requires the following capabilities:

The numberingSystem option and the locale's nu extension support CLDR numeric systems, including supplementary digits such as Adlam. Two-digit fields retain two complete digits. Load the NumberFormat polyfill when the host lacks the requested numbering system.

Usage

Via polyfill-fastly.io

You can use polyfill-fastly.io URL Builder to create a polyfill script tag for Intl.DateTimeFormat. By default the created URL does not come with any locale data. In order to add locale data, append Intl.DateTimeFormat.~locale.<locale>, as well as locale data for any required polyfills, to your list of features. For example:

<!-- Polyfill Intl.DateTimeFormat, its dependencies & `en` locale data -->
<script src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.DateTimeFormat,Intl.DateTimeFormat.~locale.en,Intl.NumberFormat.~locale.en"></script>

Simple

import '@formatjs/intl-datetimeformat/polyfill.js'
import '@formatjs/intl-datetimeformat/locale-data/en.js' // locale-data for en
import '@formatjs/intl-datetimeformat/add-all-tz.js' // Add ALL tz data

The polyfill also replaces Date.prototype.toLocaleString, toLocaleDateString, and toLocaleTimeString with their ECMA-402 behavior, including the standard date and time defaults when only timeZone is set.

Optional calendars

The base polyfill and locale-data/<locale>.js include Gregorian and ISO 8601. Other calendars require both their arithmetic module and their localized names and patterns. Load them before constructing a formatter:

await import('@formatjs/intl-datetimeformat/polyfill-force.js')
await Promise.all([
  import('@formatjs/intl-datetimeformat/locale-data/en.js'),
  import('@formatjs/intl-datetimeformat-calendar-hebrew'),
  import('@formatjs/intl-datetimeformat-calendar-hebrew/locale-data/en.js'),
])

new Intl.DateTimeFormat('en', {calendar: 'hebrew'}).format(new Date())

Calendar modules register automatically and can also queue before polyfill installation. Arithmetic and locale patterns can arrive in either order. Only calendars with both loaded participate in calendar negotiation; unloaded calendar requests fall back to a loaded calendar. Existing formatters retain the calendar selected when they were constructed.

Install @formatjs/intl-datetimeformat-calendar-<calendar> for each calendar you need. Import the package and its locale-data/<locale>.js entry, as above. Requires @formatjs/intl-datetimeformat 7.7.0 or newer.

When upgrading from 7.7.x, replace these imports:

  • @formatjs/intl-datetimeformat/calendar-data/<calendar>.js with @formatjs/intl-datetimeformat-calendar-<calendar>.
  • @formatjs/intl-datetimeformat/calendar-data/<calendar>/<locale>.js with @formatjs/intl-datetimeformat-calendar-<calendar>/locale-data/<locale>.js.

add-all-calendars.js is removed; import each calendar package you need. These packages do not install globalThis.Temporal.

Custom providers can use the same registration APIs. __addCalendarData accepts {calendar, dateFromTime}; the callback receives timezone-adjusted milliseconds and returns {era, year, month, day} (zero-based month, one-based day, year within the era). __addCalendarLocaleData accepts {locale, calendar, data, formats} with that calendar's CLDR-style names and patterns. Both records must be loaded before the calendar can be selected. This provider API is a FormatJS extension.

The arithmetic modules also export their registration record for ponyfill use:

import {DateTimeFormat} from '@formatjs/intl-datetimeformat'
import hebrew from '@formatjs/intl-datetimeformat-calendar-hebrew'

DateTimeFormat.__addCalendarData(hebrew)
// Register base locale data with __addLocaleData and calendar-specific
// locale records with __addCalendarLocaleData when using the ponyfill directly.

Dynamic import + capability detection

async function polyfill(locale: string) {
  const unsupportedLocale = shouldPolyfill(locale)
  // This locale is supported
  if (!unsupportedLocale) {
    return
  }
  // Load the polyfill 1st BEFORE loading data
  await import('@formatjs/intl-datetimeformat/polyfill-force.js')

  // Parallelize CLDR data loading
  const dataPolyfills = [
    import('@formatjs/intl-datetimeformat/add-all-tz.js'),
    import(`@formatjs/intl-datetimeformat/locale-data/${unsupportedLocale}.js`),
  ]
  await Promise.all(dataPolyfills)
}

Adding IANA Timezone Database

Timezone data uses IANA tzdb 2026d, including permanent UTC-06 for America/Inuvik from November 1, 2026.

We provide 2 pre-processed IANA Timezone:

Full: contains ALL Timezone from IANA database

import '@formatjs/intl-datetimeformat/polyfill.js'
import '@formatjs/intl-datetimeformat/add-all-tz.js'
import '@formatjs/intl-datetimeformat/polyfill.js'
import '@formatjs/intl-datetimeformat/add-golden-tz.js'

UTC Offset Timezones

This polyfill supports UTC offset timezone identifiers as specified in ECMA-402 (ES2026). You can use offset-based timezone strings in addition to IANA timezone names.

Supported Formats

The following UTC offset formats are supported:

  • ±HH:MM - e.g., "+01:00", "-05:00" (recommended format)
  • ±HHMM - e.g., "+0100", "-0500"
  • ±HH - e.g., "+01", "-05"
  • ±HH:MM:SS - e.g., "+01:30:45" (with seconds)
  • ±HH:MM:SS.sss - e.g., "+01:30:45.123" (with fractional seconds)

All offset formats are automatically canonicalized to ±HH:MM format (with seconds/fractional seconds preserved if non-zero).

Usage Example

import '@formatjs/intl-datetimeformat/polyfill.js'
import '@formatjs/intl-datetimeformat/locale-data/en.js'

// Using UTC offset timezone
const formatter = new Intl.DateTimeFormat('en-GB', {
  timeZone: '+01:00',
  year: 'numeric',
  month: 'numeric',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
})

console.log(formatter.format(new Date('2024-01-01T00:00:00Z')))
// Output: "01/01/2024, 01:00"

// Works with @date-fns/tz and other libraries
const dtf = new Intl.DateTimeFormat('en-GB', {
  timeZone: '+01:00',
  hour: 'numeric',
  timeZoneName: 'longOffset',
})

Compatibility

  • UTC offset timezones work without loading any timezone data (add-all-tz.js or add-golden-tz.js)
  • Compatible with libraries like @date-fns/tz in React Native/Hermes environments
  • Follows the same behavior as Chrome and Node.js 22+ native implementations

Default Timezone

Since JS Engines do not expose default timezone, there's currently no way for us to detect local timezone that a browser is in. Therefore, the default timezone in this polyfill is UTC.

You can change this by either calling __setDefaultTimeZone or always explicitly pass in timeZone option for accurate date time calculation.

Since __setDefaultTimeZone is not in the spec, you should make sure to check for its existence before calling it & after tz data has been loaded, e.g:

import '@formatjs/intl-datetimeformat/polyfill.js'
import '@formatjs/intl-datetimeformat/add-all-tz.js'

if ('__setDefaultTimeZone' in Intl.DateTimeFormat) {
  Intl.DateTimeFormat.__setDefaultTimeZone('America/Los_Angeles')
}

Tests

The Bazel :test262 target runs the upstream suite against a reviewed failure baseline. A green baseline check does not mean every test passed. See the conformance report; run bazel test //packages/intl-datetimeformat:test262-strict to require zero failures.

Offset time zones

The timeZone option accepts offsets in ±HH, ±HHMM, and ±HH:MM form. Offsets containing seconds or fractional components throw RangeError, including an explicit zero-second component.

Loading und locale data preserves existing English data. Root patterns remain separate from language-specific patterns, regardless of registration order.

Fractional timestamps

Formatting truncates fractional millisecond timestamps toward zero before calendar conversion. For example, -0.9 formats as the Unix epoch, matching 0. The same rule applies to both range endpoints.

Built-in methods

DateTimeFormat methods cannot be constructed with new and have no own prototype property. The constructor's length is 0; supportedLocalesOf.length is 1, matching their required argument counts.

Dates outside the exact ±8,640,000,000,000,000 millisecond TimeClip bounds throw RangeError, including values one millisecond beyond either endpoint. Year calculations use Gregorian arithmetic so valid endpoints also format when their local timezone offsets cross the native Date range.

DateTimeFormat methods require an initialized receiver before coercing date arguments. Inheriting its prototype does not create a DateTimeFormat instance.

Resolved options follow specification property order: hour12 follows hourCycle, and style properties follow component properties.

Gregorian astronomical year zero formats as year 1 BC. Year 1 begins the AD era.

Zero UTC offsets normalize to +00:00. Named timezone matching ignores ASCII letter case only; non-ASCII lookalikes are rejected.

Constructor options are read once in specification order. Default date fields are applied to internal records without writing to caller options.

Every locale supports explicit h11, h12, h23, and h24 hour cycles through options and Unicode extensions. Locale preferences still select the default.

The hour12 option selects the locale’s separate 12-hour or 24-hour preference. For example, English uses h12 or h23; Japanese uses h11 or h23.

DateTimeFormat reuses pattern fields parsed during locale registration, preserving legacy RegExp statics during constructor format matching.

Hour-cycle preferences affect matching only when an hour is requested. Minute/second-only formats keep their requested fields.

Range formatting compares endpoints at the displayed precision and keeps shared locale fallback patterns unchanged across formatter instances.

The dayPeriod option uses localized CLDR day periods, including noon and variable periods such as “in the evening”. Names follow the requested width.

AM/PM spacing follows the locale's default CLDR patterns, including narrow non-breaking spaces where specified.

Locale data registers CLDR default-content variants, such as en-US for English. Loading regional data preserves explicitly loaded language data.

formatRangeToParts marks unique fields and separators as shared. Repeated fields belong to their endpoint; punctuation between repeated fields stays with that endpoint. Shared month names retain the complete date pattern for grammatical context.

An explicit hourCycle: "h24" formats midnight as 24 for both single dates and ranges. Use h23 for midnight 00; hour12: false selects h23.

Benchmarks

Run bazel run //packages/intl-datetimeformat:benchmark from the repository. The benchmark uses fixed UTC inputs and native controls for format, formatRange, and formatRangeToParts, including same-date, cross-date, and collapsed ranges. Each timed task batches 16 calls; construction and locale-data registration are excluded.