Skip to content

fix(rest): GET /forms/:slug localizes a public form's section headings under Accept-Language - #23844

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23497-public-form-section-i18n
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23497-public-form-section-i18n

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23497

Clause-②: no

What

GET /forms/:slug (registerFormEndpoints in packages/rest/src/rest-server.ts) now serves a public form's section label and description in the visitor's language under Accept-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 /i18n domain 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 reaches translateView, which reads objects.OBJECT._sections.NAME since #23632 (landed as PR #23749). It runs beside the existing objectSchema call 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 translated config then goes through the unchanged lookup / master-detail / user strip. packages/rest has no _sections walk and no copy of the locale chain. packages/spec is not edited.

What does not change: the response shape (slug, object, label, form, objectSchema), the top-level label, Vary: Accept-Language, the declared-field narrowing, the strip, and POST /forms/:slug/submit.

Resumed branch: inherited commits, hunk by hunk

This branch was started under the released claim and is continued here from 357c751a7c, with origin/main merged (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.

  • ef0bea4e8 handler hunks (the servedForm binding, the view-translator call, the config-slot swap, the strip reading servedForm): kept. In b4e56cfa5 the 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.
  • ef0bea4e8 changeset .changeset/23497-public-form-section-i18n.md (@objectstack/rest patch): kept. Its claims were re-checked against the code: translateOptionsFor carries the locale, fallback chain and default locale, and the edited-heading claim is pinned below.
  • 980fb8d6e and 357c751a7c, the pin file: kept, then extended in b4e56cfa5 and 8d4b00444. The extension adds the real-protocol and per-slot pins below, gives the select fixture 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)

anonymousFormIntakeCandidates finds an open form in three slots. translateView translates sections at a view's config only.

slot conclusion evidence
a viewKind: 'form' item's config localized the zh-CN pins below, over a protocol double AND over the real protocol
a container's form unreachable at the route see below
a container's formViews.KEY unreachable at the route see below

Why the container slots never reach GET /forms/:slug:

  • The read drops containers. The route reads p.getMetaItems({ type: 'view' }). ObjectStackProtocolImplementation drops every isAggregatedViewContainer body from that list (packages/metadata-protocol/src/protocol.ts, the enumeration filter in readFlattenedMetaItems). A container is served only as the items expandViewContainer expands it to, and each of its form slots becomes a viewKind: 'form' item with the form under config. All three registrars expand this way: the objectql boot loop, the metadata artifact loader, and the runtime door's expandRuntimeViewContainer.
  • No door writes a container slot beside a viewKind. Measured with a scratch probe over packages/spec at this head, not committed. The stack views: channel refuses a body that has a viewKind (isViewContainerShaped is false, so the boot answers INVALID_METADATA 422). ViewMetadataSchema strips form from viewKind + config + form, so the parsed keys are config, name, object, viewKind, and it strips formViews the same way. The metadata write door stores that parsed body (projectStorableViewBody). The viewItems: channel registers the parsed AssembledViewArtifactSchema body. The probe also showed that the door refuses viewKind + form without config, and that strict ViewItemSchema refuses all three hybrid bodies.
  • Pinned over the real protocol. A new pin builds the real ObjectStackProtocolImplementation over a real SchemaRegistry. It registers a container with the default form and formViews.contact, both public, the way the boot registers it. It asserts that the view list the route reads carries no form or formViews key and serves the two forms as ticket.form / ticket.contact items with publicLink under config. Both slugs then answer zh-CN translated headings. The showcase's own public form (examples/app-showcase, formViews.contact) has this container shape.
  • Boundary, pinned. If the route is handed a container anyway (from a protocol double, or a row stored verbatim before the write door stored parsed bodies), it serves the matched form's own sections as authored, never the item's config. Two pins cover this (form, formViews.contact). No out_of_scope_findings entry follows from it, because no current producer reaches it (see Acceptance notes).

Dispatch assumptions 2 and 3

  • 2, the packaged view base: holds. The real-protocol pin seeds an environment overlay row that edits ticket.contact's about heading. The real getPackagedViewBase('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.
  • 3, translate before the strip: holds, and the assertion stays. Ablation M2 serves the translated config past 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.mjs WRAP mode with a trap-backed restore)

The pin file imports ./rest-server from src, 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 HEAD 730c2aaa03f2 and an empty git diff HEAD, and the tree was clean afterwards. All legs ran at b4e56cfa5, whose rest-server.ts blob is unchanged at the final head.

leg mutation red green
M1 swap line replaced by void view (the fix ablated) 5: every zh-CN translated-heading pin, double and real protocol 9, all controls
M2 form: servedForm !== match.form ? servedForm : safeForm 4: translated section keeps its lookup field, the strip control, real-protocol fields x2 10
M3 swap without the config-slot guard 2: the two container-slot boundary pins 12
M4 the view call's packagedBase set to undefined 2: the override pin over the double and over the real protocol 12

Verification (final head 8d4b00444)

  • Pin file 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-typecheck reads 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 at b4e56cfa5. Between that head and the final one, only this pin file's engine double changed.
  • Dependency closure built first: pnpm --filter '@objectstack/rest...' build, exit 0.
  • pnpm exec eslint --no-inline-config over the two changed source files: exit 0. The repo-wide run is this PR's Lint & Repo Gates check-run.
  • Gate families: node scripts/pm/dispatch-gates.mjs --commands (merge base 1ecb08a1c) derived 66 commands. All 66 ran at 8d4b00444, and each exited 0. --ran reports 66 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-test needs its pinned fixture commit, which a shallow fetch supplied. check:dual-build-cjs-loads needs every package built; check:type-check-debt's own closure build supplied that before the run.
  • The engine-double ledgers are untouched. The real-protocol double carries only registry and find. A one-off proxy probe recorded the members the protocol read on these paths: find, registry, and the probes writeEpoch and getDriverForObject. It never read findOne. check:engine-double-contract and check:where-matcher both exited 0.

