Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/26759-i18n-skill-inline-title-report-blocks.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion content/docs/ui/translations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default defineStack({
|:---|:---|
| Object label / plural / description | `objects.<name>.label` / `pluralLabel` / `description` |
| Field labels, help text, placeholders | `objects.<name>.fields.<field>.label` / `help` / `placeholder` |
| 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 |
| Picklist option labels | `objects.<name>.fields.<field>.options.<value>` |
| View titles, descriptions, empty states | `objects.<name>._views.<view>` |
| 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` |
Expand All @@ -82,7 +83,7 @@ export default defineStack({
| 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 |
| Analytics dataset label / description | `datasets.<name>.label` / `description` |
| Dataset dimension and measure labels | `datasets.<name>.dimensions.<dimension>.label` / `datasets.<name>.measures.<measure>.label` |
| 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 |
| 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` |
| 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 |
| 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` |
| 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 |
Expand Down
4 changes: 3 additions & 1 deletion packages/lint/src/validate-translation-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@
* `reports.<name>` 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.<block_name>.label` / `.description` by the block's
* `name` (strict).
*
* ── Cross-package objects ────────────────────────────────────────────────
*
Expand Down
10 changes: 9 additions & 1 deletion scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions skills/objectstack-i18n/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand All @@ -172,12 +172,21 @@ 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. 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.

Expand Down
14 changes: 12 additions & 2 deletions skills/objectstack-i18n/evals/bundles-locales-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -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[]`, 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", "--no-objects-only"],
"must_contain": ["reports", "permissions", "positions", "picklists"],
"must_not_contain": ["permissionSets", "report: {"]
}
},
Expand All @@ -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"]
}
}
]
}
Loading