Changelog - 2026-10-14
TemporalHelper runs in every browser and speaks a locale
FeatureIn one line. A browser app can drop dayjs: TemporalHelper now covers names, relative time and comparisons, without needing Temporal.
import { TemporalHelper, TemporalUnits } from '@venizia/ignis-helpers/temporal';
const temporal = new TemporalHelper({ timeZone: 'Asia/Ho_Chi_Minh', locale: 'vi-VN' });
temporal.format({ value: order.createdAt, pattern: 'dddd, D MMMM YYYY hh:mm A' }); // 'Thứ Bảy, 19 Tháng 9 2026 12:00 CH'
temporal.relative({ value: order.createdAt }); // '3 giờ trước'
temporal.relative({ value: order.createdAt, unit: TemporalUnits.DAY, numeric: 'auto' }); // 'Hôm qua'
temporal.diff({ value: end, other: start, unit: TemporalUnits.MONTH }); // whole months
temporal.tryParse({ value: input }); // a Date, or null
temporal.formatDuration({ milliseconds: 91_800_000 }); // '1d1h30m'What changed
Before, the default adapter needed the runtime's Temporal, which not every browser ships, and the pattern dialect wrote digits only.
A browser-safe adapter. IntlTemporalAdapter reads offsets through Intl.DateTimeFormat alone. It matched NativeTemporalAdapter on 240,000 random instants across 12 zones, years 1 to 9999. A helper built without an adapter uses Temporal where the runtime has it and IntlTemporalAdapter everywhere else.
Writing in a locale. The constructor takes locale (default en-US); every call can pass its own.
| New | What it writes |
|---|---|
MMMM / MMM | Month name, long / short |
dddd / ddd | Weekday name, long / short |
hh / h, A / a | 12-hour clock and its AM/PM marker |
L / LL | The date in the locale's numeric order / its long form |
formatIntl({ value, options }) | Any Intl.DateTimeFormat options, in the helper's zone |
These tokens write only. parse stays numeric and strict, and a parse pattern holding one throws a 500.
New methods.
| Method | Answers |
|---|---|
relative({ value, base, unit, numeric }) | 3 hours ago, yesterday, through Intl.RelativeTimeFormat. With a unit, it counts the unit's boundaries in the zone |
diff({ value, other, unit }) | Whole units, truncated. The inverse of add: diff(add(x, n), x) is n, outside a zone that skipped a whole day |
compare({ value, other, unit }) | -1, 0 or 1, at a unit's granularity in the zone |
tryParse(...) / isValid({ value }) | null / false where parse would throw a 400 |
durationParts({ milliseconds }) / formatDuration(...) | { days, hours, ... } / 1d1h30m, or the locale's unit words |
Who is affected
- A browser app on a runtime without
Temporal.new TemporalHelper()used to throw; it now works throughIntl. Nothing to change. - A pattern with
MMM,MMMMordddd. It used to throw an "unknown token" 500 when formatting; it now writes the name. As a parse pattern it still throws. - Everyone else. No change. Existing tokens, units, strict parsing and the 1-9999 year range behave as before.
Files: packages/helpers/src/modules/temporal/adapters/intl.ts, packages/helpers/src/modules/temporal/locale/locale.ts, packages/helpers/src/modules/temporal/duration/duration.ts, packages/helpers/src/modules/temporal/helper.ts, packages/helpers/src/modules/temporal/pattern/tokens.ts