temporal-fmt 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,13 +3,14 @@
3
3
  Format `Temporal.PlainDate` / `PlainTime` / `PlainDateTime` / `ZonedDateTime` objects
4
4
  using date-fns-style token strings.
5
5
 
6
- Native `Temporal` shipped in Node 26 and modern browsers, but it deliberately has
7
- no custom-string formatter — the TC39 authors punted that to userland in favor of
8
- `Intl.DateTimeFormat`. This fills that specific gap for people who want the
9
- `'yyyy-MM-dd'`-style syntax they already know from date-fns / moment / dayjs.
6
+ Node 26 shipped native `Temporal` and then pointedly left out a custom-string
7
+ formatter. TC39's take: use `Intl.DateTimeFormat` and leave string-token syntax
8
+ to userland. Fair enough, but if you've spent years typing `'yyyy-MM-dd'` out of
9
+ muscle memory from date-fns, moment, or dayjs, that's a rough adjustment. This
10
+ library exists so you don't have to make it.
10
11
 
11
- Zero dependencies. Requires a global `Temporal` (native in Node 26+, or bring your
12
- own polyfill like `temporal-polyfill`).
12
+ Zero dependencies. You'll need a global `Temporal` native on Node 26+, or
13
+ bring your own polyfill (`temporal-polyfill` works fine).
13
14
 
14
15
  ## Install
15
16
 
@@ -33,42 +34,94 @@ const zdt = Temporal.ZonedDateTime.from('2026-08-04T15:45:30-04:00[America/New_Y
33
34
  format(zdt, 'yyyy-MM-dd HH:mm zzz'); // "2026-08-04 15:45 America/New_York"
34
35
  ```
35
36
 
36
- Quote literal text with single quotes: `'at'`. Use `''` for a literal single quote.
37
+ Wrap literal text in single quotes, like `'at'` above. Need an actual single
38
+ quote in your output? Use `''`.
39
+
40
+ ## Locale support
41
+
42
+ Pass a BCP 47 locale tag as a third argument and month names, weekday names,
43
+ and AM/PM markers all localize accordingly. Defaults to `'en-US'` if you don't.
44
+
45
+ ```js
46
+ format(date, 'MMMM d, yyyy', { locale: 'fr-FR' }); // "août 4, 2026"
47
+ format(date, 'EEEE d MMMM', { locale: 'ar-EG' }); // Arabic weekday/month names
48
+ format(dt, 'h:mm a', { locale: 'ja-JP' }); // "3:45 午後"
49
+ ```
50
+
51
+ The named fields (`MMMM`, `MMM`, `EEEE`, `EEE`, `a`) go through
52
+ `Intl.DateTimeFormat` under the hood, which means non-Gregorian calendars
53
+ work too, as long as the `Temporal` object is already carrying one:
54
+
55
+ ```js
56
+ const hebrewDate = date.withCalendar('hebrew');
57
+ format(hebrewDate, 'MMMM d, yyyy'); // "Av 21, 5786"
58
+ ```
59
+
60
+ **Numeric fields (`yyyy`, `MM`, `dd`, `HH`, `mm`, `ss`, `SSS`) always come out
61
+ in Western (0-9) digits, no matter what locale you pass.** That's on purpose,
62
+ not an oversight. Most things reading this output back in — logs, APIs,
63
+ filenames — want boring, predictable ASCII digits. And honestly, locale-native
64
+ numeral systems like Arabic-Indic or Devanagari don't play nicely with this
65
+ library's own zero-padding logic anyway. If you actually need localized
66
+ digits, run the numeric pieces through `Intl.NumberFormat` yourself.
67
+
68
+ **One more catch: this needs native `Intl`/`Temporal` interop to work.** On
69
+ Node 26+ with native `Temporal`, you're fine. On older Node with a userland
70
+ polyfill, locale-aware tokens will throw — unless you swap in the polyfill's
71
+ own `Intl` export in place of the global one. Why? Because `Intl.DateTimeFormat`
72
+ can't read fields off a non-native `Temporal` object; you'll get a
73
+ `Cannot use valueOf` error for your trouble. That's a limitation baked into how
74
+ `Intl` and `Temporal` currently talk to each other, not something this library
75
+ can paper over.
37
76
 
38
77
  ## Tokens
39
78
 
40
- | Token | Meaning | Example |
41
- |-------|------------------|---------|
42
- | yyyy | 4-digit year | 2026 |
43
- | yy | 2-digit year | 26 |
44
- | MMMM | full month name | August |
45
- | MMM | short month name | Aug |
46
- | MM | 2-digit month | 08 |
47
- | M | month | 8 |
48
- | dd | 2-digit day | 04 |
49
- | d | day | 4 |
50
- | EEEE | full weekday | Tuesday |
51
- | EEE | short weekday | Tue |
52
- | HH | 2-digit hour (24h) | 15 |
53
- | H | hour (24h) | 15 |
54
- | hh | 2-digit hour (12h) | 03 |
55
- | h | hour (12h) | 3 |
56
- | mm | 2-digit minute | 45 |
57
- | m | minute | 45 |
58
- | ss | 2-digit second | 30 |
59
- | s | second | 30 |
60
- | SSS | milliseconds | 000 |
61
- | a | AM/PM | PM |
62
- | zzz | IANA time zone id | America/New_York |
63
-
64
- Passing a token the input type doesn't support (e.g. `HH` on a `PlainDate`) throws
65
- a clear error instead of silently printing `undefined`.
79
+ | Token | Meaning | Example |
80
+ |-------|--------------------|---------|
81
+ | yyyy | 4-digit year | 2026 |
82
+ | yy | 2-digit year | 26 |
83
+ | MMMM | full month name | August |
84
+ | MMM | short month name | Aug |
85
+ | MM | 2-digit month | 08 |
86
+ | M | month | 8 |
87
+ | dd | 2-digit day | 04 |
88
+ | d | day | 4 |
89
+ | EEEE | full weekday | Tuesday |
90
+ | EEE | short weekday | Tue |
91
+ | HH | 2-digit hour (24h) | 15 |
92
+ | H | hour (24h) | 15 |
93
+ | hh | 2-digit hour (12h) | 03 |
94
+ | h | hour (12h) | 3 |
95
+ | mm | 2-digit minute | 45 |
96
+ | m | minute | 45 |
97
+ | ss | 2-digit second | 30 |
98
+ | s | second | 30 |
99
+ | SSS | milliseconds | 000 |
100
+ | a | AM/PM | PM |
101
+ | zzz | IANA time zone id | America/New_York |
102
+
103
+ Try to use a token your input type doesn't support `HH` on a `PlainDate`,
104
+ say — and you'll get a real error telling you so, not a silent `undefined`
105
+ sitting in your output waiting to confuse someone in three weeks.
106
+
107
+ ## Known limitations
108
+
109
+ - Numeral systems are always Western digits — see [Locale support](#locale-support).
110
+ - Requires native `Temporal`/`Intl` interop (Node 26+) for locale-aware tokens.
66
111
 
67
112
  ## Dev notes
68
113
 
69
- `tsconfig.json` sets `ignoreDeprecations: "6.0"` as a workaround for a tsup bug
70
- (tsup#1388/#1389) — tsup's dts build step injects a deprecated `baseUrl` internally,
71
- which TypeScript 6+ hard-errors on. Remove this once tsup ships a fix upstream.
114
+ `tsconfig.json` sets `ignoreDeprecations: "6.0"` to work around a tsup bug
115
+ (tsup#1388/#1389) — tsup's dts build step quietly injects a deprecated
116
+ `baseUrl`, and TypeScript 6+ hard-errors on it. This is a workaround, not a
117
+ fix; drop it the moment tsup ships a real one upstream.
118
+
119
+ Tests pull from `temporal-polyfill/full`, not the slim `temporal-polyfill`,
120
+ because the Hebrew-calendar test needs the full build's calendar data — the
121
+ slim one won't cut it. If you're on Node < 26 without native `Temporal`,
122
+ expect the locale-aware tests to fail with `Cannot use valueOf`. That's the
123
+ same polyfill/`Intl` interop gap mentioned above, not a bug in the tests
124
+ themselves. Everything passes clean on Node 26+.
72
125
 
73
126
  ## License
74
127
 
package/dist/index.cjs CHANGED
@@ -28,34 +28,47 @@ module.exports = __toCommonJS(index_exports);
28
28
  function pad(n, len) {
29
29
  return String(n).padStart(len, "0");
30
30
  }
31
- var MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
32
- var MONTHS_LONG = [
33
- "January",
34
- "February",
35
- "March",
36
- "April",
37
- "May",
38
- "June",
39
- "July",
40
- "August",
41
- "September",
42
- "October",
43
- "November",
44
- "December"
45
- ];
46
- var DAYS_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
47
- var DAYS_LONG = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
31
+ var DEFAULT_LOCALE = "en-US";
32
+ var formatterCache = /* @__PURE__ */ new Map();
33
+ function getFormatter(locale, options) {
34
+ const key = locale + JSON.stringify(options);
35
+ let formatter = formatterCache.get(key);
36
+ if (!formatter) {
37
+ formatter = new Intl.DateTimeFormat(locale, options);
38
+ formatterCache.set(key, formatter);
39
+ }
40
+ return formatter;
41
+ }
42
+ function intlPart(temporal, locale, options, partType) {
43
+ const isZoned = typeof temporal?.toInstant === "function" && typeof temporal?.timeZoneId === "string";
44
+ const intlSafeTemporal = isZoned ? temporal.toInstant() : temporal;
45
+ const calendar = temporal?.calendarId;
46
+ const formatterOptions = {
47
+ ...options,
48
+ ...calendar && calendar !== "iso8601" ? { calendar } : {},
49
+ ...isZoned ? { timeZone: temporal.timeZoneId } : {}
50
+ };
51
+ const formatter = getFormatter(locale, formatterOptions);
52
+ const parts = formatter.formatToParts(intlSafeTemporal);
53
+ const part = parts.find((p) => p.type === partType);
54
+ if (!part) {
55
+ throw new Error(
56
+ `temporal-fmt: locale "${locale}" produced no "${partType}" part for this token. This usually means the Temporal object is missing the field the token needs.`
57
+ );
58
+ }
59
+ return part.value;
60
+ }
48
61
  var TOKENS = [
49
62
  ["yyyy", (t) => pad(t.year, 4), "year"],
50
63
  ["yy", (t) => pad(t.year % 100, 2), "year"],
51
- ["MMMM", (t) => MONTHS_LONG[t.month - 1], "month"],
52
- ["MMM", (t) => MONTHS_SHORT[t.month - 1], "month"],
64
+ ["MMMM", (t, locale) => intlPart(t, locale, { month: "long" }, "month"), "month"],
65
+ ["MMM", (t, locale) => intlPart(t, locale, { month: "short" }, "month"), "month"],
53
66
  ["MM", (t) => pad(t.month, 2), "month"],
54
67
  ["M", (t) => String(t.month), "month"],
55
68
  ["dd", (t) => pad(t.day, 2), "day"],
56
69
  ["d", (t) => String(t.day), "day"],
57
- ["EEEE", (t) => DAYS_LONG[t.dayOfWeek - 1], "dayOfWeek"],
58
- ["EEE", (t) => DAYS_SHORT[t.dayOfWeek - 1], "dayOfWeek"],
70
+ ["EEEE", (t, locale) => intlPart(t, locale, { weekday: "long" }, "weekday"), "dayOfWeek"],
71
+ ["EEE", (t, locale) => intlPart(t, locale, { weekday: "short" }, "weekday"), "dayOfWeek"],
59
72
  ["HH", (t) => pad(t.hour, 2), "hour"],
60
73
  ["H", (t) => String(t.hour), "hour"],
61
74
  ["hh", (t) => pad(t.hour % 12 || 12, 2), "hour"],
@@ -65,7 +78,11 @@ var TOKENS = [
65
78
  ["ss", (t) => pad(t.second, 2), "second"],
66
79
  ["s", (t) => String(t.second), "second"],
67
80
  ["SSS", (t) => pad(t.millisecond, 3), "millisecond"],
68
- ["a", (t) => t.hour < 12 ? "AM" : "PM", "hour"],
81
+ // dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific
82
+ // — some locales render it differently or don't split 12-hour at all. We
83
+ // still require .hour on the input either way, since that's what Intl
84
+ // needs to compute which period it is.
85
+ ["a", (t, locale) => intlPart(t, locale, { hour: "numeric", hour12: true }, "dayPeriod"), "hour"],
69
86
  ["zzz", (t) => t.timeZoneId, "timeZoneId"]
70
87
  ];
71
88
 
@@ -120,7 +137,8 @@ function tokenize(format2) {
120
137
 
121
138
  // src/format.ts
122
139
  var HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));
123
- function format(temporal, formatStr) {
140
+ function format(temporal, formatStr, options = {}) {
141
+ const locale = options.locale ?? DEFAULT_LOCALE;
124
142
  const pieces = tokenize(formatStr);
125
143
  let result = "";
126
144
  for (const piece of pieces) {
@@ -137,7 +155,7 @@ function format(temporal, formatStr) {
137
155
  `temporal-fmt: token "${piece.value}" requires "${handler.field}", which this Temporal object doesn't have. (e.g. PlainDate has no time fields, PlainTime has no date fields)`
138
156
  );
