frontend / es features / 06_temporal_and_intl.md

Temporal and Modern Intl

6 min read source

Temporal and Modern Intl

TL;DR

JavaScript’s Date is broken — mutable, timezone-confused, bad API. Temporal is the replacement: immutable, timezone-aware, with separate types for “plain date,” “instant,” “zoned datetime,” and durations. Stage 3 → 4, polyfill available, native in Firefox/Node Stable since 2024. Intl is the modern internationalization API — Intl.DateTimeFormat, Intl.NumberFormat, Intl.RelativeTimeFormat, Intl.ListFormat, Intl.Collator, Intl.Segmenter — produces locale-correct output without library bloat. Together they replace ~90% of date-fns/moment/luxon use cases.

Date Q&A

Q: Why is Date considered broken?

A:

  • Mutabledate.setMonth(2) mutates in place. Hidden bug source.
  • Timezone implicitnew Date("2025-03-15") is interpreted as UTC midnight; new Date(2025, 2, 15) is local midnight. Inconsistent.
  • No “date without time” type — birthdays end up with random hours from timezone math.
  • Awkward arithmetic — adding a month requires manual setMonth; daylight-saving-time bugs lurk.
  • No durations or intervals as first-class.
  • String parsing is implementation-defined outside ISO 8601 — new Date("13/01/2025") may parse or NaN, browser-dependent.

Temporal fixes all of this with explicit types.

Q: Temporal — the types.

A:

Type What
Temporal.Now.instant() current instant (UTC nanosecond timestamp)
Temporal.Instant exact moment in time
Temporal.ZonedDateTime instant + timezone + civil view (year/month/etc)
Temporal.PlainDate date only (no time, no zone)
Temporal.PlainTime time only
Temporal.PlainDateTime date + time, no zone
Temporal.PlainYearMonth e.g. “2025-03”
Temporal.PlainMonthDay e.g. “–03-15” (birthdays)
Temporal.Duration “3 months, 2 days, 4 hours”
Temporal.TimeZone timezone identifier
Temporal.Calendar calendar system (ISO, Hebrew, etc.)

The senior insight: pick the right type for the data. A birthday is PlainMonthDay, not Date. A meeting time is ZonedDateTime. A delivery date is PlainDate.

Q: Show me Temporal in action.

A:

import { Temporal } from "@js-temporal/polyfill";   // until native everywhere

// Current
const now = Temporal.Now.zonedDateTimeISO();
console.log(now.toString());   // 2026-05-16T14:23:00+00:00[UTC]

// Plain date
const date = Temporal.PlainDate.from("2025-12-25");
date.dayOfWeek;                // 4 (Thursday)
date.add({ months: 1 });       // 2026-01-25
date.until(today);             // Duration

// Duration math
const dur = Temporal.Duration.from({ hours: 2, minutes: 30 });
now.add(dur);                  // ZonedDateTime 2.5h later

// Timezones
const lon = now.withTimeZone("Europe/London");
const ny = now.withTimeZone("America/New_York");
lon.equals(ny);                // false in civil terms, true in instant (compare instants)

// Comparison
Temporal.PlainDate.compare(a, b);   // -1, 0, 1

Immutable — all methods return new values. Type-safe — PlainDate.add({ hours: 1 }) errors (no time component).

Q: Browser/Node support.

A:

  • Firefox — native since 124 (2024).
  • Chrome/Edge — Stage 3 implementation rolling out, behind a flag in 2024, expected enabled in 2025-2026.
  • Safari — landed in Safari 18.
  • Node--experimental-temporal-api flag in Node 22.
  • Polyfill@js-temporal/polyfill works everywhere. ~30 KB minified.

Use the polyfill until native is stable everywhere; switch via globalThis.Temporal ??= polyfill pattern.

Q: Migrating from date-fns / moment / luxon.

A:

  • moment — already deprecated by its maintainers. Migrate to Temporal or Luxon.
  • date-fns — tree-shakeable, modern. Still useful for niche formatting until Temporal + Intl covers it.
  • luxon — Eric Wasserman’s library, design directly inspired Temporal. Migration is straightforward.

Most apps need: format date, parse date, add duration, compare, timezone conversion. Temporal + Intl covers all of these.

Intl Q&A

Q: Intl.DateTimeFormat — locale-correct formatting.

A:

const date = new Date("2025-12-25T10:00:00Z");

new Intl.DateTimeFormat("en-US").format(date);                    // "12/25/2025"
new Intl.DateTimeFormat("en-GB").format(date);                    // "25/12/2025"
new Intl.DateTimeFormat("de-DE").format(date);                    // "25.12.2025"
new Intl.DateTimeFormat("ja-JP").format(date);                    // "2025/12/25"

new Intl.DateTimeFormat("en-US", {
  dateStyle: "long",
  timeStyle: "short",
  timeZone: "America/New_York",
}).format(date);                                                  // "December 25, 2025 at 5:00 AM"

No external library needed. Format objects can be reused (perf — caching is automatic).

Q: Intl.NumberFormat — currencies, units, compact.

A:

new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(1234.5);
// "$1,234.50"

new Intl.NumberFormat("en-US", { style: "currency", currency: "EUR" }).format(1234.5);
// "€1,234.50"

new Intl.NumberFormat("en-US", { style: "unit", unit: "kilometer" }).format(5);
// "5 km"

new Intl.NumberFormat("en-US", { notation: "compact" }).format(1_234_000);
// "1.2M"

new Intl.NumberFormat("en-US", { style: "percent" }).format(0.42);
// "42%"

Currencies, units, percentages, compact, scientific, engineering — all built in.

Q: Intl.RelativeTimeFormat.

A:

const rtf = new Intl.RelativeTimeFormat("en-US", { numeric: "auto" });
rtf.format(-1, "day");        // "yesterday"
rtf.format(0, "day");         // "today"
rtf.format(1, "day");         // "tomorrow"
rtf.format(-3, "month");      // "3 months ago"
rtf.format(2, "year");        // "in 2 years"

Replaces “X days ago” libraries. Pair with Temporal.until() to compute the delta:

const dur = past.until(now, { largestUnit: "day" });
rtf.format(-dur.days, "day");

Q: Intl.ListFormat, Intl.Collator, Intl.Segmenter.

A:

// List
new Intl.ListFormat("en-US", { type: "conjunction" }).format(["apple", "banana", "cherry"]);
// "apple, banana, and cherry"

new Intl.ListFormat("en-US", { type: "disjunction" }).format(["red", "green", "blue"]);
// "red, green, or blue"

// Sort with locale collation
["é", "z", "a"].sort(new Intl.Collator("fr").compare);   // ["a", "é", "z"]

// Segment a string into words/sentences/graphemes
const seg = new Intl.Segmenter("en-US", { granularity: "word" });
[...seg.segment("Hello, world!")];
// [{ segment: "Hello", isWordLike: true }, { segment: ", ", isWordLike: false }, ...]

Intl.Segmenter is what you reach for to count words, graphemes (“” is one user-perceived character but multiple code points), or split by sentence.

Q: How locale-aware should your code be?

A: Most apps:

  • Numbers — always Intl.NumberFormat. "1234.5" vs "1.234,5" matters.
  • Dates — always Intl.DateTimeFormat. US-style dates confuse most of the world.
  • Currencies — always Intl.NumberFormat with currency.
  • SortingIntl.Collator when displaying user-facing lists.

Detect locale from navigator.language (browser) or Accept-Language (server). Fall back to "en" if undetectable.

Q: i18n string interpolation — Intl.MessageFormat.

A: Stage 3 proposal for ICU MessageFormat in JS. Replaces libraries like @formatjs/intl-messageformat.

// Proposed
const mf = new Intl.MessageFormat("en-US", "You have {count, plural, one {# message} other {# messages}}.");
mf.format({ count: 1 });    // "You have 1 message."
mf.format({ count: 5 });    // "You have 5 messages."

Until stable, libraries like react-i18next and FormatJS cover this.

Gotchas / edge cases

  • Temporal polyfill is ~30 KB — fine for most apps, large for size-sensitive ones. Per-feature imports may help.
  • Date interopTemporal.Instant.from(date.toISOString()) converts; temporal.toDate() goes back. Useful during migration.
  • Locale strings"en" falls back to "en-US" in many browsers but not all. Be explicit ("en-US").
  • Intl.DateTimeFormat constructor is expensive — cache the instance, reuse for formatting.
  • Intl.NumberFormat rounding — uses roundingMode option (ES2023+) for explicit control.
  • Intl.Segmenter not in older browsers — Firefox added in 125 (2024); polyfill available.
  • Timezone abbreviations are ambiguous (EST vs America/New_York); use IANA names.

What a senior is expected to say

  • Date is mutable + timezone-implicit + lacks domain types. Temporal replaces it with immutable, timezone-explicit types per use case — PlainDate, ZonedDateTime, Duration, etc.”
  • “Pick the type for the data: a birthday is PlainMonthDay, a meeting is ZonedDateTime, a delivery date is PlainDate. Avoids whole classes of timezone bugs.”
  • “Polyfill @js-temporal/polyfill until native everywhere — Firefox + Safari have it; Chrome/Node rolling out.”
  • Intl covers most i18n needs without a library — DateTimeFormat, NumberFormat, RelativeTimeFormat, ListFormat, Collator, Segmenter.”
  • “Cache Intl.*Format instances — the constructor is expensive.”
  • “Locale detection: navigator.language (browser), Accept-Language (server). Be explicit with locale strings ('en-US').”

Cross-references

Further reading