Skip to content

fix(cli): i18n 覆盖走查收集 objects.*._sections —— 段落标题不再是「声明了、渲染了、却零告警」的面 (#5405) - #5416

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-5405-i18n-walker-sections
Aug 5, 2026
Merged

fix(cli): i18n 覆盖走查收集 objects.*._sections —— 段落标题不再是「声明了、渲染了、却零告警」的面 (#5405)#5416
baozhoutao merged 2 commits into
mainfrom
claude/issue-5405-i18n-walker-sections

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5405

正文刻意不写 < 紧跟字母的写法(GitHub 正文清洗器会把它当 HTML 标签吞掉)。占位符一律写成 {type} / {slot} 形式。

前提核验(先证伪,再动手)

按 rule 6 对 origin/main(b4872a868)逐条验证 issue 正文,三条全部成立:

  • grep -c "_sections" packages/cli/src/utils/i18n-extract.ts0;反查对照组 _views9fieldGroups0。零命中不是 grep 失灵。
  • ExpectedEntry['source'] 联合类型(i18n-extract.ts:96-111)确无 section;两个 metadataForm* 项走的是 metadataForms.{type}.sections.* 这条 Studio 元数据表单命名空间,与 app object 无关。
  • COVERAGE_SOURCE(i18n-coverage.ts)照抄同一联合,computeI18nCoverage 结构上不可能报出缺失的段落标题。

反向面同样核实过:声明面 ObjectTranslationDataSchema._sections(packages/spec/src/system/translation.zod.ts:216,并把 sections 列为 authoring alias);消费面 @object-ui/i18nsectionLabel(useObjectLabel.ts:397-400)被 plugin-detailrecord:detailsplugin-formObjectForm/ModalForm、以及 ObjectFormDesigner 三处读取。只有走查器不知道这个面存在。

改法

新增 section kind,双源取齐,同一 (object, section) 只产一个 key:

(a) fieldGroups × 字段 groupderiveFieldGroupLayout(ADR-0085 §5)判定 —— 这正是渲染端自己消费的那份共享推导(plugin-detailderiveFieldGroupDetailSections 把它的 key 直接映射成 sectionLabel(object, name, …) 查的那个 name)。因此:没有可见字段引用的已声明分组不出 key,fieldGroups 未声明的 group: 也不出 key,尾部无 key 的 ungrouped 桶不出 key —— 屏幕上不存在的标题,不该记一笔翻译债。fieldGroups[].label 供默认语言文本;分组自身没写 label 时仍产出脚手架 key(种子退回分组名),但 inline 留空,覆盖门不去追一句没人写过的话。

(b) 具名 sections[] 表单视图(含容器的默认 form)与记录页组件树。无 name 的段落一律跳过 —— 每个渲染端都把查表挂在它上面(s?.name ? sectionLabel(…) : s?.label),没有 name 就是构造性不可翻译。

COVERAGE_SOURCE 映射之,os lint 得到 i18n/missing-section 类别,落在用户桶而非平台桶(不需要 --include-platform 才看得见);os i18n extract 的骨架经共享走查器免费获得。

页面这半边:复用,而不是再抄一份

页面段落走 @objectstack/lintwalkPageComponents(本 PR 顺带把它连同 isSourceAuthoredPage / WalkedComponent 从 lint 的 index 导出)。page-walk.ts 的模块头就写着它为何独此一份:重复实现已经造出过一条死规则(#3583)。复用不只是 DRY,它是这半边正确仅仅存在的分界:

  • 组件挂在 regions[].components slots.{slot}(后者可以是裸组件而非数组);
  • 子树藏在无类型的 properties 袋里(page:tabsproperties.items[].children[],page:cardproperties.body/.footer)—— 也就是 issue 点名的那层深嵌套;
  • kind: 'html' | 'react' | 'jsx' 的页面,regions 只是 source 的派生缓存,拿它刮 key 等于凭空发明一个作者面;
  • 每个组件解析自己的绑定(dataSource.objectproperties.object → 页面 object)。这条比「漏」更要紧:被 dataSource 改绑到别的对象的 record:details,若按页面对象记 key,产出的是一个解析器永远不读的路径。

验证

pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2
  Test Files  78 passed (78)      Tests  749 passed (749)

pnpm --filter @objectstack/lint test
  Test Files  57 passed (57)      Tests  1198 passed (1198)

pnpm --filter @objectstack/cli --filter @objectstack/lint typecheck
  packages/lint typecheck: Done
  packages/cli typecheck: Done

新增 packages/cli/test/i18n-section-coverage.test.ts(22 例)。

反向核验 —— 方向是先判后跑,结果是常规的「红」: 这是补一个从未存在的面,不是收窄既有规则,所以预判为「注释掉 walkObjectSections(config, out) 调用 → 新用例转红」。实测 16 failed | 4 passed,方向吻合。那 4 例正是断言「不该产出」的负向用例 —— 它们在删除态下因为什么都没产出而绿,即 PR #5046 记录的空集假绿。据此逐条整改,而不是留给下一个读者踩:

  • 「区域名不当作段落名」与「未绑定对象的页面跳过」两例,各自补上同配置下的正向断言(同一个 region 里放一个真段落 / 同一个组件挂到一个绑定了对象的页面上),现在它们无法靠「什么都没有」通过;
  • 「无 name 的段落跳过」由同 describe 首例的精确 toEqual([三个 key]) 兜住,负向断言只是把意图写明;
  • 「单语项目零告警」是刻意的 opt-in 不变量,同一对象的正向覆盖用例就在上面。

对账锚(声明即强制),两面各一处:

  • 声明面 —— 把 extractTranslations 产出的 bundle 直接喂给 ObjectTranslationDataSchema.safeParse,要求全绿且 _sections 键集合精确匹配。走查器若发明了拼写(sections / _section / .title),在这里被严格 schema 当场拒绝,而不是安静地刮出一堆 bundle 合法性存疑的 key。
  • 消费面 —— 断言字段分组产出的 key 集合 等于 deriveFieldGroupLayout 自己返回的有 key 段落集合。渲染端消费的就是这份推导,于是「声明的」与「被门禁的」是同一个集合,而不是两张会漂移的清单。

真实取数 —— 直接 import 仓内示例 app 的两个模块,不造假数据:examples/app-showcase/src/ui/views/contact.view.ts(容器默认 form 的四个具名段落 contact/work/status/notes,绑 showcase_contact)与 examples/app-showcase/src/data/objects/semantic-zoo.object.ts(fieldGroups: basics/money,两者都被真实字段的 group: 引用),断言六个 key 一个不多一个不少。按车道纪律该用例显式 }, 60_000)

越界与顺带发现

`ExpectedEntry['source']` had no `section` kind, so
`objects.<o>._sections.<s>.label` was structurally unreachable: `os i18n
extract` never scaffolded a heading and `os lint` could not report one
missing — while `ObjectTranslationDataSchema` declares `_sections` and
`@object-ui/i18n`'s `sectionLabel` resolves it for `record:details`,
`ObjectForm`/`ModalForm` and the field-group designer.

Adds a `section` kind fed by both authoring surfaces, deduped to one key
per (object, section):

  (a) `fieldGroups` × field `group`, read through the shared ADR-0085
      `deriveFieldGroupLayout` so a group nothing visible references —
      or a `group:` no `fieldGroups` entry declares — yields no key;
  (b) a NAMED `sections[]` on a form view (including a container's
      default `form`) or inside a record page's component tree.

`COVERAGE_SOURCE` maps it, giving `os lint` an `i18n/missing-section`
category in the user bucket (not folded away with `--include-platform`).

Page sections go through `@objectstack/lint`'s shared page traversal,
now exported, instead of a private copy — that walk exists because
duplicating it produced a dead rule once (#3583), and it is what makes
this half correct rather than merely present: `slots.<slot>`, the
untyped `page:tabs` -> `properties.items[].children[]` nesting, skipping
source-authored pages whose regions are a derived cache, and per
component binding resolution so a re-bound `record:details` keys its
headings under the object it actually shows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 11:07am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/lint.

22 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, @objectstack/lint)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli, @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

`check:i18n-coverage`(仓库级棘轮门)在 CI 抓到 showcase 未译声明串
451 → 461:新增的 `section` kind 把段落标题纳入覆盖面后,showcase
声明支持 zh-CN 却没有这些译文。门要的是译文,不是豁免 —— 基线与
`supportedLocales` 均未改动。

zh-CN 新增 10 条,均为既有体例:

  showcase_task._sections   —— task.view.ts 三个表单投影的段落名
    overview 概览 / schedule 排期 / details 详细信息
    step_basics 基本信息 / step_assign 指派 / step_schedule 排期
    split_task 任务 / split_schedule 排期

  showcase_semantic_zoo._sections —— ADR-0085 fixture 的两个 fieldGroups
    basics 基本信息 / money 财务信息

后者按 `showcase_inquiry` / `showcase_business_unit` 的先例,只加
`_sections` 块(该对象本无 zh-CN 块,其余债务留在冻结基线内)。
`money` 取「财务信息」以贴合分组自己的 description(Financial
fields),而非直译 Money。

`en` 不加条目:默认语言由内联 label 满足,复述源串只会伪造覆盖率。

check-i18n-coverage: OK (12 config(s), 660 baselined untranslated
string(s), none new) —— showcase 精确回到 451,零 missing-section。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh

Copy link
Copy Markdown
Contributor Author

Rework:check:i18n-coverage 红 → 已修(e3d1e855c)

CI(TypeScript Type Check 工作流 / run 30997231233)的 check:i18n-coverage 步骤报 showcase 未译声明串 451 → 461这不是类型错误,而是本 PR 功能的正确后果:walker 新采出的 showcase _sections key,在 showcase 声明支持的 zh-CN 里没有译文,仓库级棘轮门当场逮住。

按门禁自己的处方处理 —— 门要的是译文,不是豁免:基线 scripts/i18n-coverage-baseline.jsoni18n.supportedLocales 均未改动,只补 examples/app-showcase/src/system/translations/index.ts 的 zh-CN 条目。

新增的 locale × key 清单(10 条,全部 zh-CN;en 不加 —— 默认语言由内联 label 满足,复述源串只会伪造覆盖率,这也是 _sections schema 注释自己的立场):

key 源串 zh-CN
showcase_task._sections.overview.label Overview 概览
showcase_task._sections.schedule.label Schedule 排期
showcase_task._sections.details.label Details 详细信息
showcase_task._sections.step_basics.label Basics 基本信息
showcase_task._sections.step_assign.label Assignment 指派
showcase_task._sections.step_schedule.label Schedule 排期
showcase_task._sections.split_task.label Task 任务
showcase_task._sections.split_schedule.label Schedule 排期
showcase_semantic_zoo._sections.basics.label Basics 基本信息
showcase_semantic_zoo._sections.money.label Money 财务信息

用词对齐 bundle 既有体例(任务 / 负责人 / 进度 / 状态),不为同一概念引入第二个词;money 取「财务信息」以贴合分组自己的 description(Financial fields)而非直译 Money。showcase_semantic_zooshowcase_inquiry / showcase_business_unit 的先例只加 _sections 块 —— 该对象本无 zh-CN 块,其余债务留在冻结基线内。

验证:

node scripts/check-i18n-coverage.mjs
  check-i18n-coverage: OK (12 config(s), 660 baselined untranslated string(s), none new).

os lint examples/app-showcase/objectstack.config.ts --json
  showcase i18n issues now: 451 (baseline 451)     ← 精确回到基线,非低于
  missing-section remaining: 0

pnpm check:i18n
  check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).

pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2
  Test Files 78 passed (78)   Tests 750 passed (750)
pnpm --filter @objectstack/example-showcase test
  Test Files 12 passed (12)   Tests 124 passed (124)
typecheck (cli / lint / app-showcase)  → 三个都 Done

精确回到 451 而非更低是有意的:该门是双向棘轮,计数下降同样报错(要求 --update 落基线),所以「多译几条」并不是更安全的选择。

教训(记进报告):上一轮只跑了包级 pnpm --filter ... test/typecheck,而这道门是仓库级(pnpm check:i18n-coverage,且需要先 turbo run buildpackages/cli/bin/run.js 才能跑)。凡是新增翻译面的改动,包级验证按定义覆盖不到消费面的仓库级棘轮 —— 下次此类改动应在推送前直接跑一次 check:i18n-coverage

顺带:构建产物核对时发现 ContactViews 从未注册进 objectstack.config.ts(产物里只有 4 个视图),所以它默认 form 的四个具名段落在真实 config 上对覆盖门完全不可见 —— 已有平行 agent 立单 #5420,按查重纪律未开重复单,只在那边补了独立复现与这条下游影响。


Generated by Claude Code

@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 123067c Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5405-i18n-walker-sections branch August 5, 2026 11:21
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…ck-ai#5420) (objectstack-ai#5438)

`src/ui/views/contact.view.ts` declares the whole "create form != edit form"
reference implementation — a default grouped `form` with four named sections,
a sparse `formViews.create`, and the list's
`addRecord: { mode: 'form', formView: 'create' }` binding — and
`src/ui/views/index.ts` exports it. `objectstack.config.ts` named the other
four containers on line 23 and line 196 and never named this one.

There is no directory scan behind `views:`; the CLI reads exactly that array.
So the metadata compiled, type-checked and linted clean while reaching
nothing: `nav_contacts` rendered a derived default form instead of the
authored one, `addRecord.formView` never bound, and no static pass
(`os validate` / `os lint` / `os i18n extract` / the coverage ratchet) could
see it either — which is also why objectstack-ai#5405's new `i18n/missing-section` gate
found 10 headings in the showcase and zero of them under `showcase_contact`.
`content/docs/ui/create-vs-edit-form.mdx` cites this file as the live
reference implementation the whole time.

Registration makes exactly five zh-CN coverage keys newly reachable, measured
on the real config rather than predicted:

  os lint examples/app-showcase/objectstack.config.ts
    483 warnings (before) -> 488 (registered, untranslated) -> 484 (translated)
    + i18n/missing-view     objects.showcase_contact._views.list.label
    + i18n/missing-section  objects.showcase_contact._sections.{contact,work,status,notes}.label

