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
29 changes: 29 additions & 0 deletions .changeset/25788-translation-reports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@objectstack/spec': minor
'@objectstack/cli': minor
'@objectstack/lint': minor
'@objectstack/metadata-protocol': minor
'@objectstack/rest': minor
'@object-ui/app-shell': minor
'@object-ui/i18n': minor
---

feat(spec,cli): reports are translatable — `reports.<name>` in the translation bundle

Clause-②: yes
Metadata-protocol: added

An app authored in one language listed its reports (`reports[].label`, `reports[].description`) in that language on the app's report index and the report page heading. Before this change the translation bundle had no key for them: `reports` was a retired object-first dialect key, so a bundle carrying it was refused by `os validate`, and `os i18n check` could not report the gap.

- **`@objectstack/spec`:** `TranslationData` (and the registered `translation` item and the platform bundle, which share its groups) gains `reports`, keyed by the report's `name`, carrying `label` and `description` (strict: any other key is refused — `name` or `title` with a message pointing at `label`, and the singular spelling `report` with a message pointing at `reports`). `reports` leaves `LEGACY_OBJECT_FIRST_KEYS`, so a stored translation row carrying it now loads through the authored-row sync instead of being skipped as a retired dialect; the key's measured footprint in the legacy dialect was zero (#3778). A new translator, `translateReport`, overlays the two strings and is registered in the metadata-document dispatch, so `GET /meta/report` (list and item) is served in the request's locale. A key the bundle does not carry keeps the authored string. A report is overlay-editable (ADR-0126 Regime O), so the translator honours the packaged base: a label or description an org re-authored is served as written (ADR-0029 D9.2a), and the strings it did not touch are still translated.
- **`@objectstack/metadata-protocol`:** `getPackagedReportBase(name)` returns the shipped report document beneath any overlay, the base the REST read path hands the translator.
- **`@objectstack/rest`:** the meta read path supplies that base for `report`, as it does for `dashboard` and `view`.
- **`@objectstack/lint`:** `translation-target-unknown` reports a `reports.<name>` key that names no report a package of the stack declares, as an error with the nearest declared name.
- **`@objectstack/cli`:** `os i18n extract` scaffolds `reports.<name>.label` / `.description` from the stack's `reports[]`, and `os i18n check` / `os lint` coverage report an untranslated one in its own bucket (`i18n/missing-report`).
- **`@object-ui/app-shell`, `@object-ui/i18n`:** the translation designer's preview draws the `reports` group (a "Reports" / 「报表」 category beside the others), and the console's translation loader recognises a payload that carries only `reports`.

Example (`zh-CN` bundle of an English-authored app):

```ts
reports: { opps_by_stage: { label: '各阶段商机', description: '按阶段汇总的商机' } },
```
5 changes: 3 additions & 2 deletions content/docs/protocol/kernel/i18n-standard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ Three rules the shape enforces, all of them closed since #4001:

- **The top level is the declared group set and nothing else** — `objects`,
`picklists`, `apps`, `messages`, `globalActions`, `dashboards`, `datasets`,
`pages`, `flows`, `positions`, `permissions`, `metadataForms`,
`reports`, `pages`, `flows`, `positions`, `permissions`, `metadataForms`,
`settingsCommon`. A group invented beside them
(`"account"`, `"fields"`, `"list"`, `"industries"`) is rejected **by name**
at both authoring doors, with the group to use instead named in the
Expand Down Expand Up @@ -483,7 +483,7 @@ what the overlay writes back as the node's `title`.
The contract is `t(key, locale, params?)` — the target locale is always passed
explicitly, and the key itself is returned when no translation is found (there
is no `defaultValue` option). The key is a **dot path into `TranslationData`**,
so it starts at one of the thirteen declared groups.
so it starts at one of the fourteen declared groups.

```typescript
const i18n = kernel.getService('i18n');
Expand Down Expand Up @@ -1063,6 +1063,7 @@ beneath an action entry are judged the same way under either.
| `flows.{flow}` | a flow's machine `name`, declared |
| `positions.{name}` | a position's `name`, declared, or an audience anchor (`everyone`, `guest`) |
| `permissions.{name}` | a permission set's `name`, declared |
| `reports.{name}` | a report's `name`, declared |
| `flows.{flow}.screens.{node}` | the `id` of a `type: 'screen'` node in that flow, nodes nested in loop, parallel and try/catch regions included |
| `flows.{flow}.screens.{node}.fields.{field}` | a `config.fields[].name` on that screen — an object-form screen (`config.objectName`) declares none, and its input labels live under `objects.{objectName}.fields` |

Expand Down
1 change: 1 addition & 0 deletions content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1776,6 +1776,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }>` | optional | Dashboard translations keyed by dashboard name |
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
| **reports** | `Record<string, { label?: string; description?: string }>` | optional | Report translations keyed by report name |
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
Expand Down
24 changes: 24 additions & 0 deletions content/docs/references/system/translation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ Platform translation data — the per-app groups plus the platform-only `setting
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }>` | optional | Dashboard translations keyed by dashboard name |
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
| **reports** | `Record<string, { label?: string; description?: string }>` | optional | Report translations keyed by report name |
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
Expand Down Expand Up @@ -247,6 +248,13 @@ Translation data for a single object
| **dimensions** | `Record<string, { label?: string }>` | optional | Dimension translations keyed by dimension name (`DatasetDimensionSchema.name`) |
| **measures** | `Record<string, { label?: string }>` | optional | Measure translations keyed by measure name (`DatasetMeasureSchema.name`) |

### Nested Shape: `PlatformTranslationData.reports[string]`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | optional | Translated report label — overlays the report's own `label` |
| **description** | `string` | optional | Translated report description — overlays the report's own `description` |

### Nested Shape: `PlatformTranslationData.pages[string]`

| Property | Type | Required | Description |
Expand Down Expand Up @@ -397,6 +405,7 @@ Per-app translation data for objects, apps, and UI messages
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }>` | optional | Dashboard translations keyed by dashboard name |
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
| **reports** | `Record<string, { label?: string; description?: string }>` | optional | Report translations keyed by report name |
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
Expand Down Expand Up @@ -466,6 +475,13 @@ Translation data for a single object
| **dimensions** | `Record<string, { label?: string }>` | optional | Dimension translations keyed by dimension name (`DatasetDimensionSchema.name`) |
| **measures** | `Record<string, { label?: string }>` | optional | Measure translations keyed by measure name (`DatasetMeasureSchema.name`) |

### Nested Shape: `TranslationData.reports[string]`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | optional | Translated report label — overlays the report's own `label` |
| **description** | `string` | optional | Translated report description — overlays the report's own `description` |

### Nested Shape: `TranslationData.pages[string]`

| Property | Type | Required | Description |
Expand Down Expand Up @@ -566,6 +582,7 @@ One locale of translations — the `translation` metadata type
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }>` | optional | Dashboard translations keyed by dashboard name |
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
| **reports** | `Record<string, { label?: string; description?: string }>` | optional | Report translations keyed by report name |
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
Expand Down Expand Up @@ -645,6 +662,13 @@ Translation data for a single object
| **dimensions** | `Record<string, { label?: string }>` | optional | Dimension translations keyed by dimension name (`DatasetDimensionSchema.name`) |
| **measures** | `Record<string, { label?: string }>` | optional | Measure translations keyed by measure name (`DatasetMeasureSchema.name`) |

