Skip to content

Commit 7c3cc88

Browse files
feat(spec): a form view's thank-you panel is translated through _views.VIEW.submitBehavior (#28195)
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D <!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoKFwjoByiJo4bxxAeWE6jk3)_ Fixes #28004 Clause-②: yes Metadata-protocol: added Skills-docs: follow-up #28215 (skills/objectstack-i18n/SKILL.md `_views.{view_name}` row, with the next i18n skill edit) ## 维护者速读 - 改了什么:表单视图的感谢页(`submitBehavior` 为 `thank-you` 时的 `title` / `message`)有了翻译键 `objects.OBJECT._views.VIEW.submitBehavior.title` / `.message`,`translateView` 会把它套到下发的表单配置上;提取器和 lint 也认这个键。 - 为什么改:公开表单用别的语言打开时,标题和字段都已翻译,提交后的感谢页却还是作者写的原文,没有任何键可以翻它。 - 风险与回滚:只放宽(新增可选键),已有翻译包不受影响;只翻作者写了的 thank-you 文案,其他三种提交行为不动;被租户改过的文案照旧以改过的为准。回滚即 revert 本 PR。 - 先看:`packages/spec/src/system/i18n-resolver.ts` 的 `translateFormSubmitBehavior`,`packages/spec/src/system/translation.zod.ts` 的 `_views.submitBehavior`,`packages/cli/src/utils/i18n-extract.ts` 的 `pushFormViewCopy`。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/system/i18n-resolver.form-thank-you.test.ts`,预期 11 条全绿。 - 需要你做的:无,席位落地。 ## What changes A form view's `submitBehavior: { kind: 'thank-you', title, message }` is the confirmation a form shows after a submit, and the public form path defaults to it. The strict `_views` translation entry declared no key for it and `translateView` never read `config.submitBehavior`, so a public form read in another language confirmed the submit in the authoring language under a translated heading and translated field labels. - **Spec, the key (`translation.zod.ts`).** The `_views.VIEW` entry gains `submitBehavior: { title?, message? }` (strict). It is nested the way the authored copy is nested, so the panel's heading never shares a key with the form's own `title` (one key cannot stand for two source strings, the same reason `emptyState.title` is nested). Its inner aliases mirror the thank-you member's own (`heading`, `text`, `body`, `description`). `url` and `kind` carry `guidance` prescriptions, since neither is display copy and `kind` is what an author copying the authored block would bring along. `thankYou` on the view node names `submitBehavior` as the rename. - **Spec, the overlay (`i18n-resolver.ts`).** `translateView` calls a new `translateFormSubmitBehavior` beside `translateFormTitle`, reading `objects.OBJECT._views.VIEW_KEY.submitBehavior.{title,message}` on the same locale chain and the same view key. Rules, each the form title's: only a `kind: 'thank-you'` panel is touched; only an authored non-empty string is translated (a key cannot invent a `message` the form leaves to the console's default); a missing key keeps the authored string; ADR-0029 D9.2a override beats the catalog, judged per string through the one `valueOverridesPackagedBase` against the packaged view's `config.submitBehavior` (a packaged view with no panel makes every served string authored). The `config` reference is kept when nothing changes. - **The extractor (`packages/cli/src/utils/i18n-extract.ts`).** A new `pushFormViewCopy` emits a served form's `_views` tails, `title` and `submitBehavior.title` / `.message`, and both form sites (container forms through `pushContainerFormTitles`, and a `viewKind: 'form'` item) call it. So `os i18n extract` scaffolds the keys and the coverage report counts an authored panel a supported locale leaves untranslated. A non-thank-you kind records both keys unauthored, exactly as an untitled form records `title`. - **The lint (`packages/lint/src/validate-translation-references.ts`).** `FORM_ONLY_VIEW_TAILS` = `title`, `submitBehavior`. A `submitBehavior` key over a list view is refused like a `title` key is, and the default form's key is legal for those two tails only. - **Showcase (`examples/app-showcase/src/system/translations/index.ts`).** The showcase authors two thank-you panels (`showcase_inquiry` `formViews.contact`, the public `/forms/contact-us` web-to-lead form, and `showcase_contact` `formViews.create`). The extractor now counts their four authored strings for zh-CN, so zh-CN gains them and `check:i18n-coverage` stays at its baseline (`none new`, below). This file was added to the claim's surface by the seat's amendment. - **Generated.** `content/docs/references/system/translation.mdx` (`gen:docs`) and `docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md` (`gen:strictness-ledger`, one more strict object). - **Changeset.** `.changeset/28004-form-thank-you-translation.md`: `@objectstack/spec`, `@objectstack/cli`, `@objectstack/lint`, all `minor`, with both declaration lines. **The public door really runs the translator.** `packages/rest/src/form-routes.ts:353` calls `translateMetadataDocument('view', match.view, bundle, { ...this.translateOptionsFor(i18n, locale), packagedBase: … })` inside `GET /api/v1/forms/SLUG`, and `:357` serves `view.config` as the response's `form` when the matched form is the item's `config`; the response carries `Vary: Accept-Language` (`:403`). The console's `FormPage` reads `loaded.form.submitBehavior` (`ui/apps/console/src/components/FormPage.tsx:2577`) and draws the authored `title` / `message` (`:2957`–`:2958`), so the translated panel reaches an anonymous visitor with no ui change. The internal `/_console/forms/NAME` path reads the view through `/meta/view`, which `meta-item-read-gate.ts:3062` already sends through `translateView`. **Shape not taken.** Flat keys on the view node (`thankYouTitle` / `thankYouMessage`) would also close the gap, but they invent names the authoring schema does not have and sit beside the form's `title`, the exact two-strings-one-node confusion the `title` key was built to avoid. The nested key mirrors `submitBehavior.title` / `.message` as authored. ## Tests Every run below is on head `af93f347` unless noted. - `packages/spec/src/system/i18n-resolver.form-thank-you.test.ts` (new, 11): parsed fixtures (container through `ViewSchema`, bundle node through `ObjectTranslationDataSchema`); zh-CN overlay on a named form and on the default form; the fallback chain; a missing key keeps the authored panel; a panel without a `message` gains none; a `redirect` behavior is untouched and keeps its `config` reference; the panel key and the form-title key do not cross; override beats catalog per string, and a packaged view with no panel makes every served string authored; no mutation; the schema face (`url` refused as `unrecognized_keys` at the panel path, `kind` refused with the prescription). - `packages/cli/test/i18n-form-thank-you-coverage.test.ts` (new, 5): the walker emits both slots per container form and per form item under the runtime key; authored copy seeds and unauthored (and non-thank-you) copy does not; no slot on a list view; the extracted skeleton parses; coverage reports exactly the authored strings a locale leaves untranslated and none once covered. - `packages/cli/test/i18n-form-title-coverage.test.ts`: its path filter now matches the view node's own `title` only, so the new `submitBehavior.title` slot one level down does not enter the title pin. - `packages/lint/src/validate-translation-references.test.ts` (+3): a panel accepted over a named form, the default form and a form item; refused over a list view; the default form's key stays a non-key for a tail beside the panel. <details><summary>Package runs</summary> - `pnpm --filter @objectstack/spec test`: Test Files 756 passed, 1 skipped; Tests 22530 passed (on `1fb17014`; `af93f347` added only the two schema-face cases, re-run: 11/11). - `pnpm --filter @objectstack/lint test`: Test Files 178 passed; Tests 7647 passed. - `pnpm --filter @objectstack/cli exec vitest run --project unit`: 352/354 files, 5072 passed; the 2 failures were `published-subpath-console.pin` and `published-subpath-hook-body.pin`, which refuse to run against an unbuilt `packages/cli` ("packages/cli is not built"). After `pnpm --filter @objectstack/cli build` both pass (with the two form coverage files: 4 files, 39 tests). - `typecheck` for `@objectstack/spec`, `@objectstack/lint`, `@objectstack/cli`: all Done (exit 0). - `pnpm --filter @objectstack/spec check:generated --fix`: regenerated `gen:docs` and `gen:strictness-ledger`; re-check of both green. </details> ## Verification <details><summary>Derived gates (dispatch-gates --commands, 108 lines)</summary> `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran 28004-gates-ran-af93f347.list`: **108 derived, 107 run (all exit 0), 1 NOT MEASURED, 0 unrun.** - NOT MEASURED: `pnpm check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET: it loads every package's built CJS entry and this worktree built only the affected closures). CI's `Lint & Repo Gates` runs it; this diff touches no package entry or build config. - The loop ran across `1fb17014` to `af93f347`; the commit in between changed one test file and the changeset's wording only. Re-run on `af93f347` after `pnpm --filter @objectstack/spec build`: `check:api-surface`, `check:dual-source-exports`, `check:entry-nameability`, `check:exported-any`, `check:generated`, `check:skill-examples` (the first pass refused on a dist older than the new test, exit 3 / 1), and the three changeset gates (`check-adr-0087-registration`, `check-changeset-no-major`, `check-empty-changeset`). All exit 0. - `check-plugin-teardown-shape --self-test` first refused on the shallow clone (its positive control commit `621a4876` was absent); after fetching that commit: 48 cases pass, exit 0. - `pnpm check:i18n-coverage` first refused (example closure unbuilt); after building the closure it names: `OK (14 config(s), 527 baselined untranslated string(s), none new)`. </details> ## Acceptance notes - **ui half.** No ui edit is in this PR. On the read above the console already draws the served panel, so the follow-up `domain:ui` card the order reserves may turn out to be a verification-only item; the seat decides. - **A page's `object-form` component.** `ObjectFormPropsSchema.submitBehavior` (`packages/spec/src/ui/component.zod.ts`) is the same thank-you block authored on a page component (the showcase's `new-project-wizard` page uses one). It is translated through the page's own component tree or not at all, and this PR does not touch it. Noted, not filed. - **Skills.** `skills/objectstack-i18n/SKILL.md` lists the `_views.{view_name}` tails; teaching `submitBehavior.title` / `.message` there is owed (line above) and stays off this PR to keep it off governed paths. --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibxqJVde7kDCjJSHEUo)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3739d8d commit 7c3cc88

