Repository navigation
feat(spec): a page object-form's thank-you panel is translated through pages.PAGE.components.ID.submitBehavior - #28269
Conversation
…h components.ID.submitBehavior translatePage overlays an object-form component's properties.submitBehavior title / message from pages.PAGE.components.ID.submitBehavior, through translateFormSubmitBehavior, now generic over its lookup so the form view and the page component share one comparison. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyMTiYRPdRNbNBCGdF
…el key Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyMTiYRPdRNbNBCGdF
…N copy Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyMTiYRPdRNbNBCGdF
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyMTiYRPdRNbNBCGdF
📓 Docs Drift CheckThis PR changes 3 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 177 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7568d8d800088a108e66d97181e60ebca3e091cb && git checkout 7568d8d800088a108e66d97181e60ebca3e091cb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 744ad0ad3afbc29fd0bfee9e8a8e3b7ac3958aec 8837fd001625eb762b195ac503cac896a3edd6b8 && git checkout -B drift-repro 744ad0ad3afbc29fd0bfee9e8a8e3b7ac3958aec && git merge --no-ff 8837fd001625eb762b195ac503cac896a3edd6b8
node scripts/docs-audit/affected-docs.mjs --json 744ad0ad3afbc29fd0bfee9e8a8e3b7ac3958aec
|
…ations key table Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyMTiYRPdRNbNBCGdF
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #28214 (body, triage 6530114050 and 6531357708, claim 6532865741, claim correction 6534307642 adding the showcase page id to the file surface, os-dev-report 6534288052), the S1 should-fix of record 6528389425 on #28195 that filed it, the PR body, the diff of 9e7da93 against merge base 0027c4a and the one docs-only commit 8837fd0 on top of it (two key-table rows in content/docs/ui/translations.mdx, the form view's panel key and the page form's panel key, nothing else), the head's check-runs (read 2026-10-30T22:34Z: check-runs on 8837fd0: 50 total, 41 success (Auto Label, Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 9 skipped path-filtered (18.x upgrade harness (opt-in), Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts); 0 failure, 0 in progress). Author ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentAgainst the card's scenario (the showcase wizard's "Project created" panel staying English in every locale) the change holds: the id gives the form an address, the zh-CN pack carries the four strings, and the resolver overlays them by the form view's rules. Against the spec on main, the factory removes the only path by which the two panel addresses could drift, and the exported constant makes resolver, extractor and lint agree on which component carries the key, the Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm 维护者速读
席位核对 · ACCEPT ·
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm
Requested by Tesla · project thread
Fixes #28214
Clause-②: yes
Metadata-protocol: added
Skills-docs: follow-up #28215 (skills/objectstack-i18n/SKILL.md: the page row gains
components.{component_id}.submitBehaviorbeside the_views.{view_name}row #28195 already owes, in the same next i18n skill edit)维护者速读
object-form组件,提交后的感谢页(submitBehavior为thank-you时的title/message)有了翻译键pages.PAGE.components.ID.submitBehavior.title/.message,translatePage会把它套到下发的页面上;提取器会生成这两个键,lint 会拒收挂错组件的键。packages/spec/src/system/i18n-resolver.ts的translateFormSubmitBehavior(改为两处共用)和translatePage里新增的object-form分支,packages/spec/src/system/translation.zod.ts的submitBehaviorTranslationSchema,packages/lint/src/validate-translation-references.ts的页面组件检查。pnpm --filter @objectstack/spec exec vitest run src/system/i18n-resolver.page-form-thank-you.test.ts,预期 12 条全绿。What changes
A page
object-formcomponent takessubmitBehaviorfrom the form view by reference (ObjectFormPropsSchema.submitBehavior), so a page can author{ kind: 'thank-you', title, message }on it. #28195 gave a form view's panel its key and overlay. The page component's panel had neither: the strictpages.PAGE.components.IDentry declared nosubmitBehavior, andtranslatePagenever readproperties.submitBehavior. The showcase'sshowcase_new_project_wizardpage confirmed a create with "Project created" in every locale.translation.zod.ts).pages.PAGE.components.IDgainssubmitBehavior: { title?, message? }(strict). The node is built by a new factory,submitBehaviorTranslationSchema(surface), which the form view's_views.VIEW.submitBehaviornow uses too, so the two addresses carry one shape: the same alias table (heading,text,body,description) and the sameurl/kindprescriptions. This follows theactionTranslationSchemaprecedent in the same file. The components node's alias table gainsthankYou: 'submitBehavior', as the_viewsnode has.i18n-resolver.ts).translateFormSubmitBehavioris now the one comparison both carriers call. It takes alookup(attr)instead of the view's object name and view key.translateViewpasses the_viewslookup it used before.translatePagepasses a newlookupPageFormSubmitBehaviorText(locale chain, key by key) for every addressed component whose type is the new exportedPAGE_FORM_COMPONENT_TYPE('object-form'), on the sameaddressedarbitration as the component copy, so a nested form is reached by the id route too. The rules are therefore the form view's, with no second copy: only akind: 'thank-you'panel is touched, only an authored non-empty string is translated (a key cannot invent amessage), and a missing key keeps the authored string. The component is returned unchanged when nothing applies.packages/cli/src/utils/i18n-extract.ts). A newpushThankYouPanel(out, root, behavior, …)emits the two slots and holds the "kind must be thank-you" reading once.pushFormViewCopyandemitPageComponentCopyboth call it, soos i18n extractscaffoldspages.PAGE.components.ID.submitBehavior.title/.messagefor everyobject-formthe page addresses, and the coverage report counts an authored panel a supported locale leaves untranslated. A non-thank-you kind records both keys unauthored, as on the view side.packages/lint/src/validate-translation-references.ts). Acomponents.ID.submitBehaviorkey is refused (translation-target-unknown) under a component the page declares with another type, and under an id the page does not declare, with the nearestobject-formid offered. A page this stack does not declare is skipped, as for the tab items.object-formhad noid, so it had no address at all. It getsid: 'new_project_form'(examples/app-showcase/src/ui/pages/new-project-wizard.page.ts), andsrc/system/translations/index.tsgains its zh-CNtitle,descriptionand paneltitle/message. The id also makes the form'stitleanddescriptionaddressable, which the coverage ratchet counts, so all four strings are translated andcheck:i18n-coveragestays at its baseline.packages/spec/api-surface/system.jsonandexport-origins/system.jsonrecord the newPAGE_FORM_COMPONENT_TYPEexport.check:docsandcheck:strictness-ledgerstayed green with no regeneration (the generated translation reference renders the page node'scomponentsmember collapsed)..changeset/28214-page-form-thank-you-translation.md:@objectstack/spec,@objectstack/cli,@objectstack/lint, allminor, with both declaration lines.Address: route (a), under the page. The key lives at
pages.PAGE.components.ID.submitBehavior, the #6080 routelookupPageComponentCopyalready serves for the same component'stitleanddescription.Route not taken: (b), under the bound object (the
_sectionsprecedent,objects.OBJECT._sections). The panel is copy the page authors, not the object. Two pages binding the same object can word their confirmation differently (a create wizard and a quick-add card), and an object-scoped key would make them share one translation or need a page-scoped sub-key, which is route (a) spelled longer._sectionsis object-scoped because a form section's fields are the object's fields. A thank-you panel has no such tie. Route (b) would also need a second walk to find the bound object (objectName, or adataSourcebinding), while (a) reuses the id addressing the extractor, lint and resolver already share throughwalkAddressedPageComponents. The cost of (a) is that anobject-formwithout anidhas no key, the same rule every other page component follows. The showcase form gains an id for that reason.The page door runs the translator.
translatePageis thepageentry of the translator table (i18n-resolver.ts,page: translatePage), which/meta/pagereads go through (packages/rest/src/meta-item-read-gate.ts,translateMetadataDocument). The console'sobject-formrenderers takesubmitBehaviorfrom the component's props (ui/packages/plugin-form/src/ObjectForm.tsx,WizardForm.tsx), so the translated panel reaches the reader with no ui change. This is a reading of the code, not a live measure.8837fd00): thecontent/docs/ui/translations.mdxkey table gains both thank-you panel rows: the form view'sobjects.NAME._views.VIEW.submitBehavior.title / .message(which feat(spec): a form view's thank-you panel is translated through _views.VIEW.submitBehavior #28195 left out) and the page form'spages.NAME.components.FORMID.submitBehavior.title / .message.Tests
packages/spec/src/system/i18n-resolver.page-form-thank-you.test.ts(new, 12): parsed fixtures (the page throughPageSchema, both bundle nodes throughTranslationDataSchema). A zh-CN overlay throughtranslateMetadataDocument('page', …), with the form's owntitletranslated beside it and a form nested in apage:cardreached by id. The fallback chain (ja-JP over[zh-CN]). A missing key keeps the authored panel. Controls: a panel with nomessagegains none; aredirectbehavior is untouched; a key under another component's id does not reach the form, and apage:cardwith asubmitBehaviorkey gains no panel; the panel key and the componenttitlekey do not cross; anobject-formwithout an id has no key; no mutation. The schema face:urlrefused asunrecognized_keysat the page path, andkindrefused with the prescription.packages/cli/test/i18n-page-form-thank-you-coverage.test.ts(new, 4): the walker emits both slots per addressedobject-form, nested ones included, and none for an id-less form or apage:card; authored copy seeds, unauthored and non-thank-you copy does not; the extracted skeleton parses; coverage reports exactly the authored strings zh-CN leaves untranslated, and none once covered.packages/lint/src/validate-translation-references.test.ts(+4): a panel over a declaredobject-form, nested included, reports nothing; refused on apage:cardby name; refused under an undeclared id withDid you mean "project_form"?; a page the stack does not declare is not judged.i18n-resolver.form-thank-you.test.ts(11) stays green through the refactor oftranslateFormSubmitBehavior.Ablation: removing the page overlay branch turns the overlay cases red
node scripts/ablation-replace.mjs --file packages/spec/src/system/i18n-resolver.ts --anchor 'if (addressed && component.type === PAGE_FORM_COMPONENT_TYPE) {' --replacement 'if (false as boolean) { /* ABLATED-28214 */'. The anchor hit once (1 → 0, blobdfefaad6→8ebe517f). The new spec test went to Tests 4 failed | 8 passed (12): the zh-CN overlay, the fallback chain, the no-message control and the title/panel crossing control. The other controls stayed green, as expected, since they assert that nothing is applied. The file was restored to its HEAD blobdfefaad6,git diff HEADempty. The test imports the resolver from source, so nodistrebuild is involved.Acceptance notes
translateFormSubmitBehaviorapplies ADR-0029 D9.2a (an override beats the catalog) when its caller hands it the packaged config.translatePagetakesResolveOptionsand no packaged base for any string (itslabel,description, header copy and component copy all apply the catalog unconditionally), so the page call passesundefinedand the panel follows the page's existing rule. The comparison is still the one function, so a later card that handstranslatePageits packaged base gets the panel for free. Noted, not filed.Basics,Health,Budget & Schedule) and their descriptions stay untranslatable on a page form:object-form'sproperties.sectionshas no page key, and_sectionsaddresses an object's form sections, not a page component's. Outside this card. Noted, not filed.examples/app-showcase/src/ui/pages/new-project-wizard.page.tsis outside the claim's surface. Route (a) needs the form to carry anid, and the showcase form had none, so the showcase strings could not land without it (4 added lines, theidand a comment).Verification
8837fd00(seat note): one docs-only commit on top of9e7da93f. Re-deriving gives 108 families, the 88 below plus 20 docs families. The 20 new ones ran on8837fd00and all exit 0;--ran: 108 derived, 108 run, 0 NOT-MEASURED.check:doc-authoringexit 0. The readings below are on9e7da93fand carry, since the commit touches docs only.All readings are on head
9e7da93f(the four feature commits through2093ae5e, merged withorigin/mainat0027c4a2), afterpnpm install --frozen-lockfileand a declarations build of the touched closures, unless noted.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 88 commands. All 88 were run with stdin from /dev/null and the exit code captured before any pipe: 86 exited 0 on the first pass, and 2 refused with exit 3 (PREREQUISITE NOT MET:check:dual-build-cjs-loadswants every published package built with declarations,check:i18n-coveragewants the example closure it names). Afterturbo run buildover./packages/**and./examples/**(77 tasks), both exit 0:check:dual-build-cjs-loads"113 published require entry point(s) across 70 package(s) load", andcheck:i18n-coverage"OK (14 config(s), 527 baselined untranslated string(s), none new)".node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE: "88 derived famil(ies) accounted for — 88 run, 0 NOT-MEASURED".pnpm --filter @objectstack/spec test(verify lock): VERDICT command-exit 0, Test Files 765 passed, Tests 22638 passed, 1 todo.pnpm --filter @objectstack/spec check:generated: "All 14 generated artifacts are up to date".pnpm --filter @objectstack/lint test: Test Files 181 passed, Tests 7676 passed.pnpm --filter @objectstack/cli exec vitest run --project unit(afterpnpm --filter @objectstack/cli build): Test Files 355 passed, Tests 5109 passed. These two ran before the merge oforigin/main, whose incoming commits touch one lint test (validate-ai-tool-references.test.ts) and one cli script (scripts/check-app-nav-i18n.mjs), neither the extractor nor the translation-reference check; the derived lint and cli gates ran on the merged head.typecheckfor@objectstack/spec,@objectstack/lint,@objectstack/cli: exit 0 (the first lint attempt hit TS7016 on a dependency closure built without declarations; rebuilt with declarations, exit 0).node scripts/check-nul-bytes.mjs: exit 0.node scripts/check-dev-prereqs.mjs: exit 1, on box state only: theui/package dists are not built in this worktree. It judges the local checkout, not the diff, and CI does not run its scan.!andClause-②carries no(narrowing)arm.Generated by Claude Code