Skip to content

Commit 47af3dd

Browse 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>
1 parent 0c81694 commit 47af3dd

24 files changed

Lines changed: 734 additions & 23 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': minor
4+
'@objectstack/lint': minor
5+
---
6+
7+
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`).
17+
18+
Example (`en` bundle of a Chinese-authored app):
19+
20+
```ts
21+
positions: { srm_legal_counsel: { label: 'Legal Counsel' } },
22+
permissions: { srm_contract_reviewer: { label: 'Contract Review', description: 'Reads and approves supplier contracts' } },
23+
```

‎content/docs/protocol/kernel/i18n-standard.mdx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,9 @@ One locale of a bundle, as JSON — the same groups a `TranslationData` declares
398398
Three rules the shape enforces, all of them closed since #4001:
399399

400400
- **The top level is the declared group set and nothing else** — `objects`,
401-
`apps`, `messages`, `globalActions`, `dashboards`, `datasets`, `pages`,
402-
`flows`, `metadataForms`, `settingsCommon`. A group invented beside them
401+
`picklists`, `apps`, `messages`, `globalActions`, `dashboards`, `datasets`,
402+
`pages`, `flows`, `positions`, `permissions`, `metadataForms`,
403+
`settingsCommon`. A group invented beside them
403404
(`"account"`, `"fields"`, `"list"`, `"industries"`) is rejected **by name**
404405
at both authoring doors, with the group to use instead named in the
405406
rejection.
@@ -482,7 +483,7 @@ what the overlay writes back as the node's `title`.
482483
The contract is `t(key, locale, params?)` — the target locale is always passed
483484
explicitly, and the key itself is returned when no translation is found (there
484485
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.
486487

487488
```typescript
488489
const i18n = kernel.getService('i18n');
@@ -1060,6 +1061,8 @@ beneath an action entry are judged the same way under either.
10601061
| `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 |
10611062
| `dashboards.{dash}` / `.widgets.{id}` / `.actions.{actionUrl}` | a dashboard declared / a widget `id` on it / a header action's `actionUrl` on it |
10621063
| `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 |
10631066
| `flows.{flow}.screens.{node}` | the `id` of a `type: 'screen'` node in that flow, nodes nested in loop, parallel and try/catch regions included |
10641067
| `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` |
10651068

‎content/docs/references/api/protocol.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1778,6 +1778,8 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
17781778
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
17791779
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
17801780
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
1781+
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
1782+
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
17811783
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
17821784
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
17831785
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |

‎content/docs/references/system/translation.mdx‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ Platform translation data — the per-app groups plus the platform-only `setting
179179
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
180180
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
181181
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
182+
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
183+
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
182184
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
183185
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
184186
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
@@ -266,6 +268,20 @@ Translation data for a single object
266268
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
267269
| **approvals** | `Record<string, { label?: string }>` | optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
268270

271+
### Nested Shape: `PlatformTranslationData.positions[string]`
272+
273+
| Property | Type | Required | Description |
274+
| :--- | :--- | :--- | :--- |
275+
| **label** | `string` | optional | Translated position label — overlays the position's own `label` |
276+
| **description** | `string` | optional | Translated position description — overlays the position's own `description` |
277+
278+
### Nested Shape: `PlatformTranslationData.permissions[string]`
279+
280+
| Property | Type | Required | Description |
281+
| :--- | :--- | :--- | :--- |
282+
| **label** | `string` | optional | Translated permission-set label — overlays the set's own `label` |
283+
| **description** | `string` | optional | Translated permission-set description — overlays the set's own `description` |
284+
269285
### Nested Shape: `PlatformTranslationData.metadataForms[string]`
270286

271287
| Property | Type | Required | Description |
@@ -383,6 +399,8 @@ Per-app translation data for objects, apps, and UI messages
383399
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
384400
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
385401
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
402+
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
403+
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
386404
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
387405
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
388406

@@ -469,6 +487,20 @@ Translation data for a single object
469487
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
470488
| **approvals** | `Record<string, { label?: string }>` | optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
471489

490+
### Nested Shape: `TranslationData.positions[string]`
491+
492+
| Property | Type | Required | Description |
493+
| :--- | :--- | :--- | :--- |
494+
| **label** | `string` | optional | Translated position label — overlays the position's own `label` |
495+
| **description** | `string` | optional | Translated position description — overlays the position's own `description` |
496+
497+
### Nested Shape: `TranslationData.permissions[string]`
498+
499+
| Property | Type | Required | Description |
500+
| :--- | :--- | :--- | :--- |
501+
| **label** | `string` | optional | Translated permission-set label — overlays the set's own `label` |
502+
| **description** | `string` | optional | Translated permission-set description — overlays the set's own `description` |
503+
472504
### Nested Shape: `TranslationData.metadataForms[string]`
473505

474506
| Property | Type | Required | Description |
@@ -536,6 +568,8 @@ One locale of translations — the `translation` metadata type
536568
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
537569
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
538570
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
571+
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
572+
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
539573
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
540574
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
541575
| **locale** | `string` | ✅ | BCP-47 locale this item translates (e.g. "zh-CN") |
@@ -632,6 +666,20 @@ Translation data for a single object
632666
| **refusals** | `Record<string, { message?: string }>` | optional | Refusal translations keyed by the node id (`FlowNode.id`) of an `end` node declaring `outcome: 'refused'` |
633667
| **approvals** | `Record<string, { label?: string }>` | optional | Approval-step translations keyed by the node id (`FlowNode.id`) of an `approval` node |
634668

669+
### Nested Shape: `TranslationItem.positions[string]`
670+
671+
| Property | Type | Required | Description |
672+
| :--- | :--- | :--- | :--- |
673+
| **label** | `string` | optional | Translated position label — overlays the position's own `label` |
674+
| **description** | `string` | optional | Translated position description — overlays the position's own `description` |
675+
676+
### Nested Shape: `TranslationItem.permissions[string]`
677+
678+
| Property | Type | Required | Description |
679+
| :--- | :--- | :--- | :--- |
680+
| **label** | `string` | optional | Translated permission-set label — overlays the set's own `label` |
681+
| **description** | `string` | optional | Translated permission-set description — overlays the set's own `description` |
682+
635683
### Nested Shape: `TranslationItem.metadataForms[string]`
636684

637685
| Property | Type | Required | Description |

‎content/docs/ui/translations.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,14 @@ export default defineStack({
8282
| 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` |
8383
| 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 |
8484
| 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` |
8587
| Global actions, messages | `globalActions`, `messages` |
8688
| 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 |
8789
| 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 |
8890

8991
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
9193
its object document, and so are the labels of any actions the object declares
9294
inline
9395
(#3370). Before that, an object document went out with its authored action

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.
1919

2020
| Dir | Sites |
2121
|---|---|
22-
| `system/` | 378 |
22+
| `system/` | 380 |

‎examples/app-crm/src/translations/crm.translation.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,17 @@ export const CrmTranslationBundle = defineTranslationBundle({
230230
},
231231
},
232232
},
233+
// The security catalog: what an admin assigns a person to, and what
234+
// Setup lists under it.
235+
positions: {
236+
sales_rep: { label: '销售代表', description: '一线销售代表。' },
237+
sales_manager: { label: '销售经理', description: '管理一支销售代表团队。' },
238+
finance_approver: { label: '财务审批人', description: '有权审批 30% 以上折扣的财务团队成员。' },
239+
},
240+
permissions: {
241+
crm_sales_user: { label: 'CRM 销售用户' },
242+
guest_portal: { label: '访客(公开表单)' },
243+
},
233244
messages: {
234245
'crm.lead.convert.success': '线索已成功转化为商机。',
235246
'crm.lead.convert.error': '线索转化失败,请重试。',

‎examples/app-showcase/src/system/translations/index.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,5 +1311,31 @@ export const ShowcaseTranslationBundle = {
13111311
},
13121312
},
13131313
},
1314+
// The security catalog — positions and permission sets. Counted by
1315+
// `check-i18n-coverage` like every other declared label: a new position or
1316+
// set that skips zh-CN widens the debt.
1317+
positions: {
1318+
contributor: { label: '贡献者', description: '处理自己项目中的任务。' },
1319+
manager: { label: '项目经理', description: '管理项目及其中的贡献者。' },
1320+
exec: { label: '高管', description: '用于报表的全量只读可见性。' },
1321+
auditor: { label: '审计员', description: '跨私有记录的合规只读视图(viewAllRecords)。' },
1322+
ops: { label: '运营', description: '后台运营 — Setup 访问、公告修复、运营应用。' },
1323+
field_ops_delegate: { label: '现场运营委派管理员', description: '对现场运营业务单元子树的范围化管理。' },
1324+
client_liaison: { label: '客户联络人', description: '准备并发布面向客户的项目简报;看不到内部预算数据。' },
1325+
client_portal_user: { label: '客户门户用户', description: '获准进入客户门户的外部客户。' },
1326+
finance: { label: '财务', description: '发票和大额费用的财务签批权(仅用于审批路由)。' },
1327+
legal: { label: '法务', description: '发票和大额费用的法务签批权(仅用于审批路由)。' },
1328+
},
1329+
permissions: {
1330+
showcase_contributor: { label: '示例贡献者' },
1331+
showcase_manager: { label: '示例经理' },
1332+
showcase_executive: { label: '示例高管' },
1333+
showcase_auditor: { label: '示例审计员' },
1334+
showcase_ops: { label: '示例运营' },
1335+
showcase_member_default: { label: '示例成员(默认)' },
1336+
showcase_guest_portal: { label: '示例访客门户' },
1337+
showcase_field_ops_delegate: { label: '现场运营委派管理员' },
1338+
showcase_client_liaison: { label: '示例客户联络人' },
1339+
},
13141340
},
13151341
};

0 commit comments

Comments
 (0)