FormatJS

A polyfill for Intl.DurationFormat

npm Version size

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 string
  • formatToParts(duration) - Format a duration and return an array of parts
  • resolvedOptions() - Return resolved formatting options
  • supportedLocalesOf(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, daysDisplay
  • hoursDisplay, minutesDisplay, secondsDisplay
  • millisecondsDisplay, 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"}
// ]

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.

OperationTS ops/sTS full helpers ops/sNative JS ops/s
format long89,66489,67531,950
formatToParts long91,34089,47029,683
format digital132,696176,61777,434
formatToParts digital134,372172,71565,289
construct + resolvedOptions184,968175,15233,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/callRust core ops/s
format long4.084244,837
formatToParts long3.996250,227
format digital1.719581,591
formatToParts digital1.628614,293
construct + resolvedOptions0.8061,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.