Repository navigation
Commit 361ea24
feat(spec): a page object-form's thank-you panel is translated through pages.PAGE.components.ID.submitBehavior (#28269)
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao1Jx7WKoJNdFZFmj4TXFV34)_
Fixes #28214
Clause-②: yes
Metadata-protocol: added
Skills-docs: follow-up #28215 (skills/objectstack-i18n/SKILL.md: the
page row gains `components.{component_id}.submitBehavior` beside 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 会拒收挂错组件的键。
- 为什么改:表单视图自己的感谢页在 #28195 已经能翻译,但页面里的表单组件引用的是同一个配置却没有键,showcase
的“新建项目(向导)”在任何语言下创建成功后都显示英文的 “Project created”。
- 风险与回滚:只放宽(新增可选键);规则与表单视图的感谢页共用同一个函数:只翻 thank-you、只翻作者写了的文案、缺键保留原文。回滚即
revert 本 PR。
- 先看:`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-form` component takes `submitBehavior` from 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 strict `pages.PAGE.components.ID` entry declared no
`submitBehavior`, and `translatePage` never read
`properties.submitBehavior`. The showcase's
`showcase_new_project_wizard` page confirmed a create with "Project
created" in every locale.
- **Spec, the key (`translation.zod.ts`).** `pages.PAGE.components.ID`
gains `submitBehavior: { title?, message? }` (strict). The node is built
by a new factory, `submitBehaviorTranslationSchema(surface)`, which the
form view's `_views.VIEW.submitBehavior` now uses too, so the two
addresses carry one shape: the same alias table (`heading`, `text`,
`body`, `description`) and the same `url` / `kind` prescriptions. This
follows the `actionTranslationSchema` precedent in the same file. The
components node's alias table gains `thankYou: 'submitBehavior'`, as the
`_views` node has.
- **Spec, the overlay (`i18n-resolver.ts`).**
`translateFormSubmitBehavior` is now the one comparison both carriers
call. It takes a `lookup(attr)` instead of the view's object name and
view key. `translateView` passes the `_views` lookup it used before.
`translatePage` passes a new `lookupPageFormSubmitBehaviorText` (locale
chain, key by key) for every addressed component whose type is the new
exported `PAGE_FORM_COMPONENT_TYPE` (`'object-form'`), on the same
`addressed` arbitration 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 a `kind: 'thank-you'` panel is touched, only
an authored non-empty string is translated (a key cannot invent a
`message`), and a missing key keeps the authored string. The component
is returned unchanged when nothing applies.
- **The extractor (`packages/cli/src/utils/i18n-extract.ts`).** A new
`pushThankYouPanel(out, root, behavior, …)` emits the two slots and
holds the "kind must be thank-you" reading once. `pushFormViewCopy` and
`emitPageComponentCopy` both call it, so `os i18n extract` scaffolds
`pages.PAGE.components.ID.submitBehavior.title` / `.message` for every
`object-form` the 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.
- **The lint (`packages/lint/src/validate-translation-references.ts`).**
A `components.ID.submitBehavior` key 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
nearest `object-form` id offered. A page this stack does not declare is
skipped, as for the tab items.
- **Showcase.** The wizard's `object-form` had no `id`, so it had no
address at all. It gets `id: 'new_project_form'`
(`examples/app-showcase/src/ui/pages/new-project-wizard.page.ts`), and
`src/system/translations/index.ts` gains its zh-CN `title`,
`description` and panel `title` / `message`. The id also makes the
form's `title` and `description` addressable, which the coverage ratchet
counts, so all four strings are translated and `check:i18n-coverage`
stays at its baseline.
- **Generated.** `packages/spec/api-surface/system.json` and
`export-origins/system.json` record the new `PAGE_FORM_COMPONENT_TYPE`
export. `check:docs` and `check:strictness-ledger` stayed green with no
regeneration (the generated translation reference renders the page
node's `components` member collapsed).
- **Changeset.** `.changeset/28214-page-form-thank-you-translation.md`:
`@objectstack/spec`, `@objectstack/cli`, `@objectstack/lint`, all
`minor`, with both declaration lines.
**Address: route (a), under the page.** The key lives at
`pages.PAGE.components.ID.submitBehavior`, the #6080 route
`lookupPageComponentCopy` already serves for the same component's
`title` and `description`.
**Route not taken: (b), under the bound object** (the `_sections`
precedent, `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. `_sections` is
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 a `dataSource` binding),
while (a) reuses the id addressing the extractor, lint and resolver
already share through `walkAddressedPageComponents`. The cost of (a) is
that an `object-form` without an `id` has 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.** `translatePage` is the `page`
entry of the translator table (`i18n-resolver.ts`, `page:
translatePage`), which `/meta/page` reads go through
(`packages/rest/src/meta-item-read-gate.ts`,
`translateMetadataDocument`). The console's `object-form` renderers take
`submitBehavior` from 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.
- **Docs (seat-approved, commit `8837fd00`):** the
`content/docs/ui/translations.mdx` key table gains both thank-you panel
rows: the form view's `objects.NAME._views.VIEW.submitBehavior.title /
.message` (which #28195 left out) and the page form's
`pages.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 through `PageSchema`, both bundle
nodes through `TranslationDataSchema`). A zh-CN overlay through
`translateMetadataDocument('page', …)`, with the form's own `title`
translated beside it and a form nested in a `page:card` reached by id.
The fallback chain (ja-JP over `[zh-CN]`). A missing key keeps the
authored panel. Controls: a panel with no `message` gains none; a
`redirect` behavior is untouched; a key under another component's id
does not reach the form, and a `page:card` with a `submitBehavior` key
gains no panel; the panel key and the component `title` key do not
cross; an `object-form` without an id has no key; no mutation. The
schema face: `url` refused as `unrecognized_keys` at the page path, and
`kind` refused with the prescription.
- `packages/cli/test/i18n-page-form-thank-you-coverage.test.ts` (new,
4): the walker emits both slots per addressed `object-form`, nested ones
included, and none for an id-less form or a `page: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 declared `object-form`, nested included, reports nothing;
refused on a `page:card` by name; refused under an undeclared id with
`Did you mean "project_form"?`; a page the stack does not declare is not
judged.
- The existing `i18n-resolver.form-thank-you.test.ts` (11) stays green
through the refactor of `translateFormSubmitBehavior`.
<details><summary>Ablation: removing the page overlay branch turns the
overlay cases red</summary>
`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, blob
`dfefaad6` → `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 blob `dfefaad6`, `git diff HEAD` empty. The
test imports the resolver from source, so no `dist` rebuild is involved.
</details>
## Acceptance notes
- **No packaged-base comparison on pages.**
`translateFormSubmitBehavior` applies ADR-0029 D9.2a (an override beats
the catalog) when its caller hands it the packaged config.
`translatePage` takes `ResolveOptions` and no packaged base for any
string (its `label`, `description`, header copy and component copy all
apply the catalog unconditionally), so the page call passes `undefined`
and the panel follows the page's existing rule. The comparison is still
the one function, so a later card that hands `translatePage` its
packaged base gets the panel for free. Noted, not filed.
- **The wizard's form section labels** (`Basics`, `Health`, `Budget &
Schedule`) and their descriptions stay untranslatable on a page form:
`object-form`'s `properties.sections` has no page key, and `_sections`
addresses an object's form sections, not a page component's. Outside
this card. Noted, not filed.
- **File surface.**
`examples/app-showcase/src/ui/pages/new-project-wizard.page.ts` is
outside the claim's surface. Route (a) needs the form to carry an `id`,
and the showcase form had none, so the showcase strings could not land
without it (4 added lines, the `id` and a comment).
## Verification
- **Final head `8837fd00`** (seat note): one docs-only commit on top of
`9e7da93f`. Re-deriving gives 108 families, the 88 below plus 20 docs
families. The 20 new ones ran on `8837fd00` and all exit 0; `--ran`: 108
derived, 108 run, 0 NOT-MEASURED. `check:doc-authoring` exit 0. The
readings below are on `9e7da93f` and carry, since the commit touches
docs only.
All readings are on head `9e7da93f` (the four feature commits through
`2093ae5e`, merged with `origin/main` at `0027c4a2`), after `pnpm
install --frozen-lockfile` and a declarations build of the touched
closures, unless noted.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 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-loads` wants every published package
built with declarations, `check:i18n-coverage` wants the example closure
it names). After `turbo run build` over `./packages/**` and
`./examples/**` (77 tasks), both exit 0: `check:dual-build-cjs-loads`
"113 published require entry point(s) across 70 package(s) load", and
`check: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` (after `pnpm --filter @objectstack/cli build`): Test Files 355
passed, Tests 5109 passed. These two ran before the merge of
`origin/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.
- `typecheck` for `@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: the `ui/`
package dists are not built in this worktree. It judges the local
checkout, not the diff, and CI does not run its scan.
- Dogfood: not owed. The title carries no `!` and `Clause-②` carries no
`(narrowing)` arm.
- CI on this PR: not yet read; the seat reads the check-runs.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibyMTiYRPdRNbNBCGdF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f64724a commit 361ea24
13 files changed
Lines changed: 652 additions & 44 deletions
File tree
- .changeset
- content/docs/ui
- examples/app-showcase/src
- system/translations
- ui/pages
- packages
- cli
- src/utils
- test
- lint/src
- spec
- api-surface
- export-origins
- src/system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
75 | 76 | | |
76 | 77 | | |
77 | 78 | | |
| |||
87 | 88 | | |
88 | 89 | | |
89 | 90 | | |
| 91 | + | |
90 | 92 | | |
91 | 93 | | |
92 | 94 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1253 | 1253 | | |
1254 | 1254 | | |
1255 | 1255 | | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
1256 | 1270 | | |
1257 | 1271 | | |
1258 | 1272 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
26 | 30 | | |
27 | 31 | | |
28 | 32 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
| 116 | + | |
| 117 | + | |
116 | 118 | | |
117 | 119 | | |
118 | 120 | | |
| |||
154 | 156 | | |
155 | 157 | | |
156 | 158 | | |
| 159 | + | |
157 | 160 | | |
158 | 161 | | |
159 | 162 | | |
| |||
489 | 492 | | |
490 | 493 | | |
491 | 494 | | |
492 | | - | |
493 | | - | |
494 | | - | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
495 | 518 | | |
496 | 519 | | |
497 | 520 | | |
| |||
1348 | 1371 | | |
1349 | 1372 | | |
1350 | 1373 | | |
| 1374 | + | |
| 1375 | + | |
| 1376 | + | |
| 1377 | + | |
| 1378 | + | |
1351 | 1379 | | |
1352 | 1380 | | |
1353 | 1381 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2490 | 2490 | | |
2491 | 2491 | | |
2492 | 2492 | | |
| 2493 | + | |
| 2494 | + | |
| 2495 | + | |
| 2496 | + | |
| 2497 | + | |
| 2498 | + | |
| 2499 | + | |
| 2500 | + | |
| 2501 | + | |
| 2502 | + | |
| 2503 | + | |
| 2504 | + | |
| 2505 | + | |
| 2506 | + | |
| 2507 | + | |
| 2508 | + | |
| 2509 | + | |
| 2510 | + | |
| 2511 | + | |
| 2512 | + | |
| 2513 | + | |
| 2514 | + | |
| 2515 | + | |
| 2516 | + | |
| 2517 | + | |
| 2518 | + | |
| 2519 | + | |
| 2520 | + | |
| 2521 | + | |
| 2522 | + | |
| 2523 | + | |
| 2524 | + | |
| 2525 | + | |
| 2526 | + | |
| 2527 | + | |
| 2528 | + | |
| 2529 | + | |
| 2530 | + | |
| 2531 | + | |
| 2532 | + | |
| 2533 | + | |
| 2534 | + | |
| 2535 | + | |
| 2536 | + | |
| 2537 | + | |
| 2538 | + | |
| 2539 | + | |
| 2540 | + | |
| 2541 | + | |
| 2542 | + | |
| 2543 | + | |
| 2544 | + | |
| 2545 | + | |
| 2546 | + | |
| 2547 | + | |
| 2548 | + | |
| 2549 | + | |
| 2550 | + | |
| 2551 | + | |
| 2552 | + | |
| 2553 | + | |
| 2554 | + | |
| 2555 | + | |
| 2556 | + | |
| 2557 | + | |
| 2558 | + | |
| 2559 | + | |
| 2560 | + | |
| 2561 | + | |
| 2562 | + | |
| 2563 | + | |
| 2564 | + | |
| 2565 | + | |
| 2566 | + | |
2493 | 2567 | | |
2494 | 2568 | | |
2495 | 2569 | | |
| |||
0 commit comments