Skip to content

fix(collaboration,console): two more dates read the display locale, not the UI language (objectui#10375) - #10431

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-10375-display-locale-dates
Sep 25, 2026
Merged

os-litant merged 1 commit into
mainfrom
claude/issue-10375-display-locale-dates

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10375
Clause-②: no

Two more date faces take their locale from useDisplayLocale() instead of the UI language. Implemented by a dispatched dev subagent under seat domain:ui#4 (session session_01BP8CMtACxTdLjqR6rhd33C), on base 74fcda829.

Premise, re-derived on 74fcda829

Both sites were on origin/main as the card and the triage comment (5823975048) quote them:

  • ApprovalsInboxPage.tsx, in formatRelative: try { return new Date(s).toLocaleDateString(language); } catch { return s; }, with language from useObjectTranslation(). The page already read useDisplayLocale() for every other date and number face.
  • CommentThread.tsx: formatAbsoluteDate(date, language) ran date.toLocaleDateString(language), with language from useCollaborationTranslation(), reached from formatTimestamp for comments seven or more days old.

A one-off probe that walks the census population exactly as machineLocaleCensus-9909.test.ts does (same manifests, same tooling filter, same comment masker, same matcher) finds exactly these two call sites passing a language-named first argument at 74fcda829, and none at this branch's head.

What changed

  • CommentThread calls useDisplayLocale() in the component body, next to useCollaborationTranslation(), and passes the tag to formatTimestamp and on to formatAbsoluteDate. language had no other use there, so it is no longer destructured. The parameter is renamed locale, and the two doc comments now say the date follows the display locale and why objectui#3441 had chosen the session language. The inner try/catch for a malformed tag is unchanged.
  • ApprovalsInboxPage: formatRelative's date bucket passes the page's existing displayLocale, and the callback's dependency list names it instead of language. language had no other use in the page, so it is no longer destructured.
  • Neither site adds a fallback. The hook owns the last-resort tag.
  • The relative buckets ("just now", "5m ago", "3d ago") still go through t / tr and the UI language.
  • machineLocaleCensus-9909.test.ts: the CommentThread.tsx DECLARED entry (the lit control) keeps its file, expression and count. The bare call in formatAbsoluteDate's catch is still there, still counted once, and the lit-control case is green. Only its reason text changed, because its "whose try already passes the session language" would have gone false; it now names the display locale.
  • Changeset: patch for @object-ui/collaboration and @object-ui/console. The dispatch named collaboration only; console is added because its source changes user-visibly too, and a changeset that describes the inbox without naming its package would leave the console changelog without the entry. The body states both behaviour changes.

Mechanism readings (the dispatch's A1 to A4)

  • A1, no provider. With no provider mounted, useLocalization() returns an empty object and useDisplayLocale() falls through to useObjectTranslation().language, the same tag useCollaborationTranslation() returned before. Measured with a new case in comment-thread-no-provider-fallback.test.tsx: every toLocaleDateString call receives exactly 'en', never the machine locale. The file's existing week-old-date case stays green. The file still does not import @object-ui/i18n; its docblock now says the tag is the display locale's fallthrough. Under an I18nProvider with no LocalizationProvider, the existing zh / de / en_US absolute-date cases in comment-thread-i18n.test.tsx stay green unchanged.
  • A2, relative buckets. Both new pin files have a case where a three-day-old item under an English UI with a de-CH display locale reads 3d ago. It stayed green on both ablation legs, as an invariance pin should.
  • A3, the pin. See Tests and Ablation below.
  • A4, dependencies. @object-ui/collaboration already depends on @object-ui/i18n (workspace:*), which exports useDisplayLocale. No dependency was added. The collaboration pins record the formatter argument with vi.spyOn(Date.prototype, 'toLocaleDateString') instead of @object-ui/test-support, which that package does not declare.

Tests

New pins follow the objectui#10331 shape: the real I18nProvider with an English UI, and the display locale declared through LocalizationProvider, so the display locale is the only variable.

  • packages/collaboration/src/__tests__/comment-thread-display-locale-10375.test.tsx: the de-CH pin (4.3.2020, and equal to the runtime's own de-CH rendering), the en-US control (3/4/2020), a differential no runner locale can satisfy, an argument check (every toLocaleDateString call received de-CH), the relative-bucket invariance case, and a malformed display locale (de_CH) that still falls back to the runtime date, never the raw ISO string.
  • apps/console/src/pages/system/ApprovalsInboxPage.displayLocale-10375.test.tsx: the same pin, control, differential and relative-bucket cases, read from the queue cell whose title tooltip is the (untouched) formatDate face, plus the @object-ui/test-support tripwire. consoleFaces.displayLocale-9909.test.tsx could not see this defect: it stubs the UI language to 'en' and its tripwire tolerates any declared tag, so only the machine locale reads as red there.

Runs from the repo root at 5a40f03e0, through the shared verify lock:

  • pnpm exec vitest run --maxWorkers=2 packages/collaboration/ apps/console/src/pages/system/ apps/console/src/pages/consoleFaces.displayLocale-9909.test.tsx apps/console/src/__tests__/approvalsInboxComponentRef.test.tsx packages/app-shell/src/views/__tests__/DeclaredActionsBar.test.tsx packages/i18n/src/__tests__/dead-pack-keys-retired-8754.test.ts packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts gives Test Files 30 passed (30) and Tests 264 passed (264). That covers both new pins, every collaboration suite, every approvals-inbox suite, the other suites that name ApprovalsInboxPage, and the census.
  • Type-check after the closure builds: turbo run build --filter="@object-ui/collaboration^..." (3 of 3 tasks), then pnpm --filter @object-ui/collaboration run type-check and pnpm --filter @object-ui/i18n run type-check, both exit 0. turbo run build --filter="@object-ui/console^..." (34 of 34 tasks), then the console's type-check (tsc --noEmit && tsc -b tsconfig.node.json --force) exits 0. --listFiles shows the programs contain the new files: 6 collaboration test files in its tsconfig.test.json, the census file in i18n's, and both the page and the new pin in the console program (its include is src).

Ablation: fix committed first, every leg restored with on-disk proof

Each leg puts the UI language back at ONE face through ablation-replace.mjs, then runs both pin files and the no-provider file (27 cases).

leg mutation red green
CommentThread const displayLocale = useDisplayLocale(); becomes const displayLocale = useCollaborationTranslation().language; that face's de-CH pin, differential and argument check (3) its en-US control, relative bucket and malformed-tag case, the no-provider file and the inbox pins (24)
ApprovalsInboxPage const { t } = useObjectTranslation(); becomes const { t, language } = useObjectTranslation(); (held), then toLocaleDateString(displayLocale); } catch { return s; } becomes toLocaleDateString(language); } catch { return s; } that face's de-CH pin, differential and tripwire (3) 24

The red readings:

  • CommentThread: got: 3/4/2020: expected '3/4/2020' to be '4.3.2020', and the argument check saw: [["en"]].
  • ApprovalsInboxPage: got: 3/4/2020: expected '3/4/2020' to be '4.3.2020', and the tripwire saw four Date.prototype.toLocaleDateString calls, each with "locale":"en".

The malformed-tag case stays green on the CommentThread leg. It pins the catch fallback, not the channel, so it is not offered as evidence for this change.

On-disk proof:

  • CommentThread: anchor 1 to 0, blob b3733ea1a9cf to d73d4e95d0e6, restored to b3733ea1a9cf (HEAD's), git diff HEAD empty.
  • ApprovalsInboxPage: the held leg moved the blob 7869008b0099 to d086337684fc, and the wrapped leg moved it on to f334af56992a. The wrapped leg's restore took the whole file back to HEAD, so both mutations were gone after it (on-disk counts 0 and 0), blob 7869008b0099, git diff HEAD empty.
  • A shell trap restored both paths from HEAD with absolute paths, and git status --porcelain was empty at the end.

No dist/ is on the path: the pins import the components by relative path, and vitest aliases @object-ui/i18n, @object-ui/collaboration and @object-ui/test-support to src.

Gates

dispatch-gates.mjs only works in objectstack, so these were derived by hand from this repo's package.json and .github/workflows/. All at 5a40f03e0, all exit 0:

  • check-changeset-presence: 6 source files of 3 released packages changed, 1 changeset declared.
  • check-changeset-no-major, check-changeset-overwrite (1 added, 0 modified), check-changeset-fixed.
  • check-changeset-claims: no line address in this change's own prose; no self-contradiction. The went-false reading names two pending changesets that mention machineLocaleCensus-9909.test.ts: 9786-detail-timestamps-display-locale.md and 9909-machine-locale-sweep.md. I read both paragraphs. They say collaboration's single hit is a deliberate catch fallback for a tag Intl rejected, and describe what the census refuses. Both still hold.
  • check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s). check:control-bytes: OK.
  • For the new tests: check-vi-mock-specifiers, check-vi-mock-inherit, check-vi-mock-override-shape, check-test-path-roots, check-pending-changeset-literals. For the source: check-phantom-dependencies, check-unused-dependencies, check-package-self-import, check-unreferenced-sources, check-i18n-call-site-keys.
  • Lint: the full pnpm lint is left to CI. ESLint with --no-inline-config ran on the 6 changed TS files (each package's lint is eslint . over the root config).
    • Result: 6 files, 0 errors. The four test files have no warnings. ApprovalsInboxPage.tsx has 20 warnings and CommentThread.tsx has 1, with the same per-rule counts as their base blobs.
    • eslint --print-config resolves all 6 files, so none of them is ignored.
    • eslint.config.js configures no type-aware linting (no parserOptions.project or projectService). So this diff cannot change the lint result of a file it does not touch.

The census extension to a bare language argument: not built here

The card says objectui#10369's PR body costs this extension as cheap. That body says the opposite: "Not cheap: a bare language argument (the UI-language channel) cannot be told apart from a correctly named variable without data flow", and adds that such a rule would land red on three sites. The dispatch allowed the extension only if it is as cheap as that costing says, so it is not built. The conflict is reported to the seat.

Measured here for whoever prices the follow-up: the "lands red" half no longer holds. The probe described above finds the two sites this PR changes at 74fcda829 and none at head. Among the census population's identifier first arguments at head, the last segments are locale, displayLocale, dateLocale, effectiveLocale and one constant, and none is language. The data-flow half still holds: a name rule would judge the identifier, not where its value came from.

Acceptance notes

  • formatRelative's catch (like formatDate's) returns the raw value when Intl rejects the tag. A malformed display locale would therefore show the raw ISO string in the inbox, where CommentThread falls back to the runtime date. This PR does not make that more likely: the old language argument had the same exposure. No malformed tenant locale was reproduced, so nothing was filed.
  • No README or content/docs change: neither @object-ui/collaboration's README nor the docs describe how the timestamp is formatted. The changeset carries the user-facing sentence.

Generated by Claude Code

…ot the UI language

CommentThread's absolute date for comments seven or more days old, and the
approvals inbox's date for timestamps 30 or more days old, formatted with the
UI language. Both now take their tag from useDisplayLocale(), so a regional
display locale (de-CH under an English UI) reaches them. The relative buckets
stay on the translation function and the UI language.

The machine-locale census keeps its CommentThread lit control: the bare
fallback call in formatAbsoluteDate's catch is unchanged, and only its
reason text now names the display locale its try passes.

Refs objectui#10375.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/9786-detail-timestamps-display-locale.md

  • names machineLocaleCensus-9909.test.ts → packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts — edited by this change

    Two instruments land with the repair, because the repair alone is what happened the last five times. detailTimestamps.displayLocale-9786.test.tsx renders every surface whose face reaches the DOM under a declared de-DE session and a declared en one and requires the two readings to differ — an assertion the runner's own locale cannot satisfy, since before this change both legs came from the machine and were byte-identical whatever the machine was. It also patches the Intl constructors and Date.prototype.toLocale* and inspects the argument each call actually receives, which is the only way to catch a variable that resolves to undefined, and the only way to reach RecordMetaFooter — whose absolute face is a TooltipContent child that Radix keeps out of the DOM until the tooltip opens. A census pin reddens when a new bare, undefined- or hard-coded-tag call site is written; it masks comments before it counts, and it declares InlineFieldInput's en-CA as an exemption with its reason — that call feeds ANGLE-BRACKETS(input type="date"), whose value HTML defines as YYYY-MM-DD, so it is an ISO formatter and not a display locale. It landed scoped to this package and, under the ruling on this card, became the one repository-wide census in @object-ui/i18n (machineLocaleCensus-9909.test.ts, objectui#9909), which carries that exemption forward.

.changeset/9909-machine-locale-sweep.md

  • names machineLocaleCensus-9909.test.ts → packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts — edited by this change

    One census, repository-wide. plugin-detail's machine-locale census pin (objectui#9786) is now a single test, machineLocaleCensus-9909.test.ts in @object-ui/i18n, covering every workspace package whose manifest depends on @object-ui/i18n. It refuses any call site that passes nothing, undefined, the 'default' pseudo-tag (a subtag no locale data answers, so Intl resolves it to the machine's locale) or a hard-coded tag, unless the site is declared with its reason — for example the catch fallback for a tag Intl itself rejected, or an ISO formatter feeding ANGLE-BRACKETS(input type="date"). Each declared site is counted exactly, so deleting one without its entry is refused too. The per-package pin it replaces is deleted. The runtime tripwire that observes the argument each locale-taking call (Intl constructors, Date and Number toLocale*) actually receives moved into the private @object-ui/test-support package, and every surface above is pinned with it: the same data under two declared locales must render differently, NO locale-taking call may receive the machine's locale (nothing, undefined or 'default'), and the surface's own calls must receive the declared tag. A call carrying another declared tag, such as the 'en' of the session's UI language, is tolerated by design: only the machine's locale is the defect.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 3335767b4 (merge-base with origin/main): 6 file(s) changed outside .changeset/, read against 1371 pending declaration(s) that publish a body (1946 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3043.7 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-BJKvVXaH.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 542.46KB 129.64KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.12KB 62.26KB
fields (index.js) 255.56KB 64.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 72.54KB 20.26KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.47KB 41.53KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.36KB 28.24KB
plugin-map (index.js) 22.05KB 7.14KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5a40f03e0d6e92a338335bf00ffc291ea55e7099

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (95 of 95 model stamps at the review tier). Adopted by this seat.

① Derived judgments

  • Identity. GET pulls/10431 resolves head.sha to 5a40f03e0d6e92a338335bf00ffc291ea55e7099, the expected value. One commit, whose parent is 74fcda8292280167a4c22f6edf6586f29135b7da, which is also the merge-base with origin/main: the dev's stated base 74fcda829 is the real one. Read the card body, the triage comment 5823975048, the claim 5824390875 and the dev report 5824824057 before the code.
  • (a) Completeness: 2 at base, 0 at head, confirmed twice. A git grep over the whole non-test tree at head for toLocaleDateString, toLocaleString, toLocaleTimeString and Intl.*Format whose first argument is language or *.language returns no source hit. The only residual matches are prose: CHANGELOG.md and ROADMAP.md quoting a past data-table change, and a JSON string in skills/objectui/evals/i18n.json that spells navigator.language; none is code. The same grep at 74fcda829 and at today's origin/main (9ec8ceba06) finds exactly two source hits, ApprovalsInboxPage.tsx (formatRelative's catch line) and CommentThread.tsx (formatAbsoluteDate's try), the card's two sites. A multi-line-aware scan of the head tree (first argument on its own line) and a variant grep for i18n.language, resolvedLanguage and language || / language ?? fallbacks also return 0. A census-shaped probe (same manifest-derived population of 22 packages, same tooling filter, same comment masker, same matcher) counts 2 language-named first arguments at base, both the card's sites, and 0 at head. The dev's measurement holds.
  • (a) Adjacent sites the class also covers, outside the strict target and outside this card. A supplementary grep for helpers handed the UI language as a locale option finds four places where a date face still formats with the translation hook's language rather than useDisplayLocale(); none is a direct toLocale* / Intl first argument named language, so neither the census nor the proposed name rule sees them, and none is in this card's scope (the card and triage grade exactly two sites): packages/components/src/renderers/complex/data-table.tsx (formatDateTime(value, { locale: language }) and formatDate(value, undefined, { locale: language }), language from useTableTranslation(); core's DateDisplayOptions.locale is documented as the BCP-47 display locale); packages/app-shell/src/views/RecordDetailView.tsx (audit history fmtCtx = { t, locale: language, lookupLabels }, which formatScalar in auditHistoryDisplay.ts spends on toLocaleDateString / toLocaleString); packages/plugin-gantt/src/ResourceWorkload.tsx (const locale = language || undefined, then toLocaleDateString(locale, …), with a machine-locale fallthrough on an empty language); packages/plugin-calendar/src/CalendarView.tsx (effectiveLocale = locale !== "default" ? locale : language, where the locale prop defaults to "default"). The other locale: language hits (RecordDetailView action params, useConsoleActionRuntime, FlowRunner, usePackageL10n) resolve per-locale label maps, a translation choice, not a formatter. Facts for the seat; not defects of this PR.
  • (b) Fidelity to the objectui#10331 shape: matches PR objectui#10369's diff. Both sites call useDisplayLocale() in the component body with no ?? / || fallback (const displayLocale = useDisplayLocale();), the same line the sibling wrote in InstalledList and CloudConnectionPanel. ApprovalsInboxPage reuses the page's pre-existing displayLocale (already read for formatDate, decisionAmountEntry and payloadSummary); the diff changes only the destructure (language dropped), the formatRelative date line and its dependency list ([tr, displayLocale, now]). CommentThread threads displayLocale through formatTimestamp(iso, t, locale) into formatAbsoluteDate(date, locale), parameter renamed locale at both signatures and both call sites; the inner try { toLocaleDateString(locale) } catch { toLocaleDateString() } is byte-identical apart from the rename. language is no longer destructured in either file and every remaining mention is in a comment. Both doc comments are rewritten and read true against the hook: the display locale is locale || language || 'en', so a session with no regional locale renders exactly as under objectui#3441, and with no provider the hook answers the same tag the translation hook did. @object-ui/collaboration already depends on @object-ui/i18n (workspace:*), which exports useDisplayLocale from its index; no dependency added.
  • (c) No-provider behaviour (A1) verified from source. useDisplayLocale() returns locale || language || 'en' where useLocalization() reads a context created with {} (so locale is undefined with no provider) and useObjectTranslation().language is context?.language || i18n.language || 'en'; with no instance i18n is an empty object, so the answer is the string 'en', never undefined. useCollaborationTranslation is a createSafeTranslation hook that returns language: result.language from that same useObjectTranslation(), so the tag is identical to what the branch passed before. The added case in comment-thread-no-provider-fallback.test.tsx spies on Date.prototype.toLocaleDateString and asserts every recorded call equals ['en'], which is exactly that claim. it( count is 15 at base and 16 at head; the diff touches only the vitest import (adds vi), afterEach (adds vi.restoreAllMocks()), one docblock and the new case; no existing case body changed, and the file still imports nothing from @object-ui/i18n.
  • (d) The census control is still lit. The only change to machineLocaleCensus-9909.test.ts is the reason string of the CommentThread.tsx DECLARED entry; file, expression (return date.toLocaleDateString();) and count: 1 are unchanged. At head the raw file contains that expression exactly once as code (the catch of formatAbsoluteDate); the second textual occurrence is inside formatTimestamp's doc comment, which the census masks, and the census-shaped probe reports exactly one bare site in that file. The new reason text ("whose try already passes the display locale (useDisplayLocale(), objectui#10375; the session language before that)") is true at head. The lit-control assertion (control.map(text) equals ['return date.toLocaleDateString();'] and is declared) holds; the width guards hold too (22 packages, 1273 sources, 127 sites).
  • (e) The pins are real and can fail. Both new files mount the real I18nProvider (defaultLanguage: 'en', detectBrowserLanguage: false, persistLanguage={false}) and the real LocalizationProvider (value={{ locale }}), both exported from @object-ui/i18n, the same harness as the objectui#10331 pins. comment-thread-display-locale-10375.test.tsx (6 cases): de-CH pin 4.3.2020 also asserted equal to the runtime's own de-CH rendering, en-US control 3/4/2020, the differential (de-CH reading not equal to en-US reading), an argument spy asserting every toLocaleDateString call received de-CH, the three-day-old relative-bucket control (3d ago under de-CH), and the malformed de_CH case (runtime date, never the raw ISO). ApprovalsInboxPage.displayLocale-10375.test.tsx (5 cases): the same pin, control, differential and relative-bucket control, read from the queue cell whose title is the untouched formatDate face, plus the @object-ui/test-support tripwire (recordLocaleArgumentsAsync, isMachineLocale, which flags argc === 0, undefined or 'default'). Its vi.mocks double only auth, the adapter and the approvals API, not the locale channel. The console pin has no malformed-tag case; the dev says why (that catch returns the raw ISO string, flagged as out of scope rather than pinned). Ablation arithmetic: 6 + 5 + 16 = 27 cases per leg. CommentThread leg red = de-CH pin, differential, argument spy = 3, green 24; ApprovalsInbox leg red = de-CH pin, differential, tripwire = 3, green 24. Consistent with the case lists, and the reported red readings (3/4/2020 where 4.3.2020 was expected; saw: [["en"]]) are what a language-fed formatter produces under an English UI.
  • (f) Changeset truth. .changeset/10375-comment-and-inbox-dates-display-locale.md declares @object-ui/collaboration: patch and @object-ui/console: patch. Every sentence checks out at head: the seven-or-more-days branch of formatTimestamp (a days count below 7 returns the bucket, else formatAbsoluteDate) now takes useDisplayLocale(); it used to take the UI language; the 3/4/2020 to 4.3.2020 reading is the pin; the no-display-locale and no-provider fallthrough is the hook's locale || language || 'en'; the malformed-locale fallback is the kept inner catch; formatRelative shows a date once Math.round(hr / 24) reaches 30, called on submitted_at, completed_at and the history rows' created_at; the relative words stay on t / tr. One precision note, not a falsehood: the day count is a nested rounding (seconds, then minutes, then hours, then days), so "rounded to the nearest day" is a simplification at the 30-day edge. No line-address citation in the file (regex over path:NNN finds none). @object-ui/console is a released package: not private, publishConfig.access: public, and listed in the fixed group of .changeset/config.json. Re-read .changeset/9786-detail-timestamps-display-locale.md ("collaboration's single hit is a deliberate catch fallback for a tag Intl itself rejected") and .changeset/9909-machine-locale-sweep.md (the census refuses bare / undefined / 'default' / literal sites unless declared, "for example the catch fallback for a tag Intl itself rejected"): both remain true, since the one bare call is still the catch and still declared. The twelve other pending changesets that name CommentThread, formatAbsoluteDate, ApprovalsInboxPage, formatRelative or the census were read at their matching lines; the only sentence about a touched function is 8504-emptyvalue-remaining-carriers.md's "formatRelative's if (!s) return '—' is not [converted]: that helper is declared : string", still true at head. None is made false.
  • (g) The dev's open question, facts only. The dev's claim is true as measured: a rule refusing a LOCALE_CALL whose first-argument identifier's last segment is language finds 2 sites at 74fcda829 (exactly this card's two) and 0 at 5a40f03e0, so it would land green with no new exemption. At head the identifier last segments of all expression-shaped first arguments in the census population are locale (56), displayLocale (37), dateLocale (15), effectiveLocale (4) and the constant DISPLAY_LOCALE_LAST_RESORT (1); the only other two are array-literal first arguments to Intl.DisplayNames, not identifiers. The conflict the dev reports is real: objectui#10369's PR body costs the extension as not cheap on data-flow grounds, and the card and triage call it cheap; the "lands red" half of that costing is gone after this PR, the data-flow half stands, and the four adjacent helper-level sites listed under (a) are concrete examples a name rule cannot see. Out-of-scope note confirmed: at base, formatRelative's date line already read catch { return s; } and formatDate reads catch { return s; }; both return the raw value on a tag Intl rejects, and that behaviour predates this PR (the string is present at 74fcda829 and at the earlier commit git log -S surfaces).

② Semver level

patch on both @object-ui/collaboration and @object-ui/console is right. The change is a behaviour fix with no export, prop, schema or key moved (Clause-②: no in the claim and the PR body). objectui AGENTS.md's version rule: "minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进" and "推论:changeset 里不要声明 major —— fixed 组任一 major 都会把全组推上去、脱离 objectstack 的节奏……objectui 自身的破坏性变更也标 minor". Nothing here is breaking, so patch, and no major is declared. The presence rule (AGENTS.md: released source under src/ of a fixed-group package, "含 apps/console", must carry a changeset) is why the console entry is required, not merely allowed; the dev declared that deviation from the claim's collaboration-only wording in the report and the PR body. Both packages are at the fixed group's current version, so the bump lands together.

③ Boundary flags

  • Closing keyword. Fixes #10375 is the body's first line and the only closing keyword in the title, body or commit; the commit message says Refs objectui#10375., which does not close. Clause-②: no is the second body line, matching the claim.
  • Model identifier. The title, body and commit message were scanned for model-family and version identifiers; none is present. The commit's two trailers are the fleet's standard co-author trailer (product name and no-reply address only) and the session trailer naming session_01BP8CMtACxTdLjqR6rhd33C; the body's footer names the product and the same session.
  • File surface. 7 files, matching the claim: the two sources, the census test, the changeset, the two new pins beside their components, and comment-thread-no-provider-fallback.test.tsx, a test beside CommentThread that the claim's "tests beside them" allows. The changeset naming @object-ui/console as well is the one declared deviation, covered by the presence rule quoted above.
  • Serial. The file lists of all 19 other open PRs (10436, 10430, 10429, 10428, 10426, 10424, 10421, 10417, 10413, 10412, 10411, 10410, 10388, 10278, 10169, 9488, 9391, 8941) and the release PR 5400 (all 1732 files, paged) were read: none touches any of the 7 files.
  • Draft. The PR is a draft (draft: true), opened by objectstack-fleet[bot], labels by path only (apps, tests, package: i18n, package: collaboration), no assignee.
  • mergeable_state. First read: mergeable: true, mergeable_state: behind. Re-reads at 01:07:59Z and 01:08:22Z: mergeable: true, mergeable_state: clean. The head is 5 commits behind origin/main (9ec8ceba06), with no conflict.
  • Check-runs on the head. First read: 42 runs, 37 success, 3 skipped, 2 in_progress (Test (shard 4/8), Spec Main Shape Gate). Re-read at 01:06:05Z and again at 01:07:59Z: 43 runs, 40 success, 3 skipped, 0 failure, 0 in_progress. The three non-success runs are all skipped: dependabot, Test (coverage), Test (coverage shard ${{ matrix.shard }}/4). No polling loop was needed because nothing remained in progress at the final read; every run is named in checkruns3.json in the scratchpad.

Implemented-by: claude/issue-10375-display-locale-dates
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 01:14
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit d3d134b Sep 25, 2026
45 checks passed
@os-litant
os-litant deleted the claude/issue-10375-display-locale-dates branch September 25, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants