Repository navigation
Commit 1eac7ad
fix(rest): GET /forms/:slug localizes a public form's section headings under Accept-Language (#23844)
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](https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 66d456d commit 1eac7ad
3 files changed
Lines changed: 419 additions & 6 deletions
File tree
- .changeset
- packages/rest/src
| 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 | + | |
0 commit comments