From 4f490d8e98a7a410b7d71c3cd6879403a50b6d52 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 17:44:28 +0000 Subject: [PATCH 1/3] docs(skills): the i18n skill names fields.{field}.inlineTitle and a joined report's block headings The objectstack-i18n group table did not name two keys the 18.2 bundle accepts and the runtime serves: a master-detail field's `inlineTitle` (addressed on the child object that owns the field; untranslated it keeps its authored text, no fallback to the child's plural label) and a joined report's `blocks.{block_name}.{label,description}` (keyed by the block's required `name`). Both are now in the table, with one example row each spelled as the shipped app-crm and app-showcase bundles spell them, and the hand-written author page under content/docs/ui gains the same two rows. The eval file gains one prompt for both keys; its ratchet row is pinned at the measurement after the duplicate `--no-objects-only` pin was cut, under the standing ruling. Claude-Session: https://claude.ai/code/session_01ADHkmPqouEb6yELnSqBwRY Co-authored-by: Claude --- .../26759-i18n-skill-inline-title-report-blocks.md | 10 ++++++++++ content/docs/ui/translations.mdx | 3 ++- scripts/check-skills-token-ratchet.mjs | 10 +++++++++- skills/objectstack-i18n/SKILL.md | 14 +++++++++++--- .../evals/bundles-locales-coverage.json | 14 ++++++++++++-- 5 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 .changeset/26759-i18n-skill-inline-title-report-blocks.md diff --git a/.changeset/26759-i18n-skill-inline-title-report-blocks.md b/.changeset/26759-i18n-skill-inline-title-report-blocks.md new file mode 100644 index 00000000000..69511ac690a --- /dev/null +++ b/.changeset/26759-i18n-skill-inline-title-report-blocks.md @@ -0,0 +1,10 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the i18n skill's group table names `fields.{field}.inlineTitle` and `reports.{report}.blocks.{block}.{label,description}` + +Clause-②: no +Metadata-protocol: none + +`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. diff --git a/content/docs/ui/translations.mdx b/content/docs/ui/translations.mdx index 40c9233391a..4fcb0cc4689 100644 --- a/content/docs/ui/translations.mdx +++ b/content/docs/ui/translations.mdx @@ -69,6 +69,7 @@ export default defineStack({ |:---|:---| | Object label / plural / description | `objects..label` / `pluralLabel` / `description` | | Field labels, help text, placeholders | `objects..fields..label` / `help` / `placeholder` | +| Inline child-grid heading of a master-detail field (drawn on the parent's form) | `objects..fields..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 | | Picklist option labels | `objects..fields..options.` | | View titles, descriptions, empty states | `objects.._views.` | | Bulk-action copy on a list view (button, confirm prompt, dialog fields) | `objects.._views..bulkActions..label` / `.confirmText` / `.confirmLabel` / `.params..label` / `.help` / `.placeholder` — a bulk param's hint is `help`, not `helpText` | @@ -82,7 +83,7 @@ export default defineStack({ | Dashboard global-filter label and static option labels | `dashboards..globalFilters..label` / `.options.` — 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 | | Analytics dataset label / description | `datasets..label` / `description` | | Dataset dimension and measure labels | `datasets..dimensions..label` / `datasets..measures..label` | -| Report label / description | `reports..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 | +| Report label / description | `reports..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..blocks..label` / `description`, keyed by the block's required `name` | | Page labels and `page:header` copy | `pages..label` / `description` / `title` / `subtitle` — on a `kind: 'slotted'` page the header under `slots.header` is the page's header | | Page component copy, by component id | `pages..components..title` / `description` / `label` / `placeholder` / `emptyText` — reached under `regions[].components[]` and `slots.`, through `properties.children` and a `page:tabs` / `page:accordion` panel's `items[].children` | | Page tab labels (`page:tabs` items) | `pages..components..items..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 | diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 74134bf99b9..ae76664deac 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -874,7 +874,15 @@ export const CEILINGS = new Map([ // content to delete (five prompts, five distinct surfaces), so the remainder // is the whole addition; pinned AT the measurement, zero headroom. The // raising PR's body carries the same arithmetic. - ['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1451], + // 1451 -> 1642 (2026-10-27, the owed Skills-docs follow-up of the spec PR that + // made a master-detail field's `inlineTitle` and a joined report's block + // `label` / `description` translatable): one eval prompt for both keys. Same + // ruling as the row above, tap label verbatim and untranslated: + // 「A:先削重复,余额按实测提高」. PAID DOWN FIRST: case 6's `--no-objects-only` + // pin duplicated case 5's (77 bytes cut, 20 tokens); the remainder is the new + // case; pinned AT the measurement, zero headroom. The raising PR's body carries + // the same arithmetic. + ['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1642], // 1211 -> 1458 (card #26302): one eval for a demo whose seed names people — // expected: both in `demoStaff` with a declared `position` / // `permissionSets`, created by `os dev`; NO `role:` alias and NO platform diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index 02a1ae18264..a482fbf4598 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -159,7 +159,7 @@ All translatable content for a single object is aggregated under | Sub-key | Holds | |:--------|:------| | `label` / `pluralLabel` / `description` | Object-level text (every key optional) | -| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field | +| `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 | | `_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`) | | `_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}` | | `_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,20 @@ an **error** whose message names the `objects.{owner}._actions.{action_name}` ke instead), `picklists` (`label`, `options` by stored value — every field referencing the picklist inherits them), `dashboards`, `datasets`, `reports`, `pages`, `flows`, `positions`, `permissions` (permission sets, spelled as the stack's `permissions[]`), `metadataForms`, -`settingsCommon`; `reports`, `positions` and `permissions` hold `label` + `description` by name. +`settingsCommon`; `reports`, `positions` and `permissions` hold `label` + `description` by name, +and a joined report's block headings sit at `reports.{report}.blocks.{block_name}.{label,description}`, +keyed by the block's required `name`. Two keys sit deeper than their group's `label`: a `page:tabs` item at `pages.{page}.components.{tabs_id}.items.{value}.label` (by the item's `value`; an item without one has no key) and an approval step at `flows.{flow}.approvals.{node_id}.label`. `os i18n extract --no-objects-only` scaffolds every group above but `messages` and -`settingsCommon`, both keys included. +`settingsCommon`, both keys included. Both deeper keys, as the shipped `app-crm` and +`app-showcase` bundles spell them for `zh-CN`: + +```typescript +objects: { crm_opportunity_line_item: { fields: { opportunity: { inlineTitle: '产品明细' } } } }, +reports: { showcase_task_overview: { blocks: { open_block: { label: '未完成任务' } } } }, +``` `settings` is **platform-only** — it is keyed by a settings manifest's namespace and only the platform declares one, so an app bundle carrying it is refused by name. diff --git a/skills/objectstack-i18n/evals/bundles-locales-coverage.json b/skills/objectstack-i18n/evals/bundles-locales-coverage.json index 1c300fe02b0..0999fcd276d 100644 --- a/skills/objectstack-i18n/evals/bundles-locales-coverage.json +++ b/skills/objectstack-i18n/evals/bundles-locales-coverage.json @@ -54,10 +54,10 @@ { "id": 6, "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.", - "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.", + "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.", "files": [], "assertions": { - "must_contain": ["reports", "permissions", "positions", "picklists", "--no-objects-only"], + "must_contain": ["reports", "permissions", "positions", "picklists"], "must_not_contain": ["permissionSets", "report: {"] } }, @@ -70,6 +70,16 @@ "must_contain": ["_views", "title", "_sections", "fields", "items", "approvals"], "must_not_contain": ["stepLabel", "_tabs.overview"] } + }, + { + "id": 8, + "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.", + "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.", + "files": [], + "assertions": { + "must_contain": ["inlineTitle", "crm_opportunity_line_item", "blocks", "open_block"], + "must_not_contain": ["fields.opportunity_line_item", "components.open_block"] + } } ] } From 73e5a8f1d60b4332b2da22a580cbffaff3f6aeeb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 18:09:37 +0000 Subject: [PATCH 2/3] docs(skills): name the two keys the example fence shows; the lint header lists report blocks The lead-in above the zh-CN example fence read as the two keys of the previous sentence; it now names `inlineTitle` and the report-block keys, and a blank line separates the fence from the `settings` paragraph. The validate-translation-references header's Reports section lists `blocks..label` / `.description` among a report's leaf keys, which the validator already accepts from the Zod shape. Claude-Session: https://claude.ai/code/session_01ADHkmPqouEb6yELnSqBwRY Co-authored-by: Claude --- packages/lint/src/validate-translation-references.ts | 4 +++- skills/objectstack-i18n/SKILL.md | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/lint/src/validate-translation-references.ts b/packages/lint/src/validate-translation-references.ts index 7d96c4db3e8..fda5ce00739 100644 --- a/packages/lint/src/validate-translation-references.ts +++ b/packages/lint/src/validate-translation-references.ts @@ -160,7 +160,9 @@ * `reports.` resolves against `Report.name`, the name `translateReport` * looks a served report up by, with the same verdict: a name no package of * this stack declares is an orphan, `translation-target-unknown` (`error`). - * Leaf keys below the name are the schema's (`label` / `description`, strict). + * Leaf keys below the name are the schema's: `label` / `description`, and a + * joined report's `blocks..label` / `.description` by the block's + * `name` (strict). * * ── Cross-package objects ──────────────────────────────────────────────── * diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index a482fbf4598..aef395b5384 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -179,13 +179,14 @@ Two keys sit deeper than their group's `label`: a `page:tabs` item at `pages.{page}.components.{tabs_id}.items.{value}.label` (by the item's `value`; an item without one has no key) and an approval step at `flows.{flow}.approvals.{node_id}.label`. `os i18n extract --no-objects-only` scaffolds every group above but `messages` and -`settingsCommon`, both keys included. Both deeper keys, as the shipped `app-crm` and -`app-showcase` bundles spell them for `zh-CN`: +`settingsCommon`, both keys included. The `inlineTitle` and report-block keys, as the +shipped `app-crm` and `app-showcase` bundles spell them for `zh-CN`: ```typescript objects: { crm_opportunity_line_item: { fields: { opportunity: { inlineTitle: '产品明细' } } } }, reports: { showcase_task_overview: { blocks: { open_block: { label: '未完成任务' } } } }, ``` + `settings` is **platform-only** — it is keyed by a settings manifest's namespace and only the platform declares one, so an app bundle carrying it is refused by name. From 57406c6ff6f1e8037aa9e2846f483637d74a8afb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 18:42:18 +0000 Subject: [PATCH 3/3] docs(skills): eval case 6's ideal answer no longer spells its own refused string The i18n eval's case 6 said "never `permissionSets`" in its ideal answer while listing `permissionSets` under must_not_contain, so the ideal answer graded wrong. Same-byte rewording ("not the camelCase name"); the file stays at its 1642-token pin. Claude-Session: https://claude.ai/code/session_01ADHkmPqouEb6yELnSqBwRY Co-authored-by: Claude --- skills/objectstack-i18n/evals/bundles-locales-coverage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/objectstack-i18n/evals/bundles-locales-coverage.json b/skills/objectstack-i18n/evals/bundles-locales-coverage.json index 0999fcd276d..a1b344a3ccd 100644 --- a/skills/objectstack-i18n/evals/bundles-locales-coverage.json +++ b/skills/objectstack-i18n/evals/bundles-locales-coverage.json @@ -54,7 +54,7 @@ { "id": 6, "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.", - "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.", + "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.", "files": [], "assertions": { "must_contain": ["reports", "permissions", "positions", "picklists"],