139
157
  }
140
- result += handler.fn(temporal);
158
+ result += handler.fn(temporal, locale);
141
159
  }
142
160
  return result;
143
161
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/tokens.ts","../src/tokenize.ts","../src/format.ts"],"sourcesContent":["export { format } from './format.js';\nexport type { TemporalLike } from './tokens.js';\n","// Pad a number with leading zeros to `len` digits.\nexport function pad(n: number, len: number): string {\n return String(n).padStart(len, '0');\n}\n\n// Minimal duck-typed shape covering every field we might read off a Temporal\n// object. Not every field exists on every type (PlainDate has no .hour, for\n// example) — callers check for undefined before formatting a token.\nexport interface TemporalLike {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n timeZoneId?: string;\n dayOfWeek?: number; // 1 (Mon) - 7 (Sun), per Temporal spec\n}\n\nconst MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\nconst MONTHS_LONG = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\nconst DAYS_SHORT = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\nconst DAYS_LONG = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];\n\n// Each token renders itself from a TemporalLike. The third tuple element\n// below names the field it depends on, so format.ts can check for undefined\n// before calling the handler (e.g. `HH` needs `.hour`, which PlainDate lacks).\ntype TokenHandler = (t: TemporalLike) => string;\n\n// Longest tokens first — the tokenizer is greedy, so \"yyyy\" must be tried\n// before \"yy\" or it'll never match.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n ['yy', (t) => pad(t.year! % 100, 2), 'year'],\n ['MMMM', (t) => MONTHS_LONG[t.month! - 1], 'month'],\n ['MMM', (t) => MONTHS_SHORT[t.month! - 1], 'month'],\n ['MM', (t) => pad(t.month!, 2), 'month'],\n ['M', (t) => String(t.month!), 'month'],\n ['dd', (t) => pad(t.day!, 2), 'day'],\n ['d', (t) => String(t.day!), 'day'],\n ['EEEE', (t) => DAYS_LONG[t.dayOfWeek! - 1], 'dayOfWeek'],\n ['EEE', (t) => DAYS_SHORT[t.dayOfWeek! - 1], 'dayOfWeek'],\n ['HH', (t) => pad(t.hour!, 2), 'hour'],\n ['H', (t) => String(t.hour!), 'hour'],\n ['hh', (t) => pad(t.hour! % 12 || 12, 2), 'hour'],\n ['h', (t) => String(t.hour! % 12 || 12), 'hour'],\n ['mm', (t) => pad(t.minute!, 2), 'minute'],\n ['m', (t) => String(t.minute!), 'minute'],\n ['ss', (t) => pad(t.second!, 2), 'second'],\n ['s', (t) => String(t.second!), 'second'],\n ['SSS', (t) => pad(t.millisecond!, 3), 'millisecond'],\n ['a', (t) => (t.hour! < 12 ? 'AM' : 'PM'), 'hour'],\n ['zzz', (t) => t.timeZoneId!, 'timeZoneId'],\n];","import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// Sort once, longest-first, so the greedy scanner below never matches \"M\"\n// when \"MMMM\" was actually there.\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into a sequence of\n * token and literal pieces. Text inside single quotes is always literal —\n * that's how you escape a token that would otherwise be parsed (e.g. a\n * literal \"d\" in \"3rd\" — write 'rd' in quotes so it isn't read as the day token).\n * A doubled quote ('') anywhere means a literal single quote character —\n * this works both inside an open quoted span (e.g. 'it''s' -> it's) and\n * as a standalone escape outside one (e.g. yyyy'' -> \"2026'\").\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // Doubled quote is always a literal ' — check this before treating\n // the quote as an open-delimiter, or \"''best''\" gets misread as\n // \"open quote, then bare text, then open quote\" instead of two\n // separate escaped-apostrophe literals around plain text.\n if (format[i + 1] === \"'\") {\n pieces.push({ kind: 'literal', value: \"'\" });\n i += 2;\n continue;\n }\n\n // Otherwise this opens a quoted literal span. Scan forward, treating\n // any '' we find *inside* the span as an escaped literal quote rather\n // than the closing delimiter.\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n pieces.push({ kind: 'literal', value: literal });\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // Not a token, not a quote — pass the character through as-is. This is\n // what lets you write \"yyyy-MM-dd\" with bare hyphens instead of quoting them.\n pieces.push({ kind: 'literal', value: ch });\n i += 1;\n }\n\n return pieces;\n}","import { TOKENS, type TemporalLike } from './tokens.js';\nimport { tokenize } from './tokenize.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n/**\n * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime\n * using a date-fns-style token string.\n *\n * @example\n * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // \"2026-08-04\"\n * format(zdt, \"MMM d, yyyy 'at' h:mm a\") // \"Aug 4, 2026 at 3:45 PM\"\n *\n * Throws if the format string uses a token the input type doesn't support\n * (e.g. 'HH' on a PlainDate, which has no time component) — this is\n * deliberate: silently printing \"undefined\" would be worse than failing loudly.\n */\nexport function format(temporal: TemporalLike, formatStr: string): string {\n const pieces = tokenize(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n // Shouldn't happen — tokenize() only emits tokens from TOKENS — but keep\n // TypeScript honest and fail loudly instead of silently.\n if (!handler) {\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal);\n }\n\n return result;\n}\n"],"mappings":"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","names":["format"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/tokens.ts","../src/tokenize.ts","../src/format.ts"],"sourcesContent":["export { format } from './format.js';\nexport type { TemporalLike, FormatOptions } from './tokens.js';","// Pad a number with leading zeros to `len` digits.\nexport function pad(n: number, len: number): string {\n return String(n).padStart(len, '0');\n}\n\n// Minimal duck-typed shape covering every field we might read off a Temporal\n// object. Not every field exists on every type (PlainDate has no .hour, for\n// example) — callers check for undefined before formatting a token.\n//\n// calendarId / toInstant are optional because a plain object satisfying\n// this interface in a test won't have them, but real Temporal instances\n// always do — intlPart() below relies on calendarId to keep Intl from\n// rejecting non-Gregorian objects, and on toInstant to detect ZonedDateTime.\nexport interface TemporalLike {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n timeZoneId?: string;\n dayOfWeek?: number; // 1 (Mon) - 7 (Sun), per Temporal spec\n calendarId?: string;\n toInstant?: () => unknown;\n}\n\nexport interface FormatOptions {\n /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */\n locale?: string;\n}\n\nexport const DEFAULT_LOCALE = 'en-US';\n\n// Small cache so repeated format() calls with the same (locale, options)\n// pair don't construct a fresh Intl.DateTimeFormat every time — these are\n// somewhat expensive to instantiate and format() may run in a loop (e.g.\n// rendering a table of dates).\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nfunction getFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = locale + JSON.stringify(options);\n let formatter = formatterCache.get(key);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\n return formatter;\n}\n\n// Reads a single named part (e.g. 'month', 'weekday', 'dayPeriod') out of\n// Intl's formatToParts() output. We ask Intl for exactly one field at a\n// time rather than building a full localized string and slicing it apart —\n// slicing is what breaks under RTL scripts and locales with different\n// field ordering (e.g. year-month-day vs day-month-year).\n//\n// `temporal: any` — deliberately untyped. Real Temporal.PlainDate /\n// PlainDateTime / ZonedDateTime instances (not the TemporalLike interface)\n// are what get passed through to Intl at runtime.\nfunction intlPart(\n temporal: any,\n locale: string,\n options: Intl.DateTimeFormatOptions,\n partType: Intl.DateTimeFormatPartTypes\n): string {\n // Intl.DateTimeFormat.formatToParts() always throws on Temporal.ZonedDateTime\n // specifically — deliberate per spec (see\n // Temporal.ZonedDateTime.prototype.toLocaleString docs), not a bug here.\n // Fix: convert to an Instant and pass the zone through the formatter's own\n // `timeZone` option. Converting to PlainDateTime instead would silently\n // drop the timezone info, which breaks combining e.g. 'MMMM' with 'zzz'.\n const isZoned = typeof temporal?.toInstant === 'function' && typeof temporal?.timeZoneId === 'string';\n const intlSafeTemporal = isZoned ? temporal.toInstant() : temporal;\n\n // Intl.DateTimeFormat hard-errors (\"Mismatching Calendars\") if the\n // formatter's resolved calendar doesn't match the Temporal object's own\n // calendar — an 'en-US' formatter defaults to gregory, so feeding it a\n // hebrew- or islamic-calendar PlainDate throws unless the formatter is\n // told which calendar to use. We read the calendar off the object itself\n // rather than guessing from the locale, so whatever calendar the caller's\n // Temporal object carries just works — not only Gregorian.\n //\n // Deliberately NOT passed when calendarId is 'iso8601' (the default for\n // plain Temporal objects nobody explicitly gave a calendar to): passing\n // `calendar: 'iso8601'` explicitly to Intl, combined with a single-field\n // options object like `{ month: 'long' }`, makes formatToParts() return\n // an empty parts array instead of the month — a real quirk in how Intl\n // resolves 'iso8601' with partial options, verified against\n // temporal-polyfill/full. Omitting the option entirely sidesteps it and\n // Intl's own default already matches what an iso8601 object needs.\n const calendar = temporal?.calendarId;\n const formatterOptions: Intl.DateTimeFormatOptions = {\n ...options,\n ...(calendar && calendar !== 'iso8601' ? { calendar } : {}),\n ...(isZoned ? { timeZone: temporal.timeZoneId } : {}),\n };\n\n const formatter = getFormatter(locale, formatterOptions);\n const parts = formatter.formatToParts(intlSafeTemporal);\n const part = parts.find((p) => p.type === partType);\n if (!part) {\n throw new Error(\n `temporal-fmt: locale \"${locale}\" produced no \"${partType}\" part for this token. ` +\n `This usually means the Temporal object is missing the field the token needs.`\n );\n }\n return part.value;\n}\n\n// Each token knows how to render itself from a TemporalLike + locale, and\n// which field it depends on (used to validate the input actually has that\n// field before we try to format it).\ntype TokenHandler = (t: TemporalLike, locale: string) => string;\n\n// Longest tokens first — the tokenizer is greedy, so \"yyyy\" must be tried\n// before \"yy\" or it'll never match.\n//\n// Numeric tokens (yyyy, MM, dd, HH, mm, ss, SSS) deliberately always render\n// in Western (0-9) digits regardless of locale, even though Intl could give\n// us locale-native digits (Arabic-Indic, Devanagari, etc. via\n// numberingSystem). Conscious choice, not an oversight: mixing locale\n// numeral systems into our pad()-based width logic is a real rabbit hole\n// (padding \"٣\" to 2 digits isn't the same operation as padding \"3\"), and\n// most consumers parsing these strings back out (logs, APIs, filenames)\n// want predictable ASCII digits. Documented as a known limitation in the\n// README rather than silently guessed at here.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n ['yy', (t) => pad(t.year! % 100, 2), 'year'],\n ['MMMM', (t, locale) => intlPart(t, locale, { month: 'long' }, 'month'), 'month'],\n ['MMM', (t, locale) => intlPart(t, locale, { month: 'short' }, 'month'), 'month'],\n ['MM', (t) => pad(t.month!, 2), 'month'],\n ['M', (t) => String(t.month!), 'month'],\n ['dd', (t) => pad(t.day!, 2), 'day'],\n ['d', (t) => String(t.day!), 'day'],\n ['EEEE', (t, locale) => intlPart(t, locale, { weekday: 'long' }, 'weekday'), 'dayOfWeek'],\n ['EEE', (t, locale) => intlPart(t, locale, { weekday: 'short' }, 'weekday'), 'dayOfWeek'],\n ['HH', (t) => pad(t.hour!, 2), 'hour'],\n ['H', (t) => String(t.hour!), 'hour'],\n ['hh', (t) => pad(t.hour! % 12 || 12, 2), 'hour'],\n ['h', (t) => String(t.hour! % 12 || 12), 'hour'],\n ['mm', (t) => pad(t.minute!, 2), 'minute'],\n ['m', (t) => String(t.minute!), 'minute'],\n ['ss', (t) => pad(t.second!, 2), 'second'],\n ['s', (t) => String(t.second!), 'second'],\n ['SSS', (t) => pad(t.millisecond!, 3), 'millisecond'],\n // dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific\n // — some locales render it differently or don't split 12-hour at all. We\n // still require .hour on the input either way, since that's what Intl\n // needs to compute which period it is.\n ['a', (t, locale) => intlPart(t, locale, { hour: 'numeric', hour12: true }, 'dayPeriod'), 'hour'],\n ['zzz', (t) => t.timeZoneId!, 'timeZoneId'],\n];","import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// Sort once, longest-first, so the greedy scanner below never matches \"M\"\n// when \"MMMM\" was actually there.\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into a sequence of\n * token and literal pieces. Text inside single quotes is always literal —\n * that's how you escape a token that would otherwise be parsed (e.g. a\n * literal \"d\" in \"3rd\" — write 'rd' in quotes so it isn't read as the day token).\n * A doubled quote ('') anywhere means a literal single quote character —\n * this works both inside an open quoted span (e.g. 'it''s' -> it's) and\n * as a standalone escape outside one (e.g. yyyy'' -> \"2026'\").\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // Doubled quote is always a literal ' — check this before treating\n // the quote as an open-delimiter, or \"''best''\" gets misread as\n // \"open quote, then bare text, then open quote\" instead of two\n // separate escaped-apostrophe literals around plain text.\n if (format[i + 1] === \"'\") {\n pieces.push({ kind: 'literal', value: \"'\" });\n i += 2;\n continue;\n }\n\n // Otherwise this opens a quoted literal span. Scan forward, treating\n // any '' we find *inside* the span as an escaped literal quote rather\n // than the closing delimiter.\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n pieces.push({ kind: 'literal', value: literal });\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // Not a token, not a quote — pass the character through as-is. This is\n // what lets you write \"yyyy-MM-dd\" with bare hyphens instead of quoting them.\n pieces.push({ kind: 'literal', value: ch });\n i += 1;\n }\n\n return pieces;\n}","import { TOKENS, DEFAULT_LOCALE, type TemporalLike, type FormatOptions } from './tokens.js';\nimport { tokenize } from './tokenize.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n/**\n * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime\n * using a date-fns-style token string.\n *\n * @example\n * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // \"2026-08-04\"\n * format(zdt, \"MMM d, yyyy 'at' h:mm a\") // \"Aug 4, 2026 at 3:45 PM\"\n * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // \"août 4, 2026\"\n * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names\n *\n * Numeric fields (yyyy, MM, dd, HH, mm, ss, SSS) always render in Western\n * (0-9) digits regardless of locale — this keeps output predictable for\n * anything parsing the result back out (logs, APIs, filenames). Named\n * fields (MMMM, EEEE, a) are fully localized via Intl.DateTimeFormat,\n * including non-Gregorian calendars if the Temporal object itself carries\n * one (e.g. a PlainDate constructed with a Hebrew or Islamic calendar).\n *\n * Throws if the format string uses a token the input type doesn't support\n * (e.g. 'HH' on a PlainDate, which has no time component) — this is\n * deliberate: silently printing \"undefined\" would be worse than failing loudly.\n */\nexport function format(temporal: TemporalLike, formatStr: string, options: FormatOptions = {}): string {\n const locale = options.locale ?? DEFAULT_LOCALE;\n const pieces = tokenize(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n // Shouldn't happen — tokenize() only emits tokens from TOKENS — but keep\n // TypeScript honest and fail loudly instead of silently.\n if (!handler) {\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal, locale);\n }\n\n return result;\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCO,SAAS,IAAI,GAAW,KAAqB;AAClD,SAAO,OAAO,CAAC,EAAE,SAAS,KAAK,GAAG;AACpC;AA6BO,IAAM,iBAAiB;AAM9B,IAAM,iBAAiB,oBAAI,IAAiC;AAE5D,SAAS,aAAa,QAAgB,SAA0D;AAC9F,QAAM,MAAM,SAAS,KAAK,UAAU,OAAO;AAC3C,MAAI,YAAY,eAAe,IAAI,GAAG;AACtC,MAAI,CAAC,WAAW;AACd,gBAAY,IAAI,KAAK,eAAe,QAAQ,OAAO;AACnD,mBAAe,IAAI,KAAK,SAAS;AAAA,EACnC;AACA,SAAO;AACT;AAWA,SAAS,SACP,UACA,QACA,SACA,UACQ;AAOR,QAAM,UAAU,OAAO,UAAU,cAAc,cAAc,OAAO,UAAU,eAAe;AAC7F,QAAM,mBAAmB,UAAU,SAAS,UAAU,IAAI;AAkB1D,QAAM,WAAW,UAAU;AAC3B,QAAM,mBAA+C;AAAA,IACnD,GAAG;AAAA,IACH,GAAI,YAAY,aAAa,YAAY,EAAE,SAAS,IAAI,CAAC;AAAA,IACzD,GAAI,UAAU,EAAE,UAAU,SAAS,WAAW,IAAI,CAAC;AAAA,EACrD;AAEA,QAAM,YAAY,aAAa,QAAQ,gBAAgB;AACvD,QAAM,QAAQ,UAAU,cAAc,gBAAgB;AACtD,QAAM,OAAO,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ;AAClD,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR,yBAAyB,MAAM,kBAAkB,QAAQ;AAAA,IAE3D;AAAA,EACF;AACA,SAAO,KAAK;AACd;AAmBO,IAAM,SAA4D;AAAA,EACvE,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,MAAO,CAAC,GAAG,MAAM;AAAA,EACvC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,OAAQ,KAAK,CAAC,GAAG,MAAM;AAAA,EAC3C,CAAC,QAAQ,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,OAAO,OAAO,GAAG,OAAO,GAAG,OAAO;AAAA,EAChF,CAAC,OAAO,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,OAAO,QAAQ,GAAG,OAAO,GAAG,OAAO;AAAA,EAChF,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,OAAQ,CAAC,GAAG,OAAO;AAAA,EACvC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,KAAM,GAAG,OAAO;AAAA,EACtC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,KAAM,CAAC,GAAG,KAAK;AAAA,EACnC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,GAAI,GAAG,KAAK;AAAA,EAClC,CAAC,QAAQ,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,SAAS,OAAO,GAAG,SAAS,GAAG,WAAW;AAAA,EACxF,CAAC,OAAO,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,SAAS,QAAQ,GAAG,SAAS,GAAG,WAAW;AAAA,EACxF,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,MAAO,CAAC,GAAG,MAAM;AAAA,EACrC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,IAAK,GAAG,MAAM;AAAA,EACpC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,OAAQ,MAAM,IAAI,CAAC,GAAG,MAAM;AAAA,EAChD,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,OAAQ,MAAM,EAAE,GAAG,MAAM;AAAA,EAC/C,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,QAAS,CAAC,GAAG,QAAQ;AAAA,EACzC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,MAAO,GAAG,QAAQ;AAAA,EACxC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,QAAS,CAAC,GAAG,QAAQ;AAAA,EACzC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,MAAO,GAAG,QAAQ;AAAA,EACxC,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,aAAc,CAAC,GAAG,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpD,CAAC,KAAK,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,MAAM,WAAW,QAAQ,KAAK,GAAG,WAAW,GAAG,MAAM;AAAA,EAChG,CAAC,OAAO,CAAC,MAAM,EAAE,YAAa,YAAY;AAC5C;;;AChJA,IAAM,uBAAuB,OAAO,IAAI,CAAC,CAAC,GAAG,MAAM,GAAG,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AAWnF,SAAS,SAASA,SAAyB;AAChD,QAAM,SAAkB,CAAC;AACzB,MAAI,IAAI;AAER,SAAO,IAAIA,QAAO,QAAQ;AACxB,UAAM,KAAKA,QAAO,CAAC;AAEnB,QAAI,OAAO,KAAK;AAKd,UAAIA,QAAO,IAAI,CAAC,MAAM,KAAK;AACzB,eAAO,KAAK,EAAE,MAAM,WAAW,OAAO,IAAI,CAAC;AAC3C,aAAK;AACL;AAAA,MACF;AAKA,UAAI,IAAI,IAAI;AACZ,UAAI,UAAU;AACd,UAAI,SAAS;AACb,aAAO,IAAIA,QAAO,QAAQ;AACxB,YAAIA,QAAO,CAAC,MAAM,KAAK;AACrB,cAAIA,QAAO,IAAI,CAAC,MAAM,KAAK;AACzB,uBAAW;AACX,iBAAK;AACL;AAAA,UACF;AACA,mBAAS;AACT,eAAK;AACL;AAAA,QACF;AACA,mBAAWA,QAAO,CAAC;AACnB,aAAK;AAAA,MACP;AAEA,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,sDAAsDA,OAAM,GAAG;AAAA,MACjF;AAEA,aAAO,KAAK,EAAE,MAAM,WAAW,OAAO,QAAQ,CAAC;AAC/C,UAAI;AACJ;AAAA,IACF;AAEA,UAAM,QAAQ,qBAAqB,KAAK,CAAC,QAAQA,QAAO,WAAW,KAAK,CAAC,CAAC;AAC1E,QAAI,OAAO;AACT,aAAO,KAAK,EAAE,MAAM,SAAS,OAAO,MAAM,CAAC;AAC3C,WAAK,MAAM;AACX;AAAA,IACF;AAIA,WAAO,KAAK,EAAE,MAAM,WAAW,OAAO,GAAG,CAAC;AAC1C,SAAK;AAAA,EACP;AAEA,SAAO;AACT;;;AC9EA,IAAM,mBAAmB,IAAI,IAAI,OAAO,IAAI,CAAC,CAAC,KAAK,IAAI,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,MAAM,CAAC,CAAC,CAAC;AAuBhF,SAAS,OAAO,UAAwB,WAAmB,UAAyB,CAAC,GAAW;AACrG,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,SAAS,SAAS,SAAS;AACjC,MAAI,SAAS;AAEb,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,SAAS,WAAW;AAC5B,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,UAAM,UAAU,iBAAiB,IAAI,MAAM,KAAK;AAGhD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,gCAAgC,MAAM,KAAK,GAAG;AAAA,IAChE;AAEA,QAAI,SAAS,QAAQ,KAAK,MAAM,QAAW;AACzC,YAAM,IAAI;AAAA,QACR,wBAAwB,MAAM,KAAK,eAAe,QAAQ,KAAK;AAAA,MAGjE;AAAA,IACF;AAEA,cAAU,QAAQ,GAAG,UAAU,MAAM;AAAA,EACvC;AAEA,SAAO;AACT;","names":["format"]}
package/dist/index.d.cts CHANGED
@@ -8,6 +8,12 @@ interface TemporalLike {
8
8
  millisecond?: number;
9
9
  timeZoneId?: string;
10
10
  dayOfWeek?: number;
11
+ calendarId?: string;
12
+ toInstant?: () => unknown;
13
+ }
14
+ interface FormatOptions {
15
+ /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */
16
+ locale?: string;
11
17
  }
