Repository navigation
Commit 7c3cc88
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
- .changeset
- content/docs/references/system
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- examples/app-showcase/src/system/translations
- packages
- cli
- src/utils
- test
- lint/src
- spec/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 | |
|---|---|---|---|
| |||
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
95 | | - | |
| 95 | + | |
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
| 120 | + | |
120 | 121 | | |
121 | 122 | | |
122 | 123 | | |
| |||
201 | 202 | | |
202 | 203 | | |
203 | 204 | | |
204 | | - | |
| 205 | + | |
205 | 206 | | |
206 | 207 | | |
207 | 208 | | |
| |||
375 | 376 | | |
376 | 377 | | |
377 | 378 | | |
378 | | - | |
| 379 | + | |
379 | 380 | | |
380 | 381 | | |
381 | 382 | | |
| |||
611 | 612 | | |
612 | 613 | | |
613 | 614 | | |
614 | | - | |
| 615 | + | |
615 | 616 | | |
616 | 617 | | |
617 | 618 | | |
| |||
816 | 817 | | |
817 | 818 | | |
818 | 819 | | |
819 | | - | |
| 820 | + | |
820 | 821 | | |
821 | 822 | | |
822 | 823 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
656 | 656 | | |
657 | 657 | | |
658 | 658 | | |
659 | | - | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
660 | 662 | | |
661 | 663 | | |
662 | | - | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
663 | 668 | | |
664 | 669 | | |
665 | 670 | | |
| |||
843 | 848 | | |
844 | 849 | | |
845 | 850 | | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
846 | 856 | | |
847 | 857 | | |
848 | 858 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| 76 | + | |
| 77 | + | |
76 | 78 | | |
77 | 79 | | |
78 | 80 | | |
| |||
457 | 459 | | |
458 | 460 | | |
459 | 461 | | |
460 | | - | |
461 | | - | |
462 | | - | |
463 | | - | |
464 | | - | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
465 | 468 | | |
466 | 469 | | |
467 | 470 | | |
468 | 471 | | |
469 | 472 | | |
470 | 473 | | |
471 | 474 | | |
472 | | - | |
473 | | - | |
474 | | - | |
475 | | - | |
| 475 | + | |
476 | 476 | | |
477 | 477 | | |
478 | 478 | | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
479 | 497 | | |
480 | 498 | | |
481 | 499 | | |
| |||
1489 | 1507 | | |
1490 | 1508 | | |
1491 | 1509 | | |
1492 | | - | |
1493 | | - | |
1494 | | - | |
| 1510 | + | |
| 1511 | + | |
| 1512 | + | |
| 1513 | + | |
1495 | 1514 | | |
1496 | 1515 | | |
1497 | 1516 | | |
| |||
1501 | 1520 | | |
1502 | 1521 | | |
1503 | 1522 | | |
1504 | | - | |
| 1523 | + | |
| 1524 | + | |
1505 | 1525 | | |
1506 | | - | |
1507 | | - | |
1508 | | - | |
| 1526 | + | |
1509 | 1527 | | |
1510 | 1528 | | |
1511 | 1529 | | |
| |||
0 commit comments