From 41230cb65bdf7a8e4c3a3dffcc8931e9bccd85ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 23:35:24 +0000 Subject: [PATCH] i18n(plugin-security): ship the security objects, built-in permission sets and positions in all eleven platform languages The security objects, the built-in permission sets and the built-in positions gain zh-Hant, ar, fr, pt-BR, ru, de and ko under the platform locale table's tags. Each object bundle is written and hashed by the repo's own `os i18n extract --source-hashes` and served through withSourceFallback like the existing three; the hand-written permission-set and position tables carry an entry per locale; the extract command and its merge baseline list the seven tags, so check:i18n covers them. position-rename-consistency extends its stale-word scan to zh-Hant, fr, pt-BR, ru, de and ko. Carried from the released handover commit ff28e790 (written by session_01Utp6Cai5CRdAs4u9gTipWU) onto main 06cccd32 without change. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cya5ej5GQV86RPbRPK --- .changeset/30512-plugin-security-locales.md | 10 + .../scripts/i18n-extract.config.ts | 16 +- .../src/translations/ar.objects.generated.ts | 150 ++++++++++ .../ar.source-hashes.generated.ts | 21 ++ .../src/translations/builtin-positions.ts | 221 +++++++++++++- .../src/translations/de.objects.generated.ts | 150 ++++++++++ .../de.source-hashes.generated.ts | 22 ++ .../src/translations/fr.objects.generated.ts | 150 ++++++++++ .../fr.source-hashes.generated.ts | 21 ++ .../plugin-security/src/translations/index.ts | 75 ++++- .../src/translations/ko.objects.generated.ts | 150 ++++++++++ .../ko.source-hashes.generated.ts | 21 ++ .../src/translations/permission-sets.ts | 275 +++++++++++++++++- .../position-rename-consistency.test.ts | 12 + .../translations/pt-BR.objects.generated.ts | 150 ++++++++++ .../pt-BR.source-hashes.generated.ts | 21 ++ .../src/translations/ru.objects.generated.ts | 150 ++++++++++ .../ru.source-hashes.generated.ts | 21 ++ .../translations/zh-Hant.objects.generated.ts | 150 ++++++++++ .../zh-Hant.source-hashes.generated.ts | 21 ++ 20 files changed, 1801 insertions(+), 6 deletions(-) create mode 100644 .changeset/30512-plugin-security-locales.md create mode 100644 packages/plugins/plugin-security/src/translations/ar.objects.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/ar.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/de.objects.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/de.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/fr.objects.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/fr.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/ko.objects.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/ko.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/pt-BR.objects.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/pt-BR.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/ru.objects.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/ru.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/zh-Hant.objects.generated.ts create mode 100644 packages/plugins/plugin-security/src/translations/zh-Hant.source-hashes.generated.ts diff --git a/.changeset/30512-plugin-security-locales.md b/.changeset/30512-plugin-security-locales.md new file mode 100644 index 00000000000..e7874f9f6a4 --- /dev/null +++ b/.changeset/30512-plugin-security-locales.md @@ -0,0 +1,10 @@ +--- +'@objectstack/plugin-security': patch +--- + +i18n(plugin-security): the security objects, built-in permission sets and built-in positions ship in all eleven platform languages + +Clause-②: no +Metadata-protocol: none + +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. diff --git a/packages/plugins/plugin-security/scripts/i18n-extract.config.ts b/packages/plugins/plugin-security/scripts/i18n-extract.config.ts index 994f4ec530b..5f67c5c4c2c 100644 --- a/packages/plugins/plugin-security/scripts/i18n-extract.config.ts +++ b/packages/plugins/plugin-security/scripts/i18n-extract.config.ts @@ -8,7 +8,7 @@ * strings were seeded from @objectstack/platform-objects.) * * os i18n extract packages/plugins/plugin-security/scripts/i18n-extract.config.ts \ - * --locales=zh-CN,ja-JP,es-ES --fill=default --objects-only --no-metadata-forms \ + * --locales=zh-CN,ja-JP,es-ES,zh-Hant,ar,fr,pt-BR,ru,de,ko --fill=default --objects-only --no-metadata-forms \ * --source-hashes \ * --out=packages/plugins/plugin-security/src/translations * @@ -47,6 +47,13 @@ import { enObjects } from '../src/translations/en.objects.generated.js'; import { zhCNObjects } from '../src/translations/zh-CN.objects.generated.js'; import { jaJPObjects } from '../src/translations/ja-JP.objects.generated.js'; import { esESObjects } from '../src/translations/es-ES.objects.generated.js'; +import { zhHantObjects } from '../src/translations/zh-Hant.objects.generated.js'; +import { arObjects } from '../src/translations/ar.objects.generated.js'; +import { frObjects } from '../src/translations/fr.objects.generated.js'; +import { ptBRObjects } from '../src/translations/pt-BR.objects.generated.js'; +import { ruObjects } from '../src/translations/ru.objects.generated.js'; +import { deObjects } from '../src/translations/de.objects.generated.js'; +import { koObjects } from '../src/translations/ko.objects.generated.js'; /** * The annotation is load-bearing, not decoration (#10868). `defineStack` @@ -69,6 +76,13 @@ const config: ObjectStackDefinition = defineStack({ { 'zh-CN': { objects: zhCNObjects } }, { 'ja-JP': { objects: jaJPObjects } }, { 'es-ES': { objects: esESObjects } }, + { 'zh-Hant': { objects: zhHantObjects } }, + { 'ar': { objects: arObjects } }, + { 'fr': { objects: frObjects } }, + { 'pt-BR': { objects: ptBRObjects } }, + { 'ru': { objects: ruObjects } }, + { 'de': { objects: deObjects } }, + { 'ko': { objects: koObjects } }, ], }); diff --git a/packages/plugins/plugin-security/src/translations/ar.objects.generated.ts b/packages/plugins/plugin-security/src/translations/ar.objects.generated.ts new file mode 100644 index 00000000000..8988956f29e --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/ar.objects.generated.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'ar'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const arObjects: NonNullable = { + sys_user_permission_set: { + label: "مجموعة أذونات المستخدم", + pluralLabel: "مجموعات أذونات المستخدمين", + description: "إسناد مباشر لمجموعة أذونات إلى مستخدم (ويمكن حصره في مؤسسة).", + fields: { + id: { + label: "معرّف الإسناد", + help: "معرّف UUID للإسناد." + }, + display_title: { + label: "العنوان", + help: "عنوان السجل: المستخدم ومجموعة الأذونات المسندة إليه (يُحسب عند القراءة)" + }, + user_id: { + label: "المستخدم", + help: "المستخدم الذي يتلقى مجموعة الأذونات هذه." + }, + permission_set: { + label: "مجموعة الأذونات", + help: "مجموعة الأذونات التي يمنحها هذا الإسناد للمستخدم. اختر إحدى مجموعات الأذونات المعرّفة في الإعداد؛ ويُرفض الاسم الذي لا يطابق أي مجموعة." + }, + organization_id: { + label: "المؤسسة", + help: "المؤسسة التي ينتمي إليها هذا المنح." + }, + granted_by: { + label: "مَن منحه", + help: "المستخدم الذي منح مجموعة الأذونات هذه." + }, + valid_from: { + label: "صالح من", + help: "يسري الإسناد في هذا الوقت. اتركه فارغًا ليسري فورًا. يبدأ في موعده دون انتظار أي مهمة مجدولة." + }, + valid_until: { + label: "صالح حتى", + help: "ينتهي الإسناد في هذا الوقت (UTC). اتركه فارغًا لعدم تحديد تاريخ انتهاء. مطلوب في الوصول الطارئ وفي الإسناد إلى وكيل ذكاء اصطناعي. ينتهي في موعده دون انتظار أي مهمة مجدولة." + }, + reason: { + label: "السبب", + help: "سبب وجود هذا الإسناد. مطلوب في الإسنادات المفوَّضة وإسنادات الوصول الطارئ. بالنسبة إلى وكيل ذكاء اصطناعي، اذكر المهمة التي مُنح الوصول من أجلها." + }, + last_certified_at: { + label: "آخر اعتماد في", + help: "غير مستخدم بعد: محجوز لميزة مستقبلية لمراجعة الوصول. لا شيء يملأ هذا التاريخ أو يتحقق منه، لذا فالقيمة الفارغة لا تعني أن الإسناد لم يُراجَع." + }, + certified_by: { + label: "مَن اعتمده", + help: "غير مستخدم بعد: محجوز لميزة مستقبلية لمراجعة الوصول. المراجِع المُدخَل هنا لا يُتحقق منه ولا يغيّر شيئًا مما يمنحه الإسناد." + }, + created_at: { + label: "تاريخ الإنشاء" + }, + updated_at: { + label: "تاريخ التحديث" + } + }, + _validations: { + validity_window_order: { + message: "يجب أن يكون «صالح حتى» بعد «صالح من». يسري المنح من «صالح من» حتى «صالح حتى» دون أن يشمله، لذا فالنافذة التي تنتهي عند بدايتها أو قبلها لا تمنح شيئًا. اترك أيًّا منهما فارغًا لنافذة مفتوحة." + } + } + }, + sys_user_position: { + label: "منصب المستخدم", + pluralLabel: "مناصب المستخدمين", + description: "المناصب التي يشغلها كل مستخدم، صف واحد لكل إسناد.", + fields: { + id: { + label: "معرّف الإسناد", + help: "معرّف UUID لإسناد المستخدم إلى المنصب." + }, + display_title: { + label: "العنوان", + help: "عنوان السجل: المستخدم والمنصب الذي يشغله (يُحسب عند القراءة)" + }, + user_id: { + label: "المستخدم", + help: "المستخدم الذي يشغل هذا المنصب." + }, + position: { + label: "المنصب", + help: "الاسم الآلي للمنصب، ويُحلّ في كتالوج الأمان." + }, + business_unit_id: { + label: "وحدة الأعمال", + help: "مرساة وحدة الأعمال على مستوى الإسناد: حيث ينطبق إسناد هذا المنصب. مرساة العمق لـ readScope/writeScope، وحدّ الإدارة المفوَّضة، وحقيقة تدقيق. القيمة الفارغة = غير مرسَى (قديم / على مستوى المستأجر)؛ ويجب على المسؤولين المفوَّضين إرساء الإسنادات داخل شجرتهم الفرعية." + }, + organization_id: { + label: "المؤسسة", + help: "المؤسسة التي ينتمي إليها هذا الإسناد." + }, + granted_by: { + label: "مَن منحه", + help: "المستخدم الذي منح إسناد هذا المنصب (يختمه حارس الإدارة المفوَّضة عند الكتابة المفوَّضة)." + }, + valid_from: { + label: "صالح من", + help: "يسري الإسناد في هذا الوقت. اتركه فارغًا ليسري فورًا. يبدأ في موعده دون انتظار أي مهمة مجدولة." + }, + valid_until: { + label: "صالح حتى", + help: "ينتهي الإسناد في هذا الوقت (UTC). اتركه فارغًا لعدم تحديد تاريخ انتهاء. مطلوب في الإسناد المفوَّض. ينتهي في موعده دون انتظار أي مهمة مجدولة." + }, + reason: { + label: "السبب", + help: "سبب وجود هذا المنح. نص حر؛ مطلوب في صفوف التفويض والوصول الطارئ (كسر الزجاج)." + }, + delegated_from: { + label: "مفوَّض من", + help: "المفوِّض الذي يحمل هذا الصف صلاحيته (职务代理). الصف الذي ضُبط فيه delegated_from لا يمكن تفويضه بدوره ولا تجديده ذاتيًا — تُقطع السلاسل في الاتجاهين." + }, + last_certified_at: { + label: "آخر اعتماد في", + help: "غير مستخدم بعد: محجوز لميزة مستقبلية لمراجعة الوصول. لا شيء يملأ هذا التاريخ أو يتحقق منه، لذا فالقيمة الفارغة لا تعني أن الإسناد لم يُراجَع." + }, + certified_by: { + label: "مَن اعتمده", + help: "غير مستخدم بعد: محجوز لميزة مستقبلية لمراجعة الوصول. المراجِع المُدخَل هنا لا يُتحقق منه ولا يغيّر شيئًا مما يمنحه الإسناد." + }, + created_at: { + label: "تاريخ الإنشاء" + }, + updated_at: { + label: "تاريخ التحديث" + } + }, + _validations: { + reserved_identity_position: { + message: "لا يمكن أن يكون «position» اسم هوية مدمجة محجوزًا للإطار (platform_admin، org_owner، org_admin، org_member، org_external). هذه الأسماء هويات مدمجة: تُسقطها المنصة في current_user.positions من مصادرها الموثوقة، والصف الذي يحمل أحدها ليس إسنادًا لتلك الهوية. اختر اسمًا آخر." + } + } + } +}; diff --git a/packages/plugins/plugin-security/src/translations/ar.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/ar.source-hashes.generated.ts new file mode 100644 index 00000000000..728daa2a100 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/ar.source-hashes.generated.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'ar'. Do not hand-edit. + * + * Each entry is the digest of the SOURCE REVISION that this locale's leaf at + * that path is still a byte copy of — provenance for the generated half of the + * bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead. + * + * An entry exists only while the leaf IS such a copy. Re-translate the leaf in + * `.objects.generated.ts` and the next extract drops its entry by + * itself — the table makes no claim about text a translator wrote. A path with + * no entry is LEGACY-TRUSTED and never reported stale. + * + * ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest + * records that the current text was copied from the current source, which is + * the false claim the mechanism exists to detect. Fix the TRANSLATION. + */ + +export const arGeneratedSourceHashes: Readonly> = { +}; diff --git a/packages/plugins/plugin-security/src/translations/builtin-positions.ts b/packages/plugins/plugin-security/src/translations/builtin-positions.ts index 0c233713928..70510441acd 100644 --- a/packages/plugins/plugin-security/src/translations/builtin-positions.ts +++ b/packages/plugins/plugin-security/src/translations/builtin-positions.ts @@ -12,8 +12,8 @@ * `../builtin-positions.ts` (the identity names' text comes from the spec's * `BUILTIN_IDENTITY_METADATA`, the audience anchors' from that module); `en` * needs no entry, because an untranslated locale serves the declaration's own - * text. When a source label or sentence changes, re-translate its three - * entries here in the same change: nothing marks a translation stale. + * text. When a source label or sentence changes, re-translate its entry + * in every locale here in the same change: nothing marks a translation stale. * `builtin-positions.test.ts` pins both strings per built-in position per * locale, and the served overlay. * @@ -118,3 +118,220 @@ export const esESBuiltinPositions: PositionTranslations = { 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.', }, }; + +export const zhHantBuiltinPositions: PositionTranslations = { + platform_admin: { + label: '平台管理員', + description: '平台維運人員(SaaS 管理員),不是租戶內的使用者角色。', + }, + org_owner: { + label: '組織擁有者', + description: '租戶內組織的擁有者。', + }, + org_admin: { + label: '組織管理員', + description: '租戶內組織的管理員。', + }, + org_member: { + label: '組織成員', + description: '租戶內組織的成員。', + }, + org_external: { + label: '組織外部成員', + description: '租戶內的外部成員(入口網站使用者):只能看到本人的身分記錄,僅透過明確授予的權限存取資料。', + }, + everyone: { + label: '所有人', + description: '內建對象錨點:每位已登入成員都隱含擔任此職位。綁定到此職位的權限集就是租戶的預設授權。高權限的權限集不能綁定到這裡。', + }, + guest: { + label: '訪客', + description: '內建對象錨點:未登入的存取者隱含且唯一地擔任此職位。這裡的綁定受到最嚴格的檢查 — 只能指定具名物件、以唯讀為主、絕不允許萬用字元。', + }, +}; + +export const arBuiltinPositions: PositionTranslations = { + platform_admin: { + label: 'مسؤول المنصة', + description: 'مشغّل المنصة (مسؤول SaaS)، وليس دور مستخدم داخل المستأجر.', + }, + org_owner: { + label: 'مالك المؤسسة', + description: 'مالك مؤسسة داخل المستأجر.', + }, + org_admin: { + label: 'مسؤول المؤسسة', + description: 'مسؤول مؤسسة داخل المستأجر.', + }, + org_member: { + label: 'عضو المؤسسة', + description: 'عضو في مؤسسة داخل المستأجر.', + }, + org_external: { + label: 'عضو خارجي في المؤسسة', + description: 'عضو خارجي داخل المستأجر (مستخدم بوابة): لا يرى إلا سجلات هويته، ولا يصل إلى البيانات إلا عبر منح صريحة.', + }, + everyone: { + label: 'الجميع', + description: 'مرساة جمهور مدمجة: كل عضو مسجّل الدخول يشغل هذا المنصب ضمنيًا. مجموعات الأذونات المرتبطة به هي المنح الافتراضية للمستأجر. ولا يمكن ربط مجموعات ذات صلاحيات عالية هنا.', + }, + guest: { + label: 'ضيف', + description: 'مرساة جمهور مدمجة: يشغل غير المسجّلين هذا المنصب ضمنيًا وحصريًا. تخضع الروابط هنا لأشد الفحوص — كائنات مسمّاة فقط، وقراءة في الغالب، ولا أحرف بدل أبدًا.', + }, +}; + +export const frBuiltinPositions: PositionTranslations = { + platform_admin: { + label: 'Administrateur de la plateforme', + description: 'Opérateur de la plateforme (administrateur SaaS). Ce n’est pas un rôle d’utilisateur au sein du locataire.', + }, + org_owner: { + label: 'Propriétaire de l’organisation', + description: 'Propriétaire d’une organisation au sein d’un locataire.', + }, + org_admin: { + label: 'Administrateur de l’organisation', + description: 'Administrateur d’une organisation au sein d’un locataire.', + }, + org_member: { + label: 'Membre de l’organisation', + description: 'Membre d’une organisation au sein d’un locataire.', + }, + org_external: { + label: 'Membre externe de l’organisation', + description: 'Membre externe au sein d’un locataire (utilisateur du portail) : ne voit que ses propres enregistrements d’identité et n’accède aux données que par des octrois explicites.', + }, + everyone: { + label: 'Tout le monde', + description: 'Ancre d’audience intégrée : chaque membre connecté occupe implicitement ce poste. Les ensembles d’autorisations qui y sont liés sont les octrois par défaut du locataire. Les ensembles à privilèges élevés ne peuvent pas y être liés.', + }, + guest: { + label: 'Invité', + description: 'Ancre d’audience intégrée : les visiteurs non connectés occupent implicitement et exclusivement ce poste. Ses liaisons subissent les contrôles les plus stricts — objets nommés uniquement, surtout en lecture, jamais de caractère générique.', + }, +}; + +export const ptBRBuiltinPositions: PositionTranslations = { + platform_admin: { + label: 'Administrador da plataforma', + description: 'Operador da plataforma (administrador SaaS). Não é uma função de usuário dentro do locatário.', + }, + org_owner: { + label: 'Proprietário da organização', + description: 'Proprietário de uma organização dentro de um locatário.', + }, + org_admin: { + label: 'Administrador da organização', + description: 'Administrador de uma organização dentro de um locatário.', + }, + org_member: { + label: 'Membro da organização', + description: 'Membro de uma organização dentro de um locatário.', + }, + org_external: { + label: 'Membro externo da organização', + description: 'Membro externo dentro de um locatário (usuário do portal): vê apenas os próprios registros de identidade e acessa dados só por concessões explícitas.', + }, + everyone: { + label: 'Todos', + description: 'Âncora de público integrada: todo membro conectado ocupa este cargo implicitamente. Os conjuntos de permissões vinculados a ele são as concessões padrão do locatário. Conjuntos de privilégio elevado não podem ser vinculados aqui.', + }, + guest: { + label: 'Convidado', + description: 'Âncora de público integrada: quem não está conectado ocupa este cargo de forma implícita e exclusiva. Os vínculos passam pelas verificações mais rígidas — só objetos nomeados, principalmente leitura, nunca curinga.', + }, +}; + +export const ruBuiltinPositions: PositionTranslations = { + platform_admin: { + label: 'Администратор платформы', + description: 'Оператор платформы (администратор SaaS). Это не роль пользователя внутри арендатора.', + }, + org_owner: { + label: 'Владелец организации', + description: 'Владелец организации внутри арендатора.', + }, + org_admin: { + label: 'Администратор организации', + description: 'Администратор организации внутри арендатора.', + }, + org_member: { + label: 'Участник организации', + description: 'Участник организации внутри арендатора.', + }, + org_external: { + label: 'Внешний участник организации', + description: 'Внешний участник внутри арендатора (пользователь портала): видит только свои записи идентичности и получает доступ к данным лишь через явные предоставления.', + }, + everyone: { + label: 'Все', + description: 'Встроенный якорь аудитории: каждый вошедший участник неявно занимает эту должность. Привязанные к ней наборы разрешений — это выдачи арендатора по умолчанию. Наборы с высокими привилегиями сюда привязать нельзя.', + }, + guest: { + label: 'Гость', + description: 'Встроенный якорь аудитории: не вошедшие в систему неявно и исключительно занимают эту должность. Привязки проходят самые строгие проверки — только именованные объекты, преимущественно чтение, никаких подстановочных знаков.', + }, +}; + +export const deBuiltinPositions: PositionTranslations = { + platform_admin: { + label: 'Plattformadministrator', + description: 'Plattformbetreiber (SaaS-Administrator). Keine Benutzerrolle innerhalb des Mandanten.', + }, + org_owner: { + label: 'Organisationseigentümer', + description: 'Eigentümer einer Organisation innerhalb eines Mandanten.', + }, + org_admin: { + label: 'Organisationsadministrator', + description: 'Administrator einer Organisation innerhalb eines Mandanten.', + }, + org_member: { + label: 'Organisationsmitglied', + description: 'Mitglied einer Organisation innerhalb eines Mandanten.', + }, + org_external: { + label: 'Externes Organisationsmitglied', + description: 'Externes Mitglied innerhalb eines Mandanten (Portalbenutzer): sieht nur die eigenen Identitätsdatensätze und greift nur über ausdrückliche Gewährungen auf Daten zu.', + }, + everyone: { + label: 'Alle', + description: 'Integrierter Zielgruppenanker: Jedes angemeldete Mitglied hat diese Position implizit inne. Daran gebundene Berechtigungssätze sind die Standardgewährungen des Mandanten. Hochprivilegierte Sätze können hier nicht gebunden werden.', + }, + guest: { + label: 'Gast', + description: 'Integrierter Zielgruppenanker: Nicht angemeldete Zugreifende haben diese Position implizit und ausschließlich inne. Bindungen unterliegen den strengsten Prüfungen — nur benannte Objekte, überwiegend lesend, nie ein Platzhalter.', + }, +}; + +export const koBuiltinPositions: PositionTranslations = { + platform_admin: { + label: '플랫폼 관리자', + description: '플랫폼 운영자(SaaS 관리자)이며, 테넌트 안의 사용자 역할이 아닙니다.', + }, + org_owner: { + label: '조직 소유자', + description: '테넌트 안에 있는 조직의 소유자입니다.', + }, + org_admin: { + label: '조직 관리자', + description: '테넌트 안에 있는 조직의 관리자입니다.', + }, + org_member: { + label: '조직 구성원', + description: '테넌트 안에 있는 조직의 구성원입니다.', + }, + org_external: { + label: '조직 외부 구성원', + description: '테넌트 안의 외부 구성원(포털 사용자): 본인의 ID 레코드만 볼 수 있고, 명시적으로 부여된 권한으로만 데이터에 접근합니다.', + }, + everyone: { + label: '모든 사람', + description: '기본 제공 대상 앵커: 로그인한 모든 구성원이 이 직위를 암묵적으로 갖습니다. 여기에 연결한 권한 세트가 테넌트의 기본 부여가 됩니다. 높은 권한의 세트는 여기에 연결할 수 없습니다.', + }, + guest: { + label: '게스트', + description: '기본 제공 대상 앵커: 로그인하지 않은 접근자만이 이 직위를 암묵적으로 갖습니다. 여기의 연결은 가장 엄격한 검사를 받습니다 — 이름을 지정한 객체만, 주로 읽기, 와일드카드는 절대 불가.', + }, +}; diff --git a/packages/plugins/plugin-security/src/translations/de.objects.generated.ts b/packages/plugins/plugin-security/src/translations/de.objects.generated.ts new file mode 100644 index 00000000000..8d0a2173fe0 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/de.objects.generated.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'de'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const deObjects: NonNullable = { + sys_user_permission_set: { + label: "Berechtigungssatz des Benutzers", + pluralLabel: "Berechtigungssätze der Benutzer", + description: "Direkte Zuweisung eines Berechtigungssatzes an einen Benutzer (optional auf eine Organisation beschränkt).", + fields: { + id: { + label: "Zuweisungs-ID", + help: "UUID der Zuweisung." + }, + display_title: { + label: "Titel", + help: "Datensatztitel: der Benutzer und der ihm zugewiesene Berechtigungssatz (beim Lesen berechnet)" + }, + user_id: { + label: "Benutzer", + help: "Der Benutzer, der diesen Berechtigungssatz erhält." + }, + permission_set: { + label: "Berechtigungssatz", + help: "Der Berechtigungssatz, den diese Zuweisung dem Benutzer gibt. Wählen Sie einen der im Setup definierten Berechtigungssätze; ein Name, der zu keinem passt, wird abgelehnt." + }, + organization_id: { + label: "Organisation", + help: "Die Organisation, zu der diese Gewährung gehört." + }, + granted_by: { + label: "Gewährt von", + help: "Der Benutzer, der diesen Berechtigungssatz gewährt hat." + }, + valid_from: { + label: "Gültig ab", + help: "Die Zuweisung wird zu diesem Zeitpunkt wirksam. Leer lassen, damit sie sofort gilt. Sie beginnt pünktlich, ohne auf einen geplanten Job zu warten." + }, + valid_until: { + label: "Gültig bis", + help: "Die Zuweisung endet zu diesem Zeitpunkt (UTC). Leer lassen für kein Enddatum. Pflicht bei Notfallzugriff und bei einer Zuweisung an einen KI-Agenten. Sie endet pünktlich, ohne auf einen geplanten Job zu warten." + }, + reason: { + label: "Grund", + help: "Warum diese Zuweisung besteht. Pflicht bei delegierten Zuweisungen und Notfallzugriff. Nennen Sie bei einem KI-Agenten die Aufgabe, für die er Zugriff erhielt." + }, + last_certified_at: { + label: "Zuletzt zertifiziert am", + help: "Noch nicht in Gebrauch: für eine künftige Zugriffsprüfung vorgesehen. Nichts füllt oder prüft dieses Datum, ein leerer Wert bedeutet also nicht, dass die Zuweisung ungeprüft blieb." + }, + certified_by: { + label: "Zertifiziert von", + help: "Noch nicht in Gebrauch: für eine künftige Zugriffsprüfung vorgesehen. Ein hier eingetragener Prüfer wird nicht verifiziert und ändert nichts an dem, was die Zuweisung gewährt." + }, + created_at: { + label: "Erstellt am" + }, + updated_at: { + label: "Aktualisiert am" + } + }, + _validations: { + validity_window_order: { + message: "„Gültig bis“ muss nach „Gültig ab“ liegen. Die Gewährung gilt ab „Gültig ab“ bis ausschließlich „Gültig bis“; ein Zeitfenster, das zu Beginn oder davor endet, gewährt also nichts. Lassen Sie eines der beiden leer für ein offenes Zeitfenster." + } + } + }, + sys_user_position: { + label: "Position des Benutzers", + pluralLabel: "Positionen der Benutzer", + description: "Welche Positionen jeder Benutzer innehat, eine Zeile pro Zuweisung.", + fields: { + id: { + label: "Zuweisungs-ID", + help: "UUID der Benutzer-Positions-Zuweisung." + }, + display_title: { + label: "Titel", + help: "Datensatztitel: der Benutzer und die Position, die er innehat (beim Lesen berechnet)" + }, + user_id: { + label: "Benutzer", + help: "Der Benutzer, der diese Position innehat." + }, + position: { + label: "Position", + help: "Maschinenname der Position, aufgelöst im Sicherheitskatalog." + }, + business_unit_id: { + label: "Geschäftsbereich", + help: "GB-Anker auf Zuweisungsebene: wo diese Positionszuweisung gilt. Tiefenanker für readScope/writeScope, Grenze der delegierten Administration und Audit-Fakt. Null = nicht verankert (Altbestand / mandantenweit); delegierte Administratoren MÜSSEN Zuweisungen innerhalb ihres Teilbaums verankern." + }, + organization_id: { + label: "Organisation", + help: "Die Organisation, zu der diese Zuweisung gehört." + }, + granted_by: { + label: "Gewährt von", + help: "Der Benutzer, der diese Positionszuweisung gewährt hat (bei delegierten Schreibvorgängen vom Gate der delegierten Administration gestempelt)." + }, + valid_from: { + label: "Gültig ab", + help: "Die Zuweisung wird zu diesem Zeitpunkt wirksam. Leer lassen, damit sie sofort gilt. Sie beginnt pünktlich, ohne auf einen geplanten Job zu warten." + }, + valid_until: { + label: "Gültig bis", + help: "Die Zuweisung endet zu diesem Zeitpunkt (UTC). Leer lassen für kein Enddatum. Pflicht bei einer delegierten Zuweisung. Sie endet pünktlich, ohne auf einen geplanten Job zu warten." + }, + reason: { + label: "Grund", + help: "Warum diese Gewährung besteht. Freitext; PFLICHT bei Delegierungs- und Notfallzugriffszeilen (Break-Glass)." + }, + delegated_from: { + label: "Delegiert von", + help: "Der Delegierende, dessen Befugnis diese Zeile trägt (职务代理). Eine Zeile mit gesetztem delegated_from ist selbst weder delegierbar noch selbst verlängerbar — Ketten werden in beide Richtungen abgeschnitten." + }, + last_certified_at: { + label: "Zuletzt zertifiziert am", + help: "Noch nicht in Gebrauch: für eine künftige Zugriffsprüfung vorgesehen. Nichts füllt oder prüft dieses Datum, ein leerer Wert bedeutet also nicht, dass die Zuweisung ungeprüft blieb." + }, + certified_by: { + label: "Zertifiziert von", + help: "Noch nicht in Gebrauch: für eine künftige Zugriffsprüfung vorgesehen. Ein hier eingetragener Prüfer wird nicht verifiziert und ändert nichts an dem, was die Zuweisung gewährt." + }, + created_at: { + label: "Erstellt am" + }, + updated_at: { + label: "Aktualisiert am" + } + }, + _validations: { + reserved_identity_position: { + message: "„position“ darf keinen vom Framework reservierten Namen einer integrierten Identität tragen (platform_admin, org_owner, org_admin, org_member, org_external). Diese Namen sind integrierte Identitäten: Die Plattform projiziert sie aus ihren eigenen maßgeblichen Quellen in current_user.positions, und eine Zeile mit einem davon ist keine Zuweisung dieser Identität. Wählen Sie einen anderen Namen." + } + } + } +}; diff --git a/packages/plugins/plugin-security/src/translations/de.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/de.source-hashes.generated.ts new file mode 100644 index 00000000000..c2850ec7328 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/de.source-hashes.generated.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'de'. Do not hand-edit. + * + * Each entry is the digest of the SOURCE REVISION that this locale's leaf at + * that path is still a byte copy of — provenance for the generated half of the + * bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead. + * + * An entry exists only while the leaf IS such a copy. Re-translate the leaf in + * `.objects.generated.ts` and the next extract drops its entry by + * itself — the table makes no claim about text a translator wrote. A path with + * no entry is LEGACY-TRUSTED and never reported stale. + * + * ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest + * records that the current text was copied from the current source, which is + * the false claim the mechanism exists to detect. Fix the TRANSLATION. + */ + +export const deGeneratedSourceHashes: Readonly> = { + "objects.sys_user_position.fields.position.label": "9a24c02e441eb0b4", +}; diff --git a/packages/plugins/plugin-security/src/translations/fr.objects.generated.ts b/packages/plugins/plugin-security/src/translations/fr.objects.generated.ts new file mode 100644 index 00000000000..6d9fa3ebe56 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/fr.objects.generated.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'fr'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const frObjects: NonNullable = { + sys_user_permission_set: { + label: "Ensemble d’autorisations de l’utilisateur", + pluralLabel: "Ensembles d’autorisations des utilisateurs", + description: "Attribution directe d’un ensemble d’autorisations à un utilisateur (éventuellement limitée à une organisation).", + fields: { + id: { + label: "ID d’attribution", + help: "UUID de l’attribution." + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : l’utilisateur et l’ensemble d’autorisations qui lui est attribué (calculé à la lecture)" + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur qui reçoit cet ensemble d’autorisations." + }, + permission_set: { + label: "Ensemble d’autorisations", + help: "L’ensemble d’autorisations que cette attribution donne à l’utilisateur. Choisissez l’un des ensembles définis dans Configuration ; un nom qui ne correspond à aucun ensemble est refusé." + }, + organization_id: { + label: "Organisation", + help: "L’organisation à laquelle appartient cet octroi." + }, + granted_by: { + label: "Accordé par", + help: "L’utilisateur qui a accordé cet ensemble d’autorisations." + }, + valid_from: { + label: "Valide à partir du", + help: "L’attribution prend effet à ce moment. Laissez vide pour un effet immédiat. Elle commence à l’heure, sans tâche planifiée à attendre." + }, + valid_until: { + label: "Valide jusqu’au", + help: "L’attribution prend fin à ce moment (UTC). Laissez vide pour aucune date de fin. Obligatoire pour un accès d’urgence et pour une attribution à un agent d’IA. Elle se termine à l’heure, sans tâche planifiée à attendre." + }, + reason: { + label: "Motif", + help: "Pourquoi cette attribution existe. Obligatoire pour les attributions déléguées et d’accès d’urgence. Pour un agent d’IA, indiquez la tâche pour laquelle il a reçu l’accès." + }, + last_certified_at: { + label: "Dernière certification le", + help: "Pas encore utilisé : réservé à une future fonction de revue des accès. Rien ne remplit ni ne vérifie cette date, donc une valeur vide ne signifie pas que l’attribution n’a pas été revue." + }, + certified_by: { + label: "Certifié par", + help: "Pas encore utilisé : réservé à une future fonction de revue des accès. Un réviseur saisi ici n’est pas vérifié et ne change rien à ce que l’attribution accorde." + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _validations: { + validity_window_order: { + message: "« Valide jusqu’au » doit être postérieur à « Valide à partir du ». L’octroi est actif de « Valide à partir du » jusqu’à « Valide jusqu’au » exclu : une période qui se termine au moment où elle commence, ou avant, n’accorde rien. Laissez l’un des deux vide pour une période ouverte." + } + } + }, + sys_user_position: { + label: "Poste de l’utilisateur", + pluralLabel: "Postes des utilisateurs", + description: "Les postes occupés par chaque utilisateur, une ligne par attribution.", + fields: { + id: { + label: "ID d’attribution", + help: "UUID de l’attribution utilisateur-poste." + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : l’utilisateur et le poste qu’il occupe (calculé à la lecture)" + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur qui occupe ce poste." + }, + position: { + label: "Poste", + help: "Nom machine du poste, résolu dans le catalogue de sécurité." + }, + business_unit_id: { + label: "Unité commerciale", + help: "Ancre d’UC au niveau de l’attribution : là où s’applique cette attribution de poste. Ancre de profondeur pour readScope/writeScope, limite de l’administration déléguée et fait d’audit. Null = non ancrée (héritée / à l’échelle du locataire) ; les administrateurs délégués DOIVENT ancrer les attributions dans leur sous-arbre." + }, + organization_id: { + label: "Organisation", + help: "L’organisation à laquelle appartient cette attribution." + }, + granted_by: { + label: "Accordé par", + help: "L’utilisateur qui a accordé cette attribution de poste (apposé par le contrôle d’administration déléguée lors des écritures déléguées)." + }, + valid_from: { + label: "Valide à partir du", + help: "L’attribution prend effet à ce moment. Laissez vide pour un effet immédiat. Elle commence à l’heure, sans tâche planifiée à attendre." + }, + valid_until: { + label: "Valide jusqu’au", + help: "L’attribution prend fin à ce moment (UTC). Laissez vide pour aucune date de fin. Obligatoire pour une attribution déléguée. Elle se termine à l’heure, sans tâche planifiée à attendre." + }, + reason: { + label: "Motif", + help: "Pourquoi cet octroi existe. Texte libre ; OBLIGATOIRE pour les lignes de délégation et d’accès d’urgence (bris de glace)." + }, + delegated_from: { + label: "Délégué par", + help: "Le délégant dont cette ligne porte l’autorité (职务代理). Une ligne où delegated_from est renseigné n’est elle-même ni délégable ni auto-renouvelable — les chaînes sont coupées dans les deux sens." + }, + last_certified_at: { + label: "Dernière certification le", + help: "Pas encore utilisé : réservé à une future fonction de revue des accès. Rien ne remplit ni ne vérifie cette date, donc une valeur vide ne signifie pas que l’attribution n’a pas été revue." + }, + certified_by: { + label: "Certifié par", + help: "Pas encore utilisé : réservé à une future fonction de revue des accès. Un réviseur saisi ici n’est pas vérifié et ne change rien à ce que l’attribution accorde." + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _validations: { + reserved_identity_position: { + message: "« position » ne peut pas reprendre un nom d’identité intégrée réservé au framework (platform_admin, org_owner, org_admin, org_member, org_external). Ces noms sont des identités intégrées : la plateforme les projette dans current_user.positions à partir de leurs propres sources de vérité, et une ligne qui en reprend un n’attribue pas cette identité. Choisissez un autre nom." + } + } + } +}; diff --git a/packages/plugins/plugin-security/src/translations/fr.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/fr.source-hashes.generated.ts new file mode 100644 index 00000000000..56e398625bb --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/fr.source-hashes.generated.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'fr'. Do not hand-edit. + * + * Each entry is the digest of the SOURCE REVISION that this locale's leaf at + * that path is still a byte copy of — provenance for the generated half of the + * bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead. + * + * An entry exists only while the leaf IS such a copy. Re-translate the leaf in + * `.objects.generated.ts` and the next extract drops its entry by + * itself — the table makes no claim about text a translator wrote. A path with + * no entry is LEGACY-TRUSTED and never reported stale. + * + * ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest + * records that the current text was copied from the current source, which is + * the false claim the mechanism exists to detect. Fix the TRANSLATION. + */ + +export const frGeneratedSourceHashes: Readonly> = { +}; diff --git a/packages/plugins/plugin-security/src/translations/index.ts b/packages/plugins/plugin-security/src/translations/index.ts index 17041442e4c..6047ce30a0c 100644 --- a/packages/plugins/plugin-security/src/translations/index.ts +++ b/packages/plugins/plugin-security/src/translations/index.ts @@ -15,11 +15,47 @@ import { enObjects } from './en.objects.generated.js'; import { zhCNObjects } from './zh-CN.objects.generated.js'; import { jaJPObjects } from './ja-JP.objects.generated.js'; import { esESObjects } from './es-ES.objects.generated.js'; +import { zhHantObjects } from './zh-Hant.objects.generated.js'; +import { arObjects } from './ar.objects.generated.js'; +import { frObjects } from './fr.objects.generated.js'; +import { ptBRObjects } from './pt-BR.objects.generated.js'; +import { ruObjects } from './ru.objects.generated.js'; +import { deObjects } from './de.objects.generated.js'; +import { koObjects } from './ko.objects.generated.js'; import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js'; import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js'; import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js'; -import { zhCNPermissionSets, jaJPPermissionSets, esESPermissionSets } from './permission-sets.js'; -import { zhCNBuiltinPositions, jaJPBuiltinPositions, esESBuiltinPositions } from './builtin-positions.js'; +import { zhHantGeneratedSourceHashes } from './zh-Hant.source-hashes.generated.js'; +import { arGeneratedSourceHashes } from './ar.source-hashes.generated.js'; +import { frGeneratedSourceHashes } from './fr.source-hashes.generated.js'; +import { ptBRGeneratedSourceHashes } from './pt-BR.source-hashes.generated.js'; +import { ruGeneratedSourceHashes } from './ru.source-hashes.generated.js'; +import { deGeneratedSourceHashes } from './de.source-hashes.generated.js'; +import { koGeneratedSourceHashes } from './ko.source-hashes.generated.js'; +import { + zhCNPermissionSets, + jaJPPermissionSets, + esESPermissionSets, + zhHantPermissionSets, + arPermissionSets, + frPermissionSets, + ptBRPermissionSets, + ruPermissionSets, + dePermissionSets, + koPermissionSets, +} from './permission-sets.js'; +import { + zhCNBuiltinPositions, + jaJPBuiltinPositions, + esESBuiltinPositions, + zhHantBuiltinPositions, + arBuiltinPositions, + frBuiltinPositions, + ptBRBuiltinPositions, + ruBuiltinPositions, + deBuiltinPositions, + koBuiltinPositions, +} from './builtin-positions.js'; /** * ## The provenance companions are READ here, not merely recorded @@ -74,4 +110,39 @@ export const SecurityTranslations: TranslationBundle = { permissions: esESPermissionSets, positions: esESBuiltinPositions, }, + 'zh-Hant': { + ...withSourceFallback({ objects: zhHantObjects }, enSource, undefined, zhHantGeneratedSourceHashes), + permissions: zhHantPermissionSets, + positions: zhHantBuiltinPositions, + }, + 'ar': { + ...withSourceFallback({ objects: arObjects }, enSource, undefined, arGeneratedSourceHashes), + permissions: arPermissionSets, + positions: arBuiltinPositions, + }, + 'fr': { + ...withSourceFallback({ objects: frObjects }, enSource, undefined, frGeneratedSourceHashes), + permissions: frPermissionSets, + positions: frBuiltinPositions, + }, + 'pt-BR': { + ...withSourceFallback({ objects: ptBRObjects }, enSource, undefined, ptBRGeneratedSourceHashes), + permissions: ptBRPermissionSets, + positions: ptBRBuiltinPositions, + }, + 'ru': { + ...withSourceFallback({ objects: ruObjects }, enSource, undefined, ruGeneratedSourceHashes), + permissions: ruPermissionSets, + positions: ruBuiltinPositions, + }, + 'de': { + ...withSourceFallback({ objects: deObjects }, enSource, undefined, deGeneratedSourceHashes), + permissions: dePermissionSets, + positions: deBuiltinPositions, + }, + 'ko': { + ...withSourceFallback({ objects: koObjects }, enSource, undefined, koGeneratedSourceHashes), + permissions: koPermissionSets, + positions: koBuiltinPositions, + }, }; diff --git a/packages/plugins/plugin-security/src/translations/ko.objects.generated.ts b/packages/plugins/plugin-security/src/translations/ko.objects.generated.ts new file mode 100644 index 00000000000..673fd70011c --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/ko.objects.generated.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'ko'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const koObjects: NonNullable = { + sys_user_permission_set: { + label: "사용자 권한 세트", + pluralLabel: "사용자 권한 세트", + description: "권한 세트를 사용자에게 직접 할당합니다(조직 범위로 한정할 수 있음).", + fields: { + id: { + label: "할당 ID", + help: "할당의 UUID입니다." + }, + display_title: { + label: "제목", + help: "레코드 제목: 사용자와 그에게 할당된 권한 세트(읽을 때 계산)" + }, + user_id: { + label: "사용자", + help: "이 권한 세트를 받는 사용자입니다." + }, + permission_set: { + label: "권한 세트", + help: "이 할당이 사용자에게 주는 권한 세트입니다. 설정에 정의된 권한 세트 중 하나를 고르세요. 어떤 권한 세트와도 일치하지 않는 이름은 거부됩니다." + }, + organization_id: { + label: "조직", + help: "이 부여가 속한 조직입니다." + }, + granted_by: { + label: "부여자", + help: "이 권한 세트를 부여한 사용자입니다." + }, + valid_from: { + label: "유효 시작", + help: "할당이 이 시각부터 적용됩니다. 비워 두면 즉시 적용됩니다. 예약 작업을 기다리지 않고 제시간에 시작됩니다." + }, + valid_until: { + label: "유효 종료", + help: "할당이 이 시각(UTC)에 끝납니다. 비워 두면 종료일이 없습니다. 긴급 접근과 AI 에이전트에 대한 할당에는 필수입니다. 예약 작업을 기다리지 않고 제시간에 끝납니다." + }, + reason: { + label: "사유", + help: "이 할당이 존재하는 이유입니다. 위임 및 긴급 접근 할당에는 필수입니다. AI 에이전트라면 어떤 작업을 위해 접근 권한을 받았는지 적으세요." + }, + last_certified_at: { + label: "최근 인증 일시", + help: "아직 사용하지 않음: 향후 접근 검토 기능을 위해 남겨 둔 항목입니다. 이 날짜를 채우거나 확인하는 기능이 없으므로, 비어 있다고 해서 할당이 검토되지 않았다는 뜻은 아닙니다." + }, + certified_by: { + label: "인증자", + help: "아직 사용하지 않음: 향후 접근 검토 기능을 위해 남겨 둔 항목입니다. 여기에 입력한 검토자는 검증되지 않으며, 할당이 부여하는 권한에 아무런 영향을 주지 않습니다." + }, + created_at: { + label: "생성 일시" + }, + updated_at: { + label: "수정 일시" + } + }, + _validations: { + validity_window_order: { + message: "\"유효 종료\"는 \"유효 시작\"보다 늦어야 합니다. 부여는 \"유효 시작\"부터 \"유효 종료\" 직전까지 적용되므로, 시작 시각이나 그 이전에 끝나는 기간은 아무것도 부여하지 않습니다. 기간에 끝을 두지 않으려면 둘 중 하나를 비워 두세요." + } + } + }, + sys_user_position: { + label: "사용자 직위", + pluralLabel: "사용자 직위", + description: "각 사용자가 맡은 직위로, 할당마다 한 행입니다.", + fields: { + id: { + label: "할당 ID", + help: "사용자-직위 할당의 UUID입니다." + }, + display_title: { + label: "제목", + help: "레코드 제목: 사용자와 그가 맡은 직위(읽을 때 계산)" + }, + user_id: { + label: "사용자", + help: "이 직위를 맡은 사용자입니다." + }, + position: { + label: "직위", + help: "직위의 시스템 이름으로, 보안 카탈로그에서 해석됩니다." + }, + business_unit_id: { + label: "사업부", + help: "할당 수준의 사업부 앵커로, 이 직위 할당이 적용되는 범위입니다. readScope/writeScope의 깊이 앵커이자 위임 관리 경계이며 감사 사실입니다. Null = 앵커 없음(레거시/테넌트 전체). 위임 관리자는 반드시 자신의 하위 트리 안에 할당을 고정해야 합니다." + }, + organization_id: { + label: "조직", + help: "이 할당이 속한 조직입니다." + }, + granted_by: { + label: "부여자", + help: "이 직위 할당을 부여한 사용자입니다(위임 쓰기 시 위임 관리 게이트가 기록)." + }, + valid_from: { + label: "유효 시작", + help: "할당이 이 시각부터 적용됩니다. 비워 두면 즉시 적용됩니다. 예약 작업을 기다리지 않고 제시간에 시작됩니다." + }, + valid_until: { + label: "유효 종료", + help: "할당이 이 시각(UTC)에 끝납니다. 비워 두면 종료일이 없습니다. 위임 할당에는 필수입니다. 예약 작업을 기다리지 않고 제시간에 끝납니다." + }, + reason: { + label: "사유", + help: "이 부여가 존재하는 이유입니다. 자유 텍스트이며, 위임 및 긴급(break-glass) 행에는 필수입니다." + }, + delegated_from: { + label: "위임자", + help: "이 행이 지닌 권한의 위임자입니다(职务代理). delegated_from이 설정된 행은 다시 위임하거나 스스로 갱신할 수 없으며, 연쇄는 양방향으로 끊깁니다." + }, + last_certified_at: { + label: "최근 인증 일시", + help: "아직 사용하지 않음: 향후 접근 검토 기능을 위해 남겨 둔 항목입니다. 이 날짜를 채우거나 확인하는 기능이 없으므로, 비어 있다고 해서 할당이 검토되지 않았다는 뜻은 아닙니다." + }, + certified_by: { + label: "인증자", + help: "아직 사용하지 않음: 향후 접근 검토 기능을 위해 남겨 둔 항목입니다. 여기에 입력한 검토자는 검증되지 않으며, 할당이 부여하는 권한에 아무런 영향을 주지 않습니다." + }, + created_at: { + label: "생성 일시" + }, + updated_at: { + label: "수정 일시" + } + }, + _validations: { + reserved_identity_position: { + message: "\"position\"에는 프레임워크가 예약한 기본 제공 ID 이름(platform_admin, org_owner, org_admin, org_member, org_external)을 쓸 수 없습니다. 이 이름들은 기본 제공 ID로, 플랫폼이 각자의 원본 데이터에서 current_user.positions로 투영하며, 이 중 하나를 쓴 행은 해당 ID의 할당이 아닙니다. 다른 이름을 고르세요." + } + } + } +}; diff --git a/packages/plugins/plugin-security/src/translations/ko.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/ko.source-hashes.generated.ts new file mode 100644 index 00000000000..5ec3ebc3496 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/ko.source-hashes.generated.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'ko'. Do not hand-edit. + * + * Each entry is the digest of the SOURCE REVISION that this locale's leaf at + * that path is still a byte copy of — provenance for the generated half of the + * bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead. + * + * An entry exists only while the leaf IS such a copy. Re-translate the leaf in + * `.objects.generated.ts` and the next extract drops its entry by + * itself — the table makes no claim about text a translator wrote. A path with + * no entry is LEGACY-TRUSTED and never reported stale. + * + * ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest + * records that the current text was copied from the current source, which is + * the false claim the mechanism exists to detect. Fix the TRANSLATION. + */ + +export const koGeneratedSourceHashes: Readonly> = { +}; diff --git a/packages/plugins/plugin-security/src/translations/permission-sets.ts b/packages/plugins/plugin-security/src/translations/permission-sets.ts index 97aad9a1f0a..3fc77618167 100644 --- a/packages/plugins/plugin-security/src/translations/permission-sets.ts +++ b/packages/plugins/plugin-security/src/translations/permission-sets.ts @@ -11,7 +11,7 @@ * each set's own `label` and `description` in * `../objects/default-permission-sets.ts`; `en` needs no entry, because an * untranslated locale serves the set's own text. When a source label or - * sentence changes, re-translate its three entries here in the same change: + * sentence changes, re-translate its entry in every locale here in the same change: * nothing marks a translation stale (no provenance table judges this section). * `permission-set-descriptions.test.ts` pins one label and one description per * built-in set per locale. @@ -141,3 +141,276 @@ export const esESPermissionSets: PermissionSetTranslations = { description: 'Asistentes de IA que un usuario conecta sin acceso a datos: ejecutan las acciones permitidas, pero no leen ni cambian ningún registro.', }, }; + +export const zhHantPermissionSets: PermissionSetTranslations = { + admin_full_access: { + label: '管理員 — 完整存取', + description: '平台維運人員:可讀取並變更所有組織的每一筆記錄、管理全部使用者與設定,並在 Studio 中建置應用程式。', + }, + organization_admin: { + label: '組織管理員', + description: '組織的擁有者與管理員:可在設定中全面管理本組織的記錄與成員,但不能建置應用程式或變更平台層級設定。', + }, + organization_admin_no_bypass: { + label: '組織管理員(不略過記錄權限)', + description: '組織之間未設隔離牆之部署中的組織管理員:管理權限相同,但只能存取其擁有權與共用範圍內的記錄。', + }, + member_default: { + label: '成員 — 標準存取', + description: '每位已登入成員的基本權限:本人的個人資料、金鑰、收件匣與成員目錄;業務記錄的存取來自其他權限集。', + }, + viewer_readonly: { + label: '檢視者 — 唯讀', + description: '唯讀使用者:在共用允許的範圍內檢視所有物件的記錄,但不能建立、編輯或刪除任何內容。', + }, + external_default: { + label: '外部成員 — 僅限本人記錄', + description: '組織外部人員(例如客戶或供應商):僅限本人的個人資料與記錄,以及與其共用的內容。', + }, + mcp_agent_data_read: { + label: 'MCP AI 助理 — 唯讀', + description: '使用者以讀取權限連接的 AI 助理:可讀取該使用者能讀取的內容,不做任何變更。', + }, + mcp_agent_data_write: { + label: 'MCP AI 助理 — 讀寫', + description: '使用者以讀寫權限連接的 AI 助理:可讀取並變更該使用者能存取的內容,但使用者與登入資料除外。', + }, + mcp_agent_restricted: { + label: 'MCP AI 助理 — 無資料權限', + description: '使用者在無資料權限下連接的 AI 助理:可執行已核准的動作,但不能讀取或變更任何記錄。', + }, +}; + +export const arPermissionSets: PermissionSetTranslations = { + admin_full_access: { + label: 'المسؤول — وصول كامل', + description: 'مشغّلو المنصة: يقرؤون كل سجل في كل مؤسسة ويغيّرونه، ويديرون جميع المستخدمين والإعدادات، ويبنون التطبيقات في Studio.', + }, + organization_admin: { + label: 'مسؤول المؤسسة', + description: 'مالكو المؤسسة ومسؤولوها: تحكّم كامل في سجلات مؤسستهم وأعضائها من الإعداد، دون بناء التطبيقات أو تغيير إعدادات المنصة.', + }, + organization_admin_no_bypass: { + label: 'مسؤول المؤسسة (دون تجاوز أذونات السجلات)', + description: 'مسؤولو المؤسسات في نشر بلا جدار بين المؤسسات: الإدارة نفسها، لكن فقط السجلات التي تتيحها ملكيتهم والمشاركة.', + }, + member_default: { + label: 'عضو — وصول قياسي', + description: 'الأساس لكل عضو مسجّل الدخول: ملفه الشخصي ومفاتيحه وصندوق وارده ودليل الأعضاء؛ ويأتي الوصول إلى سجلات الأعمال من مجموعات أذونات أخرى.', + }, + viewer_readonly: { + label: 'مُشاهد — قراءة فقط', + description: 'مستخدمو القراءة فقط: يقرؤون سجلات كل الكائنات بالقدر الذي تسمح به المشاركة، ولا ينشئون أو يحررون أو يحذفون أي شيء.', + }, + external_default: { + label: 'عضو خارجي — سجلاته فقط', + description: 'أشخاص من خارج المؤسسة، مثل العملاء أو المورّدين: ملفهم الشخصي وسجلاتهم فقط، وما يُشارَك معهم.', + }, + mcp_agent_data_read: { + label: 'مساعد MCP للذكاء الاصطناعي — قراءة فقط', + description: 'مساعدو الذكاء الاصطناعي الذين يربطهم المستخدم بصلاحية القراءة: يقرؤون ما يستطيع ذلك المستخدم قراءته ولا يغيّرون شيئًا.', + }, + mcp_agent_data_write: { + label: 'مساعد MCP للذكاء الاصطناعي — قراءة وكتابة', + description: 'مساعدو الذكاء الاصطناعي الذين يربطهم المستخدم بصلاحية القراءة والكتابة: يقرؤون ويغيّرون ما يستطيعه ذلك المستخدم، باستثناء بيانات المستخدمين وتسجيل الدخول.', + }, + mcp_agent_restricted: { + label: 'مساعد MCP للذكاء الاصطناعي — دون وصول إلى البيانات', + description: 'مساعدو الذكاء الاصطناعي الذين يربطهم المستخدم دون وصول إلى البيانات: ينفّذون الإجراءات المسموح بها، لكن لا يقرؤون أي سجل ولا يغيّرونه.', + }, +}; + +export const frPermissionSets: PermissionSetTranslations = { + admin_full_access: { + label: 'Administrateur — Accès complet', + description: 'Opérateurs de la plateforme : lisent et modifient tout enregistrement de toute organisation, gèrent utilisateurs et réglages et créent des apps dans Studio.', + }, + organization_admin: { + label: 'Administrateur d’organisation', + description: 'Propriétaires et administrateurs d’une organisation : contrôle total de ses enregistrements et membres dans Configuration, sans apps ni réglages globaux.', + }, + organization_admin_no_bypass: { + label: 'Administrateur d’organisation (sans contourner les droits d’enregistrement)', + description: 'Administrateurs d’organisation sans cloison entre organisations : la même administration, mais seuls les enregistrements ouverts par la propriété et le partage.', + }, + member_default: { + label: 'Membre — Accès standard', + description: 'Socle de tout membre connecté : son profil, ses clés, sa boîte de réception et l’annuaire ; les enregistrements métier viennent d’autres ensembles.', + }, + viewer_readonly: { + label: 'Lecteur — Lecture seule', + description: 'Utilisateurs en lecture seule : lisent les enregistrements de tous les objets dans la limite du partage, sans rien créer, modifier ni supprimer.', + }, + external_default: { + label: 'Membre externe — Ses propres enregistrements', + description: 'Personnes extérieures à l’organisation, comme clients ou fournisseurs : leur profil et leurs enregistrements, et seulement ce qui est partagé avec elles.', + }, + mcp_agent_data_read: { + label: 'Assistant IA MCP — Lecture seule', + description: 'Assistants IA qu’un utilisateur connecte avec un accès en lecture : lisent ce que cet utilisateur peut lire et ne modifient rien.', + }, + mcp_agent_data_write: { + label: 'Assistant IA MCP — Lecture et écriture', + description: 'Assistants IA qu’un utilisateur connecte en lecture et écriture : lisent et modifient ce que cet utilisateur peut, sauf données d’utilisateur et de connexion.', + }, + mcp_agent_restricted: { + label: 'Assistant IA MCP — Sans accès aux données', + description: 'Assistants IA qu’un utilisateur connecte sans accès aux données : exécutent les actions autorisées, mais ne lisent ni ne modifient aucun enregistrement.', + }, +}; + +export const ptBRPermissionSets: PermissionSetTranslations = { + admin_full_access: { + label: 'Administrador — Acesso total', + description: 'Operadores da plataforma: leem e alteram todos os registros de todas as organizações, gerenciam todos os usuários e configurações e criam aplicativos no Studio.', + }, + organization_admin: { + label: 'Administrador da organização', + description: 'Proprietários e administradores de uma organização: controle total dos registros e membros dela na Configuração, sem criar apps nem alterar a plataforma.', + }, + organization_admin_no_bypass: { + label: 'Administrador da organização (sem ignorar permissões de registro)', + description: 'Administradores de organização sem barreira entre organizações: a mesma administração, mas só os registros liberados por propriedade e compartilhamento.', + }, + member_default: { + label: 'Membro — Acesso padrão', + description: 'Base de todo membro conectado: o próprio perfil, chaves, caixa de entrada e o diretório de membros; o acesso aos registros de negócio vem de outros conjuntos.', + }, + viewer_readonly: { + label: 'Leitor — Somente leitura', + description: 'Usuários somente leitura: leem registros de todos os objetos até onde o compartilhamento permite, mas não criam, editam nem excluem nada.', + }, + external_default: { + label: 'Membro externo — Somente os próprios registros', + description: 'Pessoas de fora da organização, como clientes ou fornecedores: o próprio perfil e registros, e só o que é compartilhado com elas.', + }, + mcp_agent_data_read: { + label: 'Assistente de IA MCP — Somente leitura', + description: 'Assistentes de IA que um usuário conecta com acesso de leitura: leem o que esse usuário pode ler e não alteram nada.', + }, + mcp_agent_data_write: { + label: 'Assistente de IA MCP — Leitura e gravação', + description: 'Assistentes de IA que um usuário conecta com leitura e gravação: leem e alteram o que esse usuário pode, exceto dados de usuário e de login.', + }, + mcp_agent_restricted: { + label: 'Assistente de IA MCP — Sem acesso a dados', + description: 'Assistentes de IA que um usuário conecta sem acesso a dados: executam as ações permitidas, mas não leem nem alteram nenhum registro.', + }, +}; + +export const ruPermissionSets: PermissionSetTranslations = { + admin_full_access: { + label: 'Администратор — полный доступ', + description: 'Операторы платформы: читают и изменяют каждую запись во всех организациях, управляют всеми пользователями и настройками и создают приложения в Studio.', + }, + organization_admin: { + label: 'Администратор организации', + description: 'Владельцы и администраторы организации: полное управление её записями и участниками в настройках, без создания приложений и изменения настроек платформы.', + }, + organization_admin_no_bypass: { + label: 'Администратор организации (без обхода прав на записи)', + description: 'Администраторы организаций в развёртывании без стены между организациями: то же администрирование, но только записи, открытые владением и общим доступом.', + }, + member_default: { + label: 'Участник — стандартный доступ', + description: 'Основа для каждого вошедшего участника: его профиль, ключи, входящие и каталог участников; доступ к бизнес-записям дают другие наборы.', + }, + viewer_readonly: { + label: 'Наблюдатель — только чтение', + description: 'Пользователи только для чтения: читают записи всех объектов в пределах общего доступа, но ничего не создают, не изменяют и не удаляют.', + }, + external_default: { + label: 'Внешний участник — только свои записи', + description: 'Люди вне организации, например клиенты или поставщики: только их профиль и записи, а также то, чем с ними поделились.', + }, + mcp_agent_data_read: { + label: 'ИИ-помощник MCP — только чтение', + description: 'ИИ-помощники, подключённые пользователем с доступом на чтение: читают то, что может читать этот пользователь, и ничего не меняют.', + }, + mcp_agent_data_write: { + label: 'ИИ-помощник MCP — чтение и запись', + description: 'ИИ-помощники, подключённые пользователем с доступом на чтение и запись: читают и изменяют то, что может этот пользователь, кроме данных пользователей и входа.', + }, + mcp_agent_restricted: { + label: 'ИИ-помощник MCP — без доступа к данным', + description: 'ИИ-помощники, подключённые пользователем без доступа к данным: выполняют разрешённые действия, но не читают и не изменяют никаких записей.', + }, +}; + +export const dePermissionSets: PermissionSetTranslations = { + admin_full_access: { + label: 'Administrator — Vollzugriff', + description: 'Plattformbetreiber: lesen und ändern jeden Datensatz in allen Organisationen, verwalten alle Benutzer und Einstellungen und erstellen Apps in Studio.', + }, + organization_admin: { + label: 'Organisationsadministrator', + description: 'Eigentümer und Admins einer Organisation: volle Kontrolle über ihre Datensätze und Mitglieder im Setup, ohne Apps zu bauen oder die Plattform zu ändern.', + }, + organization_admin_no_bypass: { + label: 'Organisationsadministrator (ohne Umgehung der Datensatzrechte)', + description: 'Organisationsadministratoren ohne Trennwand zwischen Organisationen: dieselbe Verwaltung, aber nur die Datensätze, die Eigentum und Freigabe öffnen.', + }, + member_default: { + label: 'Mitglied — Standardzugriff', + description: 'Grundlage jedes angemeldeten Mitglieds: eigenes Profil, Schlüssel, Posteingang und Mitgliederverzeichnis; Geschäftsdatensätze kommen aus anderen Sätzen.', + }, + viewer_readonly: { + label: 'Betrachter — Nur Lesen', + description: 'Nur-Lese-Benutzer: lesen Datensätze aller Objekte, soweit die Freigabe es erlaubt, erstellen, bearbeiten oder löschen aber nichts.', + }, + external_default: { + label: 'Externes Mitglied — Nur eigene Datensätze', + description: 'Personen außerhalb der Organisation, etwa Kunden oder Lieferanten: das eigene Profil und die eigenen Datensätze sowie nur das, was mit ihnen geteilt wird.', + }, + mcp_agent_data_read: { + label: 'MCP-KI-Assistent — Nur Lesen', + description: 'KI-Assistenten, die ein Benutzer mit Lesezugriff verbindet: lesen, was dieser Benutzer lesen darf, und ändern nichts.', + }, + mcp_agent_data_write: { + label: 'MCP-KI-Assistent — Lesen und Schreiben', + description: 'KI-Assistenten, die ein Benutzer mit Lese- und Schreibzugriff verbindet: lesen und ändern, was dieser Benutzer darf, außer Benutzer- und Anmeldedaten.', + }, + mcp_agent_restricted: { + label: 'MCP-KI-Assistent — Ohne Datenzugriff', + description: 'KI-Assistenten, die ein Benutzer ohne Datenzugriff verbindet: führen erlaubte Aktionen aus, lesen oder ändern aber keinen Datensatz.', + }, +}; + +export const koPermissionSets: PermissionSetTranslations = { + admin_full_access: { + label: '관리자 — 전체 접근', + description: '플랫폼 운영자: 모든 조직의 모든 레코드를 읽고 변경하며, 전체 사용자와 설정을 관리하고, Studio에서 앱을 만듭니다.', + }, + organization_admin: { + label: '조직 관리자', + description: '조직의 소유자와 관리자: 설정에서 자기 조직의 레코드와 구성원을 모두 관리하지만, 앱을 만들거나 플랫폼 설정을 바꿀 수는 없습니다.', + }, + organization_admin_no_bypass: { + label: '조직 관리자(레코드 권한 우회 없음)', + description: '조직 간 벽이 없는 배포의 조직 관리자: 관리 권한은 같지만, 소유권과 공유로 열린 레코드만 다룹니다.', + }, + member_default: { + label: '구성원 — 표준 접근', + description: '로그인한 모든 구성원의 기본 권한으로 본인의 프로필, 키, 받은편지함, 구성원 디렉터리를 다루며, 업무 레코드 접근은 다른 권한 세트에서 받습니다.', + }, + viewer_readonly: { + label: '보기 전용 — 읽기 전용', + description: '읽기 전용 사용자: 공유가 허용하는 범위에서 모든 객체의 레코드를 읽지만, 아무것도 만들거나 편집하거나 삭제하지 않습니다.', + }, + external_default: { + label: '외부 구성원 — 본인 레코드만', + description: '고객이나 공급업체처럼 조직 외부의 사람: 본인의 프로필과 레코드, 그리고 그들과 공유된 것만 다룹니다.', + }, + mcp_agent_data_read: { + label: 'MCP AI 도우미 — 읽기 전용', + description: '사용자가 읽기 권한으로 연결한 AI 도우미: 그 사용자가 읽을 수 있는 것을 읽고, 아무것도 바꾸지 않습니다.', + }, + mcp_agent_data_write: { + label: 'MCP AI 도우미 — 읽기/쓰기', + description: '사용자가 읽기/쓰기 권한으로 연결한 AI 도우미: 그 사용자가 다룰 수 있는 것을 읽고 바꾸지만, 사용자 및 로그인 데이터는 제외합니다.', + }, + mcp_agent_restricted: { + label: 'MCP AI 도우미 — 데이터 접근 없음', + description: '사용자가 데이터 접근 없이 연결한 AI 도우미: 허용된 작업은 실행하지만, 어떤 레코드도 읽거나 바꾸지 않습니다.', + }, +}; diff --git a/packages/plugins/plugin-security/src/translations/position-rename-consistency.test.ts b/packages/plugins/plugin-security/src/translations/position-rename-consistency.test.ts index 661b3c29afc..7fe14eaa57f 100644 --- a/packages/plugins/plugin-security/src/translations/position-rename-consistency.test.ts +++ b/packages/plugins/plugin-security/src/translations/position-rename-consistency.test.ts @@ -32,6 +32,12 @@ import { enObjects } from './en.objects.generated.js'; import { zhCNObjects } from './zh-CN.objects.generated.js'; import { jaJPObjects } from './ja-JP.objects.generated.js'; import { esESObjects } from './es-ES.objects.generated.js'; +import { zhHantObjects } from './zh-Hant.objects.generated.js'; +import { frObjects } from './fr.objects.generated.js'; +import { ptBRObjects } from './pt-BR.objects.generated.js'; +import { ruObjects } from './ru.objects.generated.js'; +import { deObjects } from './de.objects.generated.js'; +import { koObjects } from './ko.objects.generated.js'; /** * The objects ADR-0090 P1 renamed that still exist — the exact surface the @@ -47,6 +53,12 @@ const LOCALES = [ { name: 'es-ES', objs: esESObjects as Record, stale: /\brol(es)?\b/i }, { name: 'zh-CN', objs: zhCNObjects as Record, stale: /角色/ }, { name: 'ja-JP', objs: jaJPObjects as Record, stale: /ロール/ }, + { name: 'zh-Hant', objs: zhHantObjects as Record, stale: /角色/ }, + { name: 'fr', objs: frObjects as Record, stale: /\brôles?\b/i }, + { name: 'pt-BR', objs: ptBRObjects as Record, stale: /\bpap(el|éis)\b/i }, + { name: 'ru', objs: ruObjects as Record, stale: /рол(ь|и|ей|ям|ями|ях)(?![а-яё])/i }, + { name: 'de', objs: deObjects as Record, stale: /\bRollen?\b/ }, + { name: 'ko', objs: koObjects as Record, stale: /역할/ }, ]; /** Every string leaf under `node`, as `[dottedPath, value]`. */ diff --git a/packages/plugins/plugin-security/src/translations/pt-BR.objects.generated.ts b/packages/plugins/plugin-security/src/translations/pt-BR.objects.generated.ts new file mode 100644 index 00000000000..542baa3be15 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/pt-BR.objects.generated.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'pt-BR'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const ptBRObjects: NonNullable = { + sys_user_permission_set: { + label: "Conjunto de permissões do usuário", + pluralLabel: "Conjuntos de permissões dos usuários", + description: "Atribuição direta de um conjunto de permissões a um usuário (opcionalmente restrita a uma organização).", + fields: { + id: { + label: "ID da atribuição", + help: "UUID da atribuição." + }, + display_title: { + label: "Título", + help: "Título do registro: o usuário e o conjunto de permissões atribuído a ele (calculado na leitura)" + }, + user_id: { + label: "Usuário", + help: "O usuário que recebe este conjunto de permissões." + }, + permission_set: { + label: "Conjunto de permissões", + help: "O conjunto de permissões que esta atribuição dá ao usuário. Escolha um dos conjuntos definidos na Configuração; um nome que não corresponda a nenhum conjunto é recusado." + }, + organization_id: { + label: "Organização", + help: "A organização à qual esta concessão pertence." + }, + granted_by: { + label: "Concedido por", + help: "O usuário que concedeu este conjunto de permissões." + }, + valid_from: { + label: "Válido a partir de", + help: "A atribuição entra em vigor neste momento. Deixe em branco para valer imediatamente. Ela começa na hora, sem esperar nenhuma tarefa agendada." + }, + valid_until: { + label: "Válido até", + help: "A atribuição termina neste momento (UTC). Deixe em branco para não ter data de término. Obrigatório em acesso de emergência e em atribuição a um agente de IA. Ela termina na hora, sem esperar nenhuma tarefa agendada." + }, + reason: { + label: "Motivo", + help: "Por que esta atribuição existe. Obrigatório em atribuições delegadas e de acesso de emergência. Para um agente de IA, informe a tarefa para a qual ele recebeu acesso." + }, + last_certified_at: { + label: "Última certificação em", + help: "Ainda não usado: reservado para um futuro recurso de revisão de acesso. Nada preenche nem verifica esta data, então um valor vazio não significa que a atribuição deixou de ser revisada." + }, + certified_by: { + label: "Certificado por", + help: "Ainda não usado: reservado para um futuro recurso de revisão de acesso. Um revisor informado aqui não é verificado e não muda nada do que a atribuição concede." + }, + created_at: { + label: "Criado em" + }, + updated_at: { + label: "Atualizado em" + } + }, + _validations: { + validity_window_order: { + message: "\"Válido até\" precisa ser posterior a \"Válido a partir de\". A concessão vale de \"Válido a partir de\" até \"Válido até\", sem incluí-lo, então uma janela que termina no início ou antes dele não concede nada. Deixe qualquer um em branco para uma janela aberta." + } + } + }, + sys_user_position: { + label: "Cargo do usuário", + pluralLabel: "Cargos dos usuários", + description: "Quais cargos cada usuário ocupa, uma linha por atribuição.", + fields: { + id: { + label: "ID da atribuição", + help: "UUID da atribuição usuário-cargo." + }, + display_title: { + label: "Título", + help: "Título do registro: o usuário e o cargo que ele ocupa (calculado na leitura)" + }, + user_id: { + label: "Usuário", + help: "O usuário que ocupa este cargo." + }, + position: { + label: "Cargo", + help: "Nome de máquina do cargo, resolvido no catálogo de segurança." + }, + business_unit_id: { + label: "Unidade de negócios", + help: "Âncora de UN no nível da atribuição: onde esta atribuição de cargo se aplica. Âncora de profundidade para readScope/writeScope, limite da administração delegada e fato de auditoria. Nulo = sem âncora (legado / todo o locatário); administradores delegados DEVEM ancorar as atribuições dentro da sua subárvore." + }, + organization_id: { + label: "Organização", + help: "A organização à qual esta atribuição pertence." + }, + granted_by: { + label: "Concedido por", + help: "O usuário que concedeu esta atribuição de cargo (registrado pelo controle de administração delegada nas gravações delegadas)." + }, + valid_from: { + label: "Válido a partir de", + help: "A atribuição entra em vigor neste momento. Deixe em branco para valer imediatamente. Ela começa na hora, sem esperar nenhuma tarefa agendada." + }, + valid_until: { + label: "Válido até", + help: "A atribuição termina neste momento (UTC). Deixe em branco para não ter data de término. Obrigatório em uma atribuição delegada. Ela termina na hora, sem esperar nenhuma tarefa agendada." + }, + reason: { + label: "Motivo", + help: "Por que esta concessão existe. Texto livre; OBRIGATÓRIO em linhas de delegação e de acesso de emergência (quebra de vidro)." + }, + delegated_from: { + label: "Delegado por", + help: "O delegante cuja autoridade esta linha carrega (职务代理). Uma linha com delegated_from preenchido não é ela mesma delegável nem autorrenovável — as cadeias são cortadas nos dois sentidos." + }, + last_certified_at: { + label: "Última certificação em", + help: "Ainda não usado: reservado para um futuro recurso de revisão de acesso. Nada preenche nem verifica esta data, então um valor vazio não significa que a atribuição deixou de ser revisada." + }, + certified_by: { + label: "Certificado por", + help: "Ainda não usado: reservado para um futuro recurso de revisão de acesso. Um revisor informado aqui não é verificado e não muda nada do que a atribuição concede." + }, + created_at: { + label: "Criado em" + }, + updated_at: { + label: "Atualizado em" + } + }, + _validations: { + reserved_identity_position: { + message: "\"position\" não pode usar um nome de identidade integrada reservado pelo framework (platform_admin, org_owner, org_admin, org_member, org_external). Esses nomes são identidades integradas: a plataforma os projeta em current_user.positions a partir das próprias fontes de verdade, e uma linha que usa um deles não é uma atribuição dessa identidade. Escolha outro nome." + } + } + } +}; diff --git a/packages/plugins/plugin-security/src/translations/pt-BR.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/pt-BR.source-hashes.generated.ts new file mode 100644 index 00000000000..8dc6d7a795a --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/pt-BR.source-hashes.generated.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'pt-BR'. Do not hand-edit. + * + * Each entry is the digest of the SOURCE REVISION that this locale's leaf at + * that path is still a byte copy of — provenance for the generated half of the + * bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead. + * + * An entry exists only while the leaf IS such a copy. Re-translate the leaf in + * `.objects.generated.ts` and the next extract drops its entry by + * itself — the table makes no claim about text a translator wrote. A path with + * no entry is LEGACY-TRUSTED and never reported stale. + * + * ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest + * records that the current text was copied from the current source, which is + * the false claim the mechanism exists to detect. Fix the TRANSLATION. + */ + +export const ptBRGeneratedSourceHashes: Readonly> = { +}; diff --git a/packages/plugins/plugin-security/src/translations/ru.objects.generated.ts b/packages/plugins/plugin-security/src/translations/ru.objects.generated.ts new file mode 100644 index 00000000000..883b34ecd48 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/ru.objects.generated.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'ru'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const ruObjects: NonNullable = { + sys_user_permission_set: { + label: "Набор разрешений пользователя", + pluralLabel: "Наборы разрешений пользователей", + description: "Прямое назначение набора разрешений пользователю (при необходимости — в пределах организации).", + fields: { + id: { + label: "ID назначения", + help: "UUID назначения." + }, + display_title: { + label: "Заголовок", + help: "Заголовок записи: пользователь и назначенный ему набор разрешений (вычисляется при чтении)" + }, + user_id: { + label: "Пользователь", + help: "Пользователь, получающий этот набор разрешений." + }, + permission_set: { + label: "Набор разрешений", + help: "Набор разрешений, который это назначение даёт пользователю. Выберите один из наборов, определённых в настройках; имя, не совпадающее ни с одним набором, отклоняется." + }, + organization_id: { + label: "Организация", + help: "Организация, к которой относится это предоставление." + }, + granted_by: { + label: "Кем выдано", + help: "Пользователь, выдавший этот набор разрешений." + }, + valid_from: { + label: "Действует с", + help: "Назначение вступает в силу в это время. Оставьте пустым, чтобы оно действовало сразу. Начинается точно в срок, без ожидания плановых заданий." + }, + valid_until: { + label: "Действует до", + help: "Назначение заканчивается в это время (UTC). Оставьте пустым, чтобы не задавать дату окончания. Обязательно для экстренного доступа и для назначения агенту ИИ. Заканчивается точно в срок, без ожидания плановых заданий." + }, + reason: { + label: "Причина", + help: "Зачем существует это назначение. Обязательно для делегированных назначений и экстренного доступа. Для агента ИИ укажите задачу, ради которой ему дан доступ." + }, + last_certified_at: { + label: "Последняя сертификация", + help: "Пока не используется: зарезервировано для будущей функции проверки доступа. Эту дату ничто не заполняет и не проверяет, поэтому пустое значение не означает, что назначение не проверялось." + }, + certified_by: { + label: "Кем сертифицировано", + help: "Пока не используется: зарезервировано для будущей функции проверки доступа. Указанный здесь проверяющий не проверяется и ничего не меняет в том, что даёт назначение." + }, + created_at: { + label: "Создано" + }, + updated_at: { + label: "Обновлено" + } + }, + _validations: { + validity_window_order: { + message: "«Действует до» должно быть позже, чем «Действует с». Предоставление действует с «Действует с» до «Действует до», не включая его, поэтому окно, которое заканчивается в момент начала или раньше, ничего не даёт. Оставьте любое из полей пустым для открытого окна." + } + } + }, + sys_user_position: { + label: "Должность пользователя", + pluralLabel: "Должности пользователей", + description: "Какие должности занимает каждый пользователь, по одной строке на назначение.", + fields: { + id: { + label: "ID назначения", + help: "UUID назначения «пользователь — должность»." + }, + display_title: { + label: "Заголовок", + help: "Заголовок записи: пользователь и занимаемая им должность (вычисляется при чтении)" + }, + user_id: { + label: "Пользователь", + help: "Пользователь, занимающий эту должность." + }, + position: { + label: "Должность", + help: "Машинное имя должности, разрешается в каталоге безопасности." + }, + business_unit_id: { + label: "Бизнес-подразделение", + help: "Привязка к подразделению на уровне назначения: где действует это назначение должности. Якорь глубины для readScope/writeScope, граница делегированного администрирования и факт аудита. Null = без привязки (устаревшее / на весь арендатор); делегированные администраторы ОБЯЗАНЫ привязывать назначения внутри своего поддерева." + }, + organization_id: { + label: "Организация", + help: "Организация, к которой относится это назначение." + }, + granted_by: { + label: "Кем выдано", + help: "Пользователь, выдавший это назначение должности (проставляется шлюзом делегированного администрирования при делегированной записи)." + }, + valid_from: { + label: "Действует с", + help: "Назначение вступает в силу в это время. Оставьте пустым, чтобы оно действовало сразу. Начинается точно в срок, без ожидания плановых заданий." + }, + valid_until: { + label: "Действует до", + help: "Назначение заканчивается в это время (UTC). Оставьте пустым, чтобы не задавать дату окончания. Обязательно для делегированного назначения. Заканчивается точно в срок, без ожидания плановых заданий." + }, + reason: { + label: "Причина", + help: "Зачем существует это предоставление. Свободный текст; ОБЯЗАТЕЛЬНО для строк делегирования и экстренного доступа." + }, + delegated_from: { + label: "Делегировано от", + help: "Делегирующий, чьи полномочия несёт эта строка (职务代理). Строку с заполненным delegated_from нельзя делегировать дальше и продлевать самой себе — цепочки обрываются в обе стороны." + }, + last_certified_at: { + label: "Последняя сертификация", + help: "Пока не используется: зарезервировано для будущей функции проверки доступа. Эту дату ничто не заполняет и не проверяет, поэтому пустое значение не означает, что назначение не проверялось." + }, + certified_by: { + label: "Кем сертифицировано", + help: "Пока не используется: зарезервировано для будущей функции проверки доступа. Указанный здесь проверяющий не проверяется и ничего не меняет в том, что даёт назначение." + }, + created_at: { + label: "Создано" + }, + updated_at: { + label: "Обновлено" + } + }, + _validations: { + reserved_identity_position: { + message: "«position» не может совпадать с зарезервированным фреймворком именем встроенной идентичности (platform_admin, org_owner, org_admin, org_member, org_external). Это встроенные идентичности: платформа проецирует их в current_user.positions из их собственных источников истины, и строка с таким именем не является назначением этой идентичности. Выберите другое имя." + } + } + } +}; diff --git a/packages/plugins/plugin-security/src/translations/ru.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/ru.source-hashes.generated.ts new file mode 100644 index 00000000000..0f794307ebc --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/ru.source-hashes.generated.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'ru'. Do not hand-edit. + * + * Each entry is the digest of the SOURCE REVISION that this locale's leaf at + * that path is still a byte copy of — provenance for the generated half of the + * bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead. + * + * An entry exists only while the leaf IS such a copy. Re-translate the leaf in + * `.objects.generated.ts` and the next extract drops its entry by + * itself — the table makes no claim about text a translator wrote. A path with + * no entry is LEGACY-TRUSTED and never reported stale. + * + * ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest + * records that the current text was copied from the current source, which is + * the false claim the mechanism exists to detect. Fix the TRANSLATION. + */ + +export const ruGeneratedSourceHashes: Readonly> = { +}; diff --git a/packages/plugins/plugin-security/src/translations/zh-Hant.objects.generated.ts b/packages/plugins/plugin-security/src/translations/zh-Hant.objects.generated.ts new file mode 100644 index 00000000000..df68b110afd --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/zh-Hant.objects.generated.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'zh-Hant'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const zhHantObjects: NonNullable = { + sys_user_permission_set: { + label: "使用者權限集", + pluralLabel: "使用者權限集", + description: "將權限集直接指派給使用者(可選擇限定於某個組織)。", + fields: { + id: { + label: "指派 ID", + help: "此指派的 UUID。" + }, + display_title: { + label: "標題", + help: "記錄標題:使用者及指派給他的權限集(讀取時計算)" + }, + user_id: { + label: "使用者", + help: "取得此權限集的使用者。" + }, + permission_set: { + label: "權限集", + help: "此指派授予使用者的權限集。請從設定中已定義的權限集選擇;不符合任何權限集的名稱會被拒絕。" + }, + organization_id: { + label: "組織", + help: "此授權所屬的組織。" + }, + granted_by: { + label: "授予者", + help: "授予此權限集的使用者。" + }, + valid_from: { + label: "生效時間", + help: "指派從此時間起生效。留空則立即生效。時間一到即生效,不需等待任何排程工作。" + }, + valid_until: { + label: "失效時間", + help: "指派在此時間(UTC)結束。留空則沒有結束日期。緊急存取與指派給 AI 代理的指派必須填寫。時間一到即結束,不需等待任何排程工作。" + }, + reason: { + label: "原因", + help: "此指派存在的原因。委派與緊急存取的指派必須填寫。指派給 AI 代理時,請寫明它為哪項工作取得存取權。" + }, + last_certified_at: { + label: "最近認證時間", + help: "尚未啟用:保留給未來的存取審查功能。目前沒有任何功能會填寫或檢查此日期,因此空值不代表該指派未經審查。" + }, + certified_by: { + label: "認證者", + help: "尚未啟用:保留給未來的存取審查功能。在此填寫的審查者不會經過驗證,也不會改變該指派授予的任何權限。" + }, + created_at: { + label: "建立時間" + }, + updated_at: { + label: "更新時間" + } + }, + _validations: { + validity_window_order: { + message: "「失效時間」必須晚於「生效時間」。授權從「生效時間」起生效,直到「失效時間」為止(不含),因此在開始之時或之前就結束的時段不會授予任何權限。任一欄留空即為不設限的時段。" + } + } + }, + sys_user_position: { + label: "使用者職位", + pluralLabel: "使用者職位", + description: "每位使用者擔任哪些職位,每筆指派一列。", + fields: { + id: { + label: "指派 ID", + help: "使用者—職位指派的 UUID。" + }, + display_title: { + label: "標題", + help: "記錄標題:使用者及其擔任的職位(讀取時計算)" + }, + user_id: { + label: "使用者", + help: "擔任此職位的使用者。" + }, + position: { + label: "職位", + help: "職位的機器名稱,在安全目錄中解析。" + }, + business_unit_id: { + label: "業務單位", + help: "指派層級的 BU 錨點:此職位指派適用的範圍。作為 readScope/writeScope 的深度錨點、委派管理邊界與稽核事實。空值 = 未錨定(舊有/租戶全域);委派管理員「必須」將指派錨定在自己的子樹內。" + }, + organization_id: { + label: "組織", + help: "此指派所屬的組織。" + }, + granted_by: { + label: "授予者", + help: "授予此職位指派的使用者(委派寫入時由委派管理閘門標記)。" + }, + valid_from: { + label: "生效時間", + help: "指派從此時間起生效。留空則立即生效。時間一到即生效,不需等待任何排程工作。" + }, + valid_until: { + label: "失效時間", + help: "指派在此時間(UTC)結束。留空則沒有結束日期。委派的指派必須填寫。時間一到即結束,不需等待任何排程工作。" + }, + reason: { + label: "原因", + help: "此授予存在的原因。自由文字;委派與緊急破窗列「必須」填寫。" + }, + delegated_from: { + label: "委派自", + help: "此列所承載權限的委派人(職務代理)。設定了 delegated_from 的列本身不可再委派、也不可自行續期——鏈條雙向切斷。" + }, + last_certified_at: { + label: "最近認證時間", + help: "尚未啟用:保留給未來的存取審查功能。目前沒有任何功能會填寫或檢查此日期,因此空值不代表該指派未經審查。" + }, + certified_by: { + label: "認證者", + help: "尚未啟用:保留給未來的存取審查功能。在此填寫的審查者不會經過驗證,也不會改變該指派授予的任何權限。" + }, + created_at: { + label: "建立時間" + }, + updated_at: { + label: "更新時間" + } + }, + _validations: { + reserved_identity_position: { + message: "「position」不能寫成框架保留的內建身分名稱(platform_admin、org_owner、org_admin、org_member、org_external)。這些名稱是內建身分:平台會從各自的資料來源將它們投射到 current_user.positions,寫成其中之一的列並不是該身分的指派。請改用其他名稱。" + } + } + } +}; diff --git a/packages/plugins/plugin-security/src/translations/zh-Hant.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/zh-Hant.source-hashes.generated.ts new file mode 100644 index 00000000000..bf521657f18 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/zh-Hant.source-hashes.generated.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'zh-Hant'. Do not hand-edit. + * + * Each entry is the digest of the SOURCE REVISION that this locale's leaf at + * that path is still a byte copy of — provenance for the generated half of the + * bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead. + * + * An entry exists only while the leaf IS such a copy. Re-translate the leaf in + * `.objects.generated.ts` and the next extract drops its entry by + * itself — the table makes no claim about text a translator wrote. A path with + * no entry is LEGACY-TRUSTED and never reported stale. + * + * ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest + * records that the current text was copied from the current source, which is + * the false claim the mechanism exists to detect. Fix the TRANSLATION. + */ + +export const zhHantGeneratedSourceHashes: Readonly> = { +};