From c582c20554665466c6ac52ed920d500f09dbef36 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 03:11:33 +0000 Subject: [PATCH] i18n(plugin-approvals): ship the approval objects, notifications, record page and e-mail link line in ru, de and ko The second half of the approvals package: ru, de and ko 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). With it the package serves all eleven platform languages. The e-mail templates' shape, the template selection and EMAIL_TEMPLATE_FLOOR_LOCALE are unchanged. The per-locale tests list the three new locales. Carried unchanged from the released handover commit 2dffb78d (written by session_01Utp6Cai5CRdAs4u9gTipWU). Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cya5ej5GQV86RPbRPK --- .../30512-plugin-approvals-locales-b.md | 10 + .../scripts/i18n-extract.config.ts | 8 +- .../src/admin-copy-vocabulary.test.ts | 5 +- .../src/approval-email-templates.ts | 5 +- .../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 | 6 + .../src/translations/de.objects.generated.ts | 396 ++++++++++++++++++ .../de.source-hashes.generated.ts | 22 + .../decision-question-i18n.test.ts | 6 + .../src/translations/index.ts | 18 + .../src/translations/ko.objects.generated.ts | 396 ++++++++++++++++++ .../ko.source-hashes.generated.ts | 21 + .../src/translations/messages.ts | 134 +++++- .../src/translations/pages.ts | 14 +- .../src/translations/ru.objects.generated.ts | 396 ++++++++++++++++++ .../ru.source-hashes.generated.ts | 21 + .../success-copy-names-request-i18n.test.ts | 6 + 19 files changed, 1464 insertions(+), 10 deletions(-) create mode 100644 .changeset/30512-plugin-approvals-locales-b.md create mode 100644 packages/plugins/plugin-approvals/src/translations/de.objects.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/de.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/ko.objects.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/ko.source-hashes.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/ru.objects.generated.ts create mode 100644 packages/plugins/plugin-approvals/src/translations/ru.source-hashes.generated.ts diff --git a/.changeset/30512-plugin-approvals-locales-b.md b/.changeset/30512-plugin-approvals-locales-b.md new file mode 100644 index 00000000000..c6688472e18 --- /dev/null +++ b/.changeset/30512-plugin-approvals-locales-b.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 Russian, German and Korean + +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 `ru`, `de` and `ko` as well, so the package ships all eleven platform languages. 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. Second half of package 9 of #30512. diff --git a/packages/plugins/plugin-approvals/scripts/i18n-extract.config.ts b/packages/plugins/plugin-approvals/scripts/i18n-extract.config.ts index ce1df91824e..44bc9ec1ed7 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,zh-Hant,ar,fr,pt-BR --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-approvals/src/translations * @@ -64,6 +64,9 @@ 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'; import { approvalsPageTranslations } from '../src/translations/pages.js'; /** @@ -92,6 +95,9 @@ const config: ObjectStackDefinition = defineStack({ { ar: { objects: arObjects, pages: approvalsPageTranslations['ar'] } }, { fr: { objects: frObjects, pages: approvalsPageTranslations['fr'] } }, { 'pt-BR': { objects: ptBRObjects, pages: approvalsPageTranslations['pt-BR'] } }, + { ru: { objects: ruObjects, pages: approvalsPageTranslations['ru'] } }, + { de: { objects: deObjects, pages: approvalsPageTranslations['de'] } }, + { ko: { objects: koObjects, pages: approvalsPageTranslations['ko'] } }, ], }); 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 f4b892bb4ff..900c38e55bd 100644 --- a/packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts +++ b/packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts @@ -33,6 +33,9 @@ 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'; +import { ruObjects } from './translations/ru.objects.generated.js'; +import { deObjects } from './translations/de.objects.generated.js'; +import { koObjects } from './translations/ko.objects.generated.js'; /** The design-record vocabulary an admin-facing string must not carry. */ const DESIGN_RECORD_VOCABULARY: ReadonlyArray = [ @@ -97,7 +100,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, 'zh-Hant': zhHantObjects, 'ar': arObjects, 'fr': frObjects, 'pt-BR': ptBRObjects } 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, 'ru': ruObjects, 'de': deObjects, 'ko': koObjects } 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 bb977f39d14..9f343ab2c17 100644 --- a/packages/plugins/plugin-approvals/src/approval-email-templates.ts +++ b/packages/plugins/plugin-approvals/src/approval-email-templates.ts @@ -52,7 +52,7 @@ 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}}', @@ -61,6 +61,9 @@ export const APPROVAL_EMAIL_LINK_LINES: Readonly { const locales = Object.keys(ApprovalsTranslations).sort(); - expect(locales).toEqual(['ar', 'en', 'es-ES', 'fr', 'ja-JP', 'pt-BR', 'zh-CN', 'zh-Hant']); + expect(locales).toEqual(['ar', 'de', 'en', 'es-ES', 'fr', 'ja-JP', 'ko', 'pt-BR', 'ru', '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 aa88e2e8bc8..458549cdc63 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', 'zh-Hant': '詳細資料', 'ar': 'التفاصيل', 'fr': 'Détails', 'pt-BR': 'Detalhes' }, + label: { en: 'Details', 'zh-CN': '详情', 'ja-JP': '詳細', 'es-ES': 'Detalles', 'zh-Hant': '詳細資料', 'ar': 'التفاصيل', 'fr': 'Détails', 'pt-BR': 'Detalhes', 'ru': 'Подробности', 'de': 'Details', 'ko': '세부 정보' }, icon: 'file-text', children: [{ type: 'record:details' }], }, { - label: { en: 'Timeline', 'zh-CN': '时间线', 'ja-JP': 'タイムライン', 'es-ES': 'Cronología', 'zh-Hant': '時間軸', 'ar': 'المخطط الزمني', 'fr': 'Chronologie', 'pt-BR': 'Linha do tempo' }, + label: { en: 'Timeline', 'zh-CN': '时间线', 'ja-JP': 'タイムライン', 'es-ES': 'Cronología', 'zh-Hant': '時間軸', 'ar': 'المخطط الزمني', 'fr': 'Chronologie', 'pt-BR': 'Linha do tempo', 'ru': 'Хронология', 'de': 'Verlauf', 'ko': '타임라인' }, 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', 'zh-Hant': '時間軸', 'ar': 'المخطط الزمني', 'fr': 'Chronologie', 'pt-BR': 'Linha do tempo' }, + title: { en: 'Timeline', 'zh-CN': '时间线', 'ja-JP': 'タイムライン', 'es-ES': 'Cronología', 'zh-Hant': '時間軸', 'ar': 'المخطط الزمني', 'fr': 'Chronologie', 'pt-BR': 'Linha do tempo', 'ru': 'Хронология', 'de': 'Verlauf', 'ko': '타임라인' }, }, }, ], 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 c8268e1d24f..e7f2fc62a37 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', 'zh-Hant', 'ar', 'fr', 'pt-BR']) { + for (const locale of ['en', 'zh-CN', 'ja-JP', 'es-ES', 'zh-Hant', 'ar', 'fr', 'pt-BR', 'ru', 'de', 'ko']) { 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 f60c77aef19..a069c0de134 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 @@ -26,6 +26,9 @@ 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 { SysApprovalRequest } from '../sys-approval-request.object.js'; interface CopyAction { @@ -53,6 +56,9 @@ const TRANSLATED = [ ['ar', arObjects], ['fr', frObjects], ['pt-BR', ptBRObjects], + ['ru', ruObjects], + ['de', deObjects], + ['ko', koObjects], ] as const; describe('approval_approve — the decision question', () => { diff --git a/packages/plugins/plugin-approvals/src/translations/de.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/de.objects.generated.ts new file mode 100644 index 00000000000..ef770b15196 --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/de.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 '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_approval_request: { + label: "Genehmigungsanfrage", + pluralLabel: "Genehmigungsanfragen", + description: "Laufende Genehmigungsinstanz, je Einreichung verfolgt", + fields: { + id: { + label: "Anfrage-ID" + }, + display_title: { + label: "Titel", + help: "Datensatztitel: die Bezeichnung des Ablaufs, der diese Anfrage geöffnet hat, sonst ihre Quelle (beim Lesen berechnet)" + }, + organization_id: { + label: "Organisation", + help: "Organisation des Datensatzes, um den es in dieser Anfrage geht (ohne eine solche die des ausführenden Kontexts)" + }, + process_name: { + label: "Quelle", + help: "Herkunft der Anfrage — `flow:` bei knotengesteuerten Genehmigungen" + }, + process_label: { + label: "Ablauf", + help: "Bezeichnung des Ablaufs, der diese Anfrage geöffnet hat, wie sie beim Öffnen lautete" + }, + object_name: { + label: "Objekt" + }, + record_id: { + label: "Datensatz" + }, + submitter_id: { + label: "Einreicher" + }, + submitter_comment: { + label: "Kommentar des Einreichers" + }, + status: { + label: "Status", + help: "Lebenszyklusstatus der Anfrage", + options: { + pending: "Ausstehend", + approved: "Genehmigt", + rejected: "Abgelehnt", + recalled: "Zurückgezogen", + returned: "Zur Überarbeitung zurück", + cancelled: "Storniert" + } + }, + cancel_reason: { + label: "Stornierungsgrund", + help: "Warum die Plattform diese Anfrage storniert hat (nur gesetzt, wenn der Status „Storniert“ ist)", + options: { + record_deleted: "Zugehöriger Datensatz gelöscht" + } + }, + current_step: { + label: "Aktueller Schritt", + help: "Maschinenname des Schritts, der auf Genehmigung wartet" + }, + current_step_label: { + label: "Genehmigungsschritt", + help: "Bezeichnung des wartenden Schritts, wie sie lautete, als die Anfrage ihn erreichte" + }, + current_step_index: { + label: "Index des aktuellen Schritts" + }, + pending_approvers: { + label: "Ausstehende Genehmiger", + help: "Kommagetrennte IDs der Benutzer, die im aktuellen Schritt handeln können" + }, + pending_approver_labels: { + label: "Wartet auf", + help: "Auf wen die Anfrage wartet: der Name jedes ausstehenden Genehmigers oder die Slot-Adresse, wenn sie keine Person nennt" + }, + payload_json: { + label: "Momentaufnahme", + help: "Momentaufnahme des Datensatzes zum Zeitpunkt der Einreichung" + }, + flow_run_id: { + label: "Ablaufausführung", + help: "ID der angehaltenen Automatisierungsausführung, die diese Anfrage absichert. Die Entscheidung setzt sie fort." + }, + flow_node_id: { + label: "Ablaufknoten", + help: "ID des Genehmigungsknotens im Ablauf, der diese Anfrage geöffnet hat." + }, + node_config_json: { + label: "Knotenkonfiguration", + help: "Momentaufnahme der Konfiguration des Genehmigungsknotens (Genehmiger/Verhalten) bei knotengesteuerten Anfragen." + }, + completed_at: { + label: "Abgeschlossen am" + }, + created_at: { + label: "Erstellt am" + }, + updated_at: { + label: "Aktualisiert am" + } + }, + _views: { + all_requests: { + label: "Alle" + }, + my_pending: { + label: "Meine ausstehenden Genehmigungen", + emptyState: { + title: "Keine ausstehenden Genehmigungen", + message: "Sie sind auf dem neuesten Stand." + } + }, + submitted_by_me: { + label: "Von mir eingereicht" + }, + completed: { + label: "Abgeschlossen" + } + }, + _actions: { + approval_approve: { + label: "Genehmigen", + description: "Diese Anfrage genehmigen? Ihre Genehmigung wird erfasst, und die Anfrage geht weiter, sobald dieser Schritt die nötigen Genehmigungen hat.", + successMessage: "„{{ result.request.record_title }}“ genehmigt", + params: { + comment: { + label: "Kommentar" + }, + attachments: { + label: "Anhänge" + } + } + }, + approval_reject: { + label: "Ablehnen", + description: "Diese Anfrage ablehnen? Eine Ablehnung ist für alle Genehmiger endgültig.", + successMessage: "„{{ result.request.record_title }}“ abgelehnt", + params: { + comment: { + label: "Kommentar" + }, + attachments: { + label: "Anhänge" + } + } + }, + approval_reassign: { + label: "Neu zuweisen", + successMessage: "„{{ result.request.record_title }}“ neu zugewiesen", + params: { + to: { + label: "Neuer Genehmiger", + helpText: "Benutzer, an den dieser Schritt übergeben wird" + }, + comment: { + label: "Notiz" + } + } + }, + approval_send_back: { + label: "Zurücksenden", + successMessage: "„{{ result.request.record_title }}“ zur Überarbeitung zurückgesendet", + params: { + comment: { + label: "Grund" + } + } + }, + approval_request_info: { + label: "Informationen anfordern", + successMessage: "Informationen zu „{{ result.request.record_title }}“ angefordert", + params: { + comment: { + label: "Was brauchen Sie?" + } + } + }, + approval_remind: { + label: "Erinnerung senden", + successMessage: "Erinnerung für „{{ result.request.record_title }}“ gesendet", + params: { + comment: { + label: "Notiz" + } + } + }, + approval_recall: { + label: "Zurückziehen", + description: "Diese Anfrage zurückziehen? Genehmiger können nicht mehr darauf reagieren, und der Datensatz wird entsperrt.", + successMessage: "„{{ result.request.record_title }}“ zurückgezogen", + params: { + comment: { + label: "Kommentar" + } + } + }, + approval_resubmit: { + label: "Erneut einreichen", + successMessage: "„{{ result.request.record_title }}“ erneut eingereicht", + params: { + comment: { + label: "Was hat sich geändert?" + } + } + }, + approval_comment: { + label: "Antworten", + successMessage: "Antwort zu „{{ result.request.record_title }}“ veröffentlicht", + params: { + comment: { + label: "Kommentar" + }, + attachments: { + label: "Anhänge" + } + } + } + } + }, + sys_approval_action: { + label: "Genehmigungsaktion", + pluralLabel: "Genehmigungsaktionen", + description: "Nur anfügbarer Prüfpfad der Genehmigungsaktionen", + fields: { + id: { + label: "Aktions-ID" + }, + display_title: { + label: "Titel", + help: "Datensatztitel: die Aktion und, falls erfasst, ihr Schritt (beim Lesen berechnet)" + }, + organization_id: { + label: "Organisation", + help: "Mandant, dem diese Aktion gehört (wie bei der übergeordneten Anfrage)" + }, + request_id: { + label: "Anfrage" + }, + step_name: { + label: "Schritt", + help: "Maschinenname des Schritts zum Zeitpunkt der Aktion" + }, + step_label: { + label: "Genehmigungsschritt", + help: "Bezeichnung des Schritts zum Zeitpunkt der Aktion" + }, + object_name: { + label: "Objekt", + help: "Objekt des Datensatzes, um den es in der Anfrage geht" + }, + record_id: { + label: "Datensatz", + help: "Der Datensatz, um den es in der Anfrage geht" + }, + step_index: { + label: "Schrittindex" + }, + action: { + label: "Aktion", + options: { + submit: "Einreichen", + approve: "Genehmigen", + reject: "Ablehnen", + recall: "Zurückziehen", + escalate: "Eskalieren", + reassign: "Neu zuweisen", + remind: "Erinnern", + request_info: "Informationen anfordern", + comment: "Kommentar", + revise: "Überarbeiten", + resubmit: "Erneut einreichen", + ooo_substitute: "Abwesenheitsvertretung", + cancel: "Stornieren" + } + }, + actor_id: { + label: "Ausführender", + help: "Der Benutzer, der diese Aktion ausgeführt hat. Leer, wenn keine Person erfasst ist: eine vom System ausgelöste Aktion oder eine Entscheidung, die vor der Erfassung des Entscheiders gespeichert wurde und weiterhin den Slot zeigt, als der sie getroffen wurde." + }, + acted_as: { + label: "Gehandelt als", + help: "Der Slot des ausstehenden Genehmigers, als der diese Aktion ausgeführt wurde, in der gespeicherten Schreibweise des Slots: eine Benutzer-ID, eine E-Mail-Adresse oder eine Positionsadresse. Leer, wenn kein Slot die Aktion zugelassen hat, etwa bei eigenen Aktionen des Einreichers, Systemaktionen und Administrator-Übersteuerungen." + }, + acted_as_label: { + label: "Handelnd als", + help: "Der Slot, als der diese Aktion ausgeführt wurde, zur Anzeige: der Name der genannten Person oder seine Adresse, wenn er keine Person nennt" + }, + comment: { + label: "Kommentar" + }, + via_override: { + label: "Per Administrator-Übersteuerung", + help: "Wahr, wenn der Ausführende keinen Slot unter den ausstehenden Genehmigern der Anfrage hatte und nur durch die privilegierte Übersteuerung zugelassen wurde, mit der ein Plattform- oder Organisationsadministrator jede ausstehende Anfrage bearbeiten kann, damit keine hängen bleibt, die niemand auf der Liste entscheiden kann." + }, + reassign_from: { + label: "Neu zugewiesen von", + help: "Der übergebene Slot des ausstehenden Genehmigers in der gespeicherten Schreibweise des Slots: eine Benutzer-ID, eine E-Mail-Adresse oder eine Positionsadresse (nur bei Neuzuweisungen)." + }, + reassign_to: { + label: "Neu zugewiesen an", + help: "Die Adresse des ausstehenden Genehmigers, an die der Slot übergeben wurde, in ihrer gespeicherten Schreibweise: eine Benutzer-ID, eine E-Mail-Adresse oder eine Positionsadresse (nur bei Neuzuweisungen)." + }, + attachments: { + label: "Anhänge", + help: "Dateien, die diese Aktion belegen — z. B. ein unterschriebener Vertrag oder Nachweise." + }, + created_at: { + label: "Erstellt am" + } + }, + _views: { + recent: { + label: "Neueste", + emptyState: { + title: "Noch keine Genehmigungsaktionen", + message: "Aktionen werden automatisch protokolliert, während Genehmigungen voranschreiten." + } + }, + by_actor: { + label: "Nach Ausführendem" + }, + all_actions: { + label: "Alle" + } + } + }, + sys_approval_delegation: { + label: "Genehmigungsvertretung", + pluralLabel: "Genehmigungsvertretungen", + description: "Während jemand abwesend ist, gehen ihm persönlich zugewiesene Genehmigungen an seine Vertretung.", + fields: { + id: { + label: "Vertretungs-ID" + }, + display_title: { + label: "Titel", + help: "Datensatztitel: der Vertretene und die Vertretung (beim Lesen berechnet)" + }, + delegator_id: { + label: "Vertretener", + help: "Der Benutzer, der abwesend sein wird. Sie können nur Ihre eigenen Genehmigungen vertreten lassen, daher sind das immer Sie: Das Feld beginnt mit Ihnen und bietet niemand anderen an. Seine persönlich zugewiesenen Genehmiger-Slots werden umgeleitet, solange die Regel aktiv ist." + }, + delegate_id: { + label: "Vertretung", + help: "Die Vertretung, die die Genehmigungen des Vertretenen erhält, solange die Regel aktiv ist. Sie handelt unter eigener Identität." + }, + valid_from: { + label: "Gültig ab", + help: "Vor diesem Zeitpunkt ist die Regel inaktiv. Leer = sofort aktiv. Wird bei jeder Weiterleitung einer Genehmigung geprüft, die Regel beginnt also pünktlich ohne geplanten Job." + }, + valid_until: { + label: "Gültig bis", + help: "Die Regel gilt ab diesem Zeitpunkt (UTC) nicht mehr. Leer lassen für kein Enddatum." + }, + reason: { + label: "Grund", + help: "Warum die Vertretung besteht (z. B. „Jahresurlaub 5/26–5/30“). Wird in der Prüfzeile der Vertretung vermerkt." + }, + organization_id: { + label: "Organisation", + help: "Die Organisation, zu der diese Vertretung gehört. Sie leitet nur die in dieser Organisation geöffneten Genehmigungen um." + }, + created_at: { + label: "Erstellt am" + }, + updated_at: { + label: "Aktualisiert am" + } + }, + _views: { + active: { + label: "Aktiv", + emptyState: { + title: "Keine Vertretungen", + message: "Legen Sie eine Abwesenheitsvertretung an, damit Genehmigungen während Ihrer Abwesenheit an eine Vertretung gehen." + } + } + } + } +}; diff --git a/packages/plugins/plugin-approvals/src/translations/de.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/de.source-hashes.generated.ts new file mode 100644 index 00000000000..353ddb10d77 --- /dev/null +++ b/packages/plugins/plugin-approvals/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_approval_request.fields.status.label": "64a420165247654b", +}; 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 eea059f839a..d5f9eabe610 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 @@ -28,6 +28,9 @@ 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 { enObjects } from './en.objects.generated.js'; const LOCALES = [ @@ -38,6 +41,9 @@ const LOCALES = [ ['ar', arObjects], ['fr', frObjects], ['pt-BR', ptBRObjects], + ['ru', ruObjects], + ['de', deObjects], + ['ko', koObjects], ] as const; const ACTIONS = ['approval_reject', 'approval_recall'] as const; diff --git a/packages/plugins/plugin-approvals/src/translations/index.ts b/packages/plugins/plugin-approvals/src/translations/index.ts index 679693e57d1..ff0c7b8a983 100644 --- a/packages/plugins/plugin-approvals/src/translations/index.ts +++ b/packages/plugins/plugin-approvals/src/translations/index.ts @@ -22,6 +22,9 @@ 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'; @@ -29,6 +32,9 @@ import { zhHantGeneratedSourceHashes } from './zh-Hant.source-hashes.generated.j 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 { approvalsPageTranslations, pageSourceHashes } from './pages.js'; import { approvalMessages } from './messages.js'; @@ -99,4 +105,16 @@ export const ApprovalsTranslations: TranslationBundle = { { objects: ptBRObjects, pages: approvalsPageTranslations['pt-BR'], messages: approvalMessages['pt-BR'] }, enSource, pageSourceHashes['pt-BR'], ptBRGeneratedSourceHashes, ), + ru: withSourceFallback( + { objects: ruObjects, pages: approvalsPageTranslations['ru'], messages: approvalMessages['ru'] }, + enSource, pageSourceHashes['ru'], ruGeneratedSourceHashes, + ), + de: withSourceFallback( + { objects: deObjects, pages: approvalsPageTranslations['de'], messages: approvalMessages['de'] }, + enSource, pageSourceHashes['de'], deGeneratedSourceHashes, + ), + ko: withSourceFallback( + { objects: koObjects, pages: approvalsPageTranslations['ko'], messages: approvalMessages['ko'] }, + enSource, pageSourceHashes['ko'], koGeneratedSourceHashes, + ), }; diff --git a/packages/plugins/plugin-approvals/src/translations/ko.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/ko.objects.generated.ts new file mode 100644 index 00000000000..b0422a02555 --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/ko.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 '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_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/ko.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/ko.source-hashes.generated.ts new file mode 100644 index 00000000000..5ec3ebc3496 --- /dev/null +++ b/packages/plugins/plugin-approvals/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-approvals/src/translations/messages.ts b/packages/plugins/plugin-approvals/src/translations/messages.ts index 6bb7b47c4cf..868eed5f3bf 100644 --- a/packages/plugins/plugin-approvals/src/translations/messages.ts +++ b/packages/plugins/plugin-approvals/src/translations/messages.ts @@ -411,8 +411,137 @@ export const ptBRMessages: ApprovalMessages = { 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.', }; +export const ruMessages: 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 deMessages: ApprovalMessages = { + approvalRequestedTitle: 'Genehmigung angefordert', + approvalRequestedBody: 'Eine Entscheidung zu {{object}} {{record}} wartet auf Sie.', + approvalReminderTitle: 'Genehmigungserinnerung', + approvalReminderBody: 'Eine Entscheidung zu {{object}} {{record}} wartet noch auf Sie.', + approvalSlaBreachedTitle: 'Genehmigungsfrist überschritten', + approvalSlaBreachedBody: 'Die Entscheidung zu {{object}} {{record}} ist überfällig.', + approvalSlaBreachedSubmitterTitle: 'Ihre Genehmigungsanfrage hat ihre Frist überschritten', + approvalSlaBreachedSubmitterBody: '{{object}} {{record}}: Die Eskalationsaktion „{{action}}“ wurde ausgeführt.', + approvalApprovedTitle: 'Anfrage genehmigt', + approvalApprovedBody: 'Ihre Anfrage zu {{object}} {{record}} wurde genehmigt.', + approvalApprovedWithCommentBody: 'Ihre Anfrage zu {{object}} {{record}} wurde genehmigt.\n\n{{comment}}', + approvalRejectedTitle: 'Anfrage abgelehnt', + approvalRejectedBody: 'Ihre Anfrage zu {{object}} {{record}} wurde abgelehnt.', + approvalRejectedWithCommentBody: 'Ihre Anfrage zu {{object}} {{record}} wurde abgelehnt.\n\n{{comment}}', + approvalOooSubstitutedTitle: 'Genehmigung an Sie weitergeleitet (Abwesenheitsvertretung)', + approvalOooSubstitutedBody: 'Sie vertreten {{person}} während der Abwesenheit bei einer Genehmigung zu {{object}} {{record}}.', + approvalOooSkippedTitle: 'Genehmigung an Ihre Vertretung weitergeleitet', + approvalOooSkippedBody: 'Eine Genehmigung zu {{object}} {{record}} wurde während Ihrer Abwesenheit an {{person}} weitergeleitet.', + approvalEscalatedTitle: 'Genehmigung an Sie eskaliert', + approvalEscalatedBody: 'Eine überfällige Genehmigung zu {{object}} {{record}} wurde an Sie eskaliert.', + approvalReassignedTitle: 'Genehmigung an Sie übergeben', + approvalReassignedBody: 'Sie sind jetzt Genehmiger für {{object}} {{record}}.', + approvalAutoRejectedTitle: 'Genehmigung automatisch abgelehnt', + approvalAutoRejectedBody: 'Ihre Anfrage zu {{object}} {{record}} hat das Überarbeitungslimit ({{limit}}) überschritten und wurde abgelehnt.', + approvalReturnedTitle: 'Zur Überarbeitung zurückgesendet', + approvalReturnedBody: 'Ihre Anfrage zu {{object}} {{record}} muss überarbeitet werden, bevor sie genehmigt werden kann.', + approvalRequestInfoTitle: 'Weitere Informationen angefordert', + approvalCommentTitle: 'Neuer Kommentar zu einer Genehmigung', + approvalAdminRescueNote: 'An die Administratoren dieser Organisation weitergeleitet, weil die Genehmigerliste niemanden nannte: {{reason}}.', + approvalAdminRescuePositionUnheld: 'die Position „{{value}}“ hat keinen Inhaber', + approvalAdminRescueSlotUnresolved: '{{type}} „{{value}}“ ergab niemanden', + approvalAdminRescueNoApprover: 'kein Genehmiger des Knotens ergab eine Person', + approvalAdminRescueFallbackUnresolved: 'auch die deklarierten fallbackApprovers ergaben niemanden', + approvalAdminRescueFallbackUndeclared: "onEmptyApprovers ist 'fallback', aber es sind keine fallbackApprovers deklariert", + approvalAdminRescueReasonSeparator: '; ', + approvalAdminRescueSubmitterExcluded: 'der einzige ermittelte Genehmiger war der Einreicher, den excludeSubmitter entfernt', + approvalSubmitterExcludedRemaining: 'Der Einreicher wurde aus den Genehmigern entfernt, weil dieser Schritt excludeSubmitter setzt; die übrigen Genehmiger entscheiden.', + approvalSubmitterExcludedFallback: 'Der Einreicher wurde aus den Genehmigern entfernt, weil dieser Schritt excludeSubmitter setzt; die deklarierten fallbackApprovers entscheiden.', + approvalSubmitterExcludedAdministrators: 'Der Einreicher wurde aus den Genehmigern entfernt, weil dieser Schritt excludeSubmitter setzt; die Administratoren dieser Organisation entscheiden.', + approvalSubmitterExcludedNoOne: 'Der Einreicher wurde aus den Genehmigern entfernt, weil dieser Schritt excludeSubmitter setzt; niemand sonst kann sie übernehmen, daher kann nur eine privilegierte Übersteuerung entscheiden.', +}; + +export const koMessages: 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가 설정되어 있어 제출자가 승인자에서 제외되었습니다. 다른 누구도 맡을 수 없어 특권 재정의로만 결정할 수 있습니다.', +}; + /** The shipped locales, keyed as the bundle beside this file keys them. */ -export const approvalMessages: Readonly> = { +export const approvalMessages: Readonly> = { en: enMessages, 'zh-CN': zhCNMessages, 'ja-JP': jaJPMessages, @@ -421,4 +550,7 @@ export const approvalMessages: Readonly; /** 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 type ApprovalsLocale = 'en' | 'zh-CN' | 'ja-JP' | 'es-ES' | 'zh-Hant' | 'ar' | 'fr' | 'pt-BR' | 'ru' | 'de' | 'ko'; export const approvalsPageTranslations: Readonly> = { en: { @@ -64,6 +64,15 @@ export const approvalsPageTranslations: Readonly, S 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' }, + ru: { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, + de: { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, + ko: { 'pages.sys_approval_request_detail.label': '903e2bc9c8f36633' }, }; diff --git a/packages/plugins/plugin-approvals/src/translations/ru.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/ru.objects.generated.ts new file mode 100644 index 00000000000..f5e9dcd1b3c --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/ru.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 '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_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/ru.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/ru.source-hashes.generated.ts new file mode 100644 index 00000000000..0f794307ebc --- /dev/null +++ b/packages/plugins/plugin-approvals/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-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 10f6114543e..599de394729 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 @@ -20,6 +20,9 @@ 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'; const LOCALES = [ ['en', enObjects], @@ -30,6 +33,9 @@ const LOCALES = [ ['ar', arObjects], ['fr', frObjects], ['pt-BR', ptBRObjects], + ['ru', ruObjects], + ['de', deObjects], + ['ko', koObjects], ] as const; /** Every action declared on the request object, with its source copy. */