feat!: require a user-provided Temporal (remove bundled shim) and fix non-Gregorian locale values - #15
Merged
Merged
Conversation
onMonthChange, rootState.viewing, and NavButtonState.target were built
with `PlainYearMonth.from({ year, month, calendar: localeCalendar })`,
feeding ISO year/month numbers in as locale-calendar fields. With the
full @js-temporal/polyfill (and native Temporal) this reinterpreted the
numbers: for locale="th-TH" (Buddhist), viewing ISO June 2026 produced
1483-06-10[u-ca=buddhist] — off by ~543 years. (The mini shim is
ISO-only and silently ignored the field, so the bug only surfaced with a
real calendar implementation.)
These values are now built in ISO. The locale calendar only ever
affected display, which goes through `new Date(...).toLocaleDateString`
independently (MonthYearString) and is unchanged — Thai still renders
"มิถุนายน 2569". Making the exposed values ISO also lets the controlled
`month`/onMonthChange pair round-trip under the identity function.
`calendarForLocale` existed only to feed this buggy pattern and is now
removed (not a public export). month/defaultMonth/onMonthChange docs now
state the ISO contract.
Adds th-TH coverage: onMonthChange and rootState.viewing emit ISO,
NavButtonState.target is ISO, the label still localizes to the Buddhist
era, and a controlled round-trip does not jump centuries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MonthYearString built `new Date(year, month-1, 1).toLocaleDateString(...)`
in component code. Format through the injected Temporal instead, so the
only native `Date` on this display path lives inside the polyfill — the
bundled shim's `PlainDate.toLocaleString` (Gregorian-only) or the full
polyfill.
Uses `PlainDate.toLocaleString`, NOT `PlainYearMonth.toLocaleString`:
verified the latter throws on any iso8601-vs-locale calendar mismatch
("cannot format PlainYearMonth with calendar iso8601 in locale with
calendar gregory") — even for en-US. An iso8601 `PlainDate` is the
documented compatibility exception and adopts the locale's calendar, so
th-TH still renders the Buddhist era (2569). The mini shim's existing
`MiniPlainDate.toLocaleString` already localizes through Intl; no new
shim method was added (a non-throwing `MiniPlainYearMonth.toLocaleString`
would diverge from real Temporal, which throws).
Documents the shim as ISO/Gregorian-only (module doc + the public
`Temporal` export): display localizes via Intl, but calendar math is
Gregorian; use @js-temporal/polyfill or native Temporal for other
calendars.
Tests: MiniPlainDate.toLocaleString (en-US, th-TH Buddhist, no day leak,
custom options) and a MonthYearString th-TH label rendered with the
bundled shim.
Note: DateString/TimeString (selected ZonedDateTime + timezone) and the
locale-prop-driven MonthSeparator still format via native Date; those are
a separate, larger surface left for a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frame it as the internal Gregorian-only date engine for callers who use non-Temporal value formats (Date/object/string) on hosts without native Temporal and don't want the @js-temporal/polyfill dependency — not as a "Gregorian Temporal" picked for calendar reasons. It is never auto-selected; resolveTemporal uses what you pass, else native Temporal, else throws. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resolveTemporal now falls back to the bundled Gregorian shim when no Temporal is provided and none is native — but only for non-Temporal value formats (`object`, `Date`), whose values never expose Temporal objects to the caller. So a `format="Date"` consumer on a host without native Temporal works with no `temporal` prop and no `@js-temporal/polyfill`. Temporal value formats (PlainDate, PlainYearMonth, …) still throw when no real Temporal is available: the caller wants real Temporal objects and the shim is not one, so silently substituting it would be wrong. The calendar passes the resolved `format` into resolveTemporal for this decision. Updates the shim docs (module + public export) to describe the auto-selection. resolveTemporal is not a public export, so the new optional `format` param is non-breaking. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oral BREAKING CHANGE: the library no longer bundles a `Temporal` implementation. Provide one via the `temporal` prop (or run where native `Temporal` exists). Two recommended polyfills: - temporal-polyfill https://github.com/fullcalendar/temporal-polyfill - @js-temporal/polyfill https://github.com/js-temporal/temporal-polyfill The `Temporal` value export is removed from the package barrel and from every `@klinking/colander/*` format subpath. resolveTemporal reverts to provided -> native -> throw (no shim fallback); the error message points to the two polyfills. The `temporal` prop docs document them. Drops the temporal-polyfill module + test, the "mini polyfill" entry in the test matrix, and the shim-specific tests. The month label still formats through the provided Temporal (PlainDate.toLocaleString), which both recommended polyfills implement. Removing the shim also drops the ~148KB Gregorian engine from the bundle. Follow-up tracked in #16: run the whole suite against both polyfills. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dogmar
commented
Jun 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit fix C, which grew into a Temporal-provisioning change. Breaking.
Summary
Fix non-Gregorian locale values (the original audit finding).
onMonthChange,rootState.viewing, andNavButtonState.targetwere built withPlainYearMonth.from({ year, month, calendar: localeCalendar })— feeding ISO year/month numbers in as locale-calendar fields. Under a real Temporal implementation this reinterprets them (e.g.th-THBuddhist: ISO June 2026 →1483-06-10[u-ca=buddhist], ~543 years off). These values are now built in ISO; the locale only affects display.Remove the bundled Temporal shim; require a user-provided
Temporal. The library no longer ships aTemporalimplementation.Breaking change
The
Temporalvalue export is removed from the package barrel and from every@klinking/colander/*format subpath. Consumers must now provide aTemporalimplementation via thetemporalprop (or run where nativeTemporalexists).resolveTemporalisprovided → native globalThis.Temporal → throw; the error and thetemporalprop TSDoc point to two recommended polyfills:temporal-polyfill(fullcalendar)@js-temporal/polyfill(js-temporal)This deletes the
temporal-polyfillmodule (~960 LOC incl. test) and drops the ~148KB Gregorian engine the audit flagged from the bundle.Display formatting
MonthYearStringnow formats the month label through the provided Temporal —T.PlainDate.from({ year, month, day: 1 }).toLocaleString(locale, opts)— instead ofnew Date(...). (NotPlainYearMonth.toLocaleString, which throws on any iso8601-vs-locale calendar mismatch — even en-US; an iso8601PlainDateis the documented Intl exception and adopts the locale's calendar, soth-THstill renders the Buddhist era2569.) Both recommended polyfills implement this.Notes / decisions
temporalprop stays optional in the type — nativeTemporalis still valid; the requirement is enforced at runtime and documented. No hard peerDependency added (two valid packages + native; documented instead).calendarForLocale(which only existed to feed the buggy calendar injection) was removed.@js-temporal/polyfill; the "mini polyfill" matrix entry and shim-specific tests are gone. Follow-up Run the full test suite against both temporal-polyfill and @js-temporal/polyfill #16 tracks matrixing the whole suite over both polyfills.ZonedDateTime+ timezone, and a locale-prop-driven component):DateString/TimeStringandMonthSeparatorstill format via nativeDate.Tests
onMonthChangeandrootState.viewingemit ISO (RED before the fix);NavButtonState.targetis ISO; the label still localizes to the Buddhist era; a controlled round-trip doesn't jump centuries.vp run ready: lint+typecheck 0/0, all suites green.🤖 Generated with Claude Code