Skip to content

Commit e00bcc6

Browse files
i18n(plugin-security): ship the security objects, built-in permission sets and positions in all eleven platform languages (#30744)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D7v5H7BaFmL7sUCoN7tSxhm)_ domain:server#1 · os-elon-musk · session_016WV4cya5ej5GQV86RPbRPK Part of #30512 — package 7 of 11 (`@objectstack/plugin-security`). The card stays open: settings, sharing, approvals and platform-objects follow, and the last PR of the series makes the bundle check read the platform locale table. Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 安全插件的对象(用户权限集指派、用户职位指派等)的名称、字段、帮助和校验提示,加上内置权限集的名称与说明、内置职位(平台管理员、组织所有者等)的名称与说明,补上繁中、阿语、法语、巴葡、俄语、德语、韩语 7 种,与平台语言表的 11 种一致。 - **为什么改:** 平台语言表要求每个界面带齐 11 种语言;这是第 3 期按包补齐的第 7 个包。用这 7 种语言的管理员在「设置 → 权限集 / 职位」里看到的不再是英文。 - **风险与回滚:** 只加翻译数据和注册行,不动逻辑、schema、接口;原有 4 种语言一字未改。唯一的测试改动是职位改名一致性测试多扫 6 种语言(覆盖变宽,没有放宽任何断言)。回滚即撤 PR。 - **先看:** `src/translations/index.ts`(按表标签注册,zh-CN 在 zh-Hant 前)、`permission-sets.ts` 与 `builtin-positions.ts`(手写的两张表),再看 `position-rename-consistency.test.ts`。 - **两分钟验证:** `node scripts/check-i18n-bundles.mjs --filter=security` 输出 `plugins/plugin-security in sync`;`pnpm --filter @objectstack/plugin-security test` 全绿。 - **需要你做的:** 无,席位落地。 ## What changes - Seven new locales for the security package under the platform locale table's tags (`zh-Hant`, `ar`, `fr`, `pt-BR`, `ru`, `de`, `ko`): each a `TAG.objects.generated.ts` with all 60 object leaves plus its `TAG.source-hashes.generated.ts`, written by the repo's own `os i18n extract --fill=default --source-hashes` and served through `withSourceFallback` like the existing three. `zh-CN` stays ahead of `zh-Hant` in `index.ts`. - The two hand-written tables gain an entry per new locale: `permission-sets.ts` (label and description of the nine built-in permission sets) and `builtin-positions.ts` (label and description of the seven built-in positions). Both are registered as each locale's `permissions` / `positions` group, and their header sentence now says "every locale" instead of "three entries". - The extract command in `scripts/i18n-extract.config.ts` lists the seven tags and its merge baseline imports the seven bundles, so `check:i18n` covers them from this PR on. ## Tests - `builtin-positions.test.ts` and `objects/permission-set-descriptions.test.ts` already iterate every locale of `SecurityTranslations`, so each new locale is held to the existing rules with no test edit: exactly the built-in names, no two labels alike, no English copy, no position id in the text, and the served `GET /meta/position` overlay. - `position-rename-consistency.test.ts` extends its stale-word scan (no "role" wording left on the renamed objects) to `zh-Hant`, `fr`, `pt-BR`, `ru`, `de` and `ko`. Coverage widens; no assertion changed. Arabic is not added: its word for "role" (دور) is also the word in بدوره ("in turn"), which a correct help text on `sys_user_position.delegated_from` uses, so a pattern for it would flag correct text. ## Acceptance notes - **Quality bar (#17508 / PR #19345):** every leaf written for the reader; placeholders, code spans and identifiers byte-identical, checked mechanically per leaf against `en` (key set, `{{holes}}`, `{holes}`, backtick spans, angle tokens: all seven locales OK, zero diffs). Arabic is RTL prose with identifiers left in Latin script. `zh-Hant` was checked character by character against OpenCC's Traditional (Taiwan) conversion: no Simplified-only character remains. - **Leaves equal to English, on purpose:** one, `de` `sys_user_position.fields.position.label` = `Position` (the German word). The extractor records it as a source copy in `de.source-hashes.generated.ts`. - The English source of `sys_user_position.fields.delegated_from.help` carries the Chinese term `(职务代理)`; every locale keeps it as `es-ES` already does. Not changed here (the source text belongs to the object, not to this bundle). - No cross-locale coincidence: `check:i18n-stale-fill` passes with the baseline untouched. - Carried unchanged from the released handover commit `ff28e790` (session `session_01Utp6Cai5CRdAs4u9gTipWU`): the patch id of this PR's diff equals that commit's (`446fab73`). The serial row "after #30208 lands" was cleared by the seat: #30208's server rows that touch this package landed in PR #30551, an ancestor of this branch. ## Verification <details><summary>Package suite, typecheck, build (verify lock)</summary> ``` turbo run build --filter=!@objectstack/docs: 118/118 tasks successful, exit 0 pnpm --filter @objectstack/plugin-security test && typecheck (at head 99bc834, after merging origin/main 447226b) Test Files 246 passed (246) Tests 5679 passed | 45 skipped (5724) check:test-typecheck: OK (tsconfig.test.json, 0 errors) os-verify-lock: VERDICT command-exit 0 same suite at 41230cb (before the merge): 246 files, 5679 passed | 45 skipped, VERDICT command-exit 0 ``` </details> <details><summary>i18n gates and the derived families</summary> ``` node scripts/check-i18n-bundles.mjs --filter=security (at 99bc834) plugins/plugin-security in sync (21 bundle(s)) EXIT 0 node scripts/check-i18n-stale-fill.mjs (at 99bc834) check-i18n-stale-fill: OK (11 bundle set(s) — no new stale fills, 1 baselined). EXIT 0 dispatch-gates --commands (at 41230cb): 61 families, each run and recorded with its exit code: 61 x exit 0 dispatch-gates --ran: 61 derived, 61 run, 0 NOT-MEASURED (a derived zero) re-derived at 99bc834: the identical 61-command list; --ran on the same record: 61 run, 0 NOT-MEASURED pnpm lint (repo-wide, at 41230cb): exit 0 eslint --no-inline-config on the changed .ts files: exit 0 changed lines 1807 (+1801 / -6), 20 files: under the 3000-line human-merge threshold ``` </details> The merge of `origin/main` brought 17 commits, none in `plugin-security`, `plugin-sharing`, `plugin-approvals` or `service-settings`; the package suite and both i18n checks were re-run on the merged head. Dogfood: not run — content-only package PR, no `!` in the title and no `(narrowing)` arm. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cya5ej5GQV86RPbRPK)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2436b2f commit e00bcc6

20 files changed

Lines changed: 1801 additions & 6 deletions
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+
i18n(plugin-security): the security objects, built-in permission sets and built-in positions ship in all eleven platform languages
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The security objects, the names and descriptions of the built-in permission sets and the names of the built-in positions now carry `zh-Hant`, `ar`, `fr`, `pt-BR`, `ru`, `de` and `ko` beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so an administrator in one of those languages reads the security screens in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion, the hand-authored permission-set and position tables carry an entry in every locale, and the package's extract command lists the tags, so the existing `check:i18n` bundle check covers them. Package 7 of #30512.

‎packages/plugins/plugin-security/scripts/i18n-extract.config.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* strings were seeded from @objectstack/platform-objects.)
99
*
1010
* os i18n extract packages/plugins/plugin-security/scripts/i18n-extract.config.ts \
11-
* --locales=zh-CN,ja-JP,es-ES --fill=default --objects-only --no-metadata-forms \
11+
* --locales=zh-CN,ja-JP,es-ES,zh-Hant,ar,fr,pt-BR,ru,de,ko --fill=default --objects-only --no-metadata-forms \
1212
* --source-hashes \
1313
* --out=packages/plugins/plugin-security/src/translations
1414
*
@@ -47,6 +47,13 @@ import { enObjects } from '../src/translations/en.objects.generated.js';
4747
import { zhCNObjects } from '../src/translations/zh-CN.objects.generated.js';
4848
import { jaJPObjects } from '../src/translations/ja-JP.objects.generated.js';
4949
import { esESObjects } from '../src/translations/es-ES.objects.generated.js';
50+
import { zhHantObjects } from '../src/translations/zh-Hant.objects.generated.js';
51+
import { arObjects } from '../src/translations/ar.objects.generated.js';
52+
import { frObjects } from '../src/translations/fr.objects.generated.js';
53+
import { ptBRObjects } from '../src/translations/pt-BR.objects.generated.js';
54+
import { ruObjects } from '../src/translations/ru.objects.generated.js';
55+
import { deObjects } from '../src/translations/de.objects.generated.js';
56+
import { koObjects } from '../src/translations/ko.objects.generated.js';
5057

