Intl.DurationFormat vs Hand-Rolled Time Labels
Compare Intl.DurationFormat with hand-rolled labels, validate duration records, preserve emitted parts, expose locale resolution, and ship an honest fallback.
Joining hours + "h" looks harmless until plural rules, unit order, separators, numbering systems, negative values, and digital padding enter the interface. Intl.DurationFormat owns localization of a duration record, but it does not decide what elapsed time means, balance units, run a countdown, or remove the need for an explicit unsupported-browser state. Formatting begins only after product semantics are frozen.
Intl.DurationFormat replaces hidden label assumptions
Joining hours with “h” looks harmless until plural rules, unit order, separators, numbering systems, negative values, and digital padding enter the interface. Intl.DurationFormat owns localization of a duration record, but it does not decide what elapsed time means, balance units, run a countdown, or remove the need for an unsupported-browser state.
A hand-rolled label usually embeds one language's grammar. “1 hours” exposes plural trouble; “1h 46m” assumes unit order and Latin digits; inserting commas assumes punctuation. A product may ship those assumptions unnoticed because English fixtures happen to pass.
The ECMA-402 DurationFormat section defines construction, duration validation, styles, parts, and resolved options. The API begins after the product has chosen fields. It formats the supplied record; it does not convert 90 minutes into 1 hour 30 minutes unless the caller supplies those fields.
Time zones are product state separates instants, wall-clock values, and durations. Make that semantic choice first. Then localized duration formatting can be reviewed as display logic rather than accidental time arithmetic.
| Requested locale | Style | Pinned native output | Review note |
|---|---|---|---|
| en | long | 1 hour, 46 minutes, 40 seconds | reader friendly |
| fr | short | 1 h, 46 min et 40 s | compact grammar |
| de | narrow | 1 Std., 46 Min. und 40 Sek. | can be ambiguous |
| ar | digital | 1:46:40 | default numeric hours; add 2-digit explicitly for 01 |
- Reading rule
- Read each row across its named columns; the text carries the diagram's exact values.
- Color and position reinforce the comparison but never replace its labels.
Define the duration record before formatting
The input record can contain years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, and nanoseconds. Fields are integral, finite numbers. Omitted and zero are different product states: omission can allow the formatter's display policy to hide a unit, while explicit zero can be important in a digital timer.
All nonzero fields must share one sign. Mixed signs are rejected because “−1 hour and 30 minutes” cannot safely communicate whether the total is negative or the fields are being balanced. The lab also rejects an empty record, fractional fields, non-finite numbers, and years, months, or weeks at or beyond the 2^32 boundary.
Normalized seconds have an ECMA-402 bound, so the validator combines day-through-nanosecond fields with exact integer arithmetic before constructing the formatter. Validation occurs before native code. A hostile value never becomes a locale-dependent exception with an ambiguous UI state.
Formatting does not balance. The record {minutes:90} remains 90 minutes; converting it to {hours:1, minutes:30} is a product rule upstream. The same applies to rounding, calendar arithmetic, elapsed-time measurement, recurrence, DST, and relative phrases. Temporal API scheduling owns several of those upstream concerns.
Choose long, short, narrow, or digital intentionally
Long style can produce reader-friendly unit names, short style trades space for recognizable abbreviations, and narrow style is compact but can be ambiguous without context. Digital style serves clock-like labels and brings padding and field-display choices to the foreground. One style is not universally best.
The record {hours:1, minutes:46, seconds:40} is a useful comparison because every field is visible. In the pinned native runtime, base digital style requested as ar resolves to Latin digits and numeric hours, producing “1:46:40.” Producing “01:46:40” requires an explicit two-digit hours option; it is not the base-style result. Render examples in declared locales, record the requested and resolved locale, and compare the emitted parts. Do not use a screenshot from one runtime as a universal punctuation contract; locale data can change.
Per-unit options determine whether a field uses long, short, narrow, numeric, or two-digit presentation within supported combinations. FractionalDigits from 0 through 9 controls subsecond display where applicable. The lab exposes a bounded option set rather than passing arbitrary keys into Intl.DurationFormat.
Narrow forms need an ambiguity warning in space-constrained product surfaces. Digital time labels need an accessible name that conveys the same duration when punctuation is visually compressed. A compact card may show “1:46:40,” while its surrounding copy or aria-label communicates hours, minutes, and seconds in the resolved locale.
Read locale negotiation and resolved options
Always pass explicit locale requests. Silently relying on the host default makes receipts change with operating system, browser profile, or server region. The lab accepts at most three BCP 47 requests, each no longer than 64 characters, validates them with Intl.getCanonicalLocales, and records the canonical requested list.
Locale negotiation may fall back from a specific request to a supported parent or another allowed match. resolvedOptions() exposes the selected locale, numbering system, style, and unit settings. Those values belong beside the output so a reviewer can explain why two environments differ.
The Unicode LDML unit and duration patterns describe the locale data concerns behind unit forms, plural grammar, widths, and duration patterns. Applications should not reconstruct those patterns by sorting or concatenating parts. The formatter owns their order and literal separators.
Numbering-system resolution is equally visible. A requested Unicode extension may be honored or replaced by supported locale data. Tests assert semantic invariants and the reported resolution rather than assuming every engine carries the same locale-data version.
- Measure or derive time under the product's temporal contract.
- Freeze an explicit integral duration record with one coherent sign.
- Pass explicit locales and bounded formatting options to Intl.DurationFormat.
- Preserve emitted text, part order, literals, and resolved options.
- Build product markup, accessible naming, truncation, and update behavior.
Use formatToParts for product markup
formatToParts() exposes an ordered sequence containing integer, fraction, decimal, unit, and literal values as applicable. Product code can wrap selected values in spans, but it must preserve order and every literal. Sorting by type or dropping punctuation rebuilds grammar and defeats the formatter.
The strongest invariant is simple: parts.map(part => part.value).join("") must equal format(record) exactly for the same formatter. The lab checks that relationship on every native run and includes the emitted index, type, value, and unit in a visible semantic table.
Create DOM nodes with textContent rather than injecting assembled HTML. Keep bidirectional behavior in mind when a right-to-left label contains numbers or surrounding left-to-right interface text. Markup can style units, yet accessible text should still communicate one coherent duration rather than a collection of disconnected tokens.
Intl.DurationFormat makes structured markup possible; it does not generate an accessible name automatically. Product components still own labeling, truncation behavior, live-region policy, and whether a countdown should announce changes. Intl.Segmenter for cursor-safe text addresses text boundaries, not duration grammar.
Compare native output with a hand-rolled path
Feature-detect the exact constructor with typeof Intl.DurationFormat === "function". On the native path, show format(), formatToParts(), resolvedOptions(), requested locales, and resolved locale. Record the support state so a saved receipt can be interpreted later.
The unsupported path in this lab is deliberately modest: it emits a clearly labeled unlocalized numeric list in fixed field order plus machine-readable JSON. It does not choose plural forms or claim locale grammar. That makes missing capability visible without turning a teaching helper into an unreviewed polyfill.
A separate comparison switch can show a naive English concatenator. Its purpose is to demonstrate defects such as “1 hours,” fixed order, ASCII digits, and punctuation assumptions. It is never the production fallback, never labeled localized, and never used to populate the canonical output field.
MDN's Intl.DurationFormat reference provides current examples and browser-support context. Recheck it against the site's supported baseline; capability detection remains authoritative at runtime.
| State | Output | Receipt label | User message |
|---|---|---|---|
| Native supported | format plus ordered parts | localized-native | resolved locale shown |
| Unsupported | numeric fields plus JSON | unlocalized-neutral | localization unavailable |
| Invalid record | no formatted output | rejected | fix duration fields |
| Naive comparison | English concatenation demo | comparison-only | not used as fallback |
- Reading rule
- Read each row across its named columns; the text carries the diagram's exact values.
- Color and position reinforce the comparison but never replace its labels.
Test semantics instead of punctuation
Tests should cover omitted versus zero units, singular and plural records, all four base styles, numbering-system resolution, positive and all-negative records, digital padding, and fractional seconds. Invalid cases include mixed signs, fractional or non-finite fields, empty records, over-limit calendar fields, and normalized-seconds overflow.
Do not freeze exact commas or spaces across an unpinned runtime. Instead, assert that joined parts equal format(), the resolved locale is reported, every part stays in emitted order, input fields are unchanged, and support/fallback labels are truthful. A punctuation snapshot is acceptable only when the runtime and locale-data version are pinned in the fixture.
Mutation tests make those rules concrete. A hard-coded English pluralizer fails a non-English or singular fixture. Sorting parts fails join equality. Dropping literals changes output. Omitting locales creates a host-default receipt. Balancing 90 minutes changes the input record. Labeling the neutral fallback localized violates the unsupported-state contract.
Unicode foundations for product interfaces provides the wider character and locale context. This test suite stays focused on one already-defined duration record and its display receipt.
Ship an Intl.DurationFormat display receipt
A durable receipt contains the validated duration record, explicit requested locales, base and per-unit options, display policy, fractional digits, support state, resolved options, emitted parts, joined output, fallback kind, schema version, and canonical hash. Replaying the same inputs in the same runtime should reproduce it.
The receipt must also state what remains outside the formatter: measuring elapsed time, comparing instants, balancing or rounding units, scheduling and DST, recurrence, relative phrasing, countdown ownership, and accessibility policy. Those limits prevent a localization API from becoming an accidental time model.
Revisit this Intl.DurationFormat guide on 2027-01-29, or sooner if ECMA-402 semantics, Unicode duration data, MDN baseline support, or the site's browser baseline changes. When locale data moves, review semantic invariants before updating optional snapshots.
The product decision is not “native or string concatenation.” It is an explicit duration contract followed by locale negotiation, parts-preserving rendering, and an honest unsupported state. That duration formatting JavaScript sequence replaces hidden assumptions with evidence a designer, engineer, and translator can all inspect.
Runnable local artifact — Intl.DurationFormat formats supplied fields; it does not measure, balance, round, schedule, express relative time, or guarantee stable punctuation across runtime data versions.
Validate an explicit ten-field record and locale list, call native Intl.DurationFormat when supported, preserve ordered parts and literals, and export a canonical display receipt.