12
18
 
13
19
  /**
@@ -15,13 +21,22 @@ interface TemporalLike {
15
21
  * using a date-fns-style token string.
16
22
  *
17
23
  * @example
18
- * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
19
- * format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
24
+ * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
25
+ * format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
26
+ * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
27
+ * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names
28
+ *
29
+ * Numeric fields (yyyy, MM, dd, HH, mm, ss, SSS) always render in Western
30
+ * (0-9) digits regardless of locale — this keeps output predictable for
31
+ * anything parsing the result back out (logs, APIs, filenames). Named
32
+ * fields (MMMM, EEEE, a) are fully localized via Intl.DateTimeFormat,
33
+ * including non-Gregorian calendars if the Temporal object itself carries
34
+ * one (e.g. a PlainDate constructed with a Hebrew or Islamic calendar).
20
35
  *
21
36
  * Throws if the format string uses a token the input type doesn't support
22
37
  * (e.g. 'HH' on a PlainDate, which has no time component) — this is
23
38
  * deliberate: silently printing "undefined" would be worse than failing loudly.
24
39
  */
25
- declare function format(temporal: TemporalLike, formatStr: string): string;
40
+ declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
26
41
 
27
- export { type TemporalLike, format };
42
+ export { type FormatOptions, type TemporalLike, format };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,12 @@ interface TemporalLike {
8
8
  millisecond?: number;
9
9
  timeZoneId?: string;
10
10
  dayOfWeek?: number;
11
+ calendarId?: string;
12
+ toInstant?: () => unknown;
13
+ }
14
+ interface FormatOptions {
15
+ /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */
16
+ locale?: string;
11
17
  }
