Repository navigation
fix: dates and numbers use the display locale across every @object-ui/i18n dependent, with one repo-wide machine-locale census (objectui#9909) - #10231
Conversation
…test-support objectui#9909 applies objectui#9786's differential pin pattern to every surface its sweep changes, so the tripwire that observed the argument each locale-taking intrinsic received moves out of plugin-detail's pin into the shared test-support index: one instrument, not a copy per package. It now also patches Number.prototype.toLocaleString (a number face never reaches the global Intl.NumberFormat) and counts the 'default' pseudo-tag, which Intl resolves to the runtime default, as the machine locale. Its own controls moved with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…cale displayText(c, value, locale) spent its locale on the temporal branch and dropped it on the numeric one, and both total cells called toLocaleString() with useDisplayLocale() already in scope, so one grid row read a date in the session's convention beside an amount in the machine's (objectui#9909). Pinned by a differential test: the same rows under a declared de-DE and en session must read differently, and the runtime tripwire must see the declared tag on every locale-taking call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
… in the display locale The x-axis ISO-date ticks, the compact y-axis ticks, the single-value face, the spec `format` formatter and the tooltip value handed Intl nothing or an explicit undefined, i.e. the machine's locale (objectui#9909). They now read useDisplayLocale(); formatterFor, a module-internal helper, takes the locale as a REQUIRED parameter so no caller can forget it. Its existing unit cases pass 'en' explicitly instead of relying on the runner's locale. Pinned per surface by a differential test (declared de-DE vs en session) plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
… display locale ExportProgressDialog's processed/total record counts and DebugPanel's event timestamp called toLocaleString()/toLocaleTimeString() with no tag, i.e. in the machine's locale (objectui#9909). Both now read useDisplayLocale(). Pinned per surface by a differential test plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…a threaded locale The engine is a plain class with no hook to read, and it had no way to be handed a display locale, so both default messages printed their bound with toLocaleDateString() in the machine's locale (objectui#9909). The locale now arrives on an optional ValidationContext.locale (@object-ui/types), the parameter-threading shape formatDisplayNumber already uses in this package. Pinned by a differential test (de-DE vs en threaded) plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
… the display locale FormAnalytics' total-submissions count, MasterDetailForm's subtotal / tax / grand-total stack and the OCC conflict dialog's "their save" time handed Intl no tag or an explicit undefined, i.e. the machine's locale (objectui#9909). They now read useDisplayLocale(); occSave's module-private formatVersionTime takes the locale as a required parameter. Pinned per surface by a differential test plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
… the display locale AiPendingActionsInbox's past-30-days tail and the time useObjectChat stamps on a local-mode message called toLocaleDateString()/toLocaleTimeString() with no tag, i.e. in the machine's locale (objectui#9909). The inbox tail's own doc comment claimed the bare call "already localizes it"; it is corrected with the repair. Both now read useDisplayLocale(). Pinned per surface by a differential test plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…display locale Both export templates (HTML download and PDF print document) stamped `new Date().toLocaleString()` into a file the reader keeps, i.e. the machine's locale (objectui#9909). exportAsHTML / exportAsPDF / exportReport take a trailing optional `locale` (the additive shape formatValue took in objectui#10020), defaulting to DISPLAY_LOCALE_LAST_RESORT, never to a dropped tag; ReportViewer passes useDisplayLocale(), and LiveExportOptions gains an optional `locale` it forwards. The display locale stays OFF ReportExportConfig, which is authored metadata. Pinned per surface (print document, popup-blocked HTML download) by a differential test plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…cy/date use the display locale The "N records" badge called toLocaleString() with no tag, and renderFieldValue - handed the display locale and spending it on its percent branch - called formatCurrency(value, code) and formatDate(value, fmt) without it, which those helpers read as "follow the runtime", i.e. the machine's locale (objectui#9909). The last two are invisible to a source census (the argument list merely omits the tag) and were found by reading. Pinned per surface by a differential test plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…y locale The record-detail panel's currency inference fallback and the mobile card's amount line called formatCurrency / formatCompactCurrency without the display locale already in scope (objectui#4541 threaded it into the date branch one statement above and left the currency one). Both helpers read an omitted tag as "follow the runtime", i.e. the machine's locale - a shape no source census sees, found by reading (objectui#9909). Pinned per surface by a differential test plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Each version row's time called toLocaleString() with no tag, i.e. in the machine's locale (objectui#9909). It now reads useDisplayLocale(). Pinned by a differential test plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…ay locale Organization invitations / members / accept pages, the marketplace version list, the AI conversation row (date tail and tooltip) and build-debug token count, the record approvals timeline, and the metadata-admin audit, history, external-datasource snapshot, schema-browser row estimate, flow-run start and job next-fire faces all handed Intl no tag or an explicit undefined, i.e. the machine's locale (objectui#9909). Components read useDisplayLocale(); the module-private helpers take the locale as a required parameter. The metadata-admin panels format in the DISPLAY locale, not their own UI-string `locale` (which defaults to a hard-coded 'en-US'). Pinned per surface by five differential test files plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…display locale The approvals inbox's timestamp tooltips, queue and drawer amounts and payload summary (number, epoch-ms date, ISO date-time), the audit log's timestamp column and the flow runs table formatted with no tag, i.e. in the machine's locale (objectui#9909). Their catch arms are for an unparseable value, not a rejected tag, so the bare call was the primary leg. The page helpers now take the display locale as a required parameter, supplied by useDisplayLocale(). Pinned per surface by a differential test plus the runtime tripwire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…/i18n dependent The ruling on objectui#9786 (letter B') made plugin-detail's census pin one test covering every package that depends on @object-ui/i18n, landing with objectui#9909's sweep. It moves here, beside useDisplayLocale, whose doc comment states the rule it enforces; the plugin-detail copy is deleted, so exactly one census test exists. The population is read from the manifests (the pnpm-workspace globs, any dependency field), so a new dependent is covered with no edit. It refuses a bare, `undefined` or 'default' pseudo-tag call and a hard-coded BCP-47 literal unless DECLARED with file, expression, exact count and reason; the lit control (CommentThread's catch fallback) must be scanned AND declared. Width guards and a matcher self-test keep a silent narrowing loud. Comments are masked with the repo's one declared masker. No ESLint rule and no new workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
The single-value chart pin casts `metric` (a SINGLE_VALUE_CHART_TYPES member the prop union does not name), the JobPreview pin passes the required `type`, and the console pin drops an unused React import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…nsus The pending objectui#9786 changeset named plugin-detail's census pin by file; objectui#9909 moves that pin into the one repository-wide census in @object-ui/i18n and deletes the package copy, so the paragraph is corrected in prose (frontmatter untouched), the practice check-changeset-claims asks for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
useObjectChat stamps two local-mode messages; the pin read only the user's. It now reads both, so each of the two repaired call sites has a reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…tion import line marketplace-preview-namespace-3546 asserts that page's bare useObjectTranslation import by its literal line; useDisplayLocale now comes in on a line of its own so that pin keeps reading what it was written to read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
AdvancedChartImpl and useObjectChat now read the display locale, and a provider-less first useTranslation() prints react-i18next's once-per-module NO_I18NEXT_INSTANCE notice. Three suites spy console.warn to count the component's OWN diagnostics in provider-less mounts; they now mount under an en I18nProvider, as the console always does, so the spy reads only what it was written to read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
The four new pins that cast fixtures or mocks to `any` now use precise types or `never` at the call site, so they add no no-explicit-any warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
|
changeset-claim-re-read
|
✅ 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: Rendered by an isolated review subagent spawned by this seat and fed only the card, the ruling ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Seat note: prose is held to the same standard as objectui#10225 and objectui#10230. A patch round goes to the dev for the two changeset sentences in ② and for one changeset sentence disclosing the provider-less console notice. This seat corrects the PR body's Generated by Claude Code |
…ssert Three review-tier corrections, prose only, frontmatter untouched: - the pins assert that NO locale-taking call receives the machine's locale and that the surface's own calls receive the declared tag; another declared tag is tolerated by design (it said "every locale-taking call must receive the declared tag"); - only AuditPanel defaults its UI-string `locale` prop to 'en-US'; HistoryPanel and FlowRunsPanel leave it optional; - discloses react-i18next's once-per-module NO_I18NEXT_INSTANCE console notice for a host that mounts a newly display-locale-reading component with no I18nProvider. Re-read against the diff: GridField's numeric cells are the list-mode and computed-column cells, the chat stamp is two stamps, the dashboard branch is the date-format one, the flow runs page includes its run detail, the census refuses ANY undeclared site and counts declared ones exactly. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ 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: This supersedes the record ① Derived judgmentsCarried over unchanged, because no source, test, manifest or lockfile moved. All published changes are additive ( ② Semver levelThe frontmatter is unchanged and RIGHT ( ③ Boundary flags
Implemented-by: VERDICT: PASS Landing: draft until Generated by Claude Code |
…o-wide-machine-locale-census
✅ 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
|
…ui#7659) (objectstack-ai#10709) Fixes objectstack-ai#7659 Clause-②: yes — public exports of `@object-ui/core` are removed ## What this is Executes maintainer ruling A on the card (decision batch objectstack-ai#40, item 4, recorded in card comment 5548579814): `validation-engine.js` and its four exports (`ValidationEngine`, `defaultValidationEngine`, `validate`, `validateFields`) are removed from `@object-ui/core` under ADR-0049 enforce-or-remove. The objectstack-ai#3110 carve-out sentence in `packages/core/src/validation/index.ts` no longer names the module as a kept member; it still covers `schema-validator.js`.⚠️ This is a breaking narrowing of a published package's exports. The seat applied `needs:contract-review` and removes it at landing after a contract-review PASS; the dev writes no label. ## Changes | file | change | |---|---| | `packages/core/src/validation/validation-engine.ts` | deleted | | `packages/core/src/validation/__tests__/validation-engine.test.ts` and `validation-engine.displayLocale-9909.test.ts` | deleted with it (the engine's own tests) | | `packages/core/src/validation/index.ts` | export line removed; carve-out rewritten; the docblock's opening label `Phase 3.5: Validation engine` now reads `Phase 3.5: Validation module` (the docblock ships verbatim in `dist/validation/index.d.ts`) | | `packages/core/src/validation/__tests__/validation-engine-retired-7659.test.ts` | new barrel-absence pin | | `.changeset/7659-retire-validation-engine.md` | `'@object-ui/core': minor`, BREAKING banner naming the four exports, replacement `buildValidationRules` (`@object-ui/fields`), out-of-repo consumers NOT MEASURED | | `.changeset/6342-dead-migration-guide-pointers.md`, `.changeset/9909-machine-locale-sweep.md` | the falsified sentence is scoped "At this change" and carries a dated note; front matter md5 identical before and after (`291f1f84…`, `c9e77b57…`) | | `content/docs/guide/troubleshooting.md` | section 5 named the removed class; it now names `validateSchema` / `assertValidSchema` | | `packages/types/src/data-protocol.ts` | `ValidationContext.locale` doc comment rewritten, because it explained the member by the removed engine (comment only) | | `packages/plugin-timeline/src/renderer.tsx` | the `instanceof Date` idiom pointer named the deleted file and `data-table.tsx`, which holds no `instanceof Date` since `4758b33c2` (objectui#10625); it now points at `toDay` in `components/src/renderers/form/calendar.tsx` (comment only) | ### File-surface note for the seat The last two rows are outside the claim's declared file surface. Both are source comments this deletion makes false: one pointed at the deleted path, the other explains a member by "the engine is a plain class with no hook to read". The os-dev role file requires fixing what the change falsifies, and the claim says to stop on a breach. The two conflict, so I followed the role file and declared it here; the seat ruled to keep both (option A). The renderer comment now points at the idiom's live home, `toDay` in `calendar.tsx`: its other pointer, `data-table.tsx`, holds no `instanceof Date` since `4758b33c2`. Both edits are comment-only: eslint warning counts are the same as on the base files (14 and 44), re-read on `renderer.tsx` after the repointing. ## Premise, re-measured at `origin/main` `bc97f9247` - `validation-engine.ts` exports exactly four names (`export class ValidationEngine`, `export const defaultValidationEngine`, `export async function validate`, `export async function validateFields`). `validation/index.ts` re-exported them with `export * from './validation-engine.js'`, and `packages/core/src/index.ts` re-exports `./validation/index.js`. - Production consumers across the whole tree (`packages/`, `apps/`, `examples/`, docs): **zero**. Anchors: `\bValidationEngine\b`, `defaultValidationEngine`, `\bvalidateFields\b` and the module path. For the bare `validate`, the anchor is an import clause from `@object-ui/core` that names it. Controls: - the same import-clause matcher finds `validateSchema` imported from `@object-ui/core` (`skills/objectui/guides/testing.md`); - nothing re-exports `@object-ui/core` wholesale; - the one namespace import of core (`nonGridRowCeiling.test.tsx`) reads two other names. - `a78cd378c` (PR objectstack-ai#10231) touched only the engine's two `toLocaleDateString` calls, its own new test and its changeset. That is maintenance and adds no production caller. - `validation-engine-stays-unwired.test.ts` guards `ObjectValidationEngine` (`validators/`, objectstack-ai#3110), which is a different module. It does not import `validation-engine.js`, it is unchanged, and it is green in the core run below. - `buildValidationRules` lives in `@object-ui/fields`, not in core. This diff touches none of its files or its consumers. ## Pin: red on the base, green on the head, red under ablation `validation-engine-retired-7659.test.ts` has two halves: - **runtime:** none of the four names is an own key of `import * as corePublicEntry from '../../index.js'`. Firing controls: the key count is above 100, and `validateSchema` and `ObjectValidationEngine` are functions. - **compile time:** `@ts-expect-error` on `import('../../index.js').ValidationEngine` and on the `typeof import(...)` of the three value names. The controls resolve `ObjectValidationEngine` and `validateSchema` through the same two query shapes. This half is compiled by `tsconfig.test.json`: `tsc --listFilesOnly` lists the file among the program's 173 core test files. | leg | tree | vitest | `tsc -p tsconfig.test.json` | |---|---|---|---| | red | base `bc97f9247` + pin | `Tests 1 failed, 1 passed (2)`, received `ValidationEngine`, `defaultValidationEngine`, `validate`, `validateFields` | exit 2, 4 × `TS2578: Unused '@ts-expect-error' directive` | | green | head | `Test Files 2 passed (2)`, `Tests 4 passed (4)` (pin + stays-unwired) | exit 0 | | ablation | head `0b7251a3e` + base `validation-engine.ts` (blob `67b32db01` = base blob) + the export line put back | `Tests 1 failed, 1 passed (2)`, same four names | exit 2, same 4 × TS2578 | Ablation mechanics: - The export line was put back with `ablation-replace.mjs`: anchor 1 → 0, replacement 0 → 1, blob `b3e4e60a9` → `02acae16a`. - Restore was proven by blob equality with HEAD (`b3e4e60a9`) and an empty `git diff HEAD`. The engine file was absent afterwards, and `git status --porcelain` was empty. -⚠️ The first ablation attempt was a no-op. Its replacement contained its anchor, so the tool refused it ("the anchor count moved 1 → 1") and took no reading. It was re-anchored on a span the replacement does not reproduce; only the second run is quoted. - The pin reads `src` through a relative import. No `dist/` is on its resolution path, so no rebuild leg applies. Built-entry reading, as an extra check: after the full build, `packages/core/dist/index.js` has 321 keys and none of the four names; `validateSchema` and `ObjectValidationEngine` are functions. ## Round 2 (head `d6b433924`) Contract review at `73bdb1c97` found two wording faults; both are fixed in `d6b433924` (diff since `73bdb1c97`: 2 files, 3 insertions, 3 deletions): - the `renderer.tsx` idiom pointer now names `toDay` in `components/src/renderers/form/calendar.tsx`; - the validation barrel's docblock opens with `Phase 3.5: Validation module`, not `Validation engine`. Gates at `d6b433924`: - `pnpm --filter … type-check` on core, types and plugin-timeline: exit 0 each (after building the three packages' dependency closures). - The pin plus `validation-engine-stays-unwired.test.ts`: `Test Files 2 passed (2)`, `Tests 4 passed (4)`. - `check:control-bytes`: `✅ check-control-bytes: OK (scanned 8851 tracked text file(s); skipped 85 binary)`. - `check:new-line-citations`: `VERDICT new-cross-file-line-citations: 0 new citation(s)`. - `check-changeset-presence`: `✅ 7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)`. - eslint on the two edited files: 0 errors; warnings 0 (`index.ts`) and 14 (`renderer.tsx`), unchanged. - Serial: fresh `origin/main` `383a34f52` was fetched into a private ref. `git merge-tree --write-tree` is clean. Main has six commits since this branch's base `bc97f9247`; none touches any of this PR's 11 files, and none adds a reference to the four names. ## Gates (round 1, at head `73bdb1c97`) - `pnpm check:readme-exports`: `✅ check-readme-exports: OK (43 tracked README(s) under packages/ …; 543 of them self-imports judged (543 real, 0 wrong-path, 0 fabricated); 3370 export symbol(s) read from 37 of 40 tracked package(s) (39 carry a README) (0 unbuilt, 3 declare no types) …`. - Population: every tracked `packages/*/README.md`, judged against each package's own built type entry, after `turbo run build --filter='./packages/*'` (`Tasks: 39 successful, 39 total`). - Tests (`pnpm exec vitest run`): - core: `Test Files 173 passed (173)`, `Tests 3504 passed (3504)`. - types: `Test Files 239 passed (239)`, `Tests 5269 passed (5269)`. - plugin-timeline: `Test Files 31 passed (31)`, `Tests 346 passed (346)`. - `buildValidationRules` suites (`ObjectForm.locationRefusal`, `ObjectForm.locationResidue`, `ObjectForm.locationStoredRange`, `richtext-field-metadata-7083`, `LocationField.refusalDiagnostic`): `Test Files 5 passed (5)`, `Tests 68 passed (68)`. - `pnpm --filter … type-check` on core, types and plugin-timeline: exit 0 for each. - Changeset gates: - `check-changeset-presence`: `✅ 7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)`. - `check-changeset-fixed` and `check-changeset-no-major`: ✅. - `check:pending-changeset-literals`: ✅. - `check:changeset-claims` (report-only): it names 6349 and 4765 for `data-protocol.ts`. I re-read both paragraphs and neither describes the edited doc comment. Its self-contradiction reading is ✅. - `check-changeset-overwrite` (report-only): it reports the two corrected pending changesets, which is the case it is meant to report. - `check:new-line-citations`: `VERDICT new-cross-file-line-citations: 0 new citation(s)`. - `check:control-bytes`: `✅ check-control-bytes: OK (scanned 8851 tracked text file(s); skipped 85 binary)`. - Docs gates: - `docs:check-links`: `Links are valid across 17 scan roots.` - `check:doc-snippets`: `674 of 674 block(s) judged, 0 failed`. - `check:doc-types`, `check:doc-fences`, `check:doc-example-ids` and `check:doc-example-readers`: ✅. - `check:unreferenced-sources`: `OK Every shipped source file in every covered package is reachable.` `check:test-path-roots`: ✅. - Governed guard (`--test` on the 11 changed paths): `NOT GOVERNED`. - Lint, narrowed to the four touched code files (`eslint --format json`): 4 files, 0 errors. Warnings are 0, 0, 14 and 44, identical to the base versions of the same files. - Population: `eslint.config.js`, one root flat config with no per-package config. - Invariance: no type-aware linting (no `parserOptions.project` or `projectService`), no import-resolution plugin, and no custom rule in `eslint-rules/` reads the filesystem. This diff therefore cannot move a verdict on an untouched file. Repo-wide `pnpm lint` is CI's. - Serial at round 1: `origin/main` `1ea21c4e6` was fetched into a private ref and `git merge-tree --write-tree` was clean. The current reading (six commits since base, against `383a34f52`) is in the Round 2 section above. ## Acceptance notes - `@object-ui/types` keeps the engine's type family: `AdvancedValidationSchema`, `AdvancedValidationRule`, `ValidationRuleType`, `ValidationFunction`, `AsyncValidationFunction`, `ValidationContext`, `AdvancedValidationResult` and `AdvancedValidationError`. At this change nothing in `packages/*/src`, `apps/` or `examples/` reads them; the engine was their only reader. The ruling covers the four core exports only, so the types stay. Retiring them would be a separate public-type removal (Clause-② again); the seat filed it as objectui#10719, and this PR does not touch it. - The section banner above those types in `data-protocol.ts` still reads "Phase 3.5: ValidationSchema - Complete Validation Engine". I left it: it is a phase label and belongs to the same follow-up. - Section 5 of `troubleshooting.md` also said the validation module "uses Zod schemas from `@object-ui/types`", but `schema-validator.ts` imports no Zod. That was the sentence naming "the validation engine", so it was rewritten with the rest of the paragraph. - `ValidationContext.locale` (added by PR objectstack-ai#10231, still pending in `9909-machine-locale-sweep.md`) stays declared with no reader in core. The dated note in that changeset says so. --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #9909
Clause-②: yes
What this is
The one-time sweep of the machine-locale class across every package that depends on
@object-ui/i18n, landing together with the ONE repo-wide census test the ruling on objectui#9786 (batch #194 item 4, letter B′, maintainer 「同意」 2026-09-20) asked for. ⛔ No ESLint rule and no new workflow: the census is an ordinary test in@object-ui/i18n, and it replaces (it is not added beside)plugin-detail's package-scoped census pin, which this PR deletes.Every face below handed
Intlno locale tag, an explicitundefined, or the'default'pseudo-tag. None of those means "the user's locale":Intlreads each of them as the MACHINE's locale, which is neither of this repository's two locale channels. They now readuseDisplayLocale()(tenant regional default, then the active UI language, then'en'), and every plain helper takes the tag from its caller as a REQUIRED parameter (theformatImportJobTimeshape), except where the helper is a published export, where the new parameter is optional and additive.The census this PR reflects
origin/main62597c588(unchanged at the time of the final run), taken 2026-09-24T05:46Z.scripts/js-comment-mask.mjs(self-test: 45 cases pass); nothing hand-rolled. Scope: non-test sources of every workspace package, then partitioned by the manifest rule below.plugin-ganttthat quotes a past repair of this very class.undefined), 1 disguised ('default'), 6 hard-coded literals. Plus a bounded reading for the half a source scan cannot see (optional-locale helpers called without one): 4 more sites found.packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts), per AGENTS.md 完善设计器的每一个细节 #9.The repo-wide census test
packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts, besideuseDisplayLocale.ts, whose doc comment states the rule it enforces.pnpm-workspace.yamlglobs expand to whosepackage.jsonnames@object-ui/i18nin any dependency field. A package that gains the dependency is covered with no edit.undefined, or'default', and a hard-coded BCP-47 literal, unless DECLARED with file, expression (cited by content, never by line), exact count and reason.CommentThread.tsx'scatchmust be scanned AND declared.packages/i18nandpackages/coremust be OUT), plus a matcher self-test on a fixture.@object-ui/coreis outside by the ruling's rule (no@object-ui/i18ndependency, React-free by charter, and its declared contract for a caller with no locale in hand is "follow the runtime default"). Its two rows are repaired by threading below.The runtime tripwire (
recordLocaleArguments) moved fromplugin-detail's differential pin into the private@object-ui/test-support, now also patchingNumber.prototype.toLocaleString(a number face never reaches the globalIntl.NumberFormat) and counting'default'as the machine locale. Every repaired surface is pinned with it: two declared locales (de-DEvsen, UI languageenon both) must render differently, and every locale-taking call must receive the declared tag.Per-site table
Verdicts: DEFECT = this defect; FALLBACK = deliberate fallback; N/A = not applicable. "(outside)" = not in the census population.
DEFECT, repaired (50)
apps/consolepages/developer/FlowRunsPage.tsx·fmtDate(runs table)locale;useDisplayLocale()in the runs panelconsoleFaces.displayLocale-9909apps/consolepages/system/ApprovalsInboxPage.tsx·formatDate(three tooltips)localeapps/consoleApprovalsInboxPage.tsx·formatPayloadValueepoch-ms date branchlocale, viapayloadSummaryapps/consoleApprovalsInboxPage.tsx·formatPayloadValuenumber branchapps/consoleApprovalsInboxPage.tsx·formatPayloadValueISO date-time branchapps/consoleApprovalsInboxPage.tsx·decisionAmountEntrydisplay amountlocale(queue, sort, drawer)apps/consolepages/system/AuditLogPage.tsx·formatDate(timestamp column)localeapp-shellconsole/organizations/manage/InvitationsPage.tsx· expiryuseDisplayLocale()orgDates.displayLocale-9909app-shell…/manage/MembersPage.tsx· join dateapp-shell…/manage/AcceptInvitationPage.tsx· expiryapp-shellconsole/marketplace/MarketplacePackagePage.tsx· version dateMarketplacePackagePage.displayLocale-9909app-shellconsole/ai/ConversationsSidebar.tsx·formatTimestampolder-than-a-week taillocaleaiFaces.displayLocale-9909app-shellConversationsSidebar.tsx·absoluteTimestamptooltiplocaleapp-shellconsole/ai/BuildDebugDrawer.tsx· token countuseDisplayLocale()app-shellviews/RecordApprovalsPanel.tsx·formatDate(timeline)localeRecordApprovalsPanel.displayLocale-9909app-shellviews/metadata-admin/AuditPanel.tsx·fmtTimelocale, default'en-US')metadataAdminFaces.displayLocale-9909app-shell…/ResourceHistoryPage.tsx·formatWhen(HistoryPanel)localeapp-shell…/external/ExternalDatasourcePanel.tsx·formatSnapshotlocaleapp-shell…/external/SchemaBrowser.tsx· row estimateuseDisplayLocale()app-shell…/previews/FlowRunsPanel.tsx·fmtTime(run start)app-shell…/previews/JobPreview.tsx·formatWhen(next fires; was explicitundefined)localeplugin-chartsAdvancedChartImpl.tsx· x-axis ISO-date tick, short spanuseDisplayLocale()chartFaces.displayLocale-9909plugin-chartsAdvancedChartImpl.tsx· x-axis ISO-date tick, long spanplugin-chartsAdvancedChartImpl.tsx·formatYTickcompact ticksplugin-chartsAdvancedChartImpl.tsx· single-value faceplugin-chartsChartContainerImpl.tsx· tooltip valueplugin-chartsnormalizeChartSchema.ts·formatterFor(specformat)locale(module-internal)fieldswidgets/GridField.tsx·displayTextcurrency branchlocalealready passedGridField.displayLocale-9909fieldsGridField.tsx·displayTextnumeric branchfieldsGridField.tsx· read-only totaldisplayLocalein scopefieldsGridField.tsx· line-items totalcomponentscustom/export-progress-dialog.tsx· processed countuseDisplayLocale()displayLocale-9909(components)componentsexport-progress-dialog.tsx· total countcomponentsdebug/DebugPanel.tsx· event timecore(outside)validation/validation-engine.ts·date_mindefault messageValidationContext.locale(types) is read when passed. No caller threads it yet, and no production code outsidecoreimportsValidationEngineat this head, so the default message still formats in the runtime default wherever it is reached. (Seat correction, review record5811064032.)validation-engine.displayLocale-9909(the engine directly)core(outside)validation-engine.ts·date_maxdefault messageplugin-formFormAnalytics.tsx· total submissionsuseDisplayLocale()formFaces.displayLocale-9909plugin-formMasterDetailForm.tsx·moneystack (was explicitundefined)plugin-formoccSave.tsx·formatVersionTime(conflict dialog)localeplugin-chatbotAiPendingActionsInbox.tsx·formatRelativepast-30-days taillocale(its doc comment, which said the bare call "already localizes it", is corrected)chatbotFaces.displayLocale-9909plugin-chatbotuseObjectChat.ts· user message stampuseDisplayLocale()plugin-chatbotuseObjectChat.ts· auto-response stampplugin-reportReportExportEngine.ts· HTML "Generated:"locale, defaultDISPLAY_LOCALE_LAST_RESORT;ReportViewerpasses the display localereportExportGenerated.displayLocale-9909plugin-reportReportExportEngine.ts· PDF "Generated:"plugin-dashboardDashboardRenderer.tsx· record-count badgeuseDisplayLocale()dashboardFaces.displayLocale-9909plugin-dashboardrecordFields.tsx·renderFieldValuecurrency branch ⭐ found by readingdisplayLocaleit was already handedplugin-dashboardrecordFields.tsx·renderFieldValuedate-format branch ⭐ found by readingplugin-designercomponents/VersionHistory.tsx· version timeuseDisplayLocale()VersionHistory.displayLocale-9909plugin-gridObjectGrid.tsx· record-detail inferred currency ⭐ found by readingdisplayLocale(its date sibling already did)gridCurrencyFaces.displayLocale-9909plugin-gridObjectGrid.tsx· mobile card amount line ⭐ found by reading⭐ = invisible to any source scan: the call passes an argument list that merely OMITS the tag to a helper whose locale is optional (
formatCurrency,formatCompactCurrency,formatDate). Found by a bounded reading of every call to the optional-locale formatters in@object-ui/core/@object-ui/fields/@object-ui/i18n; pinned by the runtime tripwire.DEFECT, NOT repaired here (1)
plugin-ai(outside)NLQueryInput.tsx· query-history date@object-ui/i18ndependency and every visible string is hard-coded English. The repo's precedent is that an unwired component is wired WHOLE, not one face at a time (objectui#7173, after objectui#7142), and that needs a manifest and lockfile change this card does not own. Carried as a finding. The census catches it by itself: the moment a wiring PR adds the dependency, this exact site turns the census red until it is repaired in that PR.FALLBACK, declared in the census with its reason (7 sites), plus 1 outside
collaboration⭐ LIT CONTROLCommentThread.tsx·formatAbsoluteDatecatch { return date.toLocaleDateString(); }tryalready passes the session language; thecatchfires only on the RangeError a structurally malformed tag raises, and restores the pre-localisation face instead of printing the raw ISO string. Repairing it would BE the defect. ⛔ Not touched.plugin-detailInlineFieldInput.tsx·d.toLocaleDateString('en-CA')input type=date), whose value is defined asYYYY-MM-DD.plugin-ganttGanttView.tsx·task.start.toLocaleDateString('en-CA')(×2: double-click and context-menu edit)plugin-ganttGanttView.tsx·task.end.toLocaleDateString('en-CA')(×2)plugin-ganttGanttView.tsx·tzOffsetMs'snew Intl.DateTimeFormat('en-US', …)formatToPartsis read back throughNumber(), so the tag must guarantee Latin digits and a 24-hour cycle. Nothing is shown to a reader.core(outside)utils/number-display.ts·formatDisplayNumber'scatchretry withundefinedcatchfor a tagIntlitself rejected (a malformed tenant tag), retrying with every other option kept. Not in the census population.N/A, declared in the census with its reason (5)
fieldscurrency.ts·currencyFractionDigitsproberesolvedOptions(); formats nothing.componentsui/calendar.tsx·data-day={day.date.toLocaleDateString()}componentsui/calendar.tsx·formatMonthDropdowntoLocaleString("default", …)⭐ newly seen'default'), in the same No-Touch file. Reached only under a dropdowncaptionLayout, which no first-party caller sets;formattersis the declared override for a wrapper incomponents/src/custom/. ⛔ Not edited.apps/consolesdui-tiers-preview.tsx·'$' + r.amount.toLocaleString()rollupOptions.input, so onlyindex.htmlis a build entry (no build was run for this reading).apps/consolesdui-tiers-preview.tsx·'Total: $' + total.toLocaleString()EXPRESSION rows read for the runtime half, judged NOT this defect (3)
plugin-ganttGanttView.tsx·const dateLocale = language || undefined|| undefinedleg is unreachable:useObjectTranslation()returnscontext?.language || i18n.language || 'en'. Measured with the tripwire on a provider-lessResourceWorkloadmount: every call received'en', none receivedundefined. Listed, not repaired. The comment beside it, which says the leg falls back to the browser locale, is false (Acceptance notes).plugin-ganttResourceWorkload.tsx·const locale = language || undefinedapp-shellconsole/marketplace/InstalledListWidget.tsx·toLocaleString(language || undefined)useObjectTranslation()reasoning.Contract changes (Clause-② at-tier review)
Additive only; nothing that compiled before stops compiling.
@object-ui/types:ValidationContextgains an optionallocale.@object-ui/plugin-report:exportReport(format, report, data, config?, locale?),exportAsHTML(report, data, config?, locale?),exportAsPDF(report, data, config?, locale?), each defaulting toDISPLAY_LOCALE_LAST_RESORT('en'). ⛔ Deliberately not aReportExportConfigmember, since that type is authored report metadata.@object-ui/plugin-report:LiveExportOptionsgains an optionallocale, forwarded byexportWithLiveData.plugin-charts'sformatterFor(format, locale)now REQUIRES the locale.@object-ui/test-support(never published): new exportsrecordLocaleArguments,recordLocaleArgumentsAsync,isMachineLocale,LOCALE_TAKING_INTL_CONSTRUCTORS, typeLocaleCall.Evidence
node ../objectstack/scripts/ablation-replace.mjsturned GridField's numericn.toLocaleString(locale)back into a bare call (anchor 1 → 0, blobbde59eed→3e15e4fe). Census:1 failed | 6 passed, naming the re-inserted GridField call in its computed diagnostic. Restored: blob == HEAD,git diff HEADempty, census7 passed. The first attempt used an anchor that missed; the tool refused to write, so it was a no-op and was re-run with the right anchor.CommentThreadcatch turns the census red on two tests: the lit control, anddeclared 1, found 0. Restored to HEAD.git diff HEADempty.pnpm --filter … type-check) passes for test-support, types, core, i18n, components, fields, plugin-charts, plugin-form, plugin-chatbot, plugin-report, plugin-dashboard, plugin-grid, plugin-designer, plugin-detail, app-shell and console, after a turbo build of the dependency closure.689004ac: 19 pin, census and tripwire files, 221 tests green.check:control-bytesOK,check:new-line-citations0 new,check-changeset-presenceOK,check-hand-rolled-comment-maskOK,check-changeset-no-majorOK. Also run: vi-mock-specifiers / inherit / override-shape, test-path-roots, phantom-deps, lint-coverage, type-check-coverage, unreferenced-sources, pending-changeset-literals, readme-exports (after building@object-ui/cli). All exit 0.--no-inline-config, 62 files by the JSON count, 0 errors. Every warning is pre-existing (no-explicit-anyand hooks rules on untouched lines, plus 4 pre-existinganys on lines this diff re-flowed). This is a narrowed run, not the repo-wide lint, which belongs to CI. The config enables no type-aware linting, so this diff cannot move a verdict in an untouched file.Acceptance notes
normalizeChartSchema.test.tsnow passes'en'toformatterForinstead of relying on the runner's locale. Three suites that countconsole.warnin provider-less mounts (AdvancedChartImpl.missingCategoryKey,AdvancedChartImpl.unprojectedSeriesDimension,useObjectChat.maxToolRoundtrips) now mount under an enI18nProvider: a component that reads the display locale makes react-i18next print its once-per-moduleNO_I18NEXT_INSTANCEnotice on a provider-less first mount, and those spies were written to count only the component's own diagnostics.I18nProviderat all (the console always mounts one).MarketplacePackagePage.tsximportsuseDisplayLocaleon its own line, becausemarketplace-preview-namespace-3546pins that page'suseObjectTranslationimport by its literal line.check-changeset-claimsasks for.check-changeset-overwritereports the edit (report-only, its "correcting a declaration on purpose" case).ApprovalsInboxPage's relative-age tail formats withlanguage(UI language) while this page's repaired faces use the display locale. Not machine-locale and not this class; noted as a channel mix on one page.plugin-calendar's sub-views defaultlocale = "default"(the disguised machine locale), butCalendarViewalways passeseffectiveLocaleand the sub-views are not exported. Unreachable today; noted.'¥'currency prefix hard-coded inGridFieldandMasterDetailForm, andplugin-timeline'slocale = 'en'default, were noted by the earlier census and are unchanged.@object-ui/core'sformatDisplayNumberretry drops a malformed tag (machine locale), whileplugin-report'sformatNumberInDisplayLocaledegrades to'en': two answers to the same question. The disagreement is noted, not changed.package.jsonand nopnpm-lock.yamlchange.Session:
https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC(the dispatch seat's session; this PR was implemented by theos-devagent it dispatched).Generated by Claude Code