You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4f05de4
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec): a joined report's block headings and a master-detail field's inline grid heading are translatable (#26757)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBBHHmbAUS4ebUhc4iQ5UTie)_Fixes#26112Fixes#26142
Clause-②: yes
Metadata-protocol: added
Skills-docs: follow-up owed #26759 — `skills/objectstack-i18n/SKILL.md`
(the `fields.{field_name}` row and the `reports` sentence) misses
`inlineTitle` and `reports.{report}.blocks.{block}`; `skills/**` is Tier
H, so the two-line edit rides that card after this PR lands
## 维护者速读
- 改了什么:翻译包新增两个槽位——联合报表每个区块的标题/说明(`reports.REPORT.blocks.BLOCK.label` /
`.description`),以及主从字段的内嵌表格标题(`objects.CHILD.fields.FIELD.inlineTitle`);服务端翻译器按请求语言覆盖,`os
i18n extract` / `check` 也开始统计这两个键。
- 为什么改:中文用户打开联合报表时标题是中文、区块标题却是英文;新建表单上的内嵌子表标题(如「Line
Items」)也一直是英文,因为翻译包根本没有地方放这些译文,覆盖率检查也看不到缺口。
- 风险与回滚:只新增可选键,已有翻译包与元数据不受影响;组织自行改过的区块标题或内嵌标题仍保留原文(与报表标题同一规则)。回滚即撤销本 PR。
- 先看:`packages/spec/src/system/i18n-resolver.ts`(`translateReportBlocks`
与 `translateObject` 字段处理中的
`inlineTitle`)、`packages/spec/src/system/translation.zod.ts`(两个新槽位)、`packages/cli/src/utils/i18n-extract.ts`(`walkReports`
与字段遍历)。
- 两分钟验证:`pnpm --filter @objectstack/example-showcase exec vitest run
test/zh-cn-report-blocks-inline-titles.test.ts`,预期 3
个用例通过:示例应用的联合报表区块显示「未完成任务 / 已完成任务」,发票与费用报销的内嵌表格标题显示「发票明细 / 费用明细」。
- 需要你做的:无,席位落地。
## What changes
- **Spec face** (`translation.zod.ts`): `FieldTranslationSchema` gains
`inlineTitle`; the `reports` group gains `blocks`, a record keyed by the
joined block's required `name`, closed at `label` / `description`
(aliases `name` / `title` / `heading` onto `label`).
- **Serving translators** (`i18n-resolver.ts`): `translateReport`
overlays each block through a new `translateReportBlocks`;
`translateObject`'s field pass overlays `inlineTitle`. Both follow the
packaged-base rule (ADR-0029 D9.2a), judged per block (matched by
`name`) and per field. A document whose blocks and fields the bundle
does not address comes back as the same reference.
- **Landing point, measured:** both labels already reach the console as
the served document. The joined renderer
(`ui/packages/plugin-report/src/DatasetReportRenderer.tsx`) draws
`block.label` off `GET /meta/report`, and `attachInlineSubforms`
(`ui/packages/app-shell/src/providers/MetadataProvider.tsx`) copies the
child field's `inlineTitle` off `GET /meta/object`, both localized at
the REST boundary. So the fix lands in the two translators, and neither
ui file changes.
- **#26142 fallback decision:** no fallback to the child object's
translated plural label. An authored `inlineTitle` with no entry keeps
its authored text, because a bundle translates an attribute and never
swaps another attribute in over authored text (the rule
`pages.PAGE.title` follows). An absent `inlineTitle` already renders the
child's translated label.
- **Extract / check** (`i18n-extract.ts`): `walkReports` walks
`blocks[]`, the field walk emits `inlineTitle`, both via `pushOptional`
(a block without a `label`, or a field without an `inlineTitle`, is
recorded and never demanded). `os i18n check` counts them through the
same entries: blocks in the report bucket, `inlineTitle` in the field
bucket.
- **Examples:** the showcase zh-CN bundle translates both blocks of
`showcase_task_overview` and the invoice / expense-report inline
headings; the CRM zh-CN bundle translates the opportunity's product grid
heading. Each key is translated at birth, so `check:i18n-coverage` stays
at its baseline (527, none new).
- **Ledgers and generated files:** a `reports.blocks` liveness row with
`label` / `description` children (translation 36 → 38 live), plus
`gen:api-surface`, `gen:export-origins`, `gen:docs`,
`gen:strictness-ledger`, `gen:liveness-counts` output.
## Tests
- `packages/spec/src/system/i18n-report-blocks.test.ts` (10): the slot
at all three doors, the closed face, the `title` alias, the overlay, the
BCP-47 ladder, reference identity, and the packaged-base rule per block.
- `packages/spec/src/system/i18n-field-inline-title.test.ts` (7): the
slot, a non-string refusal, the overlay through the type dispatch with
no `unrecognized_keys`, no plural-label fallback, and the packaged-base
rule.
- `packages/cli/test/i18n-block-inline-title-walk.test.ts` (5): the
extracted keys, unwritten strings recorded without being demanded, the
bundle round trip, and coverage reporting both gaps and then clearing
them.
- `examples/app-showcase/test/zh-cn-report-blocks-inline-titles.test.ts`
(3): the acceptance pin for both cards, on the app's own metadata and
bundle.
## Acceptance notes
- `validate-translation-references` (lint) checks that `reports.REPORT`
names a real report, but it does not check that `blocks.BLOCK` names a
real block of that report, so a mistyped block key resolves nothing
without an orphan finding. This is noted, not filed.
- The joined renderer's `resolveText` reads an inline locale map's
`default` only. That predates this change and is unrelated to the bundle
path.
## Verification
All readings were taken at `bcbc83749` (after merging `origin/main`
`5cc31c788` through `os-regen-merge.sh`) unless marked as taken at
`595f62162` (the pre-merge head).
<details><summary>Tests and typecheck</summary>
- `pnpm --filter @objectstack/spec test` (595f621): Test Files 733
passed, Tests 21938 passed / 1 todo.
- `pnpm --filter @objectstack/spec test:repo` (595f621): Test Files 80
passed, Tests 1295 passed.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`
(595f621): Test Files 340 passed, Tests 4928 passed. The integration
tier is left to CI: no spawn entry or integration file is touched.
- `pnpm --filter @objectstack/runtime test` (595f621): Test Files 459
passed, Tests 7443 passed / 19 skipped.
- typecheck for spec, cli, example-showcase and example-crm (595f621):
all exit 0.
- After the merge (bcbc837): `src/system/` + `src/ui/report*` spec
tests 64 files / 2150 tests, the two cli i18n walk files 9 tests, and
the showcase pin 3 tests, all passing.
- The ui packages are untouched, so no ui test run is owed.
</details>
<details><summary>Ablations (one per behaviour; src-resolved suites,
each restored byte-identical by blob hash)</summary>
- `translateReport` with the block pass removed:
`i18n-report-blocks.test.ts` 6 failed / 4 passed.
- `translateObject` with the `inlineTitle` assignment removed:
`i18n-field-inline-title.test.ts` 3 failed / 4 passed.
- extract with the `inlineTitle` push deleted: the cli walk test 3
failed / 2 passed.
- extract with the block `label` push deleted: the cli walk test 4
failed / 1 passed.
</details>
<details><summary>Gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 109 families (595f621; the
derivation at bcbc837 is the same list). 107 exit 0. Two exit 3
(prerequisite: a whole-repo build), `spec check:skill-examples` and
`check:dual-build-cjs-loads`, are NOT MEASURED locally and left to CI.
`--ran` reconciliation: 109 derived, 107 run, 2 NOT-MEASURED, 0 UNRUN.
- Re-run at bcbc837 after the merge: `check:i18n`,
`check:i18n-coverage`, `check:i18n-walk-parity`, spec `check:liveness`,
`check:empty-state`, `check:strictness-ledger`, `check:api-surface`,
`check:authorable-surface`, `check:generated`, `check:nul-bytes`, all
exit 0.
- No full dogfood run: there is no `!` in the title and no `(narrowing)`
arm.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufziF7p6fS8zz6Gtt1gq)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec): a joined report's block headings and a master-detail field's inline grid heading are translatable
7
+
8
+
Clause-②: yes
9
+
Metadata-protocol: added
10
+
11
+
Two authored, user-visible labels had no translation slot, so a zh-CN user read them in the authoring language and `os i18n check` could not see the gap:
12
+
13
+
-**A joined report's blocks.** The `reports` translation group declares `blocks`, keyed by the block's `name`: `reports.<report>.blocks.<block>.label` / `.description`. `translateReport` overlays them on each block of the served report (`GET /meta/report`), which is what the joined renderer draws above each block.
14
+
-**A field's `inlineTitle`.**`FieldTranslationSchema` declares `inlineTitle`: `objects.<child>.fields.<field>.inlineTitle`, on the child object whose master-detail (or lookup) field declares the heading. `translateObject` overlays it on the served child object (`GET /meta/object`), which is what the console copies onto the parent's form as the inline grid's heading.
15
+
16
+
Both follow the packaged-base rule (ADR-0029 D9.2a): a string an org re-authored on top of the packaged declaration keeps its text. An authored `inlineTitle` with no entry keeps its authored text; the child object's plural label is never substituted for it.
17
+
18
+
`os i18n extract` scaffolds both keys and `os i18n check` counts them: a block's `label` / `description` under the report bucket, `inlineTitle` under the field bucket. A block without a `label`, or a field without an `inlineTitle`, is recorded without being demanded.
0 commit comments