Acceptance notes

  • A view row stored verbatim before the write door stored parsed bodies could still carry a container slot beside a viewKind. 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.
  • No live server boot was run. The real-protocol pin covers the composed read the route performs.
  • The branch is two commits behind origin/main 0cfd0c49b. Those commits touch plugin-auth and scripts/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.tsx pin needs amending. The view-level field placeholder / helpText question is not touched here; it is the card's "Not this card", routed to domain:spec.


Generated by Claude Code

…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>
…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0cfd0c49bd2635da048fae96b35a38448141fa9d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcd4ae8e7b08d5ef71dccc9384d1be43ddeb8f05 — the merge of head 8d4b00444c753c80212aa799f6cd9b9f03e07dd2 into base 0cfd0c49bd2635da048fae96b35a38448141fa9d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #23497 · domain:cli seat 2 · os-tesla · session session_018t6a5rvqPRvEDMCVVW9kYT · 2026-10-16T08:14Z · reviewed head 8d4b00444c753c80212aa799f6cd9b9f03e07dd2, against GitHub and the branch, not against the report's prose.

  1. Shape. Draft, base main, first line Fixes #23497; no other closing keyword (Regression of #8813 on 18.0.0-next.0: the anonymous public form /f/:slug shows English section titles, help text and placeholder in zh-CN, though each section's name is translated in the pack and the signed-in form shows Chinese objectui#12333 and i18n(spec): no exported resolver localizes a form view's sections through the declared objects.<object>._sections.<name> keys, so a server-side reader (the public form read, #23497) cannot translate a section label without copying the locale chain #23632 sit in non-closing sentences). Clause-②: no at line start in the body and in the changeset, matching the takeover claim 6209637282. Files: rest-server.ts (the GET /forms/:slug handler only: the servedForm binding, one translateMetadataDocument('view', …) call, the config-slot swap, the strip reading servedForm), the new pin file, the inherited changeset; inside the claim's surface. check-governed-merges --pr 23844: 0 of 3 paths governed; 425 changed lines. No review face. A resumed branch: the inherited commits ef0bea4e8 / 980fb8d6e / 357c751a7c are accounted for hunk by hunk in the body, and origin/main came in by a real merge (dd9822efb).
  2. Premise and the PM's assumptions, read on the branch. (1) The three form slots: ObjectStackProtocolImplementation drops every isAggregatedViewContainer body from a view enumeration (packages/metadata-protocol/src/protocol.ts about :10226), so the route only ever matches a viewKind: 'form' item's config; the two container slots are unreachable there and are pinned as a served-as-matched boundary. The PR body gives each slot a conclusion. (2) The override base: packagedObjectBase(p, 'view', name) reaches packagedObjectBaseOf's accessor table (meta-item-read-gate.ts about :2756), and the edited-heading pin runs over the real protocol; ablation M4 reds exactly the two override pins. (3) Translate before strip: ablation M2 reds four strip assertions, so they stay. No packages/spec edit; no _sections walk or locale chain in packages/rest.
  3. Changeset, sentence by sentence. @objectstack/rest patch, published. "the same objects.OBJECT._sections.NAME keys … with the request's locale, the deployment's fallback chain and default locale, and the packaged view as the override base": the call spreads translateOptionsFor(i18n, locale) and passes the view's packaged base. FROM / TO match the zh-CN pin. "a section whose key the bundle lacks keeps its authored text; a section with no name is served as authored; … a heading edited on top of the packaged view keeps the edit": each is a pin in the 14. "The response shape …, Vary: Accept-Language, … POST /forms/:slug/submit are unchanged": the diff touches none of them.
  4. Gates, pinned to the head. 8d4b00444c check-runs read at review: 37; 16 success, 6 skipped, 15 in_progress (Lint, Type Check and Test Core shards). Landing waits for every check green or an expected skip; this record is not a green reading. The dev's local subset is on that head: 66 of 66 derived gate commands exit 0 (--ran: 0 NOT-MEASURED, 0 UNRUN), the pin file 14 passed, @objectstack/rest typecheck exit 0; the full @objectstack/rest suite (5890 passed) ran at b4e56cfa5, whose rest-server.ts blob equals the head's.
  5. Accepted deviations. (1) Before the container restart, long runs went to the background; after it, everything re-ran in the foreground at the final head. (2) No live server boot; the real-protocol pin covers the composed read. (3) A git fetch --depth=1 of fixture commit 621a48760 into the shared object store, a read. (4) The branch is two commits behind origin/main (plugin-auth, scripts/ci), disjoint; the merge queue rebuilds on main.
  6. Out of scope. One note: a view row stored verbatim before the write door stored parsed bodies could carry a container slot beside a viewKind; such a form is served as matched and untranslated (pinned). No current door writes that shape, so it has no reach:: Acceptance notes, not filed. Consumer: Regression of #8813 on 18.0.0-next.0: the anonymous public form /f/:slug shows English section titles, help text and placeholder in zh-CN, though each section's name is translated in the pack and the signed-in form shows Chinese objectui#12333 stays pm:blocked until a release carrying this is installable there.

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 MERGED.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants