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:
- Mutable —
date.setMonth(2)mutates in place. Hidden bug source. - Timezone implicit —
new 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-apiflag in Node 22. - Polyfill —
@js-temporal/polyfillworks 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 toTemporalorLuxon.date-fns— tree-shakeable, modern. Still useful for niche formatting untilTemporal+Intlcovers 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.NumberFormatwithcurrency. - Sorting —
Intl.Collatorwhen 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
Temporalpolyfill is ~30 KB — fine for most apps, large for size-sensitive ones. Per-feature imports may help.Dateinterop —Temporal.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.DateTimeFormatconstructor is expensive — cache the instance, reuse for formatting.Intl.NumberFormatrounding — usesroundingModeoption (ES2023+) for explicit control.Intl.Segmenternot in older browsers — Firefox added in 125 (2024); polyfill available.- Timezone abbreviations are ambiguous (
ESTvsAmerica/New_York); use IANA names.
What a senior is expected to say
- “
Dateis mutable + timezone-implicit + lacks domain types.Temporalreplaces 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 isZonedDateTime, a delivery date isPlainDate. Avoids whole classes of timezone bugs.” - “Polyfill
@js-temporal/polyfilluntil native everywhere — Firefox + Safari have it; Chrome/Node rolling out.” - “
Intlcovers most i18n needs without a library —DateTimeFormat,NumberFormat,RelativeTimeFormat,ListFormat,Collator,Segmenter.” - “Cache
Intl.*Formatinstances — the constructor is expensive.” - “Locale detection:
navigator.language(browser),Accept-Language(server). Be explicit with locale strings ('en-US').”
Cross-references
- TypeScript
libfor these features: ../04_typescript/ - Other ES additions: 05_modern_collection_methods.md
Further reading
- TC39 — Temporal: https://tc39.es/proposal-temporal/docs/
@js-temporal/polyfill: https://github.com/js-temporal/temporal-polyfill- MDN —
Intl: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl - “You don’t need Moment.js”: https://github.com/you-dont-need/You-Dont-Need-Momentjs