Skip to content

Commit 4b8f8cd

Browse files
docs(skills): the i18n skill names fields.{field}.inlineTitle and a joined report's block headings (#26832)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi98Ny4V8ebAep3LPcP3m5TDK)_ Fixes #26759 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`skills/objectstack-i18n/SKILL.md` 的分组表补上两把 18.2 已接受、运行时已下发但技能没教的键:`fields.{field_name}` 行加 `inlineTitle`(主从字段在父表单上的内嵌子网格标题,键挂在拥有该字段的子对象下;未翻译时保留作者原文,不回退到子对象复数标签);`reports` 那句加 `reports.{report}.blocks.{block_name}.{label,description}`(联合报表各区块标题,按区块必填的 `name` 取键)。表后各给一行示例,照搬 `app-crm` / `app-showcase` 已发货的 zh-CN 包写法。手写作者页 `content/docs/ui/translations.mdx` 同补两处。eval 文件加第 8 题覆盖两把键;其 token 棘轮行先削重复再按实测钉在 1642;`@objectstack/skills` 补丁 changeset。 - 为什么改:PR #26757 把这两把键加进了协议,卡 #26759 是它欠下的 `Skills-docs:` 后续——AI 作者照技能写翻译包时,这两处标题会一直停在作者语言。 - 风险与回滚:只改技能文本、一页文档、一个 eval JSON、一行棘轮上限和一个 changeset;不动 schema、运行时或任何键。回滚即 revert 本 PR。 - 先看:`skills/objectstack-i18n/SKILL.md`(L162 字段行、L175–177 reports 句、L182–188 示例)、`skills/objectstack-i18n/evals/bundles-locales-coverage.json`(第 6 题删重复、第 8 题新增)、`scripts/check-skills-token-ratchet.mjs`(棘轮行 1451 → 1642,算式在注释里)。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期 `objectstack-i18n/SKILL.md` ✓ 余量 877、`bundles-locales-coverage.json` ✓ 余量 0,总行 `81 authored bundle file(s) within their ceilings`。 - 需要你做的:批准本 PR(`skills/**` 受管面,Tier H)。 ## What changes - **`skills/objectstack-i18n/SKILL.md`** (the group table a bundle author picks keys from): - the `fields.{field_name}` row names `inlineTitle` — the heading of the inline child grid a `master_detail` field draws on its parent's form; the field lives on the child object, so the key is the child's; untranslated it keeps its authored text and never falls back to the child's plural label (wording read off `FieldTranslationSchema`'s docblock, `packages/spec/src/system/translation.zod.ts` L133–147 on `origin/main` 0ea90c7); - the sentence that said `reports`, `positions` and `permissions` hold `label` + `description` by name now continues with `reports.{report}.blocks.{block_name}.{label,description}`, keyed by the block's required `name` (`JoinedReportBlockSchema.name`, `packages/spec/src/ui/report.zod.ts`; the translation slot at `translation.zod.ts` L1199); - one example row each, after the `os i18n extract --no-objects-only` sentence, spelled exactly as the shipped bundles spell them for `zh-CN`: `objects.crm_opportunity_line_item.fields.opportunity.inlineTitle` (`examples/app-crm/src/translations/crm.translation.ts` L99) and `reports.showcase_task_overview.blocks.open_block.label` (`examples/app-showcase/src/system/translations/index.ts` L1362). The skill's own example bundle mirrors `examples/app-todo`, which has neither a master-detail field nor a joined report, so the rows sit in a two-line fragment fence instead of being invented into the todo bundle. - **`content/docs/ui/translations.mdx`** (the card's hand-written-twin item, named in the claim's file surface): a row for the inline child-grid heading under the field row, and the report row gains the `reports.NAME.blocks.BLOCK.label` / `description` spelling. Not governed; it rides this PR because the claim's surface names it and the card thread has one PR. - **`skills/objectstack-i18n/evals/bundles-locales-coverage.json`**: case 8 asks for the `Products` grid heading on the `crm_opportunity` form and the `Open Tasks` block of the joined `showcase_task_overview` report, and expects the two keys above; `must_contain` `inlineTitle` / `crm_opportunity_line_item` / `blocks` / `open_block`, `must_not_contain` `fields.opportunity_line_item` (the key on the parent) and `components.open_block` (a page-component spelling). Paid down first: case 6's `os i18n extract --no-objects-only` sentence and its `--no-objects-only` assertion duplicated case 5's pin of the same flag and were cut (77 bytes). Every name in the prompt and assertions is a shipped example's or the Zod source's; none is invented. - **`scripts/check-skills-token-ratchet.mjs`**: the eval row 1451 → 1642, pinned AT the measurement with the arithmetic and the standing ruling quoted verbatim beside the sibling row: 「A:先削重复,余额按实测提高」. The `SKILL.md` row does not move (5456 of 6333). - **Changeset** `.changeset/26759-i18n-skill-inline-title-report-blocks.md`: `@objectstack/skills` patch, both declaration lines at line start. - `packages/lint/src/validate-translation-references.ts` (round 2, claim amendment 6449881440 — the card's "Stale header line" item): the Reports header comment now lists `blocks.<block_name>.label` / `.description`, keyed by the block's `name`, among a report's leaf keys. Comment only; the validator reads the Zod shape. **Skills readings** (`skills/**` touched; before = `origin/main` 0ea90c7, after = head; tokens = the ratchet's `ceil(bytes/4)`, stamp line excluded): | Reading | Before | After | | --- | --- | --- | | `skills/objectstack-i18n/SKILL.md` lines / tokens | 512 / 5282 (ceiling 6333, headroom 1051) | 520 / 5456 (ceiling 6333, headroom 877) | | `skills/objectstack-i18n/evals/bundles-locales-coverage.json` lines / tokens | 75 / 1451 (ceiling 1451, headroom 0) | 85 / 1642 (ceiling 1642, headroom 0) | | all `SKILL.md` lines / tokens (whole package) | 4603 / 56079 | 4611 / 56253 | ## Why An AI author who writes a translation bundle from this skill picks keys off the group table; the two keys PR #26757 added were missing from it, so a localized app kept a master-detail grid's heading and a joined report's block headings in English even though the runtime serves both translated (the business case on the card). ## Tests - `node scripts/check-skills-token-ratchet.mjs` at head: `81 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted`, exit 0; `objectstack-i18n/SKILL.md` ✓ (5456 / 6333), `bundles-locales-coverage.json` ✓ (1642 / 1642, headroom 0). - `node scripts/check-skills-token-ratchet.mjs --self-test`: `79 cases pass`, exit 0. - The eval file parses, and no case has a `must_contain` string in its `must_not_contain`. - No test file edited; no forced test edit. ## Acceptance notes - The eval is not run by any gate (the repo has no eval runner; `check:skill-docs` / `check:skill-refs` / `check:skill-examples` read `SKILL.md`, `references/` and the marked code fences). The new example fence is a fragment and carries no `os:check` marker on purpose. ## Verification - `node scripts/pm/dispatch-gates.mjs --tier --repo objectstack-ai/objectstack`: MANDATORY — `skills/**`, clause ①, no one-line exit; changed lines 51 vs the 3000 threshold. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at head `4f490d8e` (the final commit) derived 87 commands. All 87 ran in the worktree after that commit, each exit code captured before any pipe: 87 of 87 exit 0 (70 source-reading commands in one serial runner, the 17 dist-reading ones in a second after the build) — among them `node scripts/check-skills-token-ratchet.mjs` and its `--self-test`, `pnpm --filter @objectstack/spec run check:skill-docs` / `check:skill-examples` / `check:docs`, `pnpm check:pm-governed-merges`, `pnpm check:pm-dispatch-gates`, `pnpm check:published-files`, `node scripts/check-empty-changeset.mjs --base origin/main`, `node scripts/check-adr-0087-registration.mjs --base origin/main`, `node scripts/check-changeset-no-major.mjs --base origin/main`, `pnpm check:skill-identifier-liveness`, `pnpm check:skill-frame-sync`, `pnpm check:skill-compatibility`, `pnpm check:doc-authoring`, `pnpm check:docs-spec-enumerations`, `pnpm check:ratchet-remedy-authority`, `pnpm check:nul-bytes`. - `dispatch-gates --ran`: `87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED (a DERIVED zero — all 87 recorded an exit code and none of them is 3)`, exit 0; no UNRUN family named on stderr. - Build closure before the dist-reading gates: `turbo run build --filter='./packages/**' --concurrency=2` under `scripts/pm/os-verify-lock.sh`, `VERDICT command-exit 0` (held 8m20s). - The edited gate script's own test suite is its `--self-test` (79 cases); no package source is touched, so no package `test` / `typecheck` is owed; `pnpm lint` is CI's run. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (the repo-wide run is CI's; no source file edited). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a, not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ADHkmPqouEb6yELnSqBwRY --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmPqouEb6yELnSqBwRY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 912076f commit 4b8f8cd

6 files changed

Lines changed: 48 additions & 8 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the i18n skill's group table names `fields.{field}.inlineTitle` and `reports.{report}.blocks.{block}.{label,description}`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`skills/objectstack-i18n/SKILL.md` did not name two keys the 18.2 translation bundle already accepts and the runtime already serves, so an AI author writing a bundle from the skill left a master-detail field's inline child-grid heading and a joined report's block headings in the authoring language. The `fields.{field_name}` row now names `inlineTitle` (addressed on the child object that owns the field; untranslated it keeps its authored text, with no fallback to the child's plural label), the `reports` sentence names `reports.{report}.blocks.{block_name}.{label,description}` (keyed by the block's required `name`), and one example row each, spelled as the shipped `app-crm` and `app-showcase` bundles spell them, follows the group table. The eval file gains one prompt covering both keys.

‎content/docs/ui/translations.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default defineStack({
6969
|:---|:---|
7070
| Object label / plural / description | `objects.<name>.label` / `pluralLabel` / `description` |
7171
| Field labels, help text, placeholders | `objects.<name>.fields.<field>.label` / `help` / `placeholder` |
72+
| Inline child-grid heading of a master-detail field (drawn on the parent's form) | `objects.<child>.fields.<field>.inlineTitle` — keyed on the child object that owns the field; untranslated it keeps its authored text, with no fallback to the child's plural label |
7273
| Picklist option labels | `objects.<name>.fields.<field>.options.<value>` |
7374
| View titles, descriptions, empty states | `objects.<name>._views.<view>` |
7475
| Bulk-action copy on a list view (button, confirm prompt, dialog fields) | `objects.<name>._views.<view>.bulkActions.<def>.label` / `.confirmText` / `.confirmLabel` / `.params.<param>.label` / `.help` / `.placeholder` — a bulk param's hint is `help`, not `helpText` |
@@ -82,7 +83,7 @@ export default defineStack({
8283
| Dashboard global-filter label and static option labels | `dashboards.<name>.globalFilters.<filterName>.label` / `.options.<value>` — the key is the filter's `name`, or its `field` when it authors no `name`; an option is keyed by its `value` spelled as a string |
8384
| Analytics dataset label / description | `datasets.<name>.label` / `description` |
8485
| Dataset dimension and measure labels | `datasets.<name>.dimensions.<dimension>.label` / `datasets.<name>.measures.<measure>.label` |
85-
| Report label / description | `reports.<name>.label` / `description` — keyed by the report's `name`; overlaid on every served report, which is what the console's report index lists and the report page is headed by. A label or description an org re-authored on a packaged report is served as edited |
86+
| Report label / description | `reports.<name>.label` / `description` — keyed by the report's `name`; overlaid on every served report, which is what the console's report index lists and the report page is headed by. A label or description an org re-authored on a packaged report is served as edited; a joined report's block headings are `reports.<name>.blocks.<block>.label` / `description`, keyed by the block's required `name` |
8687
| Page labels and `page:header` copy | `pages.<name>.label` / `description` / `title` / `subtitle` — on a `kind: 'slotted'` page the header under `slots.header` is the page's header |
8788
| Page component copy, by component id | `pages.<name>.components.<id>.title` / `description` / `label` / `placeholder` / `emptyText` — reached under `regions[].components[]` and `slots.<slot>`, through `properties.children` and a `page:tabs` / `page:accordion` panel's `items[].children` |
8889
| Page tab labels (`page:tabs` items) | `pages.<name>.components.<tabsId>.items.<value>.label` — keyed by the tabs component's `id` and the item's `value`; an item without a `value` has no key (localize it with an inline locale map), and `page:accordion` panels have none |

‎packages/lint/src/validate-translation-references.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@
160160
* `reports.<name>` resolves against `Report.name`, the name `translateReport`
161161
* looks a served report up by, with the same verdict: a name no package of
162162
* this stack declares is an orphan, `translation-target-unknown` (`error`).
163-
* Leaf keys below the name are the schema's (`label` / `description`, strict).
163+
* Leaf keys below the name are the schema's: `label` / `description`, and a
164+
* joined report's `blocks.<block_name>.label` / `.description` by the block's
165+
* `name` (strict).
164166
*
165167
* ── Doc books ────────────────────────────────────────────────────────────
166168
*

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,15 @@ export const CEILINGS = new Map([
925925
// content to delete (five prompts, five distinct surfaces), so the remainder
926926
// is the whole addition; pinned AT the measurement, zero headroom. The
927927
// raising PR's body carries the same arithmetic.
928-
['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1451],
928+
// 1451 -> 1642 (2026-10-27, the owed Skills-docs follow-up of the spec PR that
929+
// made a master-detail field's `inlineTitle` and a joined report's block
930+
// `label` / `description` translatable): one eval prompt for both keys. Same
931+
// ruling as the row above, tap label verbatim and untranslated:
932+
// 「A:先削重复,余额按实测提高」. PAID DOWN FIRST: case 6's `--no-objects-only`
933+
// pin duplicated case 5's (77 bytes cut, 20 tokens); the remainder is the new
934+
// case; pinned AT the measurement, zero headroom. The raising PR's body carries
935+
// the same arithmetic.
936+
['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1642],
929937
// 1211 -> 1458 (card #26302): one eval for a demo whose seed names people —
930938
// expected: both in `demoStaff` with a declared `position` /
931939
// `permissionSets`, created by `os dev`; NO `role:` alias and NO platform

‎skills/objectstack-i18n/SKILL.md‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ All translatable content for a single object is aggregated under
159159
| Sub-key | Holds |
160160
|:--------|:------|
161161
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
162-
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
162+
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label), `inlineTitle` (the heading of the inline child grid a `master_detail` field draws on its parent's form — the field lives on the child object, so the key is the child's; untranslated it keeps its authored text, never the child's plural label) per field |
163163
| `_views.{view_name}` | `label`, `description`, `title` (a form view's own heading — only a form that authors one is translated; a list view has none), `emptyState.title` / `emptyState.message`, `bulkActions.{def_name}` (`label`, `confirmText`, `confirmLabel`, `params`) |
164164
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}` (and `.options.{value}` beneath it — an option's stored `value` in that param's inline `options`), `outcomeMessages.{outcome}` (an outcome the action's own `outcomeMessages` declares), `resultDialog.title` / `.description` / `.acknowledge` (only under an action that declares a `resultDialog`), `resultDialog.fields.{path}` (the literal `path` of a field in `resultDialog.fields[]`, dots included — `client.secret` is one key); the same keys hold under `globalActions.{action_name}` |
165165
| `_sections.{section_name}` | Form section `label`, `description`; `fields.{field_name}` — the `label`, `placeholder`, `helpText` an entry overrides for that form alone (`helpText` here, `help` under `fields.{field_name}` above: each spells what its source does) |
@@ -172,12 +172,21 @@ an **error** whose message names the `objects.{owner}._actions.{action_name}` ke
172172
instead), `picklists` (`label`, `options` by stored value — every field referencing the
173173
picklist inherits them), `dashboards`, `datasets`, `reports`, `pages`, `flows`, `positions`,
174174
`permissions` (permission sets, spelled as the stack's `permissions[]`), `metadataForms`,
175-
`settingsCommon`; `reports`, `positions` and `permissions` hold `label` + `description` by name.
175+
`settingsCommon`; `reports`, `positions` and `permissions` hold `label` + `description` by name,
176+
and a joined report's block headings sit at `reports.{report}.blocks.{block_name}.{label,description}`,
177+
keyed by the block's required `name`.
176178
Two keys sit deeper than their group's `label`: a `page:tabs` item at
177179
`pages.{page}.components.{tabs_id}.items.{value}.label` (by the item's `value`; an item without
178180
one has no key) and an approval step at `flows.{flow}.approvals.{node_id}.label`.
179181
`os i18n extract --no-objects-only` scaffolds every group above but `messages` and
180-
`settingsCommon`, both keys included.
182+
`settingsCommon`, both keys included. The `inlineTitle` and report-block keys, as the
183+
shipped `app-crm` and `app-showcase` bundles spell them for `zh-CN`:
184+
185+
```typescript
186+
objects: { crm_opportunity_line_item: { fields: { opportunity: { inlineTitle: '产品明细' } } } },
187+
reports: { showcase_task_overview: { blocks: { open_block: { label: '未完成任务' } } } },
188+
```
189+
181190
`settings` is **platform-only** — it is keyed by a settings manifest's namespace and only
182191
the platform declares one, so an app bundle carrying it is refused by name.
183192

‎skills/objectstack-i18n/evals/bundles-locales-coverage.json‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@
5454
{
5555
"id": 6,
5656
"prompt": "Add zh-CN for the `pipeline_by_stage` report, the `sales_manager` permission set, the `regional_lead` position and the `deal_stage` picklist's option labels.",
57-
"expected_output": "Top-level groups keyed by name: `reports.pipeline_by_stage` and `positions.regional_lead` (`label`, `description`), `permissions.sales_manager` (the same, spelled as the stack's `permissions[]`, never `permissionSets`), `picklists.deal_stage.options` by stored value, inherited by every field referencing the picklist. `os i18n extract --no-objects-only` scaffolds all four.",
57+
"expected_output": "Top-level groups keyed by name: `reports.pipeline_by_stage` and `positions.regional_lead` (`label`, `description`), `permissions.sales_manager` (the same, spelled as the stack's `permissions[]`, not the camelCase name), `picklists.deal_stage.options` by stored value, inherited by every field referencing the picklist.",
5858
"files": [],
5959
"assertions": {
60-
"must_contain": ["reports", "permissions", "positions", "picklists", "--no-objects-only"],
60+
"must_contain": ["reports", "permissions", "positions", "picklists"],
6161
"must_not_contain": ["permissionSets", "report: {"]
6262
}
6363
},
@@ -70,6 +70,16 @@
7070
"must_contain": ["_views", "title", "_sections", "fields", "items", "approvals"],
7171
"must_not_contain": ["stepLabel", "_tabs.overview"]
7272
}
73+
},
74+
{
75+
"id": 8,
76+
"prompt": "In zh-CN the `Products` grid heading on the `crm_opportunity` form and the `Open Tasks` block heading of the joined `showcase_task_overview` report are still English.",
77+
"expected_output": "`objects.crm_opportunity_line_item.fields.opportunity.inlineTitle` — the field lives on the child object, so the key is the child's, and an untranslated `inlineTitle` keeps its authored text (no fallback to the child's plural label); `reports.showcase_task_overview.blocks.open_block.label`, keyed by the block's required `name`. Neither is a `pages.*.components` key.",
78+
"files": [],
79+
"assertions": {
80+
"must_contain": ["inlineTitle", "crm_opportunity_line_item", "blocks", "open_block"],
81+
"must_not_contain": ["fields.opportunity_line_item", "components.open_block"]
82+
}
7383
}
7484
]
7585
}

0 commit comments

Comments
 (0)