Repository navigation
feat(spec,rest,lint,cli): a form view's title is translated through objects.OBJECT._views.VIEW.title, and GET /forms/:slug serves it in the visitor's language - #25035
Conversation
…nd GET /forms/:slug serves it untranslated Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…bjects.OBJECT._views.VIEW.title Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…rm-view-title-key
…eset Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…rm-view-title-key
📓 Docs Drift CheckThis PR changes 4 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 152 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 483b10b24b4a67930c9dc84c96f29c089a0db0c2 && git checkout 483b10b24b4a67930c9dc84c96f29c089a0db0c2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 601b258d72cc6dde7e838a79d20eef18ba381103 6f43b6b2e08d7e8f0a57b1bfd3213f3911c90602 && git checkout -B drift-repro 601b258d72cc6dde7e838a79d20eef18ba381103 && git merge --no-ff 6f43b6b2e08d7e8f0a57b1bfd3213f3911c90602
node scripts/docs-audit/affected-docs.mjs --json 601b258d72cc6dde7e838a79d20eef18ba381103
|
Contract reviewServed-tier: Reviewed against card #24969 (body, claim 6303839918, dev report 6307030278), standing ruling 「甲」 (#6015 comments 6292005854 and 6303865025), the PR body and file list, the three-dot diff Check-runs on the head at the time of this record: 40 runs, 0 red. 33 completed (25 success, 8 skipped by their own filters: Console Pin Gate, Dogfood Verify CLI, Temporal Conformance, Dogfood Regression Gate shards, 18.x upgrade harness, Packed-tarball smoke). 7 still in progress: Test Core (1/6 through 6/6) and Type Check · workspace. Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Lint · ESLint, Lint & Repo Gates, Check Changeset, Spec property liveness, Governed Surface Queue Guard and Build Core are green. The verdict below is conditional on none of the 7 pending runs going red; a red there is to be named and this record re-read. ① Derived judgments
② Semver levelChangeset declares ③ Boundary flags
Implemented-by: VERDICT: PASS Seat re-read of the check-runs at landing: all 42 runs on |
Closes #24969
Clause-②: yes (widening)
What changes
A public form could not be titled in more than one language.
FormViewSchema.titlehad no translation key: the_views.VIEWtranslation entry namedtitleas a rename ontolabel,translateViewnever readconfig.title, andGET /forms/:sluganswered the top-levellabelfrom the matched view before translation. A form that declared a title served it in the authoring language, so declaring one replaced a translated heading (the object label) with an untranslated one in every other locale.packages/spec/src/system/translation.zod.ts):objects.OBJECT._views.VIEW.titleis a declared key. Thetitle -> labelalias on that entry is removed, andheadingandformTitleare named as renames ontotitle.packages/spec/src/system/i18n-resolver.ts):translateViewoverlays the key onto a served form item'sconfig.title(newtranslateFormTitle), through the same one_viewsread (lookupViewText) as the view'slabelanddescription. It follows the rules every other view string follows: only an authored title is translated (a form without one gains none), a missing key keeps the authored title, and an explicit override beats the catalog against the packaged view (ADR-0029 D9.2a,valueOverridesPackagedBase).packages/rest/src/form-routes.ts): the servedform.titleis the translated one, because the route already serves the translated view'sconfig. The top-levellabelis now taken from the translated view when the view authors alabel. When the view authors none, the route still answers none, because the translator falls back to the view's machine name and that must not shadow the title. The console'spayload.label ?? form.title ?? objectSchema.labelstays meaningful: all three arrive translated, in the same order.objectstack validate(packages/lint/src/validate-translation-references.ts): a newformsfact set holds the_viewskeys of every form the composer serves:formViews.KEYentries, the defaultform(formor itsname, asked ofexpandViewContainer), andviewKind: 'form'items. Atitlekey over a form resolves. Over a list view it is refused, astranslation-target-unknown, with:View "all_leads" of object "crm_lead" is not a form view, so no title resolves this key.The fix line points at_views.VIEW.label. The default form's key is legal fortitlealone, and its other tails stay non-keys as before.packages/cli/src/utils/i18n-extract.ts): it emits_views.VIEW.titlefor every form a container serves, keyed by the composer's runtime name (newpushContainerFormTitles), and for aviewKind: 'form'item. Soos i18n extractscaffolds the slot, andcheck:i18n-coveragereports an authored title that a supported locale leaves untranslated.examples/app-showcaseauthorstitle: 'New contact'onformViews.create. The new slot raised its untranslated count by one (414 to 415), so the zh-CN bundle now carries_views.create.title.Why its own key, not the view's
labelkeyThe card asked for one of two options: reuse
_views.VIEW.labeliftitlealiaseslabel, or add a key. I read the schemas. Thetitle -> labelalias exists only on the TRANSLATION entry (a "did you mean" for a refused key). The authored shapes keep them separate:ViewItem.labelnames the view.FormViewSchema.titleis the heading the form draws.formViews.KEY,form) carry nolabelat all, becauseFormViewSchemadeclares none.expandViewContainerstampslabel: v.label, which is undefined.viewKind: 'form'item can author both, with different text, and the console preferslabel.So they are two source strings. If both used one key, one translation would stand for two different strings. The new key mirrors the authored key, the way
emptyState.titledoes. It sits in the same_viewsnode and uses the same view key, so it adds no second key convention.Tests
Measured at head
6f43b6b2e0, which is merged withorigin/main73432299c0.87d4974eabadds the tests before the fix.7532ea3c08, in the working tree only. The mutation was confirmed on disk:translateFormTitlecount 0,forms.addcount 0,pushContainerFormTitlescount 0,servedLabelcount 0. I rebuilt spec, andablation-dist-preflight --absentfound the marker in 0 of 232 built files. Results:6 failed | 162 passed. The new resolver cases failed, and so did thetitleandheadingschema cases.3 failed | 6 passed. These were the translated title, the label-not-machine-name case and the authored-label case. The "no title" controls stayed green.3 failed | 188 passed.4 failed | 1 passed.git checkout HEAD -- PATHS.git diff HEADwas empty andgit status --porcelainwas clean. After the spec rebuild, the preflight found the marker in 6 built files./forms/:slugstill answers nolabel, so the heading falls back to the translated object label (工单).labelkey does not reach the title, and the title key does not reach thelabel.enrequest is served as authored.ObjectStackProtocolImplementationover a realSchemaRegistry.labelkey is still accepted._viewsentry stays closed.32124e9b0aplus the showcase commit:pnpm --filter @objectstack/spec test: 704 files, 21381 passed.pnpm --filter @objectstack/spec typecheckpassed too.pnpm --filter @objectstack/rest test: 308 files, 6166 passed.pnpm --filter @objectstack/lint test: 152 files, 7030 passed.pnpm --filter @objectstack/cli exec vitest run --project unit: 4654 of 4655 passed. The one failure wasboot-preparation-parity.test.ts, which hit its 5000 ms timeout under shared-box load. It passes alone, and this diff does not touchserve.tsor that file.6f43b6b2e0:boot-preparation-parity);typecheckpassed for rest, lint and cli.integrationtier is declared to CI.pnpm --filter @objectstack/spec check:generatedreports all 14 up to date.--fixregeneratedcontent/docs/references/system/translation.mdx.check:authorable-surface,check:livenessandcheck:strictness-ledgerwere already green; theobjectsliveness row isliveand undrilled.pnpm check:i18n: OK, 10 packages in sync.pnpm check:i18n-coverage: OK, 14 configs, 621 baselined, none new.pnpm check:i18n-walk-parity: OK.pnpm check:i18n-stale-fill: OK.pnpm check:nul-bytes: OK.dispatch-gates --commandsderived 117 commands on6f43b6b2e0, the same list as before the merge. 116 exited 0.pnpm check:type-check-debtis NOT MEASURED: its closure build queued on the shared verify lock behind a sibling run and timed out (exit 99, then exit 3).check:type-check-coverageexited 0, and the rest, lint and clitypecheckruns above passed.--no-inline-config --format json): 11 files, 0 errors, 0 warnings, and no file was ignored. The config declares no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any untouched file.Review tier
packages/specis a published schema, so this PR owes aCONTRACT_REVIEW_TIERreview of record before it enters the queue (pm-dispatchreferences/contract-review.md). It touches no governed surface, so no Tier S or Tier H landing rule applies.Acceptance notes
i18n-extract.ts, inpushBulkActionDefs(confirmText), and the hunks do not overlap. No other open PR touchestranslation.zod.ts,i18n-resolver.ts,form-routes.tsorvalidate-translation-references.ts. spec/i18n: a page:tabs item label has no translation key, and translatePage never translates it, so a localized page keeps its tab labels in the authoring language #24968 is not open as a PR yet.formViews.KEYthat retargets another object (its owndata.object). The lint rule files its_viewskey under the form's own binding, as it already does for that form'slabel.translateViewand the new extractor slot key it under the container object (ExpandedViewItem.object). Thelabelkey already has the same split. No shipped example retargets a form view.viewKind: 'form'item. It is aViewItemwhosenameis spec-qualified asOBJECT.KEY. The extractor'sview.namebranch and the lint rule both use the rawnameas the_viewskey, whiletranslateViewstrips the object prefix. The new title slot follows that branch's existing key, so it inherits the behaviour oflabelthere. No shipped example authors one._views.form.labelanddescriptionare read bytranslateViewfor the servedOBJECT.formitem, but lint still calls them non-keys. Its documented reason is that_views.*is a list-view convention. This PR registers that key fortitleonly and leaves the earlier decision alone.FormViewSchema.title's own.describe()was not edited to name the key. The translation face documents the key, and editing the describe would churn the JSON schema and docs for no behaviour change.Generated by Claude Code