### Nested Shape: `TranslationItem.reports[string]`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | optional | Translated report label — overlays the report's own `label` |
| **description** | `string` | optional | Translated report description — overlays the report's own `description` |

### Nested Shape: `TranslationItem.pages[string]`

| Property | Type | Required | Description |
Expand Down
3 changes: 2 additions & 1 deletion content/docs/ui/translations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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 |
| 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 All @@ -89,7 +90,7 @@ export default defineStack({
| A label written as an inline locale map (`label: { en: 'Members', 'zh-CN': '成员' }`) | Nowhere — it is written on the metadata and resolved at render time; see **Current boundaries** below |

The metadata types resolved per request are **object, view, action, app,
dashboard, dataset, page, picklist, position and permission** — a field's labels are translated as part of
dashboard, dataset, report, page, picklist, position and permission** — a field's labels are translated as part of
its object document, and so are the labels of any actions the object declares
inline
(#3370). Before that, an object document went out with its authored action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.

| Dir | Sites |
|---|---|
| `system/` | 381 |
| `system/` | 382 |
8 changes: 8 additions & 0 deletions examples/app-showcase/src/system/translations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1337,5 +1337,13 @@ export const ShowcaseTranslationBundle = {
showcase_field_ops_delegate: { label: '现场运营委派管理员' },
showcase_client_liaison: { label: '示例客户联络人' },
},
// The reports, as the report index lists them and each report page is
// headed. Counted by `check-i18n-coverage` like the groups above.
reports: {
showcase_hours_by_status: { label: '按状态统计工时(汇总)', description: '按任务状态分组的预计工时。' },
showcase_status_priority_matrix: { label: '状态 × 优先级(矩阵)', description: '按状态和优先级交叉统计的预计工时。' },
showcase_task_overview: { label: '任务概览(联合)', description: '将多个任务子报表堆叠在一个联合视图中。' },
showcase_hours_by_status_chart: { label: '按状态统计工时(图表)', description: '以柱状图展示按状态统计的预计工时。' },
},
},
};
9 changes: 9 additions & 0 deletions examples/app-todo/src/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ export const jaJP: TranslationData = {
description: '個人タスク管理アプリケーション',
},
},
// The app's reports, as its report index lists them and each report page is
// headed (`reports.<name>.label` / `.description`).
reports: {
tasks_by_status: { label: 'ステータス別タスク', description: 'ステータス別に集計したタスク' },
tasks_by_priority: { label: '優先度別タスク', description: '優先度レベル別に集計したタスク' },
tasks_by_owner: { label: '担当者別タスク', description: '担当者別のタスク集計' },
completed_tasks: { label: '完了済みタスク', description: '作業時間の記録付きの完了済みタスク' },
time_tracking: { label: '作業時間レポート', description: '見積時間と実績時間の分析' },
},
// The quick-add wizard: the flow's name in the runner header and completion
// toast, each screen's heading and each field label.
flows: {
Expand Down
9 changes: 9 additions & 0 deletions examples/app-todo/src/translations/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ export const zhCN: TranslationData = {
description: '个人任务管理应用',
},
},
// The app's reports, as its report index lists them and each report page is
// headed (`reports.<name>.label` / `.description`).
reports: {
tasks_by_status: { label: '按状态统计任务', description: '按状态分组的任务汇总' },
tasks_by_priority: { label: '按优先级统计任务', description: '按优先级分组的任务汇总' },
tasks_by_owner: { label: '按负责人统计任务', description: '按负责人汇总的任务' },
completed_tasks: { label: '已完成任务', description: '所有已完成任务及其工时记录' },
time_tracking: { label: '工时跟踪报表', description: '预计工时与实际工时分析' },
},
// The quick-add wizard: the flow's name in the runner header and completion
// toast, each screen's heading and each field label.
flows: {
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/utils/i18n-coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface CoverageIssue {
| 'dashboard'
| 'widget'
| 'dataset'
| 'report'
| 'picklist'
| 'page'
| 'flow'
Expand Down Expand Up @@ -352,6 +353,11 @@ const COVERAGE_SOURCE: Record<ExpectedEntry['source'], CoverageIssue['source']>
// the `dashboard` bucket: the string is defined once, not once per
// presentation.
dataset: 'dataset',
// Report copy (`reports.<r>.label` / `.description`) — the author's own
// reports, listed by these two strings on the app's report index and headed
// by the label on the report page, so it keeps its own bucket and reports as
// `i18n/missing-report` rather than folding into `dashboard`.
report: 'report',
// Shared option-list copy (`picklists.<p>.label`, `.options.<value>`) — the
// author's own vocabulary, translated once for every field that references
// it, so it keeps its own bucket and reports as `i18n/missing-picklist`.
Expand Down Expand Up @@ -396,6 +402,7 @@ const SOURCE_NOUN: Record<CoverageIssue['source'], string> = {
dashboard: 'Dashboard',
widget: 'Widget',
dataset: 'Dataset',
report: 'Report',
picklist: 'Picklist',
page: 'Page',
flow: 'Flow',
Expand Down Expand Up @@ -440,6 +447,7 @@ const SOURCE_SURFACE: Record<CoverageIssue['source'], string> = {
dashboard: 'dashboards',
widget: 'widgets',
dataset: 'datasets',
report: 'reports',
picklist: 'picklists',
page: 'pages',
flow: 'flow screens, approval steps, toasts and refusals',
Expand Down
Loading
Loading