Repository navigation
fix(rest): GET /forms/:slug localizes a public form's section headings under Accept-Language - #23844
Conversation
…er Accept-Language
The public form read translated objectSchema's field labels and spread
each section as authored, so an anonymous visitor in a non-English locale
read source-language section titles beside translated field labels.
The handler now makes one call into the spec's view translator
(translateMetadataDocument('view', ...), which reads
objects.OBJECT._sections.NAME) beside the existing object call, with the
same bundle, locale options and the packaged view base, and serves the
translated config through the unchanged field strip.
Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU
Co-authored-by: Claude <noreply@anthropic.com>
…ons only The anonymous objectSchema keeps a declared lookup field's definition (the strip narrows the sections); the control pins what the strip owns. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
… the strip control stands alone Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
…real protocol and per form slot A container-authored public form (a `formViews` entry, the default `form`) reaches `GET /forms/:slug` as the `viewKind: 'form'` item the protocol expands it to, so the route's one view-translator call localizes it: pinned over the real ObjectStackProtocolImplementation and SchemaRegistry, with an environment overlay's edited heading kept against the packaged view the real protocol answers. A form handed to the route in a container slot is served as matched, its own sections as authored, never the item's config. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
…ads the route reaches The route's protocol reads touch the registry and the sys_metadata list read; the double no longer carries a findOne it never answers. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 18 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 fcd4ae8e7b08d5ef71dccc9384d1be43ddeb8f05 && git checkout fcd4ae8e7b08d5ef71dccc9384d1be43ddeb8f05
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0cfd0c49bd2635da048fae96b35a38448141fa9d 8d4b00444c753c80212aa799f6cd9b9f03e07dd2 && git checkout -B drift-repro 0cfd0c49bd2635da048fae96b35a38448141fa9d && git merge --no-ff 8d4b00444c753c80212aa799f6cd9b9f03e07dd2
node scripts/docs-audit/affected-docs.mjs --json 0cfd0c49bd2635da048fae96b35a38448141fa9d |
席位核对ACCEPT · #23497 ·
Landing: once every check on the head is green or an expected skip, this seat flips the PR ready and arms auto-merge through the relay, then follows it to Generated by Claude Code |
Fixes #23497
Clause-②: no
What
GET /forms/:slug(registerFormEndpointsinpackages/rest/src/rest-server.ts) now serves a public form's sectionlabelanddescriptionin the visitor's language underAccept-Language, next to the field labels it already translated. Before, a zh-CN anonymous visitor read the English section titles above Chinese field labels. That visitor cannot load the bundle in the browser, because the/i18ndomain answers an anonymous caller 401. So the server is the only place these strings can be translated.The handler makes ONE extra call into the spec's view translator,
translateMetadataDocument('view', match.view, bundle, …). That call reachestranslateView, which readsobjects.OBJECT._sections.NAMEsince #23632 (landed as PR #23749). It runs beside the existingobjectSchemacall and uses the same bundle, the same locale options (translateOptionsFor), and the packaged view (getPackagedViewBase, looked up by the served item's registry name) as the override base. The translatedconfigthen goes through the unchanged lookup / master-detail / user strip.packages/resthas no_sectionswalk and no copy of the locale chain.packages/specis not edited.What does not change: the response shape (
slug,object,label,form,objectSchema), the top-levellabel,Vary: Accept-Language, the declared-field narrowing, the strip, andPOST /forms/:slug/submit.Resumed branch: inherited commits, hunk by hunk
This branch was started under the released claim and is continued here from
357c751a7c, withorigin/mainmerged (dd9822efb, a real merge commit). Every check below was re-run on this branch. None of the earlier run's verification is taken as done.ef0bea4e8handler hunks (theservedFormbinding, the view-translator call, theconfig-slot swap, the strip readingservedForm): kept. Inb4e56cfa5the comment above the swap is corrected to say what the slot measurement below found: the protocol's view list serves view items only, and a form matched in any other slot is served as matched.ef0bea4e8changeset.changeset/23497-public-form-section-i18n.md(@objectstack/restpatch): kept. Its claims were re-checked against the code:translateOptionsForcarries the locale, fallback chain and default locale, and the edited-heading claim is pinned below.980fb8d6eand357c751a7c, the pin file: kept, then extended inb4e56cfa5and8d4b00444. The extension adds the real-protocol and per-slot pins below, gives theselectfixture its options (the registry warned that it was functionally incomplete), and limits the real-protocol engine double to the members the route reads.The three form slots (dispatch assumption 1)
anonymousFormIntakeCandidatesfinds an open form in three slots.translateViewtranslates sections at a view'sconfigonly.viewKind: 'form'item'sconfigformformViews.KEYWhy the container slots never reach
GET /forms/:slug:p.getMetaItems({ type: 'view' }).ObjectStackProtocolImplementationdrops everyisAggregatedViewContainerbody from that list (packages/metadata-protocol/src/protocol.ts, the enumeration filter inreadFlattenedMetaItems). A container is served only as the itemsexpandViewContainerexpands it to, and each of its form slots becomes aviewKind: 'form'item with the form underconfig. All three registrars expand this way: the objectql boot loop, the metadata artifact loader, and the runtime door'sexpandRuntimeViewContainer.viewKind. Measured with a scratch probe overpackages/specat this head, not committed. The stackviews:channel refuses a body that has aviewKind(isViewContainerShapedis false, so the boot answers INVALID_METADATA 422).ViewMetadataSchemastripsformfromviewKind + config + form, so the parsed keys areconfig, name, object, viewKind, and it stripsformViewsthe same way. The metadata write door stores that parsed body (projectStorableViewBody). TheviewItems:channel registers the parsedAssembledViewArtifactSchemabody. The probe also showed that the door refusesviewKind + formwithoutconfig, and that strictViewItemSchemarefuses all three hybrid bodies.ObjectStackProtocolImplementationover a realSchemaRegistry. It registers a container with the defaultformandformViews.contact, both public, the way the boot registers it. It asserts that the view list the route reads carries noformorformViewskey and serves the two forms asticket.form/ticket.contactitems withpublicLinkunderconfig. Both slugs then answer zh-CN translated headings. The showcase's own public form (examples/app-showcase,formViews.contact) has this container shape.config. Two pins cover this (form,formViews.contact). Noout_of_scope_findingsentry follows from it, because no current producer reaches it (see Acceptance notes).Dispatch assumptions 2 and 3
ticket.contact'saboutheading. The realgetPackagedViewBase('ticket.contact')answers the shipped item (its heading is asserted to be the English original). Under zh-CN the edited heading is served as edited and the unedited description translates. Ablation M4 below turns exactly this pin and its double-based twin red.configpast the strip, the shape a translate-after-strip ordering produces. It turns red the four assertions that check a translated section still lost its lookup field.Ablations (one-time, uncommitted;
scripts/ablation-replace.mjsWRAP mode with a trap-backed restore)The pin file imports
./rest-serverfromsrc, so no rebuild is needed. Each leg was confirmed on disk by the tool's anchor count (1 to 0) and a blob change. Each restore was proven by the blob matching HEAD730c2aaa03f2and an emptygit diff HEAD, and the tree was clean afterwards. All legs ran atb4e56cfa5, whoserest-server.tsblob is unchanged at the final head.void view(the fix ablated)form: servedForm !== match.form ? servedForm : safeFormconfig-slot guardpackagedBaseset toundefinedVerification (final head
8d4b00444)packages/rest/src/public-form-section-i18n.test.ts: 14 passed. Vitest's file line reads(14 tests) 82ms, which is the reading the 10 s per-file line grades. Import is 13.5 s on a shared box.pnpm --filter @objectstack/rest typecheck: exit 0.check:test-typecheckreads 0 errors, and the test program lists this file (--listFiles).pnpm --filter @objectstack/rest test: exit 0, 286 files, 5890 passed and 326 skipped. This ran atb4e56cfa5. Between that head and the final one, only this pin file's engine double changed.pnpm --filter '@objectstack/rest...' build, exit 0.pnpm exec eslint --no-inline-configover the two changed source files: exit 0. The repo-wide run is this PR'sLint & Repo Gatescheck-run.node scripts/pm/dispatch-gates.mjs --commands(merge base1ecb08a1c) derived 66 commands. All 66 ran at8d4b00444, and each exited 0.--ranreports66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero with every exit code recorded. Two gates needed a step first.check-plugin-teardown-shape --self-testneeds its pinned fixture commit, which a shallow fetch supplied.check:dual-build-cjs-loadsneeds every package built;check:type-check-debt's own closure build supplied that before the run.registryandfind. A one-off proxy probe recorded the members the protocol read on these paths:find,registry, and the probeswriteEpochandgetDriverForObject. It never readfindOne.check:engine-double-contractandcheck:where-matcherboth exited 0.Acceptance notes
viewrow stored verbatim before the write door stored parsed bodies could still carry a container slot beside aviewKind. The route answers such a row's form as matched and untranslated (pinned above). No current door writes that shape, so nothing is filed. Carrier: none.origin/main0cfd0c49b. Those commits touchplugin-authandscripts/ci, disjoint from this diff, so main was not merged again.Consumer
objectstack-ai/objectui#12333 is waiting on this card and remains open. Once a release carrying this change is installable there, only the header of objectui's
FormPage.sectionLabelI18n.test.tsxpin needs amending. The view-level fieldplaceholder/helpTextquestion is not touched here; it is the card's "Not this card", routed todomain:spec.Generated by Claude Code