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 47af3dd
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec,cli): positions and permission sets are translatable — positions and permissions in the translation bundle (#25701)
Fixes#25652
Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: none; triage first grade 6385078248 (a protocol addition
under the standing order, p1)
## What
A translation bundle can now carry positions and permission sets:
```ts
positions: { srm_legal_counsel: { label: 'Legal Counsel' } },
permissions: { srm_contract_reviewer: { label: 'Contract Review', description: '…' } },
```
Before, the same `en` entry in a Chinese-authored app (hotsrm#22) failed
`objectstack validate` with `Unrecognized key(s) on this locale of the
translation bundle: positions`, so position and permission-set labels
stayed in the authoring language while every object, app and flow label
beside them was translated, and `os i18n check --strict` had nowhere to
report the gap.
- **`@objectstack/spec` (`system/translation.zod.ts`):** two new groups
on the shared per-app shape, so the bundle entry (`TranslationData`),
the registered `translation` item and the platform bundle all accept
them. Each is keyed by the item's `name` and closed at `label` /
`description` (strict: any other key is refused; `name` / `title` are
refused with a message pointing at `label`, and `position` /
`permission` / `permissionSets` with a message pointing at the groups).
The group for permission sets is spelled `permissions`, as the stack
spells the collection.
- **`@objectstack/spec` (`system/i18n-resolver.ts`):**
`translatePosition` and `translatePermissionSet`, registered in
`METADATA_DOCUMENT_TRANSLATORS`. `TRANSLATABLE_METADATA_TYPES` is
derived from that table and `@objectstack/rest` reads it, so `GET
/meta/position` and `GET /meta/permission` (list and item, both
transports) are now served in the request's locale. These two lists are
what the console's `position-ref` and `permission-ref` pickers draw. A
key the bundle does not carry leaves the authored string, and an
untouched document keeps its reference.
- **`@objectstack/spec` (liveness ledger):** `translation/positions` and
`translation/permissions` were added as `live`, with drilled `label` /
`description` children. They were seeded together with their reader (the
datasets precedent), never declared first and wired later.
- **`@objectstack/cli`:** `os i18n extract` walks `positions[]` and
`permissions[]` (`walkSecurityCatalog`), and `os i18n check` / `os lint`
coverage report a gap in each group's own bucket
(`i18n/missing-position`, `i18n/missing-permission`). Every key is
recorded with `pushOptional`, so a string nobody wrote is never required
to have a translation.
- **`@objectstack/lint`:** `translation-target-unknown` resolves each
`positions.NAME` key against the stack's `positions[]` names and each
`permissions.NAME` key against its `permissions[]` names, sibling
packages' declarations included. An unknown name is an `error` with the
nearest declared name; the audience anchors `everyone` / `guest` always
resolve. `os explain translation-target-unknown` names both legs.
- **`scripts/fixtures/i18n-walk-parity/every-group.stack.json`:**
authors one member of each new group, so `check:i18n-walk-parity` sees
both walked.
- **`packages/runtime` meta-list projection census:** `permission` /
`permissions` joined the list axis, and `position` / `permission` joined
the item axis. The census requires every translatable type to be a cell.
The reference-moves block pins the zh-CN label on both reads.
- **`examples/app-crm`, `examples/app-showcase`:** zh-CN translations
for their 5 + 19 positions and permission sets. The new walk counts
them, and without these `check:i18n-coverage` grew 101 → 109 (crm) and
320 → 349 (showcase). The fix was to translate them, not to raise the
ratchet.
- **Docs:** the group lists in `protocol/kernel/i18n-standard.mdx` and
`ui/translations.mdx` now include the new groups. They also include the
missing `picklists` / `picklist`, which those same sentences claimed
exhaustively. The leg table in `i18n-standard.mdx` names the two new
`translation-target-unknown` legs.
## Acceptance notes
- **Deviation from the claim's file surface (and the reason):** the
claim scoped the spec half to the schema, typegen and CLI coverage, with
the overlay left to objectui. Shipping the schema alone would have
declared a key that parses clean and translates nothing until a
downstream card lands. That is the declared-not-enforced shape this file
pays to remove (Prime Directive #10, the datasets precedent in the
liveness ledger). The console's pickers already read `/meta/position`
and `/meta/permission`, so the one-line registration in the translator
table is the overlay for those two surfaces, and the runtime census had
to learn the two cells. Anywhere else the console renders a position or
permission-set label from another source still belongs to objectui's
card.
- **`@objectstack/lint`:** `translation-target-unknown` resolves
`positions.NAME` / `permissions.NAME` against the declared names
(anchors `everyone` / `guest` always resolve); an unknown name is an
error. Added in patch round 1, after the dev reported the silent orphan
as a finding.
- No `!` in the title and no `(narrowing)` arm, so no full dogfood run
is owed.
## Verification
**Patch round 1, at head `5ec0b745f`** (touches only `packages/lint`,
the changeset and the docs): `@objectstack/lint` `test` 161 files / 7274
tests, `typecheck` exit 0; `dispatch-gates --commands` 129 families, all
exit 0, `--ran` 0 NOT-MEASURED, 0 UNRUN. Ablation: replacing `if
(known.has(name)) continue;` with `continue;` failed 2 of the 5 new
tests, then was restored blob-identical.
The package suites ran on `8ec6209f`. The only commit after it, the
final head `f444163b7`, changes the two example bundles and nothing
else. Every result below is exit 0:
- `@objectstack/spec`: `test` 21786 passed, `test:repo` 1269 passed,
`typecheck` passed.
- `@objectstack/cli`: unit project 4821 passed, `typecheck` passed.
- `@objectstack/rest`: `test` 6406 passed, `test:repo` 280 passed.
- `@objectstack/runtime`: `test` 7080 passed, `test:repo` 908 passed,
`typecheck` passed.
- `@objectstack/lint` (7269 passed), `@objectstack/platform-objects`
(1352 passed) and `@objectstack/metadata-protocol` (21475 passed):
`test`.
- `example-crm` / `example-showcase` `typecheck` passed on `f444163b7`.
- `dispatch-gates --commands` at `f444163b7` derived 129 families. Each
was run with its exit code captured before any pipe, and all 129 exited
0. `--ran` reports 129 run, 0 NOT-MEASURED, 0 UNRUN.
- `check:generated --fix` regenerated api-surface, export-origins,
references, the strictness counts and the liveness counts.
- **Ablation** (each run committed first, then restored to the HEAD blob
with `git diff HEAD` empty):
- Deleting `position: translatePosition,` turns
`i18n-security-catalog.test.ts` red (2 of 10 failed).
- Deleting the `walkSecurityCatalog(config, out);` call turns
`i18n-security-catalog-walk.test.ts` red (5 of 5 failed).
- **Reverse type check:** a `permissionSets` key under
`positions.sales_rep` in the crm bundle fails `tsc` with TS2353 on the
rebuilt `.d.ts`, then restores.
_Body edited by the `domain:spec` seat 2 after patch round 1, as the dev
proposed in `6388798472`, plus the lint bullet and the round's
readings._
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AwcSGXmnoviawmFu9pGgNZ)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01AwcSGXmnoviawmFu9pGgNZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec,cli): positions and permission sets are translatable — `positions.<name>` and `permissions.<name>` in the translation bundle
8
+
9
+
Clause-②: yes (widening)
10
+
Metadata-protocol: added
11
+
12
+
An app authored in one language ships its positions (`positions[].label`) and permission sets (`permissions[].label`) in that language. Before this change the translation bundle had no key for them: a bundle carrying `positions` was refused by `os validate` as an unrecognized key, so those labels stayed in the authoring language next to translated object, app and flow labels, and `os i18n check` could not report the gap.
13
+
14
+
-**`@objectstack/spec`:**`TranslationData` (and the registered `translation` item and the platform bundle, which share its groups) gains `positions` and `permissions`, keyed by the position's and the permission set's `name`, each carrying `label` and `description` (strict: any other key is refused — `name` or `title` with a message pointing at `label`, and the group spellings `position`, `permission` and `permissionSets` with a message pointing at `positions` / `permissions`). Two new translators, `translatePosition` and `translatePermissionSet`, overlay them, and both are registered in the metadata-document dispatch, so `GET /meta/position` and `GET /meta/permission` — the lists the console's position and permission-set pickers draw — are served in the request's locale. A key the bundle does not carry keeps the authored string.
15
+
-**`@objectstack/lint`:**`translation-target-unknown` reports a `positions.<name>` or `permissions.<name>` key that names no position or permission set a package of the stack declares (the audience anchors `everyone` and `guest` always resolve), as an error with the nearest declared name.
16
+
-**`@objectstack/cli`:**`os i18n extract` scaffolds `positions.<name>.label` / `.description` and `permissions.<name>.label` / `.description` from the stack's `positions[]` and `permissions[]`, and `os i18n check` / `os lint` coverage report an untranslated one in its own bucket (`i18n/missing-position`, `i18n/missing-permission`).
(`"account"`, `"fields"`, `"list"`, `"industries"`) is rejected **by name**
404
405
at both authoring doors, with the group to use instead named in the
405
406
rejection.
@@ -482,7 +483,7 @@ what the overlay writes back as the node's `title`.
482
483
The contract is `t(key, locale, params?)` — the target locale is always passed
483
484
explicitly, and the key itself is returned when no translation is found (there
484
485
is no `defaultValue` option). The key is a **dot path into `TranslationData`**,
485
-
so it starts at one of the eleven declared groups.
486
+
so it starts at one of the thirteen declared groups.
486
487
487
488
```typescript
488
489
const i18n =kernel.getService('i18n');
@@ -1060,6 +1061,8 @@ beneath an action entry are judged the same way under either.
1060
1061
|`apps.{app}.navigation.{id}`| a navigation item `id` the app declares (nested `children` and `areas[]` included) or one contributed into it; under an app that is only contributed into, the contributed ids alone |
1061
1062
|`dashboards.{dash}` / `.widgets.{id}` / `.actions.{actionUrl}`| a dashboard declared / a widget `id` on it / a header action's `actionUrl` on it |
1062
1063
|`flows.{flow}`| a flow's machine `name`, declared |
1064
+
|`positions.{name}`| a position's `name`, declared, or an audience anchor (`everyone`, `guest`) |
1065
+
|`permissions.{name}`| a permission set's `name`, declared |
1063
1066
|`flows.{flow}.screens.{node}`| the `id` of a `type: 'screen'` node in that flow, nodes nested in loop, parallel and try/catch regions included |
1064
1067
|`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`|
@@ -266,6 +268,20 @@ Translation data for a single object
266
268
|**refusals**|`Record<string, { message?: string }>`| optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'`|
267
269
|**approvals**|`Record<string, { label?: string }>`| optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
@@ -469,6 +487,20 @@ Translation data for a single object
469
487
|**refusals**|`Record<string, { message?: string }>`| optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'`|
470
488
|**approvals**|`Record<string, { label?: string }>`| optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
@@ -632,6 +666,20 @@ Translation data for a single object
632
666
|**refusals**|`Record<string, { message?: string }>`| optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'`|
633
667
|**approvals**|`Record<string, { label?: string }>`| optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
Copy file name to clipboardExpand all lines: content/docs/ui/translations.mdx
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,12 +82,14 @@ export default defineStack({
82
82
| 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`|
83
83
| 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 |
84
84
| Screen-flow wizards (flow label, screen headings and body text, screen field copy) |`flows.<flow>.label` / `flows.<flow>.screens.<node_id>.title` / `.description` / `.fields.<field>.label` / `.placeholder` / `.inlineHelpText` — a screen field's help text is `inlineHelpText`, the key the field itself declares, not `help`; see the boundary note below |
85
+
| Position label / description |`positions.<name>.label` / `description` — keyed by the position's `name`; overlaid on every served position, which is the list the console's position picker draws |
86
+
| Permission-set label / description |`permissions.<name>.label` / `description` — the group is spelled as the stack spells the collection (`permissions[]`), keyed by the set's `name`|
85
87
| Global actions, messages |`globalActions`, `messages`|
86
88
| Settings UI shell copy (the source badge on a settings row) |`settingsCommon.sourceLabels.<layer>` — the per-namespace settings copy under `settings` is **platform-only**: an app bundle carrying it is refused by name, and the platform's own strings are translated in `@objectstack/service-settings`'s bundle |
87
89
| 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 |
88
90
89
91
The metadata types resolved per request are **object, view, action, app,
90
-
dashboard, dataset, and page** — a field's labels are translated as part of
92
+
dashboard, dataset, page, picklist, position and permission** — a field's labels are translated as part of
91
93
its object document, and so are the labels of any actions the object declares
92
94
inline
93
95
(#3370). Before that, an object document went out with its authored action
0 commit comments