From 63d63a1b2d8f4b0e75a9dabe4f5fd4bb5005690b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 01:43:59 +0000 Subject: [PATCH 1/2] i18n(plugin-approvals): ship the approval objects, notifications, record page and e-mail link line in zh-Hant, ar, fr and pt-BR Four new locales for sys_approval_request, sys_approval_action and sys_approval_delegation (generated and hashed by the repo's own extract), the approval notification messages, the record page's label and its Details / Timeline tabs, and the line that opens an approval from its e-mail (APPROVAL_EMAIL_LINK_LINES). Registered in index.ts under the platform-table tags (zh-CN before zh-Hant) and in the extract command + merge baseline. The e-mail templates' shape, the template selection and EMAIL_TEMPLATE_FLOOR_LOCALE are unchanged. The per-locale tests list the four new locales. Carried unchanged from the released handover commit 7db48bdd (written by session_01Utp6Cai5CRdAs4u9gTipWU). Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cya5ej5GQV86RPbRPK --- .../30512-plugin-approvals-locales-a.md | 10 + .../scripts/i18n-extract.config.ts | 10 +- .../src/admin-copy-vocabulary.test.ts | 6 +- .../src/approval-email-templates.ts | 6 +- .../src/sys-approval-request-page.test.ts | 2 +- .../src/sys-approval-request.page.ts | 6 +- .../approval-status-vocabulary.test.ts | 2 +- .../approve-decision-question.test.ts | 8 + .../src/translations/ar.objects.generated.ts | 396 ++++++++++++++++++ .../ar.source-hashes.generated.ts | 21 + .../decision-question-i18n.test.ts | 8 + .../src/translations/fr.objects.generated.ts | 396 ++++++++++++++++++ .../fr.source-hashes.generated.ts | 24 ++ .../src/translations/index.ts | 24 ++ .../src/translations/messages.ts | 180 +++++++- .../src/translations/pages.ts | 20 +- .../translations/pt-BR.objects.generated.ts | 396 ++++++++++++++++++ .../pt-BR.source-hashes.generated.ts | 22 + .../success-copy-names-request-i18n.test.ts | 8 + .../translations/zh-Hant.objects.generated.ts | 396 ++++++++++++++++++ .../zh-Hant.source-hashes.generated.ts | 21 + 21 files changed, 1950 insertions(+), 12 deletions(-) create mode 100644 .changeset/30512-plugin-approvals-locales-a.md create mode 100644 packages/plugins/plugin-approvals/src/translations/ar.objects.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/ar.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/fr.objects.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/fr.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/pt-BR.objects.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/pt-BR.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/zh-Hant.objects.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/zh-Hant.source-hashes.generated.ts diff --git a/.changeset/30512-plugin-approvals-locales-a.md b/.changeset/30512-plugin-approvals-locales-a.md new file mode 100644 index 00000000000..5263d3790d7 --- /dev/null +++ b/.changeset/30512-plugin-approvals-locales-a.md @@ -0,0 +1,10 @@ +--- +'@objectstack/plugin-approvals': patch +--- + +i18n(plugin-approvals): the approval objects, notifications, record page and e-mail link line ship in Traditional Chinese, Arabic, French and Brazilian Portuguese + +Clause-②: no +Metadata-protocol: none + +The approval objects (`sys_approval_request`, `sys_approval_action`, `sys_approval_delegation`), the approval notifications' sentences, the approval request record page (its label and its Details / Timeline tabs) and the line that opens an approval from its e-mail now carry `zh-Hant`, `ar`, `fr` and `pt-BR` beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so an approver or submitter in one of those languages reads the approval screens, notices and e-mails in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion, its page entry and its messages, and the package's extract command lists the tags, so the existing `check:i18n` bundle check covers them. The e-mail templates' shape, the template selection and `EMAIL_TEMPLATE_FLOOR_LOCALE` are unchanged; only the link line is added per locale. First half of package 9 of #30512; `ru`, `de` and `ko` follow in the second half. diff --git a/packages/plugins/plugin-approvals/scripts/i18n-extract.config.ts b/packages/plugins/plugin-approvals/scripts/i18n-extract.config.ts index 339b350ad19..ce1df91824e 100644 --- a/packages/plugins/plugin-approvals/scripts/i18n-extract.config.ts +++ b/packages/plugins/plugin-approvals/scripts/i18n-extract.config.ts @@ -8,7 +8,7 @@ * strings were seeded from @objectstack/platform-objects.) * * os i18n extract packages/plugins/plugin-approvals/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 --fill=default --objects-only --no-metadata-forms \ * --source-hashes \ * --out=packages/plugins/plugin-approvals/src/translations * @@ -60,6 +60,10 @@ 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 { approvalsPageTranslations } from '../src/translations/pages.js'; /** @@ -84,6 +88,10 @@ const config: ObjectStackDefinition = defineStack({ { 'zh-CN': { objects: zhCNObjects, pages: approvalsPageTranslations['zh-CN'] } }, { 'ja-JP': { objects: jaJPObjects, pages: approvalsPageTranslations['ja-JP'] } }, { 'es-ES': { objects: esESObjects, pages: approvalsPageTranslations['es-ES'] } }, + { 'zh-Hant': { objects: zhHantObjects, pages: approvalsPageTranslations['zh-Hant'] } }, + { ar: { objects: arObjects, pages: approvalsPageTranslations['ar'] } }, + { fr: { objects: frObjects, pages: approvalsPageTranslations['fr'] } }, + { 'pt-BR': { objects: ptBRObjects, pages: approvalsPageTranslations['pt-BR'] } }, ], }); diff --git a/packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts b/packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts index 842a35432e3..f4b892bb4ff 100644 --- a/packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts +++ b/packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts @@ -29,6 +29,10 @@ import { enObjects } from './translations/en.objects.generated.js'; import { zhCNObjects } from './translations/zh-CN.objects.generated.js'; import { jaJPObjects } from './translations/ja-JP.objects.generated.js'; import { esESObjects } from './translations/es-ES.objects.generated.js'; +import { zhHantObjects } from './translations/zh-Hant.objects.generated.js'; +import { arObjects } from './translations/ar.objects.generated.js'; +import { frObjects } from './translations/fr.objects.generated.js'; +import { ptBRObjects } from './translations/pt-BR.objects.generated.js'; /** The design-record vocabulary an admin-facing string must not carry. */ const DESIGN_RECORD_VOCABULARY: ReadonlyArray = [ @@ -93,7 +97,7 @@ function isObjectSchema(v: unknown): v is ObjectLike { && !!(v as ObjectLike).fields && typeof (v as ObjectLike).fields === 'object'; } -const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const; +const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects, 'zh-Hant': zhHantObjects, 'ar': arObjects, 'fr': frObjects, 'pt-BR': ptBRObjects } as const; describe('approval objects: admin-facing copy carries no design-record vocabulary', () => { it('the matcher recognises each kind of vocabulary it exists to keep out', () => { diff --git a/packages/plugins/plugin-approvals/src/approval-email-templates.ts b/packages/plugins/plugin-approvals/src/approval-email-templates.ts index 360b822e8d4..bb977f39d14 100644 --- a/packages/plugins/plugin-approvals/src/approval-email-templates.ts +++ b/packages/plugins/plugin-approvals/src/approval-email-templates.ts @@ -52,11 +52,15 @@ export const APPROVAL_EMAIL_CHANNELS: readonly string[] = Object.freeze(['inbox' * The line that opens the request, per locale the approval catalog carries * (`translations/messages.ts`). `{{approvalUrl}}` is the payload member. */ -export const APPROVAL_EMAIL_LINK_LINES: Readonly> = { +export const APPROVAL_EMAIL_LINK_LINES: Readonly> = { en: 'Open the approval: {{approvalUrl}}', 'zh-CN': '打开审批:{{approvalUrl}}', 'ja-JP': '承認を開く: {{approvalUrl}}', 'es-ES': 'Abrir la aprobación: {{approvalUrl}}', + 'zh-Hant': '開啟核准:{{approvalUrl}}', + ar: 'افتح الموافقة: {{approvalUrl}}', + fr: 'Ouvrir l’approbation : {{approvalUrl}}', + 'pt-BR': 'Abrir a aprovação: {{approvalUrl}}', }; /** The e-mail templates: every emailed topic in every catalog locale. */ diff --git a/packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts b/packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts index 0d88774f5e0..fc846fe04f4 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts @@ -308,7 +308,7 @@ describe('sys_approval_request detail page — its label in the translation bund it('every served locale carries the label, and a translated locale serves its translation', () => { const locales = Object.keys(ApprovalsTranslations).sort(); - expect(locales).toEqual(['en', 'es-ES', 'ja-JP', 'zh-CN']); + expect(locales).toEqual(['ar', 'en', 'es-ES', 'fr', 'ja-JP', 'pt-BR', 'zh-CN', 'zh-Hant']); for (const locale of locales) { const served = (ApprovalsTranslations as AnyRec)[locale]?.pages?.[NAME]?.label; const authored = (approvalsPageTranslations as AnyRec)[locale]?.[NAME]?.label; diff --git a/packages/plugins/plugin-approvals/src/sys-approval-request.page.ts b/packages/plugins/plugin-approvals/src/sys-approval-request.page.ts index ecca1cecd85..aa88e2e8bc8 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-request.page.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-request.page.ts @@ -122,12 +122,12 @@ export const SysApprovalRequestDetailPage: Page = { position: 'top', items: [ { - label: { en: 'Details', 'zh-CN': '详情', 'ja-JP': '詳細', 'es-ES': 'Detalles' }, + label: { en: 'Details', 'zh-CN': '详情', 'ja-JP': '詳細', 'es-ES': 'Detalles', 'zh-Hant': '詳細資料', 'ar': 'التفاصيل', 'fr': 'Détails', 'pt-BR': 'Detalhes' }, icon: 'file-text', children: [{ type: 'record:details' }], }, { - label: { en: 'Timeline', 'zh-CN': '时间线', 'ja-JP': 'タイムライン', 'es-ES': 'Cronología' }, + label: { en: 'Timeline', 'zh-CN': '时间线', 'ja-JP': 'タイムライン', 'es-ES': 'Cronología', 'zh-Hant': '時間軸', 'ar': 'المخطط الزمني', 'fr': 'Chronologie', 'pt-BR': 'Linha do tempo' }, icon: 'history', children: [ { @@ -139,7 +139,7 @@ export const SysApprovalRequestDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 50, showViewAll: true, - title: { en: 'Timeline', 'zh-CN': '时间线', 'ja-JP': 'タイムライン', 'es-ES': 'Cronología' }, + title: { en: 'Timeline', 'zh-CN': '时间线', 'ja-JP': 'タイムライン', 'es-ES': 'Cronología', 'zh-Hant': '時間軸', 'ar': 'المخطط الزمني', 'fr': 'Chronologie', 'pt-BR': 'Linha do tempo' }, }, }, ], diff --git a/packages/plugins/plugin-approvals/src/translations/approval-status-vocabulary.test.ts b/packages/plugins/plugin-approvals/src/translations/approval-status-vocabulary.test.ts index 03d5ca8e483..c8268e1d24f 100644 --- a/packages/plugins/plugin-approvals/src/translations/approval-status-vocabulary.test.ts +++ b/packages/plugins/plugin-approvals/src/translations/approval-status-vocabulary.test.ts @@ -115,7 +115,7 @@ describe('approval status vocabulary (#7232)', () => { // Ratchet for statuses and locales added later: a new entry that reaches a // bundle un-translated is seeded from the source text, which is the enum // value itself, so this case catches it without naming it. - for (const locale of ['en', 'zh-CN', 'ja-JP', 'es-ES']) { + for (const locale of ['en', 'zh-CN', 'ja-JP', 'es-ES', 'zh-Hant', 'ar', 'fr', 'pt-BR']) { const labels = resolvedStatusLabels(locale); expect(Object.keys(labels).sort()).toEqual([...APPROVAL_STATUSES].sort()); const raw = Object.entries(labels) diff --git a/packages/plugins/plugin-approvals/src/translations/approve-decision-question.test.ts b/packages/plugins/plugin-approvals/src/translations/approve-decision-question.test.ts index ffb8d755c81..f60c77aef19 100644 --- a/packages/plugins/plugin-approvals/src/translations/approve-decision-question.test.ts +++ b/packages/plugins/plugin-approvals/src/translations/approve-decision-question.test.ts @@ -22,6 +22,10 @@ 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 { SysApprovalRequest } from '../sys-approval-request.object.js'; interface CopyAction { @@ -45,6 +49,10 @@ const TRANSLATED = [ ['zh-CN', zhCNObjects], ['ja-JP', jaJPObjects], ['es-ES', esESObjects], + ['zh-Hant', zhHantObjects], + ['ar', arObjects], + ['fr', frObjects], + ['pt-BR', ptBRObjects], ] as const; describe('approval_approve — the decision question', () => { diff --git a/packages/plugins/plugin-approvals/src/translations/ar.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/ar.objects.generated.ts new file mode 100644 index 00000000000..74510e5cf45 --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/ar.objects.generated.ts @@ -0,0 +1,396 @@ +// 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_approval_request: { + label: "طلب الموافقة", + pluralLabel: "طلبات الموافقة", + description: "مثيل موافقة جارٍ يُتتبَّع لكل إرسال", + fields: { + id: { + label: "معرّف الطلب" + }, + display_title: { + label: "العنوان", + help: "عنوان السجل: تسمية التدفق الذي فتح هذا الطلب، وإلا فمصدره (يُحسب عند القراءة)" + }, + organization_id: { + label: "المؤسسة", + help: "مؤسسة السجل الذي يتعلق به هذا الطلب (وإن لم يكن للسجل مؤسسة، تُستخدم مؤسسة سياق التنفيذ)" + }, + process_name: { + label: "المصدر", + help: "مصدر الطلب — `flow:` للموافقات التي تقودها العُقد" + }, + process_label: { + label: "التدفق", + help: "تسمية التدفق الذي فتح هذا الطلب، كما كانت عند فتحه" + }, + object_name: { + label: "الكائن" + }, + record_id: { + label: "السجل" + }, + submitter_id: { + label: "مقدِّم الطلب" + }, + submitter_comment: { + label: "تعليق مقدِّم الطلب" + }, + status: { + label: "الحالة", + help: "حالة دورة حياة الطلب", + options: { + pending: "قيد الانتظار", + approved: "تمت الموافقة", + rejected: "مرفوض", + recalled: "مسحوب", + returned: "مُعاد للمراجعة", + cancelled: "ملغى" + } + }, + cancel_reason: { + label: "سبب الإلغاء", + help: "سبب إلغاء المنصة لهذا الطلب (يُضبط فقط عندما تكون الحالة «ملغى»)", + options: { + record_deleted: "حُذف السجل المرتبط" + } + }, + current_step: { + label: "الخطوة الحالية", + help: "الاسم الآلي للخطوة التي تنتظر الموافقة" + }, + current_step_label: { + label: "خطوة الموافقة", + help: "تسمية الخطوة التي تنتظر الموافقة، كما كانت عند وصول الطلب إليها" + }, + current_step_index: { + label: "فهرس الخطوة الحالية" + }, + pending_approvers: { + label: "الموافِقون المنتظَرون", + help: "معرّفات المستخدمين القادرين على التصرف في الخطوة الحالية، مفصولة بفواصل" + }, + pending_approver_labels: { + label: "بانتظار", + help: "مَن ينتظره الطلب: اسم كل موافِق منتظَر، أو عنوان الخانة حين لا تسمّي شخصًا" + }, + payload_json: { + label: "لقطة", + help: "لقطة السجل وقت الإرسال" + }, + flow_run_id: { + label: "تشغيل التدفق", + help: "معرّف تشغيل الأتمتة المعلَّق الذي يضبطه هذا الطلب. يستأنفه القرار." + }, + flow_node_id: { + label: "عقدة التدفق", + help: "معرّف عقدة الموافقة ضمن التدفق الذي فتح هذا الطلب." + }, + node_config_json: { + label: "إعداد العقدة", + help: "لقطة من إعداد عقدة الموافقة (الموافِقون/السلوك) للطلبات التي تقودها العُقد." + }, + completed_at: { + label: "تاريخ الإكمال" + }, + created_at: { + label: "تاريخ الإنشاء" + }, + updated_at: { + label: "تاريخ التحديث" + } + }, + _views: { + all_requests: { + label: "الكل" + }, + my_pending: { + label: "بانتظار موافقتي", + emptyState: { + title: "لا توجد موافقات معلّقة", + message: "لقد أنجزت كل شيء." + } + }, + submitted_by_me: { + label: "التي أرسلتُها" + }, + completed: { + label: "المكتملة" + } + }, + _actions: { + approval_approve: { + label: "موافقة", + description: "هل توافق على هذا الطلب؟ تُسجَّل موافقتك، وينتقل الطلب حين تحصل هذه الخطوة على الموافقات المطلوبة.", + successMessage: "تمت الموافقة على «{{ result.request.record_title }}»", + params: { + comment: { + label: "تعليق" + }, + attachments: { + label: "المرفقات" + } + } + }, + approval_reject: { + label: "رفض", + description: "هل ترفض هذا الطلب؟ الرفض نهائي لكل الموافِقين.", + successMessage: "تم رفض «{{ result.request.record_title }}»", + params: { + comment: { + label: "تعليق" + }, + attachments: { + label: "المرفقات" + } + } + }, + approval_reassign: { + label: "إعادة إسناد", + successMessage: "تمت إعادة إسناد «{{ result.request.record_title }}»", + params: { + to: { + label: "الموافِق الجديد", + helpText: "المستخدم الذي تُسلَّم إليه هذه الخطوة" + }, + comment: { + label: "ملاحظة" + } + } + }, + approval_send_back: { + label: "إعادة للمراجعة", + successMessage: "أُعيد «{{ result.request.record_title }}» للمراجعة", + params: { + comment: { + label: "السبب" + } + } + }, + approval_request_info: { + label: "طلب معلومات", + successMessage: "طُلبت معلومات بشأن «{{ result.request.record_title }}»", + params: { + comment: { + label: "ما الذي تحتاجه؟" + } + } + }, + approval_remind: { + label: "إرسال تذكير", + successMessage: "أُرسل تذكير بشأن «{{ result.request.record_title }}»", + params: { + comment: { + label: "ملاحظة" + } + } + }, + approval_recall: { + label: "سحب", + description: "هل تسحب هذا الطلب؟ لن يتمكن الموافِقون من التصرف فيه بعد ذلك، ويُفتح قفل السجل.", + successMessage: "تم سحب «{{ result.request.record_title }}»", + params: { + comment: { + label: "تعليق" + } + } + }, + approval_resubmit: { + label: "إعادة إرسال", + successMessage: "أُعيد إرسال «{{ result.request.record_title }}»", + params: { + comment: { + label: "ما الذي تغيّر؟" + } + } + }, + approval_comment: { + label: "رد", + successMessage: "نُشر الرد على «{{ result.request.record_title }}»", + params: { + comment: { + label: "التعليق" + }, + attachments: { + label: "المرفقات" + } + } + } + } + }, + sys_approval_action: { + label: "إجراء الموافقة", + pluralLabel: "إجراءات الموافقة", + description: "سجل تدقيق إلحاقي فقط لإجراءات الموافقة", + fields: { + id: { + label: "معرّف الإجراء" + }, + display_title: { + label: "العنوان", + help: "عنوان السجل: الإجراء، وخطوته إن سُجّلت (يُحسب عند القراءة)" + }, + organization_id: { + label: "المؤسسة", + help: "المستأجر الذي يملك هذا الإجراء (مطابق للطلب الأصل)" + }, + request_id: { + label: "الطلب" + }, + step_name: { + label: "الخطوة", + help: "الاسم الآلي للخطوة وقت تنفيذ الإجراء" + }, + step_label: { + label: "خطوة الموافقة", + help: "تسمية الخطوة وقت تنفيذ الإجراء" + }, + object_name: { + label: "الكائن", + help: "كائن السجل الذي يتعلق به الطلب" + }, + record_id: { + label: "السجل", + help: "السجل الذي يتعلق به الطلب" + }, + step_index: { + label: "فهرس الخطوة" + }, + action: { + label: "الإجراء", + options: { + submit: "إرسال", + approve: "موافقة", + reject: "رفض", + recall: "سحب", + escalate: "تصعيد", + reassign: "إعادة إسناد", + remind: "تذكير", + request_info: "طلب معلومات", + comment: "تعليق", + revise: "إعادة للمراجعة", + resubmit: "إعادة إرسال", + ooo_substitute: "استبدال بسبب الغياب", + cancel: "إلغاء" + } + }, + actor_id: { + label: "المنفِّذ", + help: "المستخدم الذي نفّذ هذا الإجراء. يكون فارغًا حين لا يُسجَّل شخص: إجراء بدأه النظام، أو قرار سُجّل قبل التقاط هوية صاحبه، ويظل يُظهر الخانة التي اتُّخذ بصفتها." + }, + acted_as: { + label: "بصفة", + help: "خانة الموافِق المنتظَر التي نُفّذ هذا الإجراء بصفتها، بالصيغة المخزّنة للخانة: معرّف مستخدم أو بريد إلكتروني أو عنوان منصب. تكون فارغة حين لم تُجِز أي خانة الإجراء، مثل إجراءات مقدِّم الطلب نفسه وإجراءات النظام وتجاوزات المسؤول." + }, + acted_as_label: { + label: "بالنيابة عن", + help: "الخانة التي نُفّذ الإجراء بصفتها، للعرض: اسم الشخص الذي تسمّيه، أو عنوانها حين لا تسمّي شخصًا" + }, + comment: { + label: "التعليق" + }, + via_override: { + label: "عبر تجاوز المسؤول", + help: "تكون صحيحة حين لم يشغل المنفِّذ أي خانة في قائمة الموافِقين المنتظَرين للطلب وأُجيز له الإجراء فقط عبر التجاوز المميَّز، الذي يتيح لمسؤول المنصة أو المؤسسة التصرف في أي طلب معلّق كي لا يبقى عالقًا طلب لا يستطيع أحد في قائمته البتّ فيه." + }, + reassign_from: { + label: "أُعيد إسناده من", + help: "خانة الموافِق المنتظَر التي سُلِّمت، بالصيغة المخزّنة للخانة: معرّف مستخدم أو بريد إلكتروني أو عنوان منصب (لإجراءات إعادة الإسناد فقط)." + }, + reassign_to: { + label: "أُعيد إسناده إلى", + help: "عنوان الموافِق المنتظَر الذي سُلِّمت إليه الخانة، بصيغته المخزّنة: معرّف مستخدم أو بريد إلكتروني أو عنوان منصب (لإجراءات إعادة الإسناد فقط)." + }, + attachments: { + label: "المرفقات", + help: "ملفات تدعم هذا الإجراء — مثل عقد موقَّع أو دليل." + }, + created_at: { + label: "تاريخ الإنشاء" + } + }, + _views: { + recent: { + label: "الأحدث", + emptyState: { + title: "لا توجد إجراءات موافقة بعد", + message: "تُسجَّل الإجراءات تلقائيًا مع تقدّم الموافقات." + } + }, + by_actor: { + label: "حسب المنفِّذ" + }, + all_actions: { + label: "الكل" + } + } + }, + sys_approval_delegation: { + label: "تفويض الموافقة", + pluralLabel: "تفويضات الموافقة", + description: "أثناء غياب شخص ما، تذهب الموافقات المسندة إليه شخصيًا إلى مفوَّضه.", + fields: { + id: { + label: "معرّف التفويض" + }, + display_title: { + label: "العنوان", + help: "عنوان السجل: المفوِّض والمفوَّض (يُحسب عند القراءة)" + }, + delegator_id: { + label: "المفوِّض", + help: "المستخدم الذي سيغيب. لا يمكنك تفويض إلا موافقاتك، لذا فهذا أنت دائمًا: يبدأ بك ولا يعرض أحدًا غيرك. تُعاد توجيه خانات الموافِق المسندة إليه شخصيًا ما دامت القاعدة نشطة." + }, + delegate_id: { + label: "المفوَّض", + help: "البديل الذي يتلقى موافقات المفوِّض ما دامت القاعدة نشطة. يتصرف بهويته هو." + }, + valid_from: { + label: "صالح من", + help: "القاعدة غير نشطة قبل هذه اللحظة. القيمة الفارغة = نشطة فورًا. يُتحقق منها في كل مرة تُوجَّه فيها موافقة، فتبدأ القاعدة في موعدها دون أي مهمة مجدولة." + }, + valid_until: { + label: "صالح حتى", + help: "تتوقف القاعدة عن السريان في هذا الوقت (UTC). اتركه فارغًا لعدم تحديد تاريخ انتهاء." + }, + reason: { + label: "السبب", + help: "سبب وجود التفويض (مثل «إجازة سنوية 5/26–5/30»). يُسجَّل في صف تدقيق الاستبدال." + }, + organization_id: { + label: "المؤسسة", + help: "المؤسسة التي ينتمي إليها هذا التفويض. لا يعيد توجيه إلا الموافقات المفتوحة في هذه المؤسسة." + }, + created_at: { + label: "تاريخ الإنشاء" + }, + updated_at: { + label: "تاريخ التحديث" + } + }, + _views: { + active: { + label: "نشطة", + emptyState: { + title: "لا توجد تفويضات", + message: "أعلِن تفويضًا للغياب كي تُوجَّه الموافقات إلى بديل أثناء غيابك." + } + } + } + } +}; diff --git a/packages/plugins/plugin-approvals/src/translations/ar.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/ar.source-hashes.generated.ts new file mode 100644 index 00000000000..728daa2a100 --- /dev/null +++ b/packages/plugins/plugin-approvals/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-approvals/src/translations/decision-question-i18n.test.ts b/packages/plugins/plugin-approvals/src/translations/decision-question-i18n.test.ts index 587f3db00a4..eea059f839a 100644 --- a/packages/plugins/plugin-approvals/src/translations/decision-question-i18n.test.ts +++ b/packages/plugins/plugin-approvals/src/translations/decision-question-i18n.test.ts @@ -24,12 +24,20 @@ import { describe, it, expect } from 'vitest'; 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 { enObjects } from './en.objects.generated.js'; const LOCALES = [ ['zh-CN', zhCNObjects], ['ja-JP', jaJPObjects], ['es-ES', esESObjects], + ['zh-Hant', zhHantObjects], + ['ar', arObjects], + ['fr', frObjects], + ['pt-BR', ptBRObjects], ] as const; const ACTIONS = ['approval_reject', 'approval_recall'] as const; diff --git a/packages/plugins/plugin-approvals/src/translations/fr.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/fr.objects.generated.ts new file mode 100644 index 00000000000..bb48783979f --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/fr.objects.generated.ts @@ -0,0 +1,396 @@ +// 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_approval_request: { + label: "Demande d’approbation", + pluralLabel: "Demandes d’approbation", + description: "Instance d’approbation en cours suivie pour chaque soumission", + fields: { + id: { + label: "ID de la demande" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : le libellé du flux qui a ouvert cette demande, sinon sa source (calculé à la lecture)" + }, + organization_id: { + label: "Organisation", + help: "Organisation de l’enregistrement concerné par cette demande (à défaut, celle du contexte d’exécution)" + }, + process_name: { + label: "Source", + help: "Origine de la demande — `flow:` pour les approbations pilotées par un nœud" + }, + process_label: { + label: "Flux", + help: "Libellé du flux qui a ouvert cette demande, tel qu’il était à son ouverture" + }, + object_name: { + label: "Objet" + }, + record_id: { + label: "Enregistrement" + }, + submitter_id: { + label: "Demandeur" + }, + submitter_comment: { + label: "Commentaire du demandeur" + }, + status: { + label: "Statut", + help: "État du cycle de vie de la demande", + options: { + pending: "En attente", + approved: "Approuvée", + rejected: "Rejetée", + recalled: "Rappelée", + returned: "Renvoyée pour révision", + cancelled: "Annulée" + } + }, + cancel_reason: { + label: "Motif d’annulation", + help: "Pourquoi la plateforme a annulé cette demande (renseigné uniquement quand le statut est « Annulée »)", + options: { + record_deleted: "Enregistrement lié supprimé" + } + }, + current_step: { + label: "Étape en cours", + help: "Nom machine de l’étape en attente d’approbation" + }, + current_step_label: { + label: "Étape d’approbation", + help: "Libellé de l’étape en attente d’approbation, tel qu’il était quand la demande l’a atteinte" + }, + current_step_index: { + label: "Index de l’étape en cours" + }, + pending_approvers: { + label: "Approbateurs en attente", + help: "ID des utilisateurs pouvant agir sur l’étape en cours, séparés par des virgules" + }, + pending_approver_labels: { + label: "En attente de", + help: "Qui la demande attend : le nom de chaque approbateur en attente, ou l’adresse de l’emplacement s’il ne désigne personne" + }, + payload_json: { + label: "Instantané", + help: "Instantané de l’enregistrement au moment de la soumission" + }, + flow_run_id: { + label: "Exécution du flux", + help: "ID de l’exécution d’automatisation suspendue que cette demande conditionne. La décision la reprend." + }, + flow_node_id: { + label: "Nœud du flux", + help: "ID du nœud d’approbation dans le flux qui a ouvert cette demande." + }, + node_config_json: { + label: "Configuration du nœud", + help: "Instantané de la configuration du nœud d’approbation (approbateurs/comportement) pour les demandes pilotées par un nœud." + }, + completed_at: { + label: "Terminée le" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all_requests: { + label: "Tout" + }, + my_pending: { + label: "Mes approbations en attente", + emptyState: { + title: "Aucune approbation en attente", + message: "Vous êtes à jour." + } + }, + submitted_by_me: { + label: "Mes soumissions" + }, + completed: { + label: "Terminées" + } + }, + _actions: { + approval_approve: { + label: "Approuver", + description: "Approuver cette demande ? Votre approbation est enregistrée, et la demande avance dès que cette étape a les approbations requises.", + successMessage: "« {{ result.request.record_title }} » approuvée", + params: { + comment: { + label: "Commentaire" + }, + attachments: { + label: "Pièces jointes" + } + } + }, + approval_reject: { + label: "Rejeter", + description: "Rejeter cette demande ? Un rejet est définitif pour tous les approbateurs.", + successMessage: "« {{ result.request.record_title }} » rejetée", + params: { + comment: { + label: "Commentaire" + }, + attachments: { + label: "Pièces jointes" + } + } + }, + approval_reassign: { + label: "Réattribuer", + successMessage: "« {{ result.request.record_title }} » réattribuée", + params: { + to: { + label: "Nouvel approbateur", + helpText: "Utilisateur à qui confier cette étape" + }, + comment: { + label: "Note" + } + } + }, + approval_send_back: { + label: "Renvoyer", + successMessage: "« {{ result.request.record_title }} » renvoyée pour révision", + params: { + comment: { + label: "Motif" + } + } + }, + approval_request_info: { + label: "Demander des informations", + successMessage: "Informations demandées sur « {{ result.request.record_title }} »", + params: { + comment: { + label: "De quoi avez-vous besoin ?" + } + } + }, + approval_remind: { + label: "Envoyer un rappel", + successMessage: "Rappel envoyé pour « {{ result.request.record_title }} »", + params: { + comment: { + label: "Note" + } + } + }, + approval_recall: { + label: "Rappeler", + description: "Rappeler cette demande ? Les approbateurs ne peuvent plus agir dessus et l’enregistrement est déverrouillé.", + successMessage: "« {{ result.request.record_title }} » rappelée", + params: { + comment: { + label: "Commentaire" + } + } + }, + approval_resubmit: { + label: "Soumettre à nouveau", + successMessage: "« {{ result.request.record_title }} » soumise à nouveau", + params: { + comment: { + label: "Qu’est-ce qui a changé ?" + } + } + }, + approval_comment: { + label: "Répondre", + successMessage: "Réponse publiée sur « {{ result.request.record_title }} »", + params: { + comment: { + label: "Commentaire" + }, + attachments: { + label: "Pièces jointes" + } + } + } + } + }, + sys_approval_action: { + label: "Action d’approbation", + pluralLabel: "Actions d’approbation", + description: "Piste d’audit en ajout seul des actions d’approbation", + fields: { + id: { + label: "ID de l’action" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : l’action et, si elle est enregistrée, son étape (calculé à la lecture)" + }, + organization_id: { + label: "Organisation", + help: "Locataire propriétaire de cette action (identique à la demande parente)" + }, + request_id: { + label: "Demande" + }, + step_name: { + label: "Étape", + help: "Nom machine de l’étape au moment de l’action" + }, + step_label: { + label: "Étape d’approbation", + help: "Libellé de l’étape au moment de l’action" + }, + object_name: { + label: "Objet", + help: "Objet de l’enregistrement concerné par la demande" + }, + record_id: { + label: "Enregistrement", + help: "L’enregistrement concerné par la demande" + }, + step_index: { + label: "Index de l’étape" + }, + action: { + label: "Action", + options: { + submit: "Soumettre", + approve: "Approuver", + reject: "Rejeter", + recall: "Rappeler", + escalate: "Escalader", + reassign: "Réattribuer", + remind: "Rappel", + request_info: "Demande d’informations", + comment: "Commentaire", + revise: "Renvoi pour révision", + resubmit: "Soumettre à nouveau", + ooo_substitute: "Remplacement pour absence", + cancel: "Annuler" + } + }, + actor_id: { + label: "Acteur", + help: "L’utilisateur qui a effectué cette action. Vide quand aucune personne n’est enregistrée : une action lancée par le système, ou une décision enregistrée avant la saisie de son auteur, qui affiche toujours l’emplacement au titre duquel elle a été prise." + }, + acted_as: { + label: "Agi en tant que", + help: "L’emplacement d’approbateur en attente au titre duquel cette action a été prise, dans la graphie stockée de l’emplacement : un ID d’utilisateur, une adresse e-mail ou une adresse de poste. Vide quand aucun emplacement n’a admis l’action, comme les actions du demandeur lui-même, les actions système et les dérogations d’administrateur." + }, + acted_as_label: { + label: "En tant que", + help: "L’emplacement au titre duquel cette action a été prise, pour l’affichage : le nom de la personne qu’il désigne, ou son adresse s’il ne désigne personne" + }, + comment: { + label: "Commentaire" + }, + via_override: { + label: "Par dérogation d’administrateur", + help: "Vrai quand l’acteur n’occupait aucun emplacement parmi les approbateurs en attente de la demande et n’a été admis à cette action que par la dérogation privilégiée, qui permet à un administrateur de la plateforme ou de l’organisation d’agir sur toute demande en attente pour qu’aucune ne reste bloquée faute de décideur." + }, + reassign_from: { + label: "Réattribué depuis", + help: "L’emplacement d’approbateur en attente qui a été cédé, dans la graphie stockée de l’emplacement : un ID d’utilisateur, une adresse e-mail ou une adresse de poste (réattributions uniquement)." + }, + reassign_to: { + label: "Réattribué à", + help: "L’adresse d’approbateur en attente à laquelle l’emplacement a été cédé, dans sa graphie stockée : un ID d’utilisateur, une adresse e-mail ou une adresse de poste (réattributions uniquement)." + }, + attachments: { + label: "Pièces jointes", + help: "Fichiers à l’appui de cette action — p. ex. un contrat signé ou une preuve." + }, + created_at: { + label: "Créé le" + } + }, + _views: { + recent: { + label: "Récentes", + emptyState: { + title: "Aucune action d’approbation pour l’instant", + message: "Les actions sont consignées automatiquement à mesure que les approbations avancent." + } + }, + by_actor: { + label: "Par acteur" + }, + all_actions: { + label: "Tout" + } + } + }, + sys_approval_delegation: { + label: "Délégation d’approbation", + pluralLabel: "Délégations d’approbation", + description: "Pendant l’absence d’une personne, les approbations qui lui sont attribuées personnellement vont à son délégué.", + fields: { + id: { + label: "ID de la délégation" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : le délégant et le délégué (calculé à la lecture)" + }, + delegator_id: { + label: "Délégant", + help: "L’utilisateur qui s’absente. Vous ne pouvez déléguer que vos propres approbations, c’est donc toujours vous : le champ commence par vous et ne propose personne d’autre. Ses emplacements d’approbateur attribués nominativement sont redirigés tant que la règle est active." + }, + delegate_id: { + label: "Délégué", + help: "Le remplaçant qui reçoit les approbations du délégant tant que la règle est active. Il agit sous sa propre identité." + }, + valid_from: { + label: "Valide à partir du", + help: "La règle est inactive avant cet instant. Vide = active immédiatement. Vérifiée à chaque acheminement d’une approbation, la règle démarre donc à l’heure sans tâche planifiée." + }, + valid_until: { + label: "Valide jusqu’au", + help: "La règle cesse de s’appliquer à ce moment (UTC). Laissez vide pour aucune date de fin." + }, + reason: { + label: "Motif", + help: "Pourquoi la délégation existe (p. ex. « Congés annuels 5/26–5/30 »). Consigné sur la ligne d’audit du remplacement." + }, + organization_id: { + label: "Organisation", + help: "L’organisation à laquelle appartient cette délégation. Elle ne redirige que les approbations ouvertes dans cette organisation." + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + active: { + label: "Actives", + emptyState: { + title: "Aucune délégation", + message: "Déclarez une délégation d’absence pour que les approbations aillent à un remplaçant pendant votre absence." + } + } + } + } +}; diff --git a/packages/plugins/plugin-approvals/src/translations/fr.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/fr.source-hashes.generated.ts new file mode 100644 index 00000000000..4a10558f978 --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/fr.source-hashes.generated.ts @@ -0,0 +1,24 @@ +// 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> = { + "objects.sys_approval_action.fields.action.label": "a36bba313a7aa9ab", + "objects.sys_approval_request._actions.approval_remind.params.comment.label": "f78f61778f71bf68", + "objects.sys_approval_request.fields.process_name.label": "8fe786e8e29c8cec", +}; diff --git a/packages/plugins/plugin-approvals/src/translations/index.ts b/packages/plugins/plugin-approvals/src/translations/index.ts index c547bc4a546..679693e57d1 100644 --- a/packages/plugins/plugin-approvals/src/translations/index.ts +++ b/packages/plugins/plugin-approvals/src/translations/index.ts @@ -18,9 +18,17 @@ 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 { 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 { 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 { approvalsPageTranslations, pageSourceHashes } from './pages.js'; import { approvalMessages } from './messages.js'; @@ -75,4 +83,20 @@ export const ApprovalsTranslations: TranslationBundle = { { objects: esESObjects, pages: approvalsPageTranslations['es-ES'], messages: approvalMessages['es-ES'] }, enSource, pageSourceHashes['es-ES'], esESGeneratedSourceHashes, ), + 'zh-Hant': withSourceFallback( + { objects: zhHantObjects, pages: approvalsPageTranslations['zh-Hant'], messages: approvalMessages['zh-Hant'] }, + enSource, pageSourceHashes['zh-Hant'], zhHantGeneratedSourceHashes, + ), + ar: withSourceFallback( + { objects: arObjects, pages: approvalsPageTranslations['ar'], messages: approvalMessages['ar'] }, + enSource, pageSourceHashes['ar'], arGeneratedSourceHashes, + ), + fr: withSourceFallback( + { objects: frObjects, pages: approvalsPageTranslations['fr'], messages: approvalMessages['fr'] }, + enSource, pageSourceHashes['fr'], frGeneratedSourceHashes, + ), + 'pt-BR': withSourceFallback( + { objects: ptBRObjects, pages: approvalsPageTranslations['pt-BR'], messages: approvalMessages['pt-BR'] }, + enSource, pageSourceHashes['pt-BR'], ptBRGeneratedSourceHashes, + ), }; diff --git a/packages/plugins/plugin-approvals/src/translations/messages.ts b/packages/plugins/plugin-approvals/src/translations/messages.ts index 6d58dc1f443..5f9ba29366c 100644 --- a/packages/plugins/plugin-approvals/src/translations/messages.ts +++ b/packages/plugins/plugin-approvals/src/translations/messages.ts @@ -239,10 +239,186 @@ export const esESMessages: ApprovalMessages = { approvalSubmitterExcludedNoOne: 'El solicitante se quitó de los aprobadores de este paso porque el paso define excludeSubmitter; nadie más puede tomarla, así que solo una anulación privilegiada puede decidirla.', }; -/** The four shipped locales, keyed as the bundle beside this file keys them. */ -export const approvalMessages: Readonly> = { +export const zhHantMessages: ApprovalMessages = { + approvalRequestedTitle: '已要求核准', + approvalRequestedBody: '{{object}} {{record}} 正在等待你的核准決定。', + approvalReminderTitle: '核准提醒', + approvalReminderBody: '{{object}} {{record}} 仍在等待你的核准決定。', + approvalSlaBreachedTitle: '核准已逾時', + approvalSlaBreachedBody: '{{object}} {{record}} 的核准決定已逾期。', + approvalSlaBreachedSubmitterTitle: '你的核准請求已逾時', + approvalSlaBreachedSubmitterBody: '{{object}} {{record}}:已執行升級動作「{{action}}」。', + approvalApprovedTitle: '請求已核准', + approvalApprovedBody: '你的{{object}} {{record}} 已獲核准。', + approvalApprovedWithCommentBody: '你的{{object}} {{record}} 已獲核准。\n\n{{comment}}', + approvalRejectedTitle: '請求已駁回', + approvalRejectedBody: '你的{{object}} {{record}} 已被駁回。', + approvalRejectedWithCommentBody: '你的{{object}} {{record}} 已被駁回。\n\n{{comment}}', + approvalOooSubstitutedTitle: '核准已轉給你(外出代理)', + approvalOooSubstitutedBody: '{{person}} 外出期間,由你代為處理 {{object}} {{record}} 的核准。', + approvalOooSkippedTitle: '核准已轉給你的代理人', + approvalOooSkippedBody: '你外出期間,{{object}} {{record}} 的核准已轉給 {{person}}。', + approvalEscalatedTitle: '核准已升級給你', + approvalEscalatedBody: '{{object}} {{record}} 的一項逾期核准已升級給你。', + approvalReassignedTitle: '核准已移交給你', + approvalReassignedBody: '你現在是 {{object}} {{record}} 的核准人。', + approvalAutoRejectedTitle: '核准已自動駁回', + approvalAutoRejectedBody: '你的{{object}} {{record}} 已超過退回修改次數上限({{limit}}),因此被駁回。', + approvalReturnedTitle: '已退回修改', + approvalReturnedBody: '你的{{object}} {{record}} 需要修改後才能獲得核准。', + approvalRequestInfoTitle: '需要補充資訊', + approvalCommentTitle: '核准有新留言', + approvalAdminRescueNote: '核准人名單未指向任何人,因此已轉給本組織的管理員:{{reason}}。', + approvalAdminRescuePositionUnheld: '職位「{{value}}」無人擔任', + approvalAdminRescueSlotUnresolved: '{{type}}「{{value}}」未解析到任何人', + approvalAdminRescueNoApprover: '節點上沒有任何核准人解析到具體人員', + approvalAdminRescueFallbackUnresolved: '宣告的 fallbackApprovers 也未解析到任何人', + approvalAdminRescueFallbackUndeclared: "onEmptyApprovers 為 'fallback',但未宣告 fallbackApprovers", + approvalAdminRescueReasonSeparator: ';', + approvalAdminRescueSubmitterExcluded: '唯一解析到的核准人是此請求的提交人,而 excludeSubmitter 會將其排除', + approvalSubmitterExcludedRemaining: '本步驟設定了 excludeSubmitter,因此提交人已從核准人中移除;由其餘核准人決定。', + approvalSubmitterExcludedFallback: '本步驟設定了 excludeSubmitter,因此提交人已從核准人中移除;由宣告的 fallbackApprovers 決定。', + approvalSubmitterExcludedAdministrators: '本步驟設定了 excludeSubmitter,因此提交人已從核准人中移除;由本組織的管理員決定。', + approvalSubmitterExcludedNoOne: '本步驟設定了 excludeSubmitter,因此提交人已從核准人中移除;沒有其他人能接手,因此只有特權越權才能做出決定。', +}; + +export const arMessages: ApprovalMessages = { + approvalRequestedTitle: 'مطلوب موافقتك', + approvalRequestedBody: 'قرار بشأن {{object}} {{record}} بانتظارك.', + approvalReminderTitle: 'تذكير بالموافقة', + approvalReminderBody: 'قرار بشأن {{object}} {{record}} لا يزال بانتظارك.', + approvalSlaBreachedTitle: 'تجاوزت الموافقة مهلتها', + approvalSlaBreachedBody: 'القرار بشأن {{object}} {{record}} متأخر.', + approvalSlaBreachedSubmitterTitle: 'تجاوز طلب الموافقة الخاص بك مهلته', + approvalSlaBreachedSubmitterBody: '{{object}} {{record}}: نُفِّذ إجراء التصعيد «{{action}}».', + approvalApprovedTitle: 'تمت الموافقة على الطلب', + approvalApprovedBody: 'تمت الموافقة على طلبك بشأن {{object}} {{record}}.', + approvalApprovedWithCommentBody: 'تمت الموافقة على طلبك بشأن {{object}} {{record}}.\n\n{{comment}}', + approvalRejectedTitle: 'رُفض الطلب', + approvalRejectedBody: 'رُفض طلبك بشأن {{object}} {{record}}.', + approvalRejectedWithCommentBody: 'رُفض طلبك بشأن {{object}} {{record}}.\n\n{{comment}}', + approvalOooSubstitutedTitle: 'وُجِّهت إليك موافقة (نيابةً عن غائب)', + approvalOooSubstitutedBody: 'أنت تنوب في موافقة على {{object}} {{record}} أثناء غياب {{person}}.', + approvalOooSkippedTitle: 'وُجِّهت الموافقة إلى مفوَّضك', + approvalOooSkippedBody: 'وُجِّهت موافقة على {{object}} {{record}} إلى {{person}} أثناء غيابك.', + approvalEscalatedTitle: 'صُعِّدت إليك موافقة', + approvalEscalatedBody: 'صُعِّدت إليك موافقة متأخرة على {{object}} {{record}}.', + approvalReassignedTitle: 'سُلِّمت إليك موافقة', + approvalReassignedBody: 'أصبحت الآن موافِقًا على {{object}} {{record}}.', + approvalAutoRejectedTitle: 'رُفضت الموافقة تلقائيًا', + approvalAutoRejectedBody: 'تجاوز طلبك بشأن {{object}} {{record}} حد المراجعات ({{limit}}) فرُفض.', + approvalReturnedTitle: 'أُعيد للمراجعة', + approvalReturnedBody: 'يحتاج طلبك بشأن {{object}} {{record}} إلى تعديل قبل أن تمكن الموافقة عليه.', + approvalRequestInfoTitle: 'مطلوب مزيد من المعلومات', + approvalCommentTitle: 'تعليق جديد على موافقة', + approvalAdminRescueNote: 'وُجِّه إلى مسؤولي هذه المؤسسة لأن قائمة الموافِقين لم تسمِّ أحدًا: {{reason}}.', + approvalAdminRescuePositionUnheld: 'المنصب «{{value}}» لا يشغله أحد', + approvalAdminRescueSlotUnresolved: '{{type}} «{{value}}» لم يُحَلّ إلى أحد', + approvalAdminRescueNoApprover: 'لم يُحَلّ أي موافِق في العقدة إلى شخص', + approvalAdminRescueFallbackUnresolved: 'ولم تُحَلّ fallbackApprovers المعلنة إلى أحد أيضًا', + approvalAdminRescueFallbackUndeclared: "قيمة onEmptyApprovers هي 'fallback' لكن لم تُعلَن أي fallbackApprovers", + approvalAdminRescueReasonSeparator: '؛ ', + approvalAdminRescueSubmitterExcluded: 'الموافِق الوحيد الذي حُلّ إليه هو مقدِّم الطلب، والذي يستبعده excludeSubmitter', + approvalSubmitterExcludedRemaining: 'أُزيل مقدِّم الطلب من الموافِقين لأن هذه الخطوة تضبط excludeSubmitter؛ ويبتّ فيه الموافِقون الباقون.', + approvalSubmitterExcludedFallback: 'أُزيل مقدِّم الطلب من الموافِقين لأن هذه الخطوة تضبط excludeSubmitter؛ ويبتّ فيه fallbackApprovers المعلنون.', + approvalSubmitterExcludedAdministrators: 'أُزيل مقدِّم الطلب من الموافِقين لأن هذه الخطوة تضبط excludeSubmitter؛ ويبتّ فيه مسؤولو هذه المؤسسة.', + approvalSubmitterExcludedNoOne: 'أُزيل مقدِّم الطلب من الموافِقين لأن هذه الخطوة تضبط excludeSubmitter؛ ولا أحد غيره يمكنه تولّيه، لذا لا يبتّ فيه إلا تجاوز مميَّز.', +}; + +export const frMessages: ApprovalMessages = { + approvalRequestedTitle: 'Approbation demandée', + approvalRequestedBody: 'Une décision sur {{object}} {{record}} vous attend.', + approvalReminderTitle: 'Rappel d’approbation', + approvalReminderBody: 'Une décision sur {{object}} {{record}} vous attend toujours.', + approvalSlaBreachedTitle: 'Délai d’approbation dépassé', + approvalSlaBreachedBody: 'La décision sur {{object}} {{record}} est en retard.', + approvalSlaBreachedSubmitterTitle: 'Votre demande d’approbation a dépassé son délai', + approvalSlaBreachedSubmitterBody: '{{object}} {{record}} : l’action d’escalade « {{action}} » a été exécutée.', + approvalApprovedTitle: 'Demande approuvée', + approvalApprovedBody: 'Votre demande sur {{object}} {{record}} a été approuvée.', + approvalApprovedWithCommentBody: 'Votre demande sur {{object}} {{record}} a été approuvée.\n\n{{comment}}', + approvalRejectedTitle: 'Demande rejetée', + approvalRejectedBody: 'Votre demande sur {{object}} {{record}} a été rejetée.', + approvalRejectedWithCommentBody: 'Votre demande sur {{object}} {{record}} a été rejetée.\n\n{{comment}}', + approvalOooSubstitutedTitle: 'Approbation qui vous est confiée (remplacement d’absence)', + approvalOooSubstitutedBody: 'Vous remplacez {{person}}, absent, pour une approbation sur {{object}} {{record}}.', + approvalOooSkippedTitle: 'Approbation confiée à votre délégué', + approvalOooSkippedBody: 'Une approbation sur {{object}} {{record}} a été confiée à {{person}} pendant votre absence.', + approvalEscalatedTitle: 'Approbation escaladée vers vous', + approvalEscalatedBody: 'Une approbation en retard sur {{object}} {{record}} vous a été escaladée.', + approvalReassignedTitle: 'Approbation qui vous est transmise', + approvalReassignedBody: 'Vous êtes désormais approbateur de {{object}} {{record}}.', + approvalAutoRejectedTitle: 'Approbation rejetée automatiquement', + approvalAutoRejectedBody: 'Votre demande sur {{object}} {{record}} a dépassé la limite de révisions ({{limit}}) et a été rejetée.', + approvalReturnedTitle: 'Renvoyé pour révision', + approvalReturnedBody: 'Votre demande sur {{object}} {{record}} doit être retravaillée avant de pouvoir être approuvée.', + approvalRequestInfoTitle: 'Informations complémentaires demandées', + approvalCommentTitle: 'Nouveau commentaire sur une approbation', + approvalAdminRescueNote: 'Confiée aux administrateurs de cette organisation, car la liste d’approbateurs ne désignait personne : {{reason}}.', + approvalAdminRescuePositionUnheld: 'le poste « {{value}} » n’a pas de titulaire', + approvalAdminRescueSlotUnresolved: '{{type}} « {{value}} » ne correspondait à personne', + approvalAdminRescueNoApprover: 'aucun approbateur du nœud ne correspondait à une personne', + approvalAdminRescueFallbackUnresolved: 'les fallbackApprovers déclarés ne correspondaient à personne non plus', + approvalAdminRescueFallbackUndeclared: "onEmptyApprovers vaut 'fallback' mais aucun fallbackApprovers n’est déclaré", + approvalAdminRescueReasonSeparator: ' ; ', + approvalAdminRescueSubmitterExcluded: 'le seul approbateur trouvé était le demandeur, qu’excludeSubmitter retire', + approvalSubmitterExcludedRemaining: 'Le demandeur a été retiré des approbateurs, car cette étape définit excludeSubmitter ; les autres approbateurs décident.', + approvalSubmitterExcludedFallback: 'Le demandeur a été retiré des approbateurs, car cette étape définit excludeSubmitter ; les fallbackApprovers déclarés décident.', + approvalSubmitterExcludedAdministrators: 'Le demandeur a été retiré des approbateurs, car cette étape définit excludeSubmitter ; les administrateurs de cette organisation décident.', + approvalSubmitterExcludedNoOne: 'Le demandeur a été retiré des approbateurs, car cette étape définit excludeSubmitter ; personne d’autre ne peut la prendre, seule une dérogation privilégiée peut donc décider.', +}; + +export const ptBRMessages: ApprovalMessages = { + approvalRequestedTitle: 'Aprovação solicitada', + approvalRequestedBody: 'Uma decisão sobre {{object}} {{record}} está aguardando você.', + approvalReminderTitle: 'Lembrete de aprovação', + approvalReminderBody: 'Uma decisão sobre {{object}} {{record}} ainda está aguardando você.', + approvalSlaBreachedTitle: 'Prazo de aprovação estourado', + approvalSlaBreachedBody: 'A decisão sobre {{object}} {{record}} está atrasada.', + approvalSlaBreachedSubmitterTitle: 'Sua solicitação de aprovação estourou o prazo', + approvalSlaBreachedSubmitterBody: '{{object}} {{record}}: a ação de escalonamento "{{action}}" foi executada.', + approvalApprovedTitle: 'Solicitação aprovada', + approvalApprovedBody: 'Sua solicitação sobre {{object}} {{record}} foi aprovada.', + approvalApprovedWithCommentBody: 'Sua solicitação sobre {{object}} {{record}} foi aprovada.\n\n{{comment}}', + approvalRejectedTitle: 'Solicitação rejeitada', + approvalRejectedBody: 'Sua solicitação sobre {{object}} {{record}} foi rejeitada.', + approvalRejectedWithCommentBody: 'Sua solicitação sobre {{object}} {{record}} foi rejeitada.\n\n{{comment}}', + approvalOooSubstitutedTitle: 'Aprovação encaminhada a você (substituição por ausência)', + approvalOooSubstitutedBody: 'Você está cobrindo uma aprovação de {{object}} {{record}} enquanto {{person}} está ausente.', + approvalOooSkippedTitle: 'Aprovação encaminhada ao seu delegado', + approvalOooSkippedBody: 'Uma aprovação de {{object}} {{record}} foi encaminhada a {{person}} enquanto você está ausente.', + approvalEscalatedTitle: 'Aprovação escalonada para você', + approvalEscalatedBody: 'Uma aprovação atrasada de {{object}} {{record}} foi escalonada para você.', + approvalReassignedTitle: 'Aprovação repassada a você', + approvalReassignedBody: 'Agora você é aprovador de {{object}} {{record}}.', + approvalAutoRejectedTitle: 'Aprovação rejeitada automaticamente', + approvalAutoRejectedBody: 'Sua solicitação sobre {{object}} {{record}} excedeu o limite de revisões ({{limit}}) e foi rejeitada.', + approvalReturnedTitle: 'Devolvido para revisão', + approvalReturnedBody: 'Sua solicitação sobre {{object}} {{record}} precisa de ajustes antes de poder ser aprovada.', + approvalRequestInfoTitle: 'Mais informações solicitadas', + approvalCommentTitle: 'Novo comentário em uma aprovação', + approvalAdminRescueNote: 'Encaminhada aos administradores desta organização porque a lista de aprovadores não indicava ninguém: {{reason}}.', + approvalAdminRescuePositionUnheld: 'o cargo "{{value}}" não tem ocupante', + approvalAdminRescueSlotUnresolved: '{{type}} "{{value}}" não correspondeu a ninguém', + approvalAdminRescueNoApprover: 'nenhum aprovador do nó correspondeu a uma pessoa', + approvalAdminRescueFallbackUnresolved: 'os fallbackApprovers declarados também não corresponderam a ninguém', + approvalAdminRescueFallbackUndeclared: "onEmptyApprovers é 'fallback', mas nenhum fallbackApprovers foi declarado", + approvalAdminRescueReasonSeparator: '; ', + approvalAdminRescueSubmitterExcluded: 'o único aprovador encontrado foi o solicitante, que excludeSubmitter remove', + approvalSubmitterExcludedRemaining: 'O solicitante foi removido dos aprovadores porque esta etapa define excludeSubmitter; os demais aprovadores decidem.', + approvalSubmitterExcludedFallback: 'O solicitante foi removido dos aprovadores porque esta etapa define excludeSubmitter; os fallbackApprovers declarados decidem.', + approvalSubmitterExcludedAdministrators: 'O solicitante foi removido dos aprovadores porque esta etapa define excludeSubmitter; os administradores desta organização decidem.', + approvalSubmitterExcludedNoOne: 'O solicitante foi removido dos aprovadores porque esta etapa define excludeSubmitter; ninguém mais pode assumi-la, então só uma substituição privilegiada pode decidir.', +}; + +/** The shipped locales, keyed as the bundle beside this file keys them. */ +export const approvalMessages: Readonly> = { en: enMessages, 'zh-CN': zhCNMessages, 'ja-JP': jaJPMessages, 'es-ES': esESMessages, + 'zh-Hant': zhHantMessages, + ar: arMessages, + fr: frMessages, + 'pt-BR': ptBRMessages, }; diff --git a/packages/plugins/plugin-approvals/src/translations/pages.ts b/packages/plugins/plugin-approvals/src/translations/pages.ts index c338a0f99f1..c12eb8f9c57 100644 --- a/packages/plugins/plugin-approvals/src/translations/pages.ts +++ b/packages/plugins/plugin-approvals/src/translations/pages.ts @@ -36,8 +36,8 @@ import type { SourceHashes } from '@objectstack/platform-objects/apps'; type PageTranslations = NonNullable; -/** The shipped locales, the same four the generated bundles carry. */ -export type ApprovalsLocale = 'en' | 'zh-CN' | 'ja-JP' | 'es-ES'; +/** The shipped locales, the same set the generated bundles carry. */ +export type ApprovalsLocale = 'en' | 'zh-CN' | 'ja-JP' | 'es-ES' | 'zh-Hant' | 'ar' | 'fr' | 'pt-BR'; export const approvalsPageTranslations: Readonly> = { en: { @@ -52,6 +52,18 @@ export const approvalsPageTranslations: Readonly, S 'zh-CN': { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, 'ja-JP': { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, 'es-ES': { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, + 'zh-Hant': { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, + ar: { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, + fr: { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, + 'pt-BR': { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, }; diff --git a/packages/plugins/plugin-approvals/src/translations/pt-BR.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/pt-BR.objects.generated.ts new file mode 100644 index 00000000000..71e06a0b6f7 --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/pt-BR.objects.generated.ts @@ -0,0 +1,396 @@ +// 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_approval_request: { + label: "Solicitação de aprovação", + pluralLabel: "Solicitações de aprovação", + description: "Instância de aprovação em andamento acompanhada por envio", + fields: { + id: { + label: "ID da solicitação" + }, + display_title: { + label: "Título", + help: "Título do registro: o rótulo do fluxo que abriu esta solicitação, senão a origem dela (calculado na leitura)" + }, + organization_id: { + label: "Organização", + help: "Organização do registro a que esta solicitação se refere (se o registro não tiver, usa a do contexto de execução)" + }, + process_name: { + label: "Origem", + help: "Origem da solicitação — `flow:` para aprovações conduzidas por nó" + }, + process_label: { + label: "Fluxo", + help: "Rótulo do fluxo que abriu esta solicitação, como estava quando ela foi aberta" + }, + object_name: { + label: "Objeto" + }, + record_id: { + label: "Registro" + }, + submitter_id: { + label: "Solicitante" + }, + submitter_comment: { + label: "Comentário do solicitante" + }, + status: { + label: "Status", + help: "Estado do ciclo de vida da solicitação", + options: { + pending: "Pendente", + approved: "Aprovada", + rejected: "Rejeitada", + recalled: "Recolhida", + returned: "Devolvida para revisão", + cancelled: "Cancelada" + } + }, + cancel_reason: { + label: "Motivo do cancelamento", + help: "Por que a plataforma anulou esta solicitação (preenchido só quando o status é \"Cancelada\")", + options: { + record_deleted: "Registro relacionado excluído" + } + }, + current_step: { + label: "Etapa atual", + help: "Nome de máquina da etapa que aguarda aprovação" + }, + current_step_label: { + label: "Etapa de aprovação", + help: "Rótulo da etapa que aguarda aprovação, como estava quando a solicitação chegou a ela" + }, + current_step_index: { + label: "Índice da etapa atual" + }, + pending_approvers: { + label: "Aprovadores pendentes", + help: "IDs dos usuários que podem agir na etapa atual, separados por vírgula" + }, + pending_approver_labels: { + label: "Aguardando", + help: "Quem a solicitação aguarda: o nome de cada aprovador pendente, ou o endereço da vaga quando ela não aponta para uma pessoa" + }, + payload_json: { + label: "Instantâneo", + help: "Instantâneo do registro no momento do envio" + }, + flow_run_id: { + label: "Execução do fluxo", + help: "ID da execução de automação suspensa que esta solicitação controla. A decisão a retoma." + }, + flow_node_id: { + label: "Nó do fluxo", + help: "ID do nó de aprovação no fluxo que abriu esta solicitação." + }, + node_config_json: { + label: "Configuração do nó", + help: "Instantâneo da configuração do nó de aprovação (aprovadores/comportamento) para solicitações conduzidas por nó." + }, + completed_at: { + label: "Concluída em" + }, + created_at: { + label: "Criado em" + }, + updated_at: { + label: "Atualizado em" + } + }, + _views: { + all_requests: { + label: "Todos" + }, + my_pending: { + label: "Minhas aprovações pendentes", + emptyState: { + title: "Nenhuma aprovação pendente", + message: "Você está em dia." + } + }, + submitted_by_me: { + label: "Enviadas por mim" + }, + completed: { + label: "Concluídas" + } + }, + _actions: { + approval_approve: { + label: "Aprovar", + description: "Aprovar esta solicitação? Sua aprovação é registrada, e a solicitação avança quando esta etapa tiver as aprovações necessárias.", + successMessage: "\"{{ result.request.record_title }}\" aprovada", + params: { + comment: { + label: "Comentário" + }, + attachments: { + label: "Anexos" + } + } + }, + approval_reject: { + label: "Rejeitar", + description: "Rejeitar esta solicitação? Uma rejeição é definitiva para todos os aprovadores.", + successMessage: "\"{{ result.request.record_title }}\" rejeitada", + params: { + comment: { + label: "Comentário" + }, + attachments: { + label: "Anexos" + } + } + }, + approval_reassign: { + label: "Reatribuir", + successMessage: "\"{{ result.request.record_title }}\" reatribuída", + params: { + to: { + label: "Novo aprovador", + helpText: "Usuário a quem entregar esta etapa" + }, + comment: { + label: "Observação" + } + } + }, + approval_send_back: { + label: "Devolver", + successMessage: "\"{{ result.request.record_title }}\" devolvida para revisão", + params: { + comment: { + label: "Motivo" + } + } + }, + approval_request_info: { + label: "Pedir informações", + successMessage: "Informações solicitadas sobre \"{{ result.request.record_title }}\"", + params: { + comment: { + label: "Do que você precisa?" + } + } + }, + approval_remind: { + label: "Enviar lembrete", + successMessage: "Lembrete enviado para \"{{ result.request.record_title }}\"", + params: { + comment: { + label: "Observação" + } + } + }, + approval_recall: { + label: "Recolher", + description: "Recolher esta solicitação? Os aprovadores não poderão mais agir sobre ela e o registro é desbloqueado.", + successMessage: "\"{{ result.request.record_title }}\" recolhida", + params: { + comment: { + label: "Comentário" + } + } + }, + approval_resubmit: { + label: "Reenviar", + successMessage: "\"{{ result.request.record_title }}\" reenviada", + params: { + comment: { + label: "O que mudou?" + } + } + }, + approval_comment: { + label: "Responder", + successMessage: "Resposta publicada em \"{{ result.request.record_title }}\"", + params: { + comment: { + label: "Comentário" + }, + attachments: { + label: "Anexos" + } + } + } + } + }, + sys_approval_action: { + label: "Ação de aprovação", + pluralLabel: "Ações de aprovação", + description: "Trilha de auditoria somente de inclusão das ações de aprovação", + fields: { + id: { + label: "ID da ação" + }, + display_title: { + label: "Título", + help: "Título do registro: a ação e, quando registrada, a etapa dela (calculado na leitura)" + }, + organization_id: { + label: "Organização", + help: "Locatário dono desta ação (igual ao da solicitação pai)" + }, + request_id: { + label: "Solicitação" + }, + step_name: { + label: "Etapa", + help: "Nome de máquina da etapa no momento da ação" + }, + step_label: { + label: "Etapa de aprovação", + help: "Rótulo da etapa no momento da ação" + }, + object_name: { + label: "Objeto", + help: "Objeto do registro a que a solicitação se refere" + }, + record_id: { + label: "Registro", + help: "O registro a que a solicitação se refere" + }, + step_index: { + label: "Índice da etapa" + }, + action: { + label: "Ação", + options: { + submit: "Enviar", + approve: "Aprovar", + reject: "Rejeitar", + recall: "Recolher", + escalate: "Escalar", + reassign: "Reatribuir", + remind: "Lembrar", + request_info: "Pedido de informações", + comment: "Comentário", + revise: "Devolver para revisão", + resubmit: "Reenviar", + ooo_substitute: "Substituição por ausência", + cancel: "Cancelar" + } + }, + actor_id: { + label: "Autor", + help: "O usuário que realizou esta ação. Vazio quando nenhuma pessoa é registrada: uma ação iniciada pelo sistema, ou uma decisão registrada antes de o autor ser capturado, que ainda mostra a vaga em nome da qual foi tomada." + }, + acted_as: { + label: "Atuou como", + help: "A vaga de aprovador pendente em nome da qual esta ação foi tomada, na grafia armazenada da vaga: um ID de usuário, um e-mail ou um endereço de cargo. Vazio quando nenhuma vaga admitiu a ação, como as ações do próprio solicitante, ações do sistema e substituições de administrador." + }, + acted_as_label: { + label: "Atuando como", + help: "A vaga em nome da qual esta ação foi tomada, para exibição: o nome da pessoa que ela indica, ou o endereço dela quando não indica ninguém" + }, + comment: { + label: "Comentário" + }, + via_override: { + label: "Por substituição de administrador", + help: "Verdadeiro quando o autor não ocupava nenhuma vaga entre os aprovadores pendentes da solicitação e foi admitido a esta ação apenas pela substituição privilegiada, que permite a um administrador da plataforma ou da organização agir em qualquer solicitação pendente para que nenhuma fique travada sem quem decida." + }, + reassign_from: { + label: "Reatribuído de", + help: "A vaga de aprovador pendente que foi repassada, na grafia armazenada da vaga: um ID de usuário, um e-mail ou um endereço de cargo (somente reatribuições)." + }, + reassign_to: { + label: "Reatribuído a", + help: "O endereço de aprovador pendente para o qual a vaga foi repassada, na grafia armazenada: um ID de usuário, um e-mail ou um endereço de cargo (somente reatribuições)." + }, + attachments: { + label: "Anexos", + help: "Arquivos que embasam esta ação — por exemplo, um contrato assinado ou uma evidência." + }, + created_at: { + label: "Criado em" + } + }, + _views: { + recent: { + label: "Recentes", + emptyState: { + title: "Nenhuma ação de aprovação ainda", + message: "As ações são registradas automaticamente conforme as aprovações avançam." + } + }, + by_actor: { + label: "Por autor" + }, + all_actions: { + label: "Todos" + } + } + }, + sys_approval_delegation: { + label: "Delegação de aprovação", + pluralLabel: "Delegações de aprovação", + description: "Enquanto alguém está ausente, as aprovações atribuídas a essa pessoa vão para o delegado dela.", + fields: { + id: { + label: "ID da delegação" + }, + display_title: { + label: "Título", + help: "Título do registro: o delegante e o delegado (calculado na leitura)" + }, + delegator_id: { + label: "Delegante", + help: "O usuário que vai se ausentar. Você só pode delegar as próprias aprovações, então este é sempre você: começa com você e não oferece mais ninguém. As vagas de aprovador atribuídas a ele individualmente são redirecionadas enquanto a regra está ativa." + }, + delegate_id: { + label: "Delegado", + help: "O substituto que recebe as aprovações do delegante enquanto a regra está ativa. Ele age com a própria identidade." + }, + valid_from: { + label: "Válido a partir de", + help: "A regra fica inativa antes deste instante. Vazio = ativa imediatamente. É verificada a cada encaminhamento de aprovação, então a regra começa na hora sem tarefa agendada." + }, + valid_until: { + label: "Válido até", + help: "A regra deixa de valer neste momento (UTC). Deixe em branco para não ter data de término." + }, + reason: { + label: "Motivo", + help: "Por que a delegação existe (ex.: \"Férias 5/26–5/30\"). Registrado na linha de auditoria da substituição." + }, + organization_id: { + label: "Organização", + help: "A organização à qual esta delegação pertence. Ela só redireciona as aprovações abertas nesta organização." + }, + created_at: { + label: "Criado em" + }, + updated_at: { + label: "Atualizado em" + } + }, + _views: { + active: { + label: "Ativas", + emptyState: { + title: "Nenhuma delegação", + message: "Declare uma delegação de ausência para que as aprovações sigam para um substituto enquanto você estiver fora." + } + } + } + } +}; diff --git a/packages/plugins/plugin-approvals/src/translations/pt-BR.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/pt-BR.source-hashes.generated.ts new file mode 100644 index 00000000000..22c8189bb53 --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/pt-BR.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 '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> = { + "objects.sys_approval_request.fields.status.label": "64a420165247654b", +}; diff --git a/packages/plugins/plugin-approvals/src/translations/success-copy-names-request-i18n.test.ts b/packages/plugins/plugin-approvals/src/translations/success-copy-names-request-i18n.test.ts index 88d9f90fc21..10f6114543e 100644 --- a/packages/plugins/plugin-approvals/src/translations/success-copy-names-request-i18n.test.ts +++ b/packages/plugins/plugin-approvals/src/translations/success-copy-names-request-i18n.test.ts @@ -16,12 +16,20 @@ 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'; const LOCALES = [ ['en', enObjects], ['zh-CN', zhCNObjects], ['ja-JP', jaJPObjects], ['es-ES', esESObjects], + ['zh-Hant', zhHantObjects], + ['ar', arObjects], + ['fr', frObjects], + ['pt-BR', ptBRObjects], ] as const; /** Every action declared on the request object, with its source copy. */ diff --git a/packages/plugins/plugin-approvals/src/translations/zh-Hant.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/zh-Hant.objects.generated.ts new file mode 100644 index 00000000000..26c8fa690b8 --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/zh-Hant.objects.generated.ts @@ -0,0 +1,396 @@ +// 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_approval_request: { + label: "核准請求", + pluralLabel: "核准請求", + description: "依每次提交追蹤的進行中核准實例", + fields: { + id: { + label: "請求 ID" + }, + display_title: { + label: "標題", + help: "記錄標題:開啟此請求的流程標籤,未記錄時為請求來源(讀取時計算)" + }, + organization_id: { + label: "組織", + help: "此請求所涉記錄所屬的組織(記錄沒有組織時,改用執行時的情境)" + }, + process_name: { + label: "來源", + help: "請求的來源——由節點驅動的核准為 `flow:`" + }, + process_label: { + label: "流程", + help: "開啟此請求的流程標籤,記錄的是請求開啟當時的值" + }, + object_name: { + label: "物件" + }, + record_id: { + label: "記錄" + }, + submitter_id: { + label: "提交人" + }, + submitter_comment: { + label: "提交備註" + }, + status: { + label: "狀態", + help: "請求的生命週期狀態", + options: { + pending: "待核准", + approved: "已核准", + rejected: "已駁回", + recalled: "已撤回", + returned: "已退回修改", + cancelled: "已作廢" + } + }, + cancel_reason: { + label: "作廢原因", + help: "平台作廢此請求的原因(僅在狀態為「已作廢」時寫入)", + options: { + record_deleted: "關聯記錄已刪除" + } + }, + current_step: { + label: "目前步驟", + help: "等待核准之步驟的機器名稱" + }, + current_step_label: { + label: "核准步驟", + help: "等待核准之步驟的標籤,記錄的是請求進入該步驟當時的值" + }, + current_step_index: { + label: "目前步驟索引" + }, + pending_approvers: { + label: "待核准人", + help: "可對目前步驟執行操作的使用者 ID,以逗號分隔" + }, + pending_approver_labels: { + label: "等待對象", + help: "請求正在等待誰:每位待核准人的姓名;槽位未指向具體人員時則為其位址" + }, + payload_json: { + label: "快照", + help: "提交時的記錄快照" + }, + flow_run_id: { + label: "流程執行", + help: "此請求所把關、已暫停的自動化執行 ID。做出決定後即恢復執行。" + }, + flow_node_id: { + label: "流程節點", + help: "開啟此請求之流程中的核准節點 ID。" + }, + node_config_json: { + label: "節點設定", + help: "由節點驅動之請求的核准節點設定(核准人/行為)快照。" + }, + completed_at: { + label: "完成時間" + }, + created_at: { + label: "建立時間" + }, + updated_at: { + label: "更新時間" + } + }, + _views: { + all_requests: { + label: "全部" + }, + my_pending: { + label: "待我核准", + emptyState: { + title: "沒有待核准的請求", + message: "全部處理完畢了。" + } + }, + submitted_by_me: { + label: "我提交的" + }, + completed: { + label: "已完成" + } + }, + _actions: { + approval_approve: { + label: "核准", + description: "要核准此請求嗎?你的核准會被記錄,此步驟取得所需核准後,請求即繼續進行。", + successMessage: "已核准「{{ result.request.record_title }}」", + params: { + comment: { + label: "核准意見" + }, + attachments: { + label: "附件" + } + } + }, + approval_reject: { + label: "駁回", + description: "要駁回此請求嗎?駁回對所有核准人都是最終決定。", + successMessage: "已駁回「{{ result.request.record_title }}」", + params: { + comment: { + label: "駁回意見" + }, + attachments: { + label: "附件" + } + } + }, + approval_reassign: { + label: "轉簽", + successMessage: "已轉簽「{{ result.request.record_title }}」", + params: { + to: { + label: "新核准人", + helpText: "要將此步驟交給的使用者" + }, + comment: { + label: "備註" + } + } + }, + approval_send_back: { + label: "退回修改", + successMessage: "已將「{{ result.request.record_title }}」退回修改", + params: { + comment: { + label: "原因" + } + } + }, + approval_request_info: { + label: "要求補充資訊", + successMessage: "已就「{{ result.request.record_title }}」要求補充資訊", + params: { + comment: { + label: "你需要什麼?" + } + } + }, + approval_remind: { + label: "發送提醒", + successMessage: "已就「{{ result.request.record_title }}」發送提醒", + params: { + comment: { + label: "備註" + } + } + }, + approval_recall: { + label: "撤回", + description: "要撤回此請求嗎?核准人將無法再處理它,記錄也會隨即解鎖。", + successMessage: "已撤回「{{ result.request.record_title }}」", + params: { + comment: { + label: "意見" + } + } + }, + approval_resubmit: { + label: "重新提交", + successMessage: "已重新提交「{{ result.request.record_title }}」", + params: { + comment: { + label: "你修改了什麼?" + } + } + }, + approval_comment: { + label: "回覆", + successMessage: "已在「{{ result.request.record_title }}」發布回覆", + params: { + comment: { + label: "回覆內容" + }, + attachments: { + label: "附件" + } + } + } + } + }, + sys_approval_action: { + label: "核准動作", + pluralLabel: "核准動作", + description: "僅可附加的核准動作稽核軌跡", + fields: { + id: { + label: "動作 ID" + }, + display_title: { + label: "標題", + help: "記錄標題:動作,以及有記錄時的步驟(讀取時計算)" + }, + organization_id: { + label: "組織", + help: "擁有此動作的租戶(與上層請求一致)" + }, + request_id: { + label: "請求" + }, + step_name: { + label: "步驟", + help: "執行動作當時之步驟的機器名稱" + }, + step_label: { + label: "核准步驟", + help: "執行動作當時之步驟的標籤" + }, + object_name: { + label: "物件", + help: "請求所涉記錄的物件" + }, + record_id: { + label: "記錄", + help: "請求所涉的記錄" + }, + step_index: { + label: "步驟索引" + }, + action: { + label: "動作", + options: { + submit: "提交", + approve: "核准", + reject: "駁回", + recall: "撤回", + escalate: "升級", + reassign: "轉簽", + remind: "提醒", + request_info: "要求補充資訊", + comment: "留言", + revise: "退回修改", + resubmit: "重新提交", + ooo_substitute: "外出代理改派", + cancel: "作廢" + } + }, + actor_id: { + label: "執行人", + help: "執行此動作的使用者。未記錄人員時為空:系統發起的動作,或在開始記錄決定者之前寫入的決定,這類決定仍會顯示其所代表的槽位。" + }, + acted_as: { + label: "代表槽位", + help: "執行此動作時所代表的待核准槽位,依槽位儲存的寫法記錄:使用者 ID、電子郵件或職位位址。若沒有槽位放行此動作(例如提交人自己的動作、系統動作與管理員越權),則為空。" + }, + acted_as_label: { + label: "代表", + help: "此動作所代表的槽位(用於顯示):指向人員時為其姓名,否則為槽位位址" + }, + comment: { + label: "留言" + }, + via_override: { + label: "經管理員越權", + help: "為真表示執行者不在此請求的待核准名單中,只是憑特權越權而獲准執行此動作;平台或組織管理員可處理任何待核准請求,讓名單中無人能決定的請求不會一直卡住。" + }, + reassign_from: { + label: "轉出方", + help: "被移交的待核准槽位,依槽位儲存的寫法記錄:使用者 ID、電子郵件或職位位址(僅轉簽動作)。" + }, + reassign_to: { + label: "轉入方", + help: "槽位被移交到的待核准位址,依其儲存的寫法記錄:使用者 ID、電子郵件或職位位址(僅轉簽動作)。" + }, + attachments: { + label: "附件", + help: "佐證此動作的檔案——例如已簽署的合約或證明文件。" + }, + created_at: { + label: "建立時間" + } + }, + _views: { + recent: { + label: "最近", + emptyState: { + title: "尚無核准動作", + message: "核准推進時會自動記錄動作。" + } + }, + by_actor: { + label: "依執行人" + }, + all_actions: { + label: "全部" + } + } + }, + sys_approval_delegation: { + label: "核准委派", + pluralLabel: "核准委派", + description: "某人不在期間,直接指派給其本人的核准會轉給其代理人。", + fields: { + id: { + label: "委派 ID" + }, + display_title: { + label: "標題", + help: "記錄標題:委派人與代理人(讀取時計算)" + }, + delegator_id: { + label: "委派人", + help: "即將不在的使用者。你只能委派自己的核准,所以這裡永遠是你:預設為你本人,不提供其他人選。規則生效期間,指派給其個人的核准人槽位會改派。" + }, + delegate_id: { + label: "代理人", + help: "規則生效期間接收委派人核准的代理人,以其本人身分行事。" + }, + valid_from: { + label: "生效時間", + help: "此時間之前規則不生效。留空即立即生效。每次分派核准時都會檢查,因此規則會準時生效,不需任何排程工作。" + }, + valid_until: { + label: "失效時間", + help: "規則在此時間(UTC)停止適用。留空則沒有結束日期。" + }, + reason: { + label: "原因", + help: "此委派存在的原因(例如「年假 5/26–5/30」)。會記入改派稽核列。" + }, + organization_id: { + label: "組織", + help: "此委派所屬的組織。它只會改派在此組織中開啟的核准。" + }, + created_at: { + label: "建立時間" + }, + updated_at: { + label: "更新時間" + } + }, + _views: { + active: { + label: "生效中", + emptyState: { + title: "沒有委派", + message: "設定一則外出委派,你不在時核准就會轉給代理人。" + } + } + } + } +}; diff --git a/packages/plugins/plugin-approvals/src/translations/zh-Hant.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/zh-Hant.source-hashes.generated.ts new file mode 100644 index 00000000000..bf521657f18 --- /dev/null +++ b/packages/plugins/plugin-approvals/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> = { +}; From 0ac1f7dd06ae8a45f1cdf1d70edde9f2b9178d1d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 03:16:37 +0000 Subject: [PATCH 2/2] i18n(plugin-approvals): the pt-BR fallback clause no longer opens with "os " MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check-cli-command-ids reads a quoted string that opens with `os ` and a lowercase word as a command id, so the pt-BR clause approvalAdminRescueFallbackUnresolved ('os fallbackApprovers declarados também não…') was taken for the command `os fallback` and refused. The clause is lowercase by design (it is joined mid-sentence after the separator), so it is reworded to 'tampouco os fallbackApprovers declarados corresponderam a ninguém', which keeps the "either" of the English and no longer opens with the article. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cya5ej5GQV86RPbRPK --- packages/plugins/plugin-approvals/src/translations/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/plugin-approvals/src/translations/messages.ts b/packages/plugins/plugin-approvals/src/translations/messages.ts index 5f9ba29366c..6bb7b47c4cf 100644 --- a/packages/plugins/plugin-approvals/src/translations/messages.ts +++ b/packages/plugins/plugin-approvals/src/translations/messages.ts @@ -401,7 +401,7 @@ export const ptBRMessages: ApprovalMessages = { approvalAdminRescuePositionUnheld: 'o cargo "{{value}}" não tem ocupante', approvalAdminRescueSlotUnresolved: '{{type}} "{{value}}" não correspondeu a ninguém', approvalAdminRescueNoApprover: 'nenhum aprovador do nó correspondeu a uma pessoa', - approvalAdminRescueFallbackUnresolved: 'os fallbackApprovers declarados também não corresponderam a ninguém', + approvalAdminRescueFallbackUnresolved: 'tampouco os fallbackApprovers declarados corresponderam a ninguém', approvalAdminRescueFallbackUndeclared: "onEmptyApprovers é 'fallback', mas nenhum fallbackApprovers foi declarado", approvalAdminRescueReasonSeparator: '; ', approvalAdminRescueSubmitterExcluded: 'o único aprovador encontrado foi o solicitante, que excludeSubmitter remove',