Repository navigation
[scheduler] Format the dates with Intl and number the weekdays like ISO 8601 - #24021
Conversation
Deploy previewBundle size
Check out the code infra dashboard for more information about this PR. |
…SO 8601 - `dateLocale` takes a locale string, like `Intl.DateTimeFormat`, instead of a `date-fns` locale object, and so does `createDateLocaleTheme()`. - The labels are formatted with `Intl.DateTimeFormat` through `formatPlainDate()` and `formatTime()`, instead of the formats of the adapter. - `weekStartsOn` goes from 1 (Monday) to 7 (Sunday), and defaults to the first day of the week of `Intl.Locale.prototype.getWeekInfo()`. - The week numbers follow the first week of the year of the region of the locale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `formatTime()` takes minutes since midnight and caches its labels per locale, like the hour labels before. - One `weekInfo` selector gives `getWeekNumber()` the first day of the week and the minimal days of the first week. - The text levels of the Timeline header name a format instead of a function. - `formatProcessedDate()` formats the date of a processed date with any format, and `formatWeekdayAndDayOfMonth()` builds the labels of the day headers. - The agenda formats its month and year together, in the order of the locale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e locale Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the locale Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anges with Intl - `ampm` defaults to the hour cycle of the `dateLocale`, read from `Intl.DateTimeFormat`, instead of `true`. - The week labels of the Timeline use `Intl.DateTimeFormat#formatRange()` instead of a hard-coded separator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`formatPlainDate(locale, plainDate, format)` read its options from a table of every format, so each bundle kept all of them. Each format is now its own function, such as `formatMonthShort(locale, plainDate)`, with its own options, and the bundles keep only the ones they call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The formatters take a `Date` rather than its timestamp: the `Intl` of a fake clock, like the one of the screenshot tests, formats the current date instead of a timestamp of `0`, so midnight showed the current time. - Format the preset selector of the Timeline with Prettier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6747ff7 to
5fca89f
Compare
rita-codes
left a comment
There was a problem hiding this comment.
PR review
Nothing merge-blocking. The week-number math was brute-forced against date-fns getWeek() for every date from 1995 to 2035, every first day and every minimal-days value (733,775 cases, no mismatch), the region table of the first week matches the CLDR data of Node's ICU for all 676 two-letter regions, every PlainDate handed to a formatter was traced to the right timezone, and the full scheduler jsdom suite (4,009 tests), TypeScript, ESLint and Prettier pass. The Intl path is also 2–3× cheaper per label than the adapter it replaces, with no change in the touched benches. What remains is a handful of labels that Intl writes differently from the bare numbers and times the views assume, in Danish, Czech, Norwegian, Japanese, Chinese and Korean mostly, plus the Firefox default and a few untested behavior changes. All verified by running Intl in Node 22 (ICU 78) and the pinned Chromium.
Bugs (7)
1. 🟡 formatDayOfMonth() keeps the day suffix of the locale, so the grids read 13. or 13日
Location: packages/x-scheduler-internals/src/internals/utils/date-locale.ts:155
const DAY_OF_MONTH: Intl.DateTimeFormatOptions = { day: 'numeric' };{ day: 'numeric' } renders the CLDR pattern of a standalone day, which is a bare number in English, French, German or Arabic but carries a literal in several locales. The adapter's dayOfMonth (d) was a bare number everywhere. Every month cell, mini-calendar button, week-view header number, agenda day number and Timeline dayAndMonth day cell goes through this function.
| locale | formatDayOfMonth |
formatToParts(...).day |
|---|---|---|
| cs-CZ, da-DK, nb-NO, sk-SK | 13. |
13 |
| ja-JP, zh-CN | 13日 |
13 |
| ko-KR | 13일 |
13 |
| ar-EG | ١٣ |
١٣ |
Failure scenario: <EventCalendar dateLocale="da-DK" /> shows 1. 2. … 31. in every month cell and mini-calendar button, and 13. under søn. in the week header; with ja-JP the mini calendar reads 1日 … 31日.
Fix: Keep the digits of the locale and drop the literal: getFormatter(locale, DAY_OF_MONTH).formatToParts(date).find((part) => part.type === 'day')!.value. (formatYear() has the same shape — 2025年, 2025년, 2025. in hu-HU — but a standalone year with its suffix is arguably right in the Timeline year row.)
2. 🟡 The time options pin the hour cycle and the hour width, which Intl resolves per locale
Location: packages/x-scheduler-internals/src/internals/utils/date-locale.ts:250
const TIME_12H: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit', hourCycle: 'h12' };
const TIME_24H: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit', hourCycle: 'h23' };Two issues in the same two lines:
hourCycle: 'h12'overrides the locales whose 12-hour clock is h11. Japanese writes the half hour after midnight午前0:30; withh12it becomes午前12:30.hour12: truelets CLDR pick h11 or h12 and gives the same output as today in every other probed locale (en, ko, zh, ar, hi, fr, es-MX, en-GB, de).hour: 'numeric'withh23pads the hour in some locales and not in others:07:30in en-US, hi-IN, ko-KR and ar-EG (٠٧:٣٠),7:30in fr-FR, de-DE, es-ES, it-IT, nl-NL, pt-BR, ja-JP, zh-CN, ru-RU, pl-PL, sv-SE, en-GB. master used the adapter'shours24h(H), so it showed7:30everywhere; the PR body's "07:30 instead of 7:30 in 24-hour format" only holds for the first group.hour: '2-digit'gives07:30in all of them, which is also the 24-hour convention of fr/de/es, and the time axis keeps one label width.
Failure scenario: dateLocale="ja-JP" with the 12-hour preference labels 00:30 as 午前12:30; dateLocale="fr-FR" shows a time axis of 7:00, 8:00, … 13:00 while an en-US one shows 07:00.
Fix: TIME_12H = { hour: 'numeric', minute: '2-digit', hour12: true } and TIME_24H = { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }; update date-locale.test.ts:147 ('0:00' → '00:00'), StandaloneWeekView.test.tsx:116 (7:30 à 8:30 → 07:30 à 08:30) and the changelog bullet.
3. 🟡 formatWeekdayAndDayOfMonth() concatenates the two parts in English order
Location: packages/x-scheduler/src/internals/utils/date-utils.ts:13
export function formatWeekdayAndDayOfMonth(locale: string, plainDate: PlainDate): string {
return `${formatWeekday(locale, plainDate)} ${formatDayOfMonth(locale, plainDate)}`;
}It builds the aria-label of the agenda day headers (AgendaView.tsx:287) and of the week-view column headers (DayTimeGrid.tsx:484). It's the last hand-assembled label of the PR, and its order and punctuation are wrong outside Western European locales:
| locale | concatenation | { weekday: 'long', month: 'long', day: 'numeric' } |
|---|---|---|
| ja-JP | 日曜日 13日 |
7月13日日曜日 |
| zh-CN | 星期日 13日 |
7月13日星期日 |
| ko-KR | 일요일 13일 |
7월 13일 일요일 |
| de-DE | Sonntag 13 |
Sonntag, 13. Juli |
| he-IL | יום ראשון 13 |
יום ראשון, 13 ביולי |
| ar-EG | الأحد ١٣ |
الأحد، ١٣ يوليو |
Failure scenario: A Japanese screen-reader user hears the weekday before the day number, the reverse of how the date is spoken, on every column of the week view.
Fix: One Intl options object in date-locale.ts, { weekday: 'long', month: 'long', day: 'numeric' } (or reuse formatFullDate() as the mini calendar does for its day cells), and delete the helper. Tests pinning the old label: EventCalendar.test.tsx:227 ('Friday 30') and WeekView.test.tsx:277 ('Sunday 4').
4. 🟡 The Monday fallback of getLocaleWeekInfo() is what every supported Firefox gets
Location: packages/x-scheduler-internals/src/internals/utils/date-locale.ts:59
// Like the ISO 8601 weeks, for the browsers that know neither.
firstDay: (intlWeekInfo?.firstDay ?? 1) as WeekStartsOn,Intl.Locale.prototype.getWeekInfo() landed in Firefox 153 (July 2026) and Firefox never shipped the weekInfo accessor. The repo's .browserslistrc supports Firefox 121–148, so in every supported Firefox the default en-US calendar starts its weeks on Monday, while Chrome, Safari and Node start them on Sunday. With SSR, Node renders a Sunday-first grid and the Firefox client hydrates a Monday-first one: the whole month grid mismatches, not just a text node. The docs sentence "the browsers that don't support it start the weeks on Monday" describes it, but it's the default locale in a supported browser.
Failure scenario: A US user on Firefox 148 opens the Event Calendar with no preferences and gets Monday-first weeks and no checked "Start week on" radio; the same page in Chrome starts on Sunday.
Fix: The PR already ships a CLDR region table for minimalDays; the first day needs one of the same size when neither API exists: 56 regions start on Sunday (AG AS BD BR BS BT BW BZ CA CO DM DO ET GT GU HK HN ID IL IN IS JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW), 14 on Saturday (AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY), MV on Friday, the rest on Monday (CLDR 48, same source as the existing table). Keep getWeekInfo() first, and read the fw extension from the tag in the fallback like rg, since de-DE-u-fw-sun gives 7 with the API and 1 without it today.
5. 🟡 dateLocale is never validated: a bad tag crashes with a bare RangeError, an unknown one silently mixes US formats with Monday weeks
Location: packages/x-scheduler-internals/src/internals/utils/SchedulerStore/SchedulerStore.ts:316
dateLocale: parameters.dateLocale ?? DEFAULT_DATE_LOCALE,The string is used as is. dateLocale="en_US" (the underscore form most backends hand out) throws RangeError: Incorrect locale information provided from new Intl.Locale() inside the weekInfo selector during render, with nothing pointing at the prop. dateLocale="english", "xx-XX" or "en-EN" are valid tags ICU doesn't know: Intl.DateTimeFormat.supportedLocalesOf() returns [], the labels fall back to en-US (Friday, January 1, 2027, 1:30 PM), but getWeekInfo() falls back to CLDR 001, so the weeks start on Monday with US formats and nothing warns.
Failure scenario: <EventCalendar dateLocale={user.locale} /> with a backend locale "en_US" unmounts the calendar in development with a message that doesn't mention dateLocale; "english" renders a plausible but mixed calendar.
Fix: A dev-only check where the parameter enters the state: Intl.getCanonicalLocales(dateLocale) in a try/catch and Intl.DateTimeFormat.supportedLocalesOf(dateLocale).length === 0, each with a Scheduler-prefixed message (MUI X Scheduler: dateLocale "en_US" is not a valid BCP 47 tag, use "en-US"), once per value. Storing the canonical tag would also key the four caches by one string per locale.
6. 🟡 Persisted weekStartsOn: 0 crashes in development and silently works in production
Location: packages/x-scheduler-internals/src/scheduler-selectors/schedulerPreferenceSelectors.ts:22
return weekStartsOn === undefined ? localeWeekInfo : { ...localeWeekInfo, firstDay: weekStartsOn };The documented preferences + onPreferencesChange pattern stores the preferences, so every app that did so with the previous alpha has weekStartsOn: 0 in localStorage or a database. After upgrading, checkWeekStartsOn() throws error 278 from a view selector in development (the calendar unmounts, and the message doesn't say that 0 used to be Sunday), while in production the modulo arithmetic of getStartOfWeek() accepts 0 as Sunday and only the "Start week on" radio of the preferences menu stays unchecked (PreferencesMenuItems.tsx:253 compares with 7). The changelog shows the prop literal but not the persisted values.
Failure scenario: An app persisting preferences upgrades: it works in production, and every developer's machine throws on the first render.
Fix: Normalize in weekInfoSelector: firstDay: weekStartsOn === 0 ? 7 : weekStartsOn with a one-time dev warning ("weekStartsOn: 0 is deprecated, use 7 for Sunday"), keeping error 278 for the other invalid values; and a changelog line about migrating persisted preferences.
7. 🟡 Locales with native digits now mix two numbering systems in one view
Location: packages/x-scheduler/src/event-calendar/header-toolbar/HeaderToolbar.tsx:164
{`${localeText.week} ${weekNumber}`}The dates now come out in the numbering system of the locale, but every number the Scheduler builds itself stays a JS number in ASCII digits: the week badge here and in MonthViewWeekRow.tsx:76, hiddenEvents(count) in MonthViewCell.tsx:283, and recurrenceMonthlyPresetLabel(startPlainDate.day) / recurrenceMonthlyDayOfMonthLabel(dayOfMonth) in event-dialog/utils.ts:316 and RecurrenceTab.tsx:445. master was consistently ASCII, since date-fns formats d as 13 in arSA and faIR too (verified).
Failure scenario: dateLocale="ar-EG" shows month cells ١٣, times ١:٣٠ م and agenda numbers ١٣ next to Week 29, 3 more.. and Repeats monthly on day 13. Same for fa-IR, bn-BD, mr-IN, ne-NP, my-MM.
Fix: A cached formatNumber(locale, value) on new Intl.NumberFormat(locale, { useGrouping: false }) in date-locale.ts (٢٩ / ۲۹ / ২৯, and 29 for en/de/hi, verified) for the week number and the hidden count; the two monthly labels can take formatDayOfMonth(locale, plainDate) once Bugs 1 strips the literal.
Tests (5)
1. 🟡 The headline week-number change has no test
Location: packages/x-scheduler/src/event-calendar/header-toolbar/HeaderToolbar.test.tsx:259
The PR body's example is "with dateLocale="en-US" and weekStartsOn: 1, Friday, January 1st, 2027 is in week 1 instead of week 53". The it.each only varies dateLocale, and date-utils.test.ts:73 covers { firstDay: 1, minimalDays: 4 }, { 7, 1 } and { 5, 4 }, never minimalDays: 1 with a non-Sunday start, which is what weekInfoSelector produces for every weekStartsOn override in en-US.
Failure scenario: A regression that rebuilds the ISO rule when weekStartsOn is set (the previous behavior) passes CI.
Fix: In date-utils.test.ts, { firstDay: 1, minimalDays: 1 }: 2026-12-27 → 52, 2026-12-28 → 1, 2027-01-01 → 1; { firstDay: 6, minimalDays: 1 }: 2026-12-25 → 52, 2026-12-26 → 1 (verified). In the it.each, a row { dateLocale: 'en-US', weekStartsOn: 1, expected: 'Week 1' }.
2. 🟡 The preferences menu checking the locale defaults is untested
Location: packages/x-scheduler/src/event-calendar/header-toolbar/preferences-menu/PreferencesMenu.test.tsx:213
The PR body lists "the preferences menu checks the first day of the week and the time format of the locale when weekStartsOn or ampm isn't set" as a behavior change. The only aria-checked test sets weekStartsOn: 1 explicitly, and the 12-hour / 24-hour radios have no checked-state test at all.
Failure scenario: Reverting PreferencesMenuItems.tsx:253 to preferences.weekStartsOn === value leaves no radio checked by default and every test green.
Fix: Two tests without preferences: en-US → the Sunday and 12-hour radios are aria-checked="true"; dateLocale="fr-FR" → Monday and 24-hour.
3. 🟡 The month label in the display timezone is untested
Location: packages/x-scheduler/src/event-calendar/header-toolbar/HeaderToolbar.test.tsx:242
The toolbar, the side panel and the mini calendar now format visiblePlainDate (display timezone) instead of the visibleDate value; the PR body lists it as a behavior change. No test renders with a display timezone that moves the month, and the agenda's new July 2025 (was July, 2025) is asserted nowhere.
Failure scenario: visibleDate at 2025-07-31T23:00:00Z with displayTimezone="Asia/Tokyo" must read August 2025; the old code read July 2025 and nothing would catch it coming back.
Fix: One toolbar test with that visibleDate and timezone, and an agenda assertion on the month-and-year label.
4. 🟡 "Should format the date itself, whatever its timezone" can't fail
Location: packages/x-scheduler-internals/src/internals/utils/date-locale.test.ts:91
The suite runs under TZ=UTC and the test doesn't change the system timezone, so new Date(dayNumber * MS_PER_DAY) formats the same with or without timeZone: 'UTC' in getFormatter(). The createLabelDate test it replaces used setSystemTimezone('America/Los_Angeles').
Failure scenario: Dropping timeZone: 'UTC' from getFormatter() passes this test (the component-level tests under setSystemTimezone still catch it, so not blocking).
Fix: it.skipIf(!isJSDOM) with setSystemTimezone('Pacific/Apia') before the assertion; the file passes under TZ=America/Los_Angeles already.
5. 🟡 Three tests depend on locales nobody else has cached
Location: packages/x-scheduler-internals/src/internals/utils/date-locale.test.ts:70
vitest.shared.mts:117 sets isolate: false, so the module caches of date-locale.ts are shared by every test file in a worker. The fallback test (:58, en-CA / de-AT), the "once" test (:71, vi.spyOn(Intl, 'Locale') expecting one call for de-DE) and the fake-timers test (:159, en-CA) all need their locale to be uncached. They pass today because no other scheduler test uses those tags.
Failure scenario: The first component test that renders with dateLocale="de-DE" (the deDE locale text is already used in StandaloneWeekView.test.tsx) makes :75 fail with "expected 0 to equal 1" depending on file order.
Fix: Private-use tags unique to each test ('de-DE-x-once', 'de-AT-x-fallback', 'en-CA-x-faked'): Intl ignores -x- subtags and getLocaleWeekInfo() already strips them, so the behavior is identical and the cache key is unique. Or export a resetDateLocaleCaches() test hook.
ℹ️ Smaller coverage notes
schedulerPreferenceSelectors.weekInfohas no test (en-US→{ 7, 1 },fr-FR→{ 1, 4 },en-US+weekStartsOn: 1→{ 1, 1 }), and no view test asserts a locale-driven first column (dateLocale="fr-FR", no preference, first header matches/lundi/).- The yearly recurrence label (
formatMonthAndDayOfMonth: "July 3" / "3 juillet") is only matched by/repeats annually/i; one unit test ofgetRecurrenceLabel()inen-USandfr-FRpins the new order. - No test shows a non-Latin numbering system passing through the grids;
formatDayOfMonth('ar-EG', …)→'١٣'andformatTime('ar-EG', 785, true)→'١:٠٥ م'are stable across Node and Chromium.
Simplifications (2)
1. ℹ️ The formatter cache is a Map of Maps keyed by the identity of the options object
Location: packages/x-scheduler-internals/src/internals/utils/date-locale.ts:87
It works because every caller passes a module constant; an inline options object would leak a formatter per call. A createDateFormat(options) factory returning (locale, plainDate) => string with its own Map<locale, DateTimeFormat> replaces the twelve constant-plus-function pairs with one line each (export const formatWeekday = /* @__PURE__ */ createDateFormat({ weekday: 'long' })), about 40 lines less and one cache level; the /* @__PURE__ */ annotation the codebase already uses in base-temporal/ keeps the tree-shaking the PR body cares about. formatTime() and the range formatter need the shared formatter, so the factory has to expose it.
2. ℹ️ Two small ones
getLocaleAmpm()(date-locale.ts:81):resolvedOptions().hour12is always a boolean whenhouris requested (probed 60 locales, including thehc-h11/h12/h23extensions), so?? falseis dead; a reader assumes a real case exists.CalendarGridHeaderCell.tsx:116computesformatWeekday()for thearia-labeleven when the caller passes one, sinceelementPropswins in the merge;elementProps['aria-label'] ?? formatWeekday(…)skips it.
Docs (4)
1. 🟡 The accessibility page still has the date-fns ordinal
Location: docs/data/scheduler/accessibility/accessibility.md:72
for example `"Running, 7:30 AM to 8:30 AM, Monday, May 26th, 2025, Resource: Sport, Recurring"`formatFullDate('en-US', …) gives Monday, May 26, 2025; the PR updated the same example in event-accessible-name.ts:27 but not here. Line 172 says the popover header shows "the full formatted date (for example, "Monday, May 26")" while MoreEventsPopover.tsx:207 renders Mon, May 26 (pre-existing, but the PR touches the component).
Fix: May 26th, 2025 → May 26, 2025; "Monday, May 26" → "Mon, May 26" and drop "full".
2. 🟡 Two test titles still say weekStartsOn is 0
Location: packages/x-scheduler/src/week-view/tests/WeekView.test.tsx:423
'shows Sunday as the first column when weekStartsOn is 0' and MiniCalendar.test.tsx:279 'should show Sunday as the first weekday header when weekStartsOn is 0' both pass weekStartsOn: 7 now.
Fix: is 0 → is 7 in both.
3. 🟡 The dateLocale JSDoc omits the clock it now controls
Location: packages/x-scheduler/src/event-calendar/EventCalendar.tsx:131
* This affects day names, month names, the first day of the week, week numbers and the other
* locale-dependent formatting.The 12-hour or 24-hour default is the most visible new effect of the locale and the one that surprises first (fr-FR switches the whole calendar to 24-hour). Same sentence in SchedulerStore.types.ts:435, EventTimelinePremium.tsx and the standalone views, and in the generated API JSON.
Fix: "…the first day of the week, the week numbers, the 12-hour or 24-hour clock and the other locale-dependent formatting." then pnpm proptypes and pnpm docs:api.
4. ℹ️ Smaller doc notes
date-locale.ts:30: the region table says "from the week data of CLDR" without a version; "minDays=4 insupplemental/weekData.xmlof CLDR 48 (ICU 78)" tells the next reader what to re-check. The Monday fallback comment (:58) could name who hits it (Firefox < 153).localization.md:97(both products): the code honors and tests-u-fw-mon,-u-rg-gbzzzz,-u-ca-iso8601and-u-hc-h23, and they are the only way to get ISO week numbers inen-USor a 12-hourfr-FRwithout the preferences; one sentence with"en-US-u-ca-iso8601"as the example would surface it.- The PR body's changelog bullet "
07:30instead of7:30in 24-hour format" is only true for some locales until Bugs 2 lands; the/simplify"Skipped" bullet still listsformatRange()for the week ranges andampmfrom the locale, which the PR applies;CalendarGrid.HeaderCelldroppingariaLabelFormatand taking aSchedulerProcessedDayis listed under "Leftovers" though it changes an exported component of the internals package. - Follow-up idea rather than a nit: now that
dateLocaleis a string, the locale files (frFR,heIL, …) could carrydefaultProps.dateLocale, which turns "French buttons, English dates" from a documented pitfall into a non-issue.
Verdict
Approve after nits — the day numbers, the time options, the hand-built weekday label and the ASCII counters write locale literals, English order or mixed digits in Danish, Czech, Norwegian, Japanese, Chinese, Korean and Arabic, and the default week start differs between Firefox and the other supported browsers.
🤖 Review generated with Claude Code
- Format the day of the month without the literal of the locale, like the `13.` of Danish or the `13日` of Japanese. - Let the locale pick the 12-hour cycle (`h11` in Japanese), and pad the hour of the 24-hour times in every locale. - Label the week view columns and the agenda days with the weekday, the month and the day in the order of the locale. - Start the weeks on the first day of the region and of the `fw` extension in the browsers without `getWeekInfo()`, like Firefox. - Warn in development when `dateLocale` is invalid or unsupported. - Write the week numbers in the digits of the locale. - Create the formatters per format instead of per options object. - Test the week numbers with another first day, the defaults of the preferences menu, the month in the display timezone, the weeks of the locale, and the formats in a system timezone; isolate the tests that need an uncached locale. - Update the docs: the accessible names, the `dateLocale` JSDoc and the extensions of the locale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. The fixes are in 0b82385, "Apply the review of the Intl formatting". The PR description is updated as well. Bugs
Tests
Simplifications
Docs
The scheduler jsdom and browser suites, TypeScript, ESLint, Prettier and Vale pass. A few jsdom tests time out under the load of the full run and pass on their own. The bundles grow by about 400 B gzip over the previous head, mostly from the region table and the formatter factories. 🤖 Comment generated with Claude |
rita-codes
left a comment
There was a problem hiding this comment.
PR review (follow-up on 0b82385)
I re-ran the review on the fixes. Everything applied checks out: the scheduler jsdom suite (2988 tests), tsc, ESLint and Prettier pass on the new head, and I probed Intl in Node (ICU 78) for 36 locales. The new region tables match ICU's weekInfo for all 649 canonical two-letter regions, ICU honors both fw and rg in weekInfo, so the Firefox fallback agrees with Chrome and Safari on those tags, hour: '2-digit' pads to 07:30 in every locale, hour12: true gives 午前0:30 in Japanese, the day part of formatToParts() is the bare number in cs/da/nb/ja/zh/ko and native digits in ar/fa/bn, and { weekday, month, day } orders correctly in all 36 locales (Sonntag, 13. Juli, 7月13日日曜日, 13 Temmuz Pazar, الأحد، ١٣ يوليو). One gap left in the fallback, non-blocking.
Bugs (1)
1. 🟡 The Firefox fallback keeps the region's first day for -u-ca-iso8601
Location: packages/x-scheduler-internals/src/internals/utils/date-locale.ts:130
firstDay: (intlWeekInfo?.firstDay ??
(FIRST_DAY_EXTENSION_VALUES.indexOf(getUnicodeExtension(tag, 'fw') ?? '') + 1 ||
getRegionFirstDay(region))) as WeekStartsOn,ICU forces Monday for the iso8601 calendar unless fw is set (new Intl.Locale('en-US-u-ca-iso8601').weekInfo → { firstDay: 1, minimalDays: 4 }, and …-fw-sun → firstDay: 7). The fallback only applies iso8601 to minimalDays, so without getWeekInfo() it returns { firstDay: 7, minimalDays: 4 } for en-US-u-ca-iso8601 (and for ja-JP-u-ca-iso8601).
Failure scenario: the localization page now recommends "en-US-u-ca-iso8601" to number the weeks like ISO 8601. In Firefox ≤ 152 the week view starts on Sunday with minimalDays: 4, which is neither ISO numbering nor what Chrome/Safari render for the same tag, so an SSR page hydrated in Firefox shifts the whole grid by a day.
Fix: apply the calendar before the region, after fw to keep ICU's precedence:
FIRST_DAY_EXTENSION_VALUES.indexOf(getUnicodeExtension(tag, 'fw') ?? '') + 1 ||
(intlLocale.calendar === 'iso8601' ? 1 : getRegionFirstDay(region))and add to the fallback test: expect(getLocaleWeekInfo('en-US-u-ca-iso8601-x-fallback')).to.deep.equal({ firstDay: 1, minimalDays: 4 }).
Tests (0)
No findings.
Simplifications (1)
1. ℹ️ checkDateLocale() runs two Intl lookups on every parameters update in development
Location: packages/x-scheduler-internals/src/internals/utils/SchedulerStore/SchedulerStore.ts:315
deriveStateFromParameters() is called from updateStateFromParameters(), which useEventCalendar runs after every render, so Intl.getCanonicalLocales() and supportedLocalesOf() run per render in dev (production is untouched). A Set of checked tags next to the other lookups makes it one-shot, like warnOnce already is for the message. Optional.
Docs (0)
No findings. The fw / hc-h12 examples of the localization pages are verified (fr-FR-u-hc-h12 resolves hour12: true); the ca-iso8601 example holds once Bug 1 is fixed.
The items left out are fine with me: documenting the migration of a stored weekStartsOn: 0 instead of reading it, keeping hiddenEvents(count) and the monthly recurrence labels on plain numbers because of the public locale text types, and the ?? false of getLocaleAmpm().
Verdict
Approve after nits — only the iso8601 first day of the fallback is left, a one-line fix.
🤖 Review generated with Claude Code
Like `getWeekInfo()`, the fallback reads the `fw` extension, then the `rg` extension, then the Monday of the `iso8601` calendar, then the region of the locale. `checkDateLocale()` checks a locale once, as the store checks its parameters on every render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR moves the date formatting of the Scheduler from the date adapter to
Intl.DateTimeFormat, and the week start to ISO weekdays.Changes
dateLocaleis a locale string. The prop takes the locales ofIntl.DateTimeFormat, such as"fr-FR", instead of adate-fnslocale object, and defaults to"en-US". So doescreateDateLocaleTheme(). The store keeps the locale in its state, and the adapter no longer carries one.Intl.DateTimeFormat. One function per format, such asformatMonthShort(locale, plainDate),formatFullDate(locale, plainDate)orformatTime(locale, minutesInDay, ampm), replacesadapter.format(),adapter.formatByString()andadapter.formatsin every view, the header toolbar, the mini calendar, the event dialog, the accessible names of the events and the headers of the Timeline. Each function has its own options, so a bundle keeps only the formats it uses, and itsIntl.DateTimeFormatis created once per locale. They format the plain date of a day, or the wall-clock time of a processed date, in UTC and on the Gregorian calendar, so the system timezone and the default calendar of the locale can't change a label. The week labels of the Timeline useformatRange(), so the locale writes the separator and the parts the two dates share once. The day of the month drops the literal some locales add, like the13.of Danish or the13日of Japanese, and the accessible names of the week view columns and of the agenda days write the weekday, the month and the day in the order of the locale. The week numbers use the digits of the locale, like the dates.ampmfollows the locale. Without the preference, the times use the hour cycle of thedateLocale, fromIntl.DateTimeFormat#resolvedOptions(): 12-hour inen-US, 24-hour infr-FRoren-GB. The 12-hour times follow the cycle of the locale (午前0:30in Japanese), and the 24-hour times pad the hour in every locale, so the time axis keeps one label width.dateLocaleis checked in development. An invalid tag, like"en_US", or a localeIntldoesn't support logs a warning that names the prop. Each locale is checked once.weekStartsOnuses ISO weekdays. The preference goes from1(Monday) to7(Sunday). Without it, the weeks start on the first day of the week ofIntl.Locale.prototype.getWeekInfo(), or of theweekInfoaccessor of older browsers, and in the browsers that have neither, like Firefox, on the first day of thefwextension, of thergregion, of the ISO 8601 calendar (Monday) or of the region of the locale, in that order, likegetWeekInfo(), from a table of the regions of CLDR that don't start on Monday.schedulerPreferenceSelectors.weekInforesolves it with the first week of the year, so the helpers that take a week start no longer take the adapter or an undefined week start.getWeekNumber(plainDate, weekInfo)counts the first week of a year as the first one withminimalDaysdays in that year. Browsers dropped theminimalDaysofgetWeekInfo(), so they come from the regions of CLDR whose first week has 4 days, with thergregion override and theca-iso8601calendar of the locale. The other regions count the week of January 1st. With weeks starting on Monday and 4 minimal days, it's the ISO 8601 week number.weekendofgetWeekInfo().createLabelDate(),formatWallClockHour()andformatHourAndMinutes()go: plain dates and wall-clock times are formatted directly, andformatTime()caches the time labels. The text levels of the Timeline header take the format function of their label. The preferences menu checks the week start in use, including the one of the locale.CalendarGrid.HeaderCelltakes a day. The internal component takes a day with its plain date and labels it with its weekday unlessaria-labelis set, instead of anariaLabelFormatof the adapter.No file of the BaseTemporal copy changes.
Changelog
Breaking changes
The
dateLocaleprop andcreateDateLocaleTheme()take a locale string, like the locales ofIntl.DateTimeFormat, instead of adate-fnslocale object:The
weekStartsOnpreference uses ISO weekdays, from1(Monday) to7(Sunday). Sunday is7instead of0:Migrate the preferences you store, for example from
onPreferencesChange: in development,weekStartsOn: 0throws error 278.Without
weekStartsOn, the weeks start on the first day of the week of the locale inIntl.Locale.prototype.getWeekInfo(). Browsers that don't support it, like Firefox, use the first day of the week of the region of the locale.Without
ampm, the times use the hour cycle of the locale instead of a 12-hour clock:dateLocale="fr-FR"shows "13:30" instead of "1:30 PM". The default value ofdefaultPreferencesno longer includesampm: true.The week numbers follow the first week of the year of the locale, also when
weekStartsOnis set. They used the ISO 8601 rule then: withdateLocale="en-US"andweekStartsOn: 1, Friday, January 1st, 2027 is in week 1 instead of week 53.The dates use the formats of
Intl.DateTimeFormatfor the locale, on the Gregorian calendar. For example, in English: "Thursday, July 3, 2025" instead of "Thursday, July 3rd, 2025", "Ends Dec 31" instead of "Ends 31 Dec", "Annually on December 13" instead of "Annually on 13 December", "July 2025" instead of "July, 2025" in the agenda, "07:30" instead of "7:30" in 24-hour format, "Sat, Dec 24 – Fri, Dec 30" instead of "Sat, Dec 24 - Fri, Dec 30" in the week row of the Timeline (an en dash between thin spaces), and "Sunday, May 4" instead of "Sunday 4" for the accessible names of the week view columns and of the agenda days. The days of the month and the week numbers use the digits of the locale, like١٣in Arabic (Egypt).Behavior changes
The breaking changes above, and:
visibleDatevalue.weekStartsOnorampmisn't set. It checked no week start before.Adapter usage
Counted like in #23943. The references go from 101 to 43.
format,formatByString,formats,getCurrentLocaleCode,getDayOfWeekandgetWeekNumberare gone, and so is theisWeekendof a comment, so the Scheduler uses 8 methods and properties instead of 15.formatformatsformatByStringgetCurrentLocaleCodegetDayOfWeekgetWeekNumberisWeekendThe ones left read and build the adapter values of the public API:
getTime,setTimezone,getTimezone,addMilliseconds,date,isValid,toJsDateandisSameDay.Bundle size
Measured from source with esbuild, minified, with third-party packages external and without the error minifier, gzip level 9. The formats and the tables of the weeks of the regions are new code; the adapter formatting they replace lives in
date-fns, outside the bundles.event-calendarevent-calendar-premiumevent-timeline-premiumThese entries render most of the formats. A bundle that imports only
formatMonthShortkeeps only its options: the Event Calendar has neither the range nor the short month of the Timeline.Validation
pnpm test:unit --project "x-scheduler*" --runandpnpm test:browser --project "x-scheduler*" --runpass. A few tests time out under the load of the full jsdom run and pass on their own.New tests:
fw,rgandca-iso8601extensions, and the fallback withoutgetWeekInfo().ampmselectors without the preference, in English and in French.dateLocale.The tests of
getWeekNumber(),getPlainDateWeekStart(), the recurrence presets and the header walk use ISO weekdays and minimal days. The tests that switched the locale of the adapter go, as the adapter has none.The expected labels follow
Intl.DateTimeFormat: no ordinals in the full dates, "Dec 31", "07:30".pnpm proptypes,pnpm docs:api,pnpm docs:typescript:formattedandpnpm extract-error-codeswere run. The message of error 278 says thatweekStartsOngoes from 1 (Monday) to 7 (Sunday); its code doesn't change.A
/simplifypass with four reviews (reuse, simplification, efficiency, altitude):formatTime()takes minutes and caches its labels per locale, like the hour labels before; oneweekInfoselector feedsgetWeekNumber(); the text levels of the Timeline header name a format;formatProcessedDate()andformatWeekdayAndDayOfMonth()replace copies; the agenda formats its month and year together.SchedulerProcessedDate, which changes a public type before the internal migration ends; making the adapter a module constant everywhere, which belongs with the removal of the adapter; formatting the week ranges of the Timeline withformatRange()and readingampmfrom the locale, which change behavior and were applied afterwards.Three review rounds with Codex and the base-ui-review skill:
rgregion override, likeen-US-u-rg-gbzzzz, started the weeks on Monday but kept the first week of the United States. The region now comes from the extension.ca-iso8601calendar extension, which now uses the ISO 8601 first week.A review of the PR found labels that
Intlwrites differently from what the views assume, and the Monday fallback of Firefox. The fixes are in "Apply the review of the Intl formatting": the day of the month without its literal, the hour cycle and the padding of the times, the order of the weekday labels, the first day of the region withoutgetWeekInfo(), the check ofdateLocale, the digits of the week numbers, the formatters created per format, and the tests and docs it listed. Not applied: readingweekStartsOn: 0as Sunday, which would keep the old value working (the changelog says to migrate it instead); the digits of the counts of the locale texts, likehiddenEvents(count), which take numbers in the public locale text types.The follow-up review found that the fallback without
getWeekInfo()kept the first day of the region for theiso8601calendar. It now follows the order of ICU, whichen-US-u-ca-iso8601,…-fw-sunand…-rg-egzzzztest.checkDateLocale()also checks each locale once.TypeScript, ESLint and Prettier pass.
I have followed (at least) the PR section of the contributing guide.
🤖 Generated with Claude Code