A polyfill for Intl.DurationFormat
ECMA-402 Spec Compliance#
This package implements Intl.DurationFormat. See the conformance report for tested coverage and known limitations.
Specification Details#
- TC39 Proposal: Intl.DurationFormat
- Stage: Stage 4 (Finalized)
- ECMA-402 Edition: 12th Edition (June 2025)
- Spec Pages: 57-62
✅ All Features Implemented#
Core Methods#
format(duration)- Format a duration object to a localized stringformatToParts(duration)- Format a duration and return an array of partsresolvedOptions()- Return resolved formatting optionssupportedLocalesOf(locales)- Check which locales are supported
Style Options#
All 4 style values are supported:
'long'(default) - Full format (e.g., "3 hours, 25 minutes")'short'- Abbreviated format (e.g., "3 hr, 25 min")'narrow'- Most compact format (e.g., "3h 25m")'digital'- Digital clock format (e.g., "3:25:00")
Duration Units#
All 10 duration fields are fully supported:
- Years (
years) - Months (
months) - Weeks (
weeks) - Days (
days) - Hours (
hours) - Minutes (
minutes) - Seconds (
seconds) - Milliseconds (
milliseconds) - Microseconds (
microseconds) - Nanoseconds (
nanoseconds)
Each unit supports multiple format styles:
'long','short','narrow'- Text-based formats'numeric'- Numeric format'2-digit'- Two-digit numeric format (for hours, minutes, seconds)
Display Control#
Per-unit display options with values 'always' | 'auto':
yearsDisplay,monthsDisplay,weeksDisplay,daysDisplayhoursDisplay,minutesDisplay,secondsDisplaymillisecondsDisplay,microsecondsDisplay,nanosecondsDisplay
When set to 'auto', zero values are omitted. When set to 'always', zero values are included.
Additional Options#
fractionalDigits- Control decimal precision (0-9 digits)numberingSystem- Alternative numbering systems (50+ supported)localeMatcher- Locale resolution algorithm ('best fit'or'lookup')
Example Usage#
Global import#
import '@formatjs/intl-durationformat/polyfill.js'
const formatter = new Intl.DurationFormat('en', {
style: 'long',
hours: 'numeric',
minutes: 'numeric',
seconds: 'numeric',
})
formatter.format({hours: 3, minutes: 25, seconds: 7})
// "3 hours, 25 minutes, 7 seconds"
// Digital format
const digital = new Intl.DurationFormat('en', {style: 'digital'})
digital.format({hours: 1, minutes: 30, seconds: 45})
// "1:30:45"
// formatToParts
const parts = formatter.formatToParts({hours: 2, minutes: 30})
// [
// {type: "integer", value: "2", unit: "hour"},
// {type: "literal", value: " hours, "},
// {type: "integer", value: "30", unit: "minute"},
// {type: "literal", value: " minutes"}
// ]
Info
The global import does not include TypeScript type declarations. For TypeScript projects, we recommend using ES module imports instead.
If you choose to use the global import, in order to prevent type errors, you must manually include the corresponding type declaration files (.d.ts) in your project.
ES Modules#
import {DurationFormat} from '@formatjs/intl-durationformat'
const formatter = new DurationFormat('en', {
style: 'long',
hours: 'numeric',
minutes: 'numeric',
seconds: 'numeric',
})
formatter.format({hours: 3, minutes: 25, seconds: 7})
// "3 hours, 25 minutes, 7 seconds"
Installation#
npm i @formatjs/intl-durationformat
Requirements#
Usage#
Simple#
import '@formatjs/intl-durationformat/polyfill.js'
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-durationformat/polyfill-force.js')
}
Numbering system validation#
A well-formed but unsupported numberingSystem option falls back to the locale's
supported numbering system. Only malformed Unicode type identifiers throw RangeError.
Duration record validation#
Each duration field is read once in the ECMA-402 order. Fields must convert to
finite integers of a common sign. Invalid numeric fields throw RangeError;
an empty record throws TypeError. Absolute years, months, and weeks must be
less than 2 ** 32; absolute normalized seconds must be less than 2 ** 53.
Subsecond contributions participate in the bound comparison exactly.
Built-in metadata#
DurationFormat.length is 0; DurationFormat.supportedLocalesOf.length is 1.
Instances identify as [object Intl.DurationFormat] through Symbol.toStringTag.
resolvedOptions() returns properties in spec order and omits fractionalDigits
when no explicit value was set.
Negative durations#
A negative duration has one minus sign, on its first displayed unit. This includes
a leading zero: digital formatting of {seconds: -1} produces "-0:00:01" in English.
Later units omit their signs in both format() and formatToParts().
Numeric duration fields omit grouping separators, even for large values. Textual styles retain locale grouping. Fractional arithmetic preserves each input Number's exact integer value, including values beyond the safe-integer range.
A numeric time style propagates through smaller units. Minutes and seconds
remain displayed by default; smaller fractional units use auto. Fractional
units cannot request always display or be followed by a textual unit style.
Zero minutes remain between displayed numeric hours and seconds, even with
minutesDisplay: "auto". For example, {hours: 1, seconds: 1} formats as "1:00:01".
Numbering-system resolution includes systems supported by the active
Intl.NumberFormat dependency, keeping the locale default first. Support is
checked lazily and refreshed when that constructor is replaced. Time-separator
data includes every numbering-system symbol record supplied by CLDR.
Temporal integration#
When a Temporal implementation is available before this package is loaded,
format and formatToParts accept ISO duration strings through Temporal.Duration.from.
Temporal durations are read with captured intrinsic getters, so later changes to
Temporal.Duration.prototype do not affect formatting. Ordinary duration-like
objects keep the ECMA-402 property-read order.
Without Temporal, duration-like objects remain supported; duration strings throw
RangeError. This integration follows the Temporal proposal's Intl changes.
Rust Hermes extension#
The native port implements duration validation, locale/options resolution and formatting in Rust. The Hermes facade preserves ordered getters, primitive coercion, receiver brands and captured Temporal intrinsics. Source and bytecode installers use the same factory; both are compared with this TypeScript package.
Duration locale and time-separator metadata stays static. Native NumberFormat and ListFormat dependencies use compiled SDK locale data; no public JS formatter callbacks are needed. See the Hermes SDK guide for installation, data hooks, Temporal timing and the independent consumer.
Measured performance#
TypeScript and native JavaScript#
Measured 2026-10-31 on Apple M4 Max, macOS arm64, Node 24.14.0 / ICU 78.2; source 56d15e997. Median of seven warmed rounds.
| Operation | TS ops/s | TS full helpers ops/s | Native JS ops/s |
|---|---|---|---|
| format long | 89,664 | 89,675 | 31,950 |
| formatToParts long | 91,340 | 89,470 | 29,683 |
| format digital | 132,696 | 176,617 | 77,434 |
| formatToParts digital | 134,372 | 172,715 | 65,289 |
| construct + resolvedOptions | 184,968 | 175,152 | 33,707 |
TS uses host Intl helpers; TS full helpers installs all FormatJS Intl APIs.
Module loading and data registration are excluded. Construction rows include
resolvedOptions(). These are desktop microbenchmarks; native locale data may
differ. † marks noisy rows (round max/min > 1.5). See workloads, commands, and raw results.
Rust core#
| Rust core operation | µs/call | Rust core ops/s |
|---|---|---|
| format long | 4.084 | 244,837 |
| formatToParts long | 3.996 | 250,227 |
| format digital | 1.719 | 581,591 |
| formatToParts digital | 1.628 | 614,293 |
| construct + resolvedOptions | 0.806 | 1,241,261 |
Same machine and source revision, optimized Rust build, seven warmed rounds. Rust core timings exclude Hermes/JSI and JS boundary work; inputs are prepared Rust strings/UTF-16. Data and API-boundary differences prevent direct speedup claims against TS/native JS. † marks round max/min > 1.5. Workload details and raw results.