All five are translated in this commit, following PR objectstack-ai#5416's rework: zh-CN
takes its words from the vocabulary this bundle already uses, and `en` gains
nothing (the default locale is satisfied by the inline labels; echoing the
source string only fakes coverage). The ratchet baseline
`scripts/i18n-coverage-baseline.json` and `i18n.supportedLocales` are
untouched — `check-i18n-coverage: OK (12 config(s), 660 baselined
untranslated string(s), none new)`, showcase back to exactly 451.

Two guards in `test/seed.test.ts`:

  * every container the barrel exports reaches `stack.views`, matched by
    target object (`defineStack` parses the config, so a registered container
    is a structural copy and never `===` the export). Reverting the config
    hunk fails it naming `ContactViews`.
  * the four section names and their zh-CN `_sections` keys are asserted as
    set EQUALITY, because the two i18n gates read that set in opposite
    directions — `i18n/missing-section` fails on a declared section with no
    entry, `translation-target-unknown` on an entry no section declares.
    With objectstack-ai#5422 on main this is the first time the relationship is testable on
    the real config: the four correctly-translated sections are NOT reported
    as orphans.

The remaining `translation-target-unknown` on `_views.list` is objectstack-ai#5164's
open contradiction between the three `_views` producers, not new here — the
showcase already carried four identical instances (project / task / inquiry /
business_unit) before this change.


Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…e heading no key can address (objectstack-ai#5417) (objectstack-ai#5455)

`_sections` is keyed by the section's `name`, and every renderer resolves a
section heading that way (`sectionLabel(objectName, section.name, authored)`),
falling back to the authored label when there is no name. So a section
authored with a `label` and no `name` is untranslatable by construction — and
both existing gates are structurally blind to it: there is no orphan key for
`validateTranslationReferences` to report, and the coverage walker (objectstack-ai#5405)
emits one expected key per `sections[].name`, so a nameless section demands
nothing and the report reads 100%.

Measured: 70/70 HotCRM form-view sections are in that state at full declared
coverage; `os validate` over examples/app-showcase reports 14 (6 form views,
8 record:details pages) and still exits 0.

`validateTranslatableSections` (`translation-section-name-missing`) joins the
reference-integrity suite, so it runs on validate/lint/compile at once, and
reads exactly the anchors objectstack-ai#5416's walker and objectstack-ai#5422's fact set already agree
on — a container's `sections`, its DEFAULT `form.sections`, every
`listViews.*`/`formViews.*` sub-container, the same three on object-embedded
views, and `record:details` sections anywhere in a page's component tree via
the shared `walkPageComponents`. fieldGroups-derived sections are out of range
by construction (their key IS the name), as is a name-keyed `sections` map.

Warning, and opt-in: a section warns only when the object it renders under
carries some translation of its own, so a monolingual stack stays silent
exactly as `computeI18nCoverage` does. The fix is a diagnostic at the
producer — slugifying the label into a lookup key would fossilize a second
de-facto contract next to the declared one (Prime Directive objectstack-ai#12, and the
renderer guard objectui#3373 pinned as correct).

Per the PM ruling on the issue, requiring `name` on `FormSection` outright
(the schema route) is a breaking authoring change left to the maintainer;
packages/spec is untouched.


Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cli: the i18n coverage walker never collects objects.<o>._sections — a bundle can ship every section heading in English and lint zero warnings

2 participants