12
18
 
13
19
  /**
@@ -15,13 +21,22 @@ interface TemporalLike {
15
21
  * using a date-fns-style token string.
16
22
  *
17
23
  * @example
18
- * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
19
- * format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
24
+ * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
25
+ * format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
26
+ * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
27
+ * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names
28
+ *
29
+ * Numeric fields (yyyy, MM, dd, HH, mm, ss, SSS) always render in Western
30
+ * (0-9) digits regardless of locale — this keeps output predictable for
31
+ * anything parsing the result back out (logs, APIs, filenames). Named
32
+ * fields (MMMM, EEEE, a) are fully localized via Intl.DateTimeFormat,
33
+ * including non-Gregorian calendars if the Temporal object itself carries
34
+ * one (e.g. a PlainDate constructed with a Hebrew or Islamic calendar).
20
35
  *
21
36
  * Throws if the format string uses a token the input type doesn't support
22
37
  * (e.g. 'HH' on a PlainDate, which has no time component) — this is
23
38
  * deliberate: silently printing "undefined" would be worse than failing loudly.
24
39
  */
25
- declare function format(temporal: TemporalLike, formatStr: string): string;
40
+ declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
26
41
 
27
- export { type TemporalLike, format };
42
+ export { type FormatOptions, type TemporalLike, format };
package/dist/index.js CHANGED
@@ -2,34 +2,47 @@
2
2
  function pad(n, len) {
3
3
  return String(n).padStart(len, "0");
4
4
  }