12 files changed

Lines changed: 598 additions & 39 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': minor
4+
'@objectstack/lint': minor
5+
---
6+
7+
feat(spec): a form view's thank-you panel is translated through `objects.OBJECT._views.VIEW.submitBehavior.title` / `.message`
8+
9+
Clause-②: yes
10+
Metadata-protocol: added
11+
12+
A form whose `submitBehavior` is `{ kind: 'thank-you', title, message }` used to confirm a submit in the authoring language only: the strict `_views` translation entry declared no key for the panel, and `translateView` never read `config.submitBehavior`. A public form read in another language showed a translated heading and field labels, then a source-language confirmation.
13+
14+
- **The key.** `objects.OBJECT._views.VIEW.submitBehavior` takes `title` and `message`, nested the way the authored copy is nested, so the panel's heading never shares a key with the form's own `title`. A `thankYou` key is refused naming `submitBehavior`, and `url` / `kind` are refused with a prescription, since neither is display copy.
15+
- **The overlay.** `translateView` overlays the two strings onto the served form's `config.submitBehavior`, on the same locale chain and under the same rule as the form's `title`: only a `kind: 'thank-you'` panel is translated, only copy the form authors (a key cannot invent a `message` the form left to the console's default), a missing key keeps the authored string, and a string edited on top of the packaged view keeps the edit (ADR-0029 D9.2a, judged per string). The anonymous `GET /api/v1/forms/SLUG` already serves the translated view config under `Accept-Language`, so a public form's visitor now reads the panel in their language with no client change.
16+
- **Extraction and coverage.** `os i18n extract` scaffolds `_views.VIEW.submitBehavior.title` / `.message` for every form a view container serves and for a `viewKind: 'form'` item, keyed by its runtime identity, and the coverage report counts an authored panel a supported locale leaves untranslated.
17+
- **Lint.** `os lint` accepts a `submitBehavior` key on exactly the views a `title` key is legal on (a named form view, the default form, a form item) and refuses one over a list view.

‎content/docs/references/system/translation.mdx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Translation data for a single object
9292
| **pluralLabel** | `string` | optional | Translated plural label |
9393
| **description** | `string` | optional | Translated object description |
9494
| **fields** | `Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string>; … }>` | optional | Field-level translations |
95-
| **_views** | `Record<string, { label?: string; description?: string; title?: string; emptyState?: object; … }>` | optional | View translations keyed by view name |
95+
| **_views** | `Record<string, { label?: string; description?: string; title?: string; submitBehavior?: object; … }>` | optional | View translations keyed by view name |
9696
| **_actions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Action translations keyed by action name |
9797
| **_sections** | `Record<string, { label?: string; description?: string; fields?: Record<string, object> }>` | optional | Section translations keyed by section name |
9898
| **_tabs** | `Record<string, { label?: string }>` | optional | Filter-preset tab translations keyed by tab name |
@@ -117,6 +117,7 @@ Translation data for a single field
117117
| **label** | `string` | optional | Translated view label |
118118
| **description** | `string` | optional | Translated view description |
119119
| **title** | `string` | optional | Translated form view title — overlays a form view's own `title` (`FormViewSchema.title`); only a form that authors one is translated |
120+
| **submitBehavior** | `{ title?: string; message?: string }` | optional | Translated thank-you panel copy — overlays a form view's `submitBehavior` `title` / `message` when its `kind` is `'thank-you'`; only copy the form authors is translated |
120121
| **emptyState** | `{ title?: string; message?: string }` | optional | Translated empty-state copy shown when the view has no rows |
121122
| **bulkActions** | `Record<string, { label?: string; confirmText?: string; confirmLabel?: string; params?: Record<string, object> }>` | optional | Selection-bar translations keyed by bulk-action def name (`BulkActionDefSchema.name`) |
122123

@@ -201,7 +202,7 @@ Translation data for a single object
201202
| **pluralLabel** | `string` | optional | Translated plural label |
202203
| **description** | `string` | optional | Translated object description |
203204
| **fields** | `Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string>; … }>` | optional | Field-level translations |
204-
| **_views** | `Record<string, { label?: string; description?: string; title?: string; emptyState?: object; … }>` | optional | View translations keyed by view name |
205+
| **_views** | `Record<string, { label?: string; description?: string; title?: string; submitBehavior?: object; … }>` | optional | View translations keyed by view name |
205206
| **_actions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Action translations keyed by action name |
206207
| **_sections** | `Record<string, { label?: string; description?: string; fields?: Record<string, object> }>` | optional | Section translations keyed by section name |
207208
| **_tabs** | `Record<string, { label?: string }>` | optional | Filter-preset tab translations keyed by tab name |
@@ -375,7 +376,7 @@ Translation data for a single object
375376
| **pluralLabel** | `string` | optional | Translated plural label |
376377
| **description** | `string` | optional | Translated object description |
377378
| **fields** | `Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string>; … }>` | optional | Field-level translations |
378-
| **_views** | `Record<string, { label?: string; description?: string; title?: string; emptyState?: object; … }>` | optional | View translations keyed by view name |
379+
| **_views** | `Record<string, { label?: string; description?: string; title?: string; submitBehavior?: object; … }>` | optional | View translations keyed by view name |
379380
| **_actions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Action translations keyed by action name |
380381
| **_sections** | `Record<string, { label?: string; description?: string; fields?: Record<string, object> }>` | optional | Section translations keyed by section name |
381382
| **_tabs** | `Record<string, { label?: string }>` | optional | Filter-preset tab translations keyed by tab name |
@@ -611,7 +612,7 @@ Translation data for a single object
611612
| **pluralLabel** | `string` | optional | Translated plural label |
612613
| **description** | `string` | optional | Translated object description |
613614
| **fields** | `Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string>; … }>` | optional | Field-level translations |
614-
| **_views** | `Record<string, { label?: string; description?: string; title?: string; emptyState?: object; … }>` | optional | View translations keyed by view name |
615+
| **_views** | `Record<string, { label?: string; description?: string; title?: string; submitBehavior?: object; … }>` | optional | View translations keyed by view name |
615616
| **_actions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Action translations keyed by action name |
616617
| **_sections** | `Record<string, { label?: string; description?: string; fields?: Record<string, object> }>` | optional | Section translations keyed by section name |
617618
| **_tabs** | `Record<string, { label?: string }>` | optional | Filter-preset tab translations keyed by tab name |
@@ -816,7 +817,7 @@ Translation data for a single object
816817
| **pluralLabel** | `string` | optional | Translated plural label |
817818
| **description** | `string` | optional | Translated object description |
818819
| **fields** | `Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string>; … }>` | optional | Field-level translations |
819-
| **_views** | `Record<string, { label?: string; description?: string; title?: string; emptyState?: object; … }>` | optional | View translations keyed by view name |
820+
| **_views** | `Record<string, { label?: string; description?: string; title?: string; submitBehavior?: object; … }>` | optional | View translations keyed by view name |
820821
| **_actions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Action translations keyed by action name |
821822
| **_sections** | `Record<string, { label?: string; description?: string; fields?: Record<string, object> }>` | optional | Section translations keyed by section name |
822823
| **_tabs** | `Record<string, { label?: string }>` | optional | Filter-preset tab translations keyed by tab name |

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.
1919

2020
| Dir | Sites |
2121
|---|---|
22-
| `system/` | 388 |
22+
| `system/` | 389 |

‎examples/app-showcase/src/system/translations/index.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,10 +656,15 @@ export const ShowcaseTranslationBundle = {
656656
// `_views.create.title` — the sparse `formViews.create` form's own
657657
// heading (`title: 'New contact'`), which `translateView` overlays
658658
// onto the served form's `config.title`. 新建联系人 follows the
659-
// 新建… shape a create heading takes.
659+
// 新建… shape a create heading takes. Its `submitBehavior` is the
660+
// thank-you panel the same form shows after a save, overlaid onto the
661+
// served form's `config.submitBehavior`.
660662
_views: {
661663
default: { label: '联系人' },
662-
create: { title: '新建联系人' },
664+
create: {
665+
title: '新建联系人',
666+
submitBehavior: { title: '联系人已创建', message: '其余信息可在记录中补充。' },
667+
},
663668
},
664669
// `_sections` — the four groups of the default edit form in
665670
// `ui/views/contact.view.ts`, plus the sparse `formViews.create`
@@ -843,6 +848,11 @@ export const ShowcaseTranslationBundle = {
843848
_views: {
844849
default: { label: '客户询问' },
845850
triage: { label: '询问分流' },
851+
// The public web-to-lead form's thank-you panel — the confirmation
852+
// an anonymous visitor reads after submitting `/forms/contact-us`.
853+
contact: {
854+
submitBehavior: { title: '感谢您!', message: '我们已收到您的留言,专员会尽快与您联系。' },
855+
},
846856
},
847857
// The public web-to-lead form's single section
848858
// (`ui/views/inquiry.view.ts` `formViews.contact`). Declares a stable

‎packages/cli/src/utils/i18n-extract.ts‎

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
* objects.<name>._views.<view>.label
7474
* objects.<name>._views.<view>.description
7575
* objects.<name>._views.<view>.emptyState.title / .message
76+
* objects.<name>._views.<form>.title
77+
* objects.<name>._views.<form>.submitBehavior.title / .message
7678
* objects.<name>._views.<view>.bulkActions.<def>.label / .confirmText
7779
* / .confirmLabel
7880
* objects.<name>._views.<view>.bulkActions.<def>.params.<param>.label
@@ -457,25 +459,41 @@ function pushViewEntries(out: ExpectedEntry[], objectName: string, viewName: str
457459
* {@link defaultListViewKey} keeps for the default list, so the key written
458460
* here is the key the runtime reads.
459461
*
460-
* **Title only.** The other `_views` tails stay unemitted for a form: a
461-
* container's form entry carries no `label`, and its `description` is not
462-
* overlaid by anything a form renders. A form that authors no title still
463-
* records the key (`pushOptional`), so the coverage gate notices a bundle
464-
* that translates one the source never wrote.
462+
* **Title and thank-you panel only** ({@link pushFormViewCopy}). The other
463+
* `_views` tails stay unemitted for a form: a container's form entry carries
464+
* no `label`, and its `description` is not overlaid by anything a form
465+
* renders. A form that authors no title still records the key
466+
* (`pushOptional`), so the coverage gate notices a bundle that translates one
467+
* the source never wrote.
465468
*/
466469
function pushContainerFormTitles(out: ExpectedEntry[], containerObject: string, container: any): void {
467470
if (!container?.form && !container?.formViews) return;
468471
const prefix = `${containerObject}.`;
469472
for (const item of expandViewContainer(containerObject, container)) {
470473
if (item.viewKind !== 'form') continue;
471474
const key = item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
472-
const config = item.config as { title?: unknown } | undefined;
473-
pushOptional(out, ['objects', containerObject, '_views', key, 'title'], config?.title, 'view', {
474-
objectName: containerObject,
475-
});
475+
pushFormViewCopy(out, containerObject, key, item.config);
476476
}
477477
}
478478

479+
/**
480+
* Emit the `_views` tails ONE served form view carries: its heading
481+
* (`_views.<key>.title`, `config.title`) and its thank-you panel
482+
* (`_views.<key>.submitBehavior.title` / `.message`, the `title` / `message`
483+
* of a `kind: 'thank-you'` `config.submitBehavior`) — the strings
484+
* `translateView` overlays. A panel of another kind authors no copy, so its
485+
* two keys are recorded unauthored (`pushOptional`), exactly as an untitled
486+
* form's `title` is.
487+
*/
488+
function pushFormViewCopy(out: ExpectedEntry[], objectName: string, key: string, config: unknown): void {
489+
const form = config && typeof config === 'object' ? config as { title?: unknown; submitBehavior?: any } : undefined;
490+
const root = ['objects', objectName, '_views', key];
491+
pushOptional(out, [...root, 'title'], form?.title, 'view', { objectName });
492+
const thankYou = form?.submitBehavior?.kind === 'thank-you' ? form.submitBehavior : undefined;
493+
pushOptional(out, [...root, 'submitBehavior', 'title'], thankYou?.title, 'view', { objectName });
494+
pushOptional(out, [...root, 'submitBehavior', 'message'], thankYou?.message, 'view', { objectName });
495+
}
496+
479497
/**
480498
* Emit `_views.<view>.bulkActions.<def>.*` for a list view's authored
481499
* `bulkActionDefs[]` (#14253's resolver, #14376's walk).
@@ -1489,9 +1507,10 @@ export function collectExpectedEntries(
14891507
// `i18n/missing-view` had zero producers repo-wide while the ratchet
14901508
// reported green (#4123).
14911509
//
1492-
// A container's form views (`formViews.*`, the default `form`) emit one
1493-
// `_views` tail, `title` — the form heading `translateView` overlays (see
1494-
// `pushContainerFormTitles`). Their `label` / `description` stay
1510+
// A container's form views (`formViews.*`, the default `form`) emit the
1511+
// `_views` tails `translateView` overlays — the form heading `title` and the
1512+
// thank-you panel's `submitBehavior.title` / `.message` (see
1513+
// `pushFormViewCopy`). Their `label` / `description` stay
14951514
// unemitted: a container form entry carries no label, and nothing a form
14961515
// renders reads a `_views` description.
14971516
const views: any[] = Array.isArray(config?.views) ? config.views : [];
@@ -1501,11 +1520,10 @@ export function collectExpectedEntries(
15011520

15021521
if (view.name) {
15031522
pushViewEntries(out, containerObject, view.name, view);
1504-
// A `viewKind: 'form'` item carries its form under `config`, title included.
1523+
// A `viewKind: 'form'` item carries its form under `config`, title and
1524+
// thank-you panel included.
15051525
if (view.viewKind === 'form' && view.config && typeof view.config === 'object') {
1506-
pushOptional(out, ['objects', containerObject, '_views', view.name, 'title'], view.config.title, 'view', {
1507-
objectName: containerObject,
1508-
});
1526+
pushFormViewCopy(out, containerObject, view.name, view.config);
15091527
}
15101528
continue;
15111529
}

0 commit comments

Comments
 (0)