5158
/**
5259
* The annotation is load-bearing, not decoration (#10868). `defineStack`
@@ -69,6 +76,13 @@ const config: ObjectStackDefinition = defineStack({
6976
{ 'zh-CN': { objects: zhCNObjects } },
7077
{ 'ja-JP': { objects: jaJPObjects } },
7178
{ 'es-ES': { objects: esESObjects } },
79+
{ 'zh-Hant': { objects: zhHantObjects } },
80+
{ 'ar': { objects: arObjects } },
81+
{ 'fr': { objects: frObjects } },
82+
{ 'pt-BR': { objects: ptBRObjects } },
83+
{ 'ru': { objects: ruObjects } },
84+
{ 'de': { objects: deObjects } },
85+
{ 'ko': { objects: koObjects } },
7286
],
7387
});
7488

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Auto-generated by 'os i18n extract' for locale 'ar'.
5+
* Edit translations in place; re-run extract (with --merge) to fill new gaps.
6+
* Do not hand-edit the structure — only the leaf string values.
7+
* Merge only fills gaps: correcting a source label/description does not
8+
* push the correction into a leaf here that already holds a translation —
9+
* a present-but-stale string is not a gap, so it is left as-is. Re-translate
10+
* it by hand when its source changes; nothing here or in `os i18n check`
11+
* tells a leaf a translator updated on purpose from one nobody has looked
12+
* at since the source moved.
13+
*/
14+
15+
import type { TranslationData } from '@objectstack/spec/system';
16+
17+
export const arObjects: NonNullable<TranslationData['objects']> = {
18+
sys_user_permission_set: {
19+
label: "مجموعة أذونات المستخدم",
20+
pluralLabel: "مجموعات أذونات المستخدمين",
21+
description: "إسناد مباشر لمجموعة أذونات إلى مستخدم (ويمكن حصره في مؤسسة).",
22+
fields: {
23+
id: {
24+
label: "معرّف الإسناد",
25+
help: "معرّف UUID للإسناد."
26+
},
27+
display_title: {
28+
label: "العنوان",
29+
help: "عنوان السجل: المستخدم ومجموعة الأذونات المسندة إليه (يُحسب عند القراءة)"
30+
},
31+
user_id: {
32+
label: "المستخدم",
33+
help: "المستخدم الذي يتلقى مجموعة الأذونات هذه."
34+
},
35+
permission_set: {
36+
label: "مجموعة الأذونات",
37+
help: "مجموعة الأذونات التي يمنحها هذا الإسناد للمستخدم. اختر إحدى مجموعات الأذونات المعرّفة في الإعداد؛ ويُرفض الاسم الذي لا يطابق أي مجموعة."
38+
},
39+
organization_id: {
40+
label: "المؤسسة",
41+
help: "المؤسسة التي ينتمي إليها هذا المنح."
42+
},
43+
granted_by: {
44+
label: "مَن منحه",
45+
help: "المستخدم الذي منح مجموعة الأذونات هذه."
46+
},
47+
valid_from: {
48+
label: "صالح من",
49+
help: "يسري الإسناد في هذا الوقت. اتركه فارغًا ليسري فورًا. يبدأ في موعده دون انتظار أي مهمة مجدولة."
50+
},
51+
valid_until: {
52+
label: "صالح حتى",
53+
help: "ينتهي الإسناد في هذا الوقت (UTC). اتركه فارغًا لعدم تحديد تاريخ انتهاء. مطلوب في الوصول الطارئ وفي الإسناد إلى وكيل ذكاء اصطناعي. ينتهي في موعده دون انتظار أي مهمة مجدولة."
54+
},
55+
reason: {
56+
label: "السبب",
57+
help: "سبب وجود هذا الإسناد. مطلوب في الإسنادات المفوَّضة وإسنادات الوصول الطارئ. بالنسبة إلى وكيل ذكاء اصطناعي، اذكر المهمة التي مُنح الوصول من أجلها."
58+
},
59+
last_certified_at: {
60+
label: "آخر اعتماد في",
61+
help: "غير مستخدم بعد: محجوز لميزة مستقبلية لمراجعة الوصول. لا شيء يملأ هذا التاريخ أو يتحقق منه، لذا فالقيمة الفارغة لا تعني أن الإسناد لم يُراجَع."
62+
},
63+
certified_by: {
64+
label: "مَن اعتمده",
65+
help: "غير مستخدم بعد: محجوز لميزة مستقبلية لمراجعة الوصول. المراجِع المُدخَل هنا لا يُتحقق منه ولا يغيّر شيئًا مما يمنحه الإسناد."
66+
},
67+
created_at: {
68+
label: "تاريخ الإنشاء"
69+
},
70+
updated_at: {
71+
label: "تاريخ التحديث"
72+
}
73+
},
74+
_validations: {
75+
validity_window_order: {
76+
message: "يجب أن يكون «صالح حتى» بعد «صالح من». يسري المنح من «صالح من» حتى «صالح حتى» دون أن يشمله، لذا فالنافذة التي تنتهي عند بدايتها أو قبلها لا تمنح شيئًا. اترك أيًّا منهما فارغًا لنافذة مفتوحة."
77+
}
78+
}
79+
},
80+
sys_user_position: {
81+
label: "منصب المستخدم",
82+
pluralLabel: "مناصب المستخدمين",
83+
description: "المناصب التي يشغلها كل مستخدم، صف واحد لكل إسناد.",
84+
fields: {
85+
id: {
86+
label: "معرّف الإسناد",
87+
help: "معرّف UUID لإسناد المستخدم إلى المنصب."
88+
},
89+
display_title: {
90+
label: "العنوان",
91+
help: "عنوان السجل: المستخدم والمنصب الذي يشغله (يُحسب عند القراءة)"
92+
},
93+
user_id: {
94+
label: "المستخدم",
95+
help: "المستخدم الذي يشغل هذا المنصب."
96+
},
97+
position: {
98+
label: "المنصب",
99+
help: "الاسم الآلي للمنصب، ويُحلّ في كتالوج الأمان."
100+
},
101+
business_unit_id: {
102+
label: "وحدة الأعمال",
103+
help: "مرساة وحدة الأعمال على مستوى الإسناد: حيث ينطبق إسناد هذا المنصب. مرساة العمق لـ readScope/writeScope، وحدّ الإدارة المفوَّضة، وحقيقة تدقيق. القيمة الفارغة = غير مرسَى (قديم / على مستوى المستأجر)؛ ويجب على المسؤولين المفوَّضين إرساء الإسنادات داخل شجرتهم الفرعية."
104+
},
105+
organization_id: {
106+
label: "المؤسسة",
107+
help: "المؤسسة التي ينتمي إليها هذا الإسناد."
108+
},
109+
granted_by: {
110+
label: "مَن منحه",
111+
help: "المستخدم الذي منح إسناد هذا المنصب (يختمه حارس الإدارة المفوَّضة عند الكتابة المفوَّضة)."
112+
},
113+
valid_from: {
114+
label: "صالح من",
115+
help: "يسري الإسناد في هذا الوقت. اتركه فارغًا ليسري فورًا. يبدأ في موعده دون انتظار أي مهمة مجدولة."
116+
},
117+
valid_until: {
118+
label: "صالح حتى",
119+
help: "ينتهي الإسناد في هذا الوقت (UTC). اتركه فارغًا لعدم تحديد تاريخ انتهاء. مطلوب في الإسناد المفوَّض. ينتهي في موعده دون انتظار أي مهمة مجدولة."
120+
},
121+
reason: {
122+
label: "السبب",
123+
help: "سبب وجود هذا المنح. نص حر؛ مطلوب في صفوف التفويض والوصول الطارئ (كسر الزجاج)."
124+
},
125+
delegated_from: {
126+
label: "مفوَّض من",
127+
help: "المفوِّض الذي يحمل هذا الصف صلاحيته (职务代理). الصف الذي ضُبط فيه delegated_from لا يمكن تفويضه بدوره ولا تجديده ذاتيًا — تُقطع السلاسل في الاتجاهين."
128+
},
129+
last_certified_at: {
130+
label: "آخر اعتماد في",
131+
help: "غير مستخدم بعد: محجوز لميزة مستقبلية لمراجعة الوصول. لا شيء يملأ هذا التاريخ أو يتحقق منه، لذا فالقيمة الفارغة لا تعني أن الإسناد لم يُراجَع."
132+
},
133+
certified_by: {
134+
label: "مَن اعتمده",
135+
help: "غير مستخدم بعد: محجوز لميزة مستقبلية لمراجعة الوصول. المراجِع المُدخَل هنا لا يُتحقق منه ولا يغيّر شيئًا مما يمنحه الإسناد."
136+
},
137+
created_at: {
138+
label: "تاريخ الإنشاء"
139+
},
140+
updated_at: {
141+
label: "تاريخ التحديث"
142+
}
143+
},
144+
_validations: {
145+
reserved_identity_position: {
146+
message: "لا يمكن أن يكون «position» اسم هوية مدمجة محجوزًا للإطار (platform_admin، org_owner، org_admin، org_member، org_external). هذه الأسماء هويات مدمجة: تُسقطها المنصة في current_user.positions من مصادرها الموثوقة، والصف الذي يحمل أحدها ليس إسنادًا لتلك الهوية. اختر اسمًا آخر."
147+
}
148+
}
149+
}
150+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Auto-generated by 'os i18n extract' for locale 'ar'. Do not hand-edit.
5+
*
6+
* Each entry is the digest of the SOURCE REVISION that this locale's leaf at
7+
* that path is still a byte copy of — provenance for the generated half of the
8+
* bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead.
9+
*
10+
* An entry exists only while the leaf IS such a copy. Re-translate the leaf in
11+
* `<locale>.objects.generated.ts` and the next extract drops its entry by
12+
* itself — the table makes no claim about text a translator wrote. A path with
13+
* no entry is LEGACY-TRUSTED and never reported stale.
14+
*
15+
* ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest
16+
* records that the current text was copied from the current source, which is
17+
* the false claim the mechanism exists to detect. Fix the TRANSLATION.
18+
*/
19+
20+
export const arGeneratedSourceHashes: Readonly<Record<string, string>> = {
21+
};

0 commit comments

Comments
 (0)