5
- var MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
6
- var MONTHS_LONG = [
7
- "January",
8
- "February",
9
- "March",
10
- "April",
11
- "May",
12
- "June",
13
- "July",
14
- "August",
15
- "September",
16
- "October",
17
- "November",
18
- "December"
19
- ];
20
- var DAYS_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
21
- var DAYS_LONG = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
5
+ var DEFAULT_LOCALE = "en-US";
6
+ var formatterCache = /* @__PURE__ */ new Map();
7
+ function getFormatter(locale, options) {
8
+ const key = locale + JSON.stringify(options);
9
+ let formatter = formatterCache.get(key);
10
+ if (!formatter) {
11
+ formatter = new Intl.DateTimeFormat(locale, options);
12
+ formatterCache.set(key, formatter);
13
+ }
14
+ return formatter;
15
+ }
16
+ function intlPart(temporal, locale, options, partType) {
17
+ const isZoned = typeof temporal?.toInstant === "function" && typeof temporal?.timeZoneId === "string";
18
+ const intlSafeTemporal = isZoned ? temporal.toInstant() : temporal;
19
+ const calendar = temporal?.calendarId;
20
+ const formatterOptions = {
21
+ ...options,
22
+ ...calendar && calendar !== "iso8601" ? { calendar } : {},
23
+ ...isZoned ? { timeZone: temporal.timeZoneId } : {}
24
+ };
25
+ const formatter = getFormatter(locale, formatterOptions);
26
+ const parts = formatter.formatToParts(intlSafeTemporal);
27
+ const part = parts.find((p) => p.type === partType);
28
+ if (!part) {
29
+ throw new Error(
30
+ `temporal-fmt: locale "${locale}" produced no "${partType}" part for this token. This usually means the Temporal object is missing the field the token needs.`
31
+ );
32
+ }
33
+ return part.value;
34
+ }
22
35
  var TOKENS = [
23
36
  ["yyyy", (t) => pad(t.year, 4), "year"],
24
37
  ["yy", (t) => pad(t.year % 100, 2), "year"],
25
- ["MMMM", (t) => MONTHS_LONG[t.month - 1], "month"],
26
- ["MMM", (t) => MONTHS_SHORT[t.month - 1], "month"],
38
+ ["MMMM", (t, locale) => intlPart(t, locale, { month: "long" }, "month"), "month"],
39
+ ["MMM", (t, locale) => intlPart(t, locale, { month: "short" }, "month"), "month"],
27
40
  ["MM", (t) => pad(t.month, 2), "month"],
28
41
  ["M", (t) => String(t.month), "month"],
29
42
  ["dd", (t) => pad(t.day, 2), "day"],
30
43
  ["d", (t) => String(t.day), "day"],
31
- ["EEEE", (t) => DAYS_LONG[t.dayOfWeek - 1], "dayOfWeek"],
32
- ["EEE", (t) => DAYS_SHORT[t.dayOfWeek - 1], "dayOfWeek"],
44
+ ["EEEE", (t, locale) => intlPart(t, locale, { weekday: "long" }, "weekday"), "dayOfWeek"],
45
+ ["EEE", (t, locale) => intlPart(t, locale, { weekday: "short" }, "weekday"), "dayOfWeek"],
33
46
  ["HH", (t) => pad(t.hour, 2), "hour"],
34
47
  ["H", (t) => String(t.hour), "hour"],
35
48
  ["hh", (t) => pad(t.hour % 12 || 12, 2), "hour"],
@@ -39,7 +52,11 @@ var TOKENS = [
39
52
  ["ss", (t) => pad(t.second, 2), "second"],
40
53
  ["s", (t) => String(t.second), "second"],
41
54
  ["SSS", (t) => pad(t.millisecond, 3), "millisecond"],
42
- ["a", (t) => t.hour < 12 ? "AM" : "PM", "hour"],
55
+ // dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific
56
+ // — some locales render it differently or don't split 12-hour at all. We
57
+ // still require .hour on the input either way, since that's what Intl
58
+ // needs to compute which period it is.
59
+ ["a", (t, locale) => intlPart(t, locale, { hour: "numeric", hour12: true }, "dayPeriod"), "hour"],
43
60
  ["zzz", (t) => t.timeZoneId, "timeZoneId"]
44
61
  ];
45
62
 
@@ -94,7 +111,8 @@ function tokenize(format2) {
94
111
 
95
112
  // src/format.ts
96
113
  var HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));
97
- function format(temporal, formatStr) {
114
+ function format(temporal, formatStr, options = {}) {
115
+ const locale = options.locale ?? DEFAULT_LOCALE;
98
116
  const pieces = tokenize(formatStr);
99
117
  let result = "";
100
118
  for (const piece of pieces) {
@@ -111,7 +129,7 @@ function format(temporal, formatStr) {
111
129
  `temporal-fmt: token "${piece.value}" requires "${handler.field}", which this Temporal object doesn't have. (e.g. PlainDate has no time fields, PlainTime has no date fields)`
112
130
  );
113
131
  }
114
- result += handler.fn(temporal);
132
+ result += handler.fn(temporal, locale);
115
133
  }
116
134
  return result;
117
135
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tokens.ts","../src/tokenize.ts","../src/format.ts"],"sourcesContent":["// Pad a number with leading zeros to `len` digits.\nexport function pad(n: number, len: number): string {\n return String(n).padStart(len, '0');\n}\n\n// Minimal duck-typed shape covering every field we might read off a Temporal\n// object. Not every field exists on every type (PlainDate has no .hour, for\n// example) — callers check for undefined before formatting a token.\nexport interface TemporalLike {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n timeZoneId?: string;\n dayOfWeek?: number; // 1 (Mon) - 7 (Sun), per Temporal spec\n}\n\nconst MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\nconst MONTHS_LONG = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\nconst DAYS_SHORT = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\nconst DAYS_LONG = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];\n\n// Each token renders itself from a TemporalLike. The third tuple element\n// below names the field it depends on, so format.ts can check for undefined\n// before calling the handler (e.g. `HH` needs `.hour`, which PlainDate lacks).\ntype TokenHandler = (t: TemporalLike) => string;\n\n// Longest tokens first — the tokenizer is greedy, so \"yyyy\" must be tried\n// before \"yy\" or it'll never match.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n ['yy', (t) => pad(t.year! % 100, 2), 'year'],\n ['MMMM', (t) => MONTHS_LONG[t.month! - 1], 'month'],\n ['MMM', (t) => MONTHS_SHORT[t.month! - 1], 'month'],\n ['MM', (t) => pad(t.month!, 2), 'month'],\n ['M', (t) => String(t.month!), 'month'],\n ['dd', (t) => pad(t.day!, 2), 'day'],\n ['d', (t) => String(t.day!), 'day'],\n ['EEEE', (t) => DAYS_LONG[t.dayOfWeek! - 1], 'dayOfWeek'],\n ['EEE', (t) => DAYS_SHORT[t.dayOfWeek! - 1], 'dayOfWeek'],\n ['HH', (t) => pad(t.hour!, 2), 'hour'],\n ['H', (t) => String(t.hour!), 'hour'],\n ['hh', (t) => pad(t.hour! % 12 || 12, 2), 'hour'],\n ['h', (t) => String(t.hour! % 12 || 12), 'hour'],\n ['mm', (t) => pad(t.minute!, 2), 'minute'],\n ['m', (t) => String(t.minute!), 'minute'],\n ['ss', (t) => pad(t.second!, 2), 'second'],\n ['s', (t) => String(t.second!), 'second'],\n ['SSS', (t) => pad(t.millisecond!, 3), 'millisecond'],\n ['a', (t) => (t.hour! < 12 ? 'AM' : 'PM'), 'hour'],\n ['zzz', (t) => t.timeZoneId!, 'timeZoneId'],\n];","import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// Sort once, longest-first, so the greedy scanner below never matches \"M\"\n// when \"MMMM\" was actually there.\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into a sequence of\n * token and literal pieces. Text inside single quotes is always literal —\n * that's how you escape a token that would otherwise be parsed (e.g. a\n * literal \"d\" in \"3rd\" — write 'rd' in quotes so it isn't read as the day token).\n * A doubled quote ('') anywhere means a literal single quote character —\n * this works both inside an open quoted span (e.g. 'it''s' -> it's) and\n * as a standalone escape outside one (e.g. yyyy'' -> \"2026'\").\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // Doubled quote is always a literal ' — check this before treating\n // the quote as an open-delimiter, or \"''best''\" gets misread as\n // \"open quote, then bare text, then open quote\" instead of two\n // separate escaped-apostrophe literals around plain text.\n if (format[i + 1] === \"'\") {\n pieces.push({ kind: 'literal', value: \"'\" });\n i += 2;\n continue;\n }\n\n // Otherwise this opens a quoted literal span. Scan forward, treating\n // any '' we find *inside* the span as an escaped literal quote rather\n // than the closing delimiter.\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n pieces.push({ kind: 'literal', value: literal });\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // Not a token, not a quote — pass the character through as-is. This is\n // what lets you write \"yyyy-MM-dd\" with bare hyphens instead of quoting them.\n pieces.push({ kind: 'literal', value: ch });\n i += 1;\n }\n\n return pieces;\n}","import { TOKENS, type TemporalLike } from './tokens.js';\nimport { tokenize } from './tokenize.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n/**\n * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime\n * using a date-fns-style token string.\n *\n * @example\n * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // \"2026-08-04\"\n * format(zdt, \"MMM d, yyyy 'at' h:mm a\") // \"Aug 4, 2026 at 3:45 PM\"\n *\n * Throws if the format string uses a token the input type doesn't support\n * (e.g. 'HH' on a PlainDate, which has no time component) — this is\n * deliberate: silently printing \"undefined\" would be worse than failing loudly.\n */\nexport function format(temporal: TemporalLike, formatStr: string): string {\n const pieces = tokenize(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n // Shouldn't happen — tokenize() only emits tokens from TOKENS — but keep\n // TypeScript honest and fail loudly instead of silently.\n if (!handler) {\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal);\n }\n\n return result;\n}\n"],"mappings":"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","names":["format"]}
1
+ {"version":3,"sources":["../src/tokens.ts","../src/tokenize.ts","../src/format.ts"],"sourcesContent":["// Pad a number with leading zeros to `len` digits.\nexport function pad(n: number, len: number): string {\n return String(n).padStart(len, '0');\n}\n\n// Minimal duck-typed shape covering every field we might read off a Temporal\n// object. Not every field exists on every type (PlainDate has no .hour, for\n// example) — callers check for undefined before formatting a token.\n//\n// calendarId / toInstant are optional because a plain object satisfying\n// this interface in a test won't have them, but real Temporal instances\n// always do — intlPart() below relies on calendarId to keep Intl from\n// rejecting non-Gregorian objects, and on toInstant to detect ZonedDateTime.\nexport interface TemporalLike {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n timeZoneId?: string;\n dayOfWeek?: number; // 1 (Mon) - 7 (Sun), per Temporal spec\n calendarId?: string;\n toInstant?: () => unknown;\n}\n\nexport interface FormatOptions {\n /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */\n locale?: string;\n}\n\nexport const DEFAULT_LOCALE = 'en-US';\n\n// Small cache so repeated format() calls with the same (locale, options)\n// pair don't construct a fresh Intl.DateTimeFormat every time — these are\n// somewhat expensive to instantiate and format() may run in a loop (e.g.\n// rendering a table of dates).\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nfunction getFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = locale + JSON.stringify(options);\n let formatter = formatterCache.get(key);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\n return formatter;\n}\n\n// Reads a single named part (e.g. 'month', 'weekday', 'dayPeriod') out of\n// Intl's formatToParts() output. We ask Intl for exactly one field at a\n// time rather than building a full localized string and slicing it apart —\n// slicing is what breaks under RTL scripts and locales with different\n// field ordering (e.g. year-month-day vs day-month-year).\n//\n// `temporal: any` — deliberately untyped. Real Temporal.PlainDate /\n// PlainDateTime / ZonedDateTime instances (not the TemporalLike interface)\n// are what get passed through to Intl at runtime.\nfunction intlPart(\n temporal: any,\n locale: string,\n options: Intl.DateTimeFormatOptions,\n partType: Intl.DateTimeFormatPartTypes\n): string {\n // Intl.DateTimeFormat.formatToParts() always throws on Temporal.ZonedDateTime\n // specifically — deliberate per spec (see\n // Temporal.ZonedDateTime.prototype.toLocaleString docs), not a bug here.\n // Fix: convert to an Instant and pass the zone through the formatter's own\n // `timeZone` option. Converting to PlainDateTime instead would silently\n // drop the timezone info, which breaks combining e.g. 'MMMM' with 'zzz'.\n const isZoned = typeof temporal?.toInstant === 'function' && typeof temporal?.timeZoneId === 'string';\n const intlSafeTemporal = isZoned ? temporal.toInstant() : temporal;\n\n // Intl.DateTimeFormat hard-errors (\"Mismatching Calendars\") if the\n // formatter's resolved calendar doesn't match the Temporal object's own\n // calendar — an 'en-US' formatter defaults to gregory, so feeding it a\n // hebrew- or islamic-calendar PlainDate throws unless the formatter is\n // told which calendar to use. We read the calendar off the object itself\n // rather than guessing from the locale, so whatever calendar the caller's\n // Temporal object carries just works — not only Gregorian.\n //\n // Deliberately NOT passed when calendarId is 'iso8601' (the default for\n // plain Temporal objects nobody explicitly gave a calendar to): passing\n // `calendar: 'iso8601'` explicitly to Intl, combined with a single-field\n // options object like `{ month: 'long' }`, makes formatToParts() return\n // an empty parts array instead of the month — a real quirk in how Intl\n // resolves 'iso8601' with partial options, verified against\n // temporal-polyfill/full. Omitting the option entirely sidesteps it and\n // Intl's own default already matches what an iso8601 object needs.\n const calendar = temporal?.calendarId;\n const formatterOptions: Intl.DateTimeFormatOptions = {\n ...options,\n ...(calendar && calendar !== 'iso8601' ? { calendar } : {}),\n ...(isZoned ? { timeZone: temporal.timeZoneId } : {}),\n };\n\n const formatter = getFormatter(locale, formatterOptions);\n const parts = formatter.formatToParts(intlSafeTemporal);\n const part = parts.find((p) => p.type === partType);\n if (!part) {\n throw new Error(\n `temporal-fmt: locale \"${locale}\" produced no \"${partType}\" part for this token. ` +\n `This usually means the Temporal object is missing the field the token needs.`\n );\n }\n return part.value;\n}\n\n// Each token knows how to render itself from a TemporalLike + locale, and\n// which field it depends on (used to validate the input actually has that\n// field before we try to format it).\ntype TokenHandler = (t: TemporalLike, locale: string) => string;\n\n// Longest tokens first — the tokenizer is greedy, so \"yyyy\" must be tried\n// before \"yy\" or it'll never match.\n//\n// Numeric tokens (yyyy, MM, dd, HH, mm, ss, SSS) deliberately always render\n// in Western (0-9) digits regardless of locale, even though Intl could give\n// us locale-native digits (Arabic-Indic, Devanagari, etc. via\n// numberingSystem). Conscious choice, not an oversight: mixing locale\n// numeral systems into our pad()-based width logic is a real rabbit hole\n// (padding \"٣\" to 2 digits isn't the same operation as padding \"3\"), and\n// most consumers parsing these strings back out (logs, APIs, filenames)\n// want predictable ASCII digits. Documented as a known limitation in the\n// README rather than silently guessed at here.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n ['yy', (t) => pad(t.year! % 100, 2), 'year'],\n ['MMMM', (t, locale) => intlPart(t, locale, { month: 'long' }, 'month'), 'month'],\n ['MMM', (t, locale) => intlPart(t, locale, { month: 'short' }, 'month'), 'month'],\n ['MM', (t) => pad(t.month!, 2), 'month'],\n ['M', (t) => String(t.month!), 'month'],\n ['dd', (t) => pad(t.day!, 2), 'day'],\n ['d', (t) => String(t.day!), 'day'],\n ['EEEE', (t, locale) => intlPart(t, locale, { weekday: 'long' }, 'weekday'), 'dayOfWeek'],\n ['EEE', (t, locale) => intlPart(t, locale, { weekday: 'short' }, 'weekday'), 'dayOfWeek'],\n ['HH', (t) => pad(t.hour!, 2), 'hour'],\n ['H', (t) => String(t.hour!), 'hour'],\n ['hh', (t) => pad(t.hour! % 12 || 12, 2), 'hour'],\n ['h', (t) => String(t.hour! % 12 || 12), 'hour'],\n ['mm', (t) => pad(t.minute!, 2), 'minute'],\n ['m', (t) => String(t.minute!), 'minute'],\n ['ss', (t) => pad(t.second!, 2), 'second'],\n ['s', (t) => String(t.second!), 'second'],\n ['SSS', (t) => pad(t.millisecond!, 3), 'millisecond'],\n // dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific\n // — some locales render it differently or don't split 12-hour at all. We\n // still require .hour on the input either way, since that's what Intl\n // needs to compute which period it is.\n ['a', (t, locale) => intlPart(t, locale, { hour: 'numeric', hour12: true }, 'dayPeriod'), 'hour'],\n ['zzz', (t) => t.timeZoneId!, 'timeZoneId'],\n];","import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// Sort once, longest-first, so the greedy scanner below never matches \"M\"\n// when \"MMMM\" was actually there.\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into a sequence of\n * token and literal pieces. Text inside single quotes is always literal —\n * that's how you escape a token that would otherwise be parsed (e.g. a\n * literal \"d\" in \"3rd\" — write 'rd' in quotes so it isn't read as the day token).\n * A doubled quote ('') anywhere means a literal single quote character —\n * this works both inside an open quoted span (e.g. 'it''s' -> it's) and\n * as a standalone escape outside one (e.g. yyyy'' -> \"2026'\").\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // Doubled quote is always a literal ' — check this before treating\n // the quote as an open-delimiter, or \"''best''\" gets misread as\n // \"open quote, then bare text, then open quote\" instead of two\n // separate escaped-apostrophe literals around plain text.\n if (format[i + 1] === \"'\") {\n pieces.push({ kind: 'literal', value: \"'\" });\n i += 2;\n continue;\n }\n\n // Otherwise this opens a quoted literal span. Scan forward, treating\n // any '' we find *inside* the span as an escaped literal quote rather\n // than the closing delimiter.\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n pieces.push({ kind: 'literal', value: literal });\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // Not a token, not a quote — pass the character through as-is. This is\n // what lets you write \"yyyy-MM-dd\" with bare hyphens instead of quoting them.\n pieces.push({ kind: 'literal', value: ch });\n i += 1;\n }\n\n return pieces;\n}","import { TOKENS, DEFAULT_LOCALE, type TemporalLike, type FormatOptions } from './tokens.js';\nimport { tokenize } from './tokenize.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n/**\n * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime\n * using a date-fns-style token string.\n *\n * @example\n * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // \"2026-08-04\"\n * format(zdt, \"MMM d, yyyy 'at' h:mm a\") // \"Aug 4, 2026 at 3:45 PM\"\n * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // \"août 4, 2026\"\n * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names\n *\n * Numeric fields (yyyy, MM, dd, HH, mm, ss, SSS) always render in Western\n * (0-9) digits regardless of locale — this keeps output predictable for\n * anything parsing the result back out (logs, APIs, filenames). Named\n * fields (MMMM, EEEE, a) are fully localized via Intl.DateTimeFormat,\n * including non-Gregorian calendars if the Temporal object itself carries\n * one (e.g. a PlainDate constructed with a Hebrew or Islamic calendar).\n *\n * Throws if the format string uses a token the input type doesn't support\n * (e.g. 'HH' on a PlainDate, which has no time component) — this is\n * deliberate: silently printing \"undefined\" would be worse than failing loudly.\n */\nexport function format(temporal: TemporalLike, formatStr: string, options: FormatOptions = {}): string {\n const locale = options.locale ?? DEFAULT_LOCALE;\n const pieces = tokenize(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n // Shouldn't happen — tokenize() only emits tokens from TOKENS — but keep\n // TypeScript honest and fail loudly instead of silently.\n if (!handler) {\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal, locale);\n }\n\n return result;\n}"],"mappings":"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","names":["format"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "temporal-fmt",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Format Temporal.PlainDate/PlainDateTime/PlainTime/ZonedDateTime objects using date-fns-style token strings.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -49,5 +49,8 @@
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24"
52
+ },
53
+ "allowScripts": {
54
+ "esbuild@0.28.1": true
52
55
  }
53
- }
56
+ }