Skip to content

Commit fb91e97

Browse files
fix(plugin-security): translate the built-in positions under zh-CN, ja-JP and es-ES (#26606)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XMLTWJMvQQWZ5RaZFnBdwkB)_ Fixes #26562 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:平台内置的七个岗位(平台管理员、组织所有者、组织管理员、组织成员、组织外部成员、全体成员、访客)在中文、日文、西班牙文界面下,名称和说明都按当前语言显示。 - **为什么改**:设置 → 岗位在中文界面下,应用自己的岗位已是中文,平台内置的七个仍是英文,其中包括每条授权都要参照的「全体成员」「访客」。内置权限集早已翻译,岗位漏掉了。 - **风险与回滚**:低。只新增翻译包里的 `positions` 组,英文、岗位本身的定义与授权都不变;回滚即 revert 本 PR。 - **先看**:`packages/plugins/plugin-security/src/translations/builtin-positions.ts`(三种语言的译文);`translations/index.ts`(三行接入);`translations/builtin-positions.test.ts`。 - **两分钟验证**:`pnpm --filter @objectstack/plugin-security exec vitest run src/translations/builtin-positions.test.ts`,预期 29/29 通过;或中文界面打开 设置 → 岗位,内置七项显示中文。 - **需要你做的**:无,席位落地。 ## What changes - New `packages/plugins/plugin-security/src/translations/builtin-positions.ts`: hand-authored `positions.NAME.label` / `.description` for the seven built-in positions (`platform_admin`, `org_owner`, `org_admin`, `org_member`, `org_external`, `everyone`, `guest`) in zh-CN, ja-JP and es-ES, the locales the built-in permission-set group (`permission-sets.ts`) covers. Hand-authored for the reason that file gives: this package's extract runs objects-only, so `os i18n extract` writes no `positions` group. English needs no entry (an untranslated locale serves the declaration's own text). Terms follow this bundle's objects: 岗位 / ポジション / puesto. - `translations/index.ts`: each translated locale of `SecurityTranslations` gains `positions` beside `permissions`, outside `withSourceFallback`'s inputs exactly like the permission-set group (`enSource` carries neither, so nothing is judged stale). - New `translations/builtin-positions.test.ts`: per locale, the group translates exactly the declared built-ins, no two labels repeat, each label and description is a non-English string naming no machine id, and `translateMetadataDocument('position', declaration, SecurityTranslations, { locale })` serves that locale's label and description with nothing else of the position changed. - Changeset: `@objectstack/plugin-security` patch. No `packages/spec/**` change. ## Why Card #26562 (triage 6437681727, p2): under zh-CN, Setup → Positions (`/_console/apps/setup/metadata/position`) listed the app's positions in Chinese and the platform's seven in English. Reader measured before writing the bundle: the page reads `GET /api/v1/meta/position`, whose list read (`translateMetaList` in `packages/rest/src/meta-item-read-gate.ts`) runs `translateMetadataDocument('position', …)` with the i18n service's merged bundle; that dispatches to `translatePosition` (`packages/spec/src/system/i18n-resolver.ts`), which reads `positions.NAME.label` / `.description`. `SecurityPlugin` loads `SecurityTranslations` into that service at `kernel:ready`. So the reader existed and the bundle was missing, as the card says. The built-ins reach the door as registry declarations (`builtin-positions.ts`, `registerBuiltinPositions`), keyed by `name`, which is what the group keys on. ## Tests Red first on `main` (`a14f3e61`): the new test, before the bundle was added: `Tests 27 failed | 2 passed (29)` (every per-locale case; the two population checks pass). On the fix: `Tests 29 passed (29)`. <details><summary>Package suite and typecheck at the fix</summary> `pnpm --filter @objectstack/plugin-security build && … typecheck && … test` under the verify lock: `VERDICT command-exit 0`; `Test Files 230 passed (230)`, `Tests 4991 passed | 45 skipped (5036)`. Lint: `eslint packages/plugins/plugin-security --no-inline-config` exit 0. </details> ## Acceptance notes - `everyone` reads 全体成员 / 全メンバー / Todos, since the description says every authenticated member holds it; `guest` reads 访客 / ゲスト / Invitado. - zh-CN `org_admin` (组织管理员) shares its text with the `organization_admin` permission set's label. They are on different pages and name the same people, so it is left as is. - A live probe was not run: the served overlay is proven in-process through the same `translateMetadataDocument` the REST door calls. ## Verification Head `352780c3` (the gate loop started on `65852aec`, the same tree before a commit-message reword; `git diff 65852aec 352780c` is empty). `node scripts/pm/dispatch-gates.mjs --ran FILE --repo objectstack-ai/objectstack`: `61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED`. Three families first answered exit 3 (prerequisite, nothing measured) and were re-run after their prerequisite, each exit 0: `check-plugin-teardown-shape.mjs --self-test` after `git fetch --unshallow origin main` (48 cases pass); `check:i18n` and `check:dual-build-cjs-loads` after a full `turbo run build` (VERDICT command-exit 0): `check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys)`. `test:repo` not run locally (the diff touches no repo-project test); CI Test Core runs it. No dogfood battery: no `!` in the title and no `(narrowing)` arm. <details><summary>Every derived family and its exit code</summary> ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/check-tenant-audit-census.mjs :: exit 0 node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:i18n :: exit 0 pnpm check:i18n-stale-fill :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> Co-authored-by: Claude <noreply@anthropic.com>
1 parent ce9bc10 commit fb91e97

4 files changed

Lines changed: 207 additions & 4 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): the seven built-in positions are served in Chinese, Japanese and Spanish
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Setup → Positions lists the platform's built-in positions (Platform Admin, Organization Owner, Organization Admin, Organization Member, Organization External Member, Everyone, Guest) beside the app's own. Under zh-CN the app's positions read in Chinese while these seven stayed English, because this plugin's translation bundle carried no `positions` group. The bundle now carries `positions.<name>.label` and `.description` for each built-in position in zh-CN, ja-JP and es-ES, the locales its permission-set translations cover, so `GET /api/v1/meta/position` serves them in the requested locale. English is unchanged, and nothing else of a position changes.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import { translateMetadataDocument } from '@objectstack/spec/system';
5+
import { securityBuiltinPositions } from '../builtin-positions.js';
6+
import { SecurityTranslations } from './index.js';
7+
8+
/**
9+
* Every built-in position reads in the administrator's language.
10+
*
11+
* Setup → Positions lists the platform's seven built-in positions beside the
12+
* app's own, each with its `label` and `description`, from
13+
* `GET /meta/position`, which overlays the bundle's `positions` group on every
14+
* served position. The app's positions came back translated and the
15+
* platform's stayed English, because this plugin's bundle carried no
16+
* `positions` group. This pins that every translated locale of the bundle
17+
* carries both strings for exactly the built-ins, and that the served
18+
* document is that locale's. It is deliberately not a copy of the wording,
19+
* which `./builtin-positions.ts` owns.
20+
*/
21+
type AnyRec = Record<string, any>;
22+
23+
const TRANSLATED_LOCALES = Object.keys(SecurityTranslations).filter((l) => l !== 'en');
24+
const POSITION_NAMES = securityBuiltinPositions.map((p) => p.name);
25+
26+
describe('built-in position translations', () => {
27+
it('declares the seven built-in positions', () => {
28+
expect([...POSITION_NAMES].sort()).toEqual(
29+
['everyone', 'guest', 'org_admin', 'org_external', 'org_member', 'org_owner', 'platform_admin'],
30+
);
31+
});
32+
33+
it('the bundle carries translated locales', () => {
34+
expect(TRANSLATED_LOCALES).toEqual(expect.arrayContaining(['zh-CN', 'ja-JP', 'es-ES']));
35+
});
36+
37+
describe.each(TRANSLATED_LOCALES)('%s', (locale) => {
38+
const group = ((SecurityTranslations[locale] as AnyRec | undefined)?.positions ?? {}) as AnyRec;
39+
40+
it('translates exactly the built-in positions', () => {
41+
expect(Object.keys(group).sort()).toEqual([...POSITION_NAMES].sort());
42+
});
43+
44+
it('gives no two positions the same label', () => {
45+
const labels = POSITION_NAMES.map((name) => (group[name] as AnyRec | undefined)?.label);
46+
expect(new Set(labels).size).toBe(labels.length);
47+
});
48+
49+
// The overlay `GET /meta/position` applies at the REST boundary, read
50+
// through the bundle this plugin loads: the served position's label and
51+
// description are this locale's, nothing else of the position changes.
52+
it.each(securityBuiltinPositions.map((p) => [p.name, p] as const))(
53+
'%s is served in this locale',
54+
(name, position) => {
55+
const entry = group[name] as AnyRec | undefined;
56+
for (const attr of ['label', 'description'] as const) {
57+
const text = entry?.[attr];
58+
expect(typeof text, `${locale} ${name}.${attr} missing`).toBe('string');
59+
expect(text, `${locale} ${name}.${attr} padded`).toBe(String(text).trim());
60+
expect(text, `${locale} ${name}.${attr}: an English copy, not a translation`).not.toBe(position[attr]);
61+
expect(text, `${locale} ${name}.${attr}: names a position id`).not.toMatch(/\b[a-z]+_[a-z_]+\b/);
62+
}
63+
const served = translateMetadataDocument('position', { ...position }, SecurityTranslations, { locale });
64+
expect(served).toEqual({ ...position, label: entry?.label, description: entry?.description });
65+
},
66+
);
67+
});
68+
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Translations of the built-in positions' labels and descriptions — the
5+
* `positions.<name>.label` / `.description` group of the translation bundle,
6+
* which `translatePosition` overlays on every served `position` document
7+
* (`GET /meta/position`, the list Setup → Positions shows).
8+
*
9+
* HAND-AUTHORED, not generated, for the reason `permission-sets.ts` gives: this
10+
* package's extract runs objects-only, so `os i18n extract` writes no
11+
* `positions` group here. The English source is each declaration in
12+
* `../builtin-positions.ts` (the identity names' text comes from the spec's
13+
* `BUILTIN_IDENTITY_METADATA`, the audience anchors' from that module); `en`
14+
* needs no entry, because an untranslated locale serves the declaration's own
15+
* text. When a source label or sentence changes, re-translate its three
16+
* entries here in the same change: nothing marks a translation stale.
17+
* `builtin-positions.test.ts` pins both strings per built-in position per
18+
* locale, and the served overlay.
19+
*
20+
* Terms follow this bundle's objects: a position is 岗位 / ポジション /
21+
* puesto, and the organization roles read as the member record's role options
22+
* do (所有者, 外部成员 …).
23+
*/
24+
25+
import type { TranslationData } from '@objectstack/spec/system';
26+
27+
type PositionTranslations = NonNullable<TranslationData['positions']>;
28+
29+
export const zhCNBuiltinPositions: PositionTranslations = {
30+
platform_admin: {
31+
label: '平台管理员',
32+
description: '平台运维人员(SaaS 管理员),不是租户内的用户角色。',
33+
},
34+
org_owner: {
35+
label: '组织所有者',
36+
description: '租户内组织的所有者。',
37+
},
38+
org_admin: {
39+
label: '组织管理员',
40+
description: '租户内组织的管理员。',
41+
},
42+
org_member: {
43+
label: '组织成员',
44+
description: '租户内组织的成员。',
45+
},
46+
org_external: {
47+
label: '组织外部成员',
48+
description: '租户内的外部成员(门户用户):只能看到本人的身份记录,仅通过明确授予的权限访问数据。',
49+
},
50+
everyone: {
51+
label: '全体成员',
52+
description: '内置受众锚点:每位已登录成员都隐式担任此岗位。绑定到此岗位的权限集就是租户的默认授权。高权限的权限集不能绑定到这里。',
53+
},
54+
guest: {
55+
label: '访客',
56+
description: '内置受众锚点:未登录的访问者隐式且唯一地担任此岗位。这里的绑定受最严格的检查 — 只能指定具体对象、以只读为主、绝不允许通配符。',
57+
},
58+
};
59+
60+
export const jaJPBuiltinPositions: PositionTranslations = {
61+
platform_admin: {
62+
label: 'プラットフォーム管理者',
63+
description: 'プラットフォーム運用者(SaaS 管理者)。テナント内のユーザーの役割ではありません。',
64+
},
65+
org_owner: {
66+
label: '組織オーナー',
67+
description: 'テナント内の組織のオーナー。',
68+
},
69+
org_admin: {
70+
label: '組織管理者',
71+
description: 'テナント内の組織の管理者。',
72+
},
73+
org_member: {
74+
label: '組織メンバー',
75+
description: 'テナント内の組織のメンバー。',
76+
},
77+
org_external: {
78+
label: '組織外部メンバー',
79+
description: 'テナント内の外部メンバー(ポータルユーザー):自分の ID レコードだけを参照でき、データには明示的に付与された権限でのみアクセスします。',
80+
},
81+
everyone: {
82+
label: '全メンバー',
83+
description: '組み込みの対象者アンカー:サインインしたすべてのメンバーが暗黙的にこのポジションを持ちます。ここに割り当てた権限セットがテナントの既定の付与になります。高権限のセットはここに割り当てられません。',
84+
},
85+
guest: {
86+
label: 'ゲスト',
87+
description: '組み込みの対象者アンカー:サインインしていないアクセス者だけが暗黙的にこのポジションを持ちます。ここへの割り当ては最も厳しく検査されます — 名前を指定したオブジェクトのみ、読み取り中心、ワイルドカードは不可。',
88+
},
89+
};
90+
91+
export const esESBuiltinPositions: PositionTranslations = {
92+
platform_admin: {
93+
label: 'Administrador de la plataforma',
94+
description: 'Operador de la plataforma (administrador SaaS). No es un rol de usuario dentro del inquilino.',
95+
},
96+
org_owner: {
97+
label: 'Propietario de la organización',
98+
description: 'Propietario de una organización dentro de un inquilino.',
99+
},
100+
org_admin: {
101+
label: 'Administrador de la organización',
102+
description: 'Administrador de una organización dentro de un inquilino.',
103+
},
104+
org_member: {
105+
label: 'Miembro de la organización',
106+
description: 'Miembro de una organización dentro de un inquilino.',
107+
},
108+
org_external: {
109+
label: 'Miembro externo de la organización',
110+
description: 'Miembro externo dentro de un inquilino (usuario del portal): solo ve sus propios registros de identidad y accede a los datos únicamente mediante concesiones explícitas.',
111+
},
112+
everyone: {
113+
label: 'Todos',
114+
description: 'Ancla de audiencia integrada: todo miembro con sesión iniciada ocupa este puesto de forma implícita. Los conjuntos de permisos vinculados a él son las concesiones predeterminadas del inquilino. Aquí no se pueden vincular conjuntos de privilegios elevados.',
115+
},
116+
guest: {
117+
label: 'Invitado',
118+
description: 'Ancla de audiencia integrada: quienes no han iniciado sesión ocupan este puesto de forma implícita y exclusiva. Sus vínculos pasan los controles más estrictos: solo objetos con nombre, sobre todo lectura y nunca un comodín.',
119+
},
120+
};

‎packages/plugins/plugin-security/src/translations/index.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js';
1919
import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js';
2020
import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js';
2121
import { zhCNPermissionSets, jaJPPermissionSets, esESPermissionSets } from './permission-sets.js';
22+
import { zhCNBuiltinPositions, jaJPBuiltinPositions, esESBuiltinPositions } from './builtin-positions.js';
2223

2324
/**
2425
* ## The provenance companions are READ here, not merely recorded
@@ -50,23 +51,27 @@ import { zhCNPermissionSets, jaJPPermissionSets, esESPermissionSets } from './pe
5051
const enSource: TranslationData = { objects: enObjects };
5152

5253
/**
53-
* The built-in permission sets' description translations (`permissions`) are
54-
* hand-authored beside the generated objects (`permission-sets.ts` says why).
55-
* They sit outside `withSourceFallback`'s inputs: `enSource` carries no
56-
* `permissions` group, so no leaf of theirs is judged stale.
54+
* The built-in permission sets' translations (`permissions`) and the built-in
55+
* positions' (`positions`) are hand-authored beside the generated objects
56+
* (`permission-sets.ts` says why). They sit outside `withSourceFallback`'s
57+
* inputs: `enSource` carries neither group, so no leaf of theirs is judged
58+
* stale.
5759
*/
5860
export const SecurityTranslations: TranslationBundle = {
5961
en: enSource,
6062
'zh-CN': {
6163
...withSourceFallback({ objects: zhCNObjects }, enSource, undefined, zhCNGeneratedSourceHashes),
6264
permissions: zhCNPermissionSets,
65+
positions: zhCNBuiltinPositions,
6366
},
6467
'ja-JP': {
6568
...withSourceFallback({ objects: jaJPObjects }, enSource, undefined, jaJPGeneratedSourceHashes),
6669
permissions: jaJPPermissionSets,
70+
positions: jaJPBuiltinPositions,
6771
},
6872
'es-ES': {
6973
...withSourceFallback({ objects: esESObjects }, enSource, undefined, esESGeneratedSourceHashes),
7074
permissions: esESPermissionSets,
75+
positions: esESBuiltinPositions,
7176
},
7277
};

0 commit comments

Comments
 (0)