Repository navigation
feat(i18n,plugin-calendar,plugin-timeline): the calendar and the timeline start the week on the locale's first day (objectui#11675) - #11710
Conversation
…cale's first day (objectui#11675) `@object-ui/i18n` exports `firstDayOfWeek(locale)`, which reads the engine's `Intl.Locale` week info (`getWeekInfo()` or the `weekInfo` accessor) and falls back to CLDR's region table where the engine has neither. The calendar's month grid, weekday heads, week view, header range and date popover, and the timeline's "This week" / "Next week" buckets, all start the week there instead of on a hard-coded Sunday (calendar) or Monday (timeline). Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…rt in the helper, the calendar and the timeline (objectui#11675) The helper's engine and table paths, with a region-by-region parity case against the runtime's week data; every calendar site the week start moved (month grid and heads, week view and header range, a wrapped span's title day, the date popover); the timeline's week buckets under en-US, en-GB, zh-CN and ar-EG; and, in the driven zone pin, the bucket bounds across a DST change. The two header helpers in the 10144 and 10442 pins took the first day as an explicit argument: they modelled the Sunday-first heads the grid no longer draws for `de` / `fr-FR`. Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…k start, and declare the changeset (objectui#11675) Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs read: card #11675 (body and its four comments: ruling 6010771237, claim 6012945073, os-dev-report 6013850731, seat ACCEPT 6013900089); PR #11710 (body, 17-file list, net diff against ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
CI red on
|
…1675 patch round 1) Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…in expects each runtime's recorded differences (objectui#11675) CI's floating `22.x` Node ships CLDR 48.0, where Iceland's week starts on Sunday; the table transcribed CLDR 47, so the parity pin went red there while it stayed green on a CLDR 47 runtime. The table now declares the release it transcribes (`WEEK_DATA_CLDR`, 48) and carries Iceland on Sunday. The pin reads the runtime's release from `process.versions.cldr` and requires the differences it finds to EQUAL the ones a measured ledger records for that release (none on the table's own release), so it is deterministic on CLDR 46, 47 and 48 and still fails on real drift in either direction. Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs read: card #11675 (body and its six comments: ruling 6010771237, claim 6012945073, os-dev-report 6013850731, seat ACCEPT 6013900089, patch-round os-dev-report 6015022005, seat ACCEPT 6015076038); PR #11710 (body as corrected by the seat, the 17-file list, the net diff against ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11675
Clause-②: yes
The calendar and the timeline now start the week on the first day of the week of the user's locale, as ruling B on the card asks (the maintainer's 「同意」, recorded in comment 6010771237): under
en-USboth start on Sunday, underen-GBorzh-CNboth start on Monday, from the locale's week info. Before this, the calendar always started on Sunday and the timeline's "This week" always on Monday. No setting is added: the first day comes from the tag each component already formats its dates with.Session:
https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju(dispatched os-dev, seatdomain:ui#2, claim comment 6012945073).Premise check, on
origin/mainatdaa7cafCalendarView.tsx:getMonthDayspadded the first row byfirstDay.getDay(),getWeekStartstepped back bygetDay(), the month view's weekday heads ran from a reference Sunday, and a span wrapping into a new row was titled ongetDay() === 0.getWeekStartalso fed the week view's columns and the header's week range.ObjectTimeline.tsx's bucket bounds usedstartOfWeek = today - ((now.getDay() + 6) % 7) * day, a Monday start.git grepforweekInfo,getWeekInfo,firstDayOfWeekandweekStartsOnunderpackages/*/srcanswered nothing, with the control wordfirstDay(a local ingetMonthDays) answering.weekaxis, which counts plan weeks from the axis's first day and does not snap to a weekday by design (its own docblock inrenderer.tsxsays so). It is left as it is (see Acceptance notes).What changed
@object-ui/i18nexportsfirstDayOfWeek(locale)andWeekdayIndex(src/utils/first-day-of-week.ts). It returns 0 to 6 asDate.prototype.getDaynumbers weekdays, which is also react-day-picker'sweekStartsOn. Both plugins already import this package, and it hostsuseDisplayLocale, the locale both format with.Intl.Locale'sgetWeekInfo(), else theweekInfoaccessor (ISO 1 to 7, 7 is Sunday), so a-u-fw-keyword is honoured.weekDatafirst-day table, keyed by the tag's region or the onemaximize()gives it (enreadsUS,zhreadsCN). An unlisted region reads Monday, CLDR's world default001. This is the deterministic fallback, and the tests cover it.WEEK_DATA_CLDR(48, the release CI's floating22.xNode ships), and was generated from Node's ICU. Engines carry their own week data, and it differs: CLDR 48 moved Iceland to a Sunday start, and Chromium 141 startsAEon Saturday where Node's CLDR 46, 47 and 48 all start it on Monday. That is why the engine's answer wins wherever there is one. A parity case re-derives the table against the running engine, region by region (see Patch round 1).en_US) throwsIntl.Locale'sRangeError, astoLocaleDateStringdoes for the same tag in the same render. No day is guessed.@object-ui/plugin-calendar.CalendarViewcomputesweekStart = firstDayOfWeek(effectiveLocale)once (the authoredlocaleprop, else the display locale) and passes it to every site:weekStartsOn.The popover used to take date-fns's own
weekStartsOn. That is not CLDR's for every tag: date-fns has noes-MX, so the tag readses, a Monday start, where CLDR starts Mexico's week on Sunday. It also readenUS, a Sunday start, until the locale loaded. The new memo dependencies includeweekStart.@object-ui/plugin-timeline.ObjectTimelinereadsuseDisplayLocale(), the same hook its renderer formats with, and starts "This week" onfirstDayOfWeek(displayLocale). The bounds block was rewritten as local midnights stepped on the local calendar (daysFromToday(n)), which includestomorrow, in place oftoday + n * 86400000. Across a DST change the old arithmetic missed "Tomorrow" and put the first day of next week into "This week". Measured in the driven zone pin (America/Los_Angeles, Sunday 8 March 2026): the 9th now reads "Tomorrow" and the 15th "Next week". This is a published defect inside the block this change rewrites, so it is corrected here rather than filed (see Acceptance notes).Built-declaration reading (Clause-②):
packages/i18n/dist/utils/first-day-of-week.d.tsdeclaresexport type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;andexport declare function firstDayOfWeek(locale: string): WeekdayIndex;.dist/index.d.tsre-exports both. So the helper widens the package's public surface, andClause-②: yesstands.Docs: the i18n README (a "The first day of the week" subsection), the calendar and timeline READMEs, and
content/docs/plugins/plugin-calendar.mdx/plugin-timeline.mdx. Changeset:.changeset/11675-week-start-from-locale.md,minorfor the three packages.Tests
packages/i18n/src/__tests__/firstDayOfWeek-11675.test.ts(14 cases):-u-fw-(read by the engine only), andgetWeekInfo()read beforeweekInfo;RangeError;RELEASE_DELTAS) records for the runtime's CLDR release, which is none on the table's own release. A rig case and a ledger-honesty case sit beside it;unitproject runsisolate: false.packages/plugin-calendar/src/__tests__/CalendarView.weekStartFromLocale-11675.test.tsx(10 cases):en-US,en-GB,zh-CNandar-EG, and under an authoredlocale;en-GBand underes-MX, the case where date-fns and CLDR disagree.packages/plugin-timeline/src/__tests__/ObjectTimeline.weekBucketsFromLocale-11675.test.tsx(4 cases): the buckets underen-US,en-GB,zh-CNandar-EG.ObjectTimeline.dateOnlyZone-10866.test.tsx, whichscripts/__tests__/date-only-zone-pins-10183.test.tsalready drives: a DST block with a rig case, a fixture-validity case (8 March is 23 hours long there) and the buckets case.CalendarView.gridcellLocale-10144.test.tsxandCalendarView.displayLocale-10442.test.tsxmodelled a Sunday-first grid, so they went red underde/de-CH/fr-FR. Their purpose is the heads' language, not their order. They now take the first day as an explicit literal: Monday forde/de-CH/fr-FR, Sunday foren-US. Each still asserts the full head row.Ablations
Every leg ran through objectstack's
scripts/ablation-replace.mjs, from the committed tree. Each leg proved the mutation landed: the anchor count went from 1 to 0 and the blob changed. Each leg proved the restore: the blob matchedHEADandgit diff HEADwas empty. No build was needed: vitest aliases@object-ui/i18ntopackages/i18n/src, and the plugin tests import the components by relative path.daysIntoWeekhard-coded Mondayen-US,ar-EGred;en-GB,zh-CNgreenendOfWeekback to 24-hour steps (zone driver)tomorrowback totoday + 86400000(zone driver)weekStarthard-coded 0en-USandes-MXgreenweekStartsOnremovedes-MXredlunes, expecteddomingogetDay() === 0-u-fw-andgetWeekInfo()cases redLocal verification at
e4f3430Every heavy run went through
os-verify-lock.sh. The exit codes below are the commands' own, captured before any pipe.pnpm --filter '@object-ui/plugin-calendar^...' --filter '@object-ui/plugin-timeline^...' buildexited 0. The doc-snippet gate's scoped closure (turbo run buildover its--build-filter) also exited 0: 35 of 35 tasks.pnpm exec vitest run packages/i18n/ packages/plugin-calendar/ packages/plugin-timeline/exited 0, with 184 files and 2281 tests passed (145 skipped, the driven zone cases among them).pnpm exec vitest run scripts/__tests__/date-only-zone-pins-10183.test.tsexited 0 with 27 passed. That driver counts a skipped child case as a failure, so the new DST cases ran.type-checkfor i18n, plugin-calendar and plugin-timeline exited 0. With--listFiles, every test file this change touches is in its package's test program.lintfor the same three packages exited 0 (0 errors). The warning counts, by rule, are identical atdaa7cafande4f3430forCalendarView.tsx,ObjectTimeline.tsxand the i18n entry files, and every new file has 0 warnings.check:new-line-citations,check:control-bytes,check:changeset-claims,check:pending-changeset-literals;check:readme-exports(after the scoped build; it judgesfirstDayOfWeekin the i18n README asreal) andcheck:doc-snippets(779 of 779 blocks judged, 0 failed);check:self-import,check:phantom-deps,check:unused-deps,check:doc-fences;check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:unreferenced-sources,check:esm-specifiers,check:shell-escape-residue;check:dist-completeness,check:published-dist;node scripts/check-changeset-no-major.mjs,check-changeset-presence.mjs,check-changeset-overwrite.mjs;node scripts/check-governed-queue-guard.mjs --testover the 17 changed paths: NOT GOVERNED.check:node-esm-load, reason: its provenance leg refused 8 entries that turbo replayed from another worktree's shared cache (auth, core, data-objectstack, mobile, permissions, providers, react-runtime, types). None of them is in this diff, and none of the three touched packages was refused or listed as failing. A clean reading needs--force-build, which is CI's whole-tree run.pnpm lintand the fullpnpm testbelong to CI.Patch round 1: CI red on the parity pin at
e4f3430Cause. CI's
node-version: '22.x'floats to the newest Node 22, which ships CLDR 48.0. In CLDR 48 Iceland's week starts on Sunday. The table transcribed CLDR 47, whereISreads the Monday default, and the pin compared it with whatever CLDR the runtime ships. So the pin was green in a CLDR 47 container and red on CI (IS: engine 0, table 1), and updating the table alone would only have moved the red to the CLDR 47 runtime.Measured runtimes. Three official Node builds were run, checksum-verified against nodejs.org's
SHASUMS256.txt: 22.13.0 (CLDR 46.0, theenginesfloor), 22.22.0 (CLDR 47.0) and 22.23.3 (CLDR 48.0, what CI's22.xresolves to). Across the three,ISis the only region whose first day differs (Monday, Monday, Sunday).Shape chosen: a declared release and an exact-equality ledger.
utils/first-day-of-week.tsdeclares the release its table transcribes,WEEK_DATA_CLDR = '48', and the table now carriesISon Sunday. All three rows equal the CLDR 48 measurement, region for region.process.versions.cldr. The set of regions where the table and the engine disagree must EQUALRELEASE_DELTAS[release], a ledger in the test whose rows were measured on those runtimes (46: { IS: Monday },47: { IS: Monday }). On the table's own release, and on a release the ledger does not list, that set is empty.Ablations on the new head, each through
scripts/ablation-replace.mjs, each with the mutation proven landed and the restore proven (blob == HEAD,git diff HEADempty), and each run on all three runtimes:{ IS: 0 }found) and ledger honesty47row removed{ IS: 1 }found,{}expectedRe-run at
a393686(after mergingmainatda35453in98877dd, a merge commit; no rebase, no force-push):pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/plugin-calendar/ packages/plugin-timeline/exited 0 on CLDR 47: 184 files, 2283 passed, 145 skipped. The same suites under Node 22.23.3 (CLDR 48, CI's release) exited 0 with the same counts.firstDayOfWeek-11675.test.tsalone: 14 of 14 passed on CLDR 46, 47 and 48.scripts/__tests__/date-only-zone-pins-10183.test.tsexited 0, 27 passed.type-checkandlintfor the three packages exited 0. The package warning totals are unchanged, and both edited files have 0 warnings.check:new-line-citations,check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check-changeset-no-major,check-changeset-presence,check-changeset-overwrite,check:unreferenced-sourcesandcheck:test-path-rootseach exited 0.check:readme-exportsandcheck:doc-snippetswere not re-run.Acceptance notes
tomorrowmoved fromtoday + dayto a local-calendar step together with the week bounds. It is pinned by the driven DST case, and ablations A1b and A1c show each half failing alone.weekaxis is unchanged. It labels plan weeks (Week 1,Week 2) counted from the axis's first day, a documented choice, not a hard-coded weekday. Snapping it to a calendar week would change what the axis means. No card.@object-ui/componentsCalendarprimitive without aweekStartsOn, still take date-fns's week start for the tag. For tags date-fns has no locale of its own for (for examplees-MX), that is not CLDR's start. Ablation A2b measured the mechanism on the primitive, not through a field's public door. Owner: none. Noted, not filed.-u-fw-or-u-rg-keyword on the tag is honoured only where the engine has week info. No producer writes those keywords today.content/docs/pluginspages and the i18nindex.ts/utils/index.tsexports are required by AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 or by the helper's own export. They are additive and are named here so the seat can amend the surface.Generated by Claude Code