Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/30512-plugin-audit-locales.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/plugin-audit': patch
---

i18n(plugin-audit): the audit, activity and comment objects and the activity-feed sentences ship in all eleven platform languages

Clause-②: no
Metadata-protocol: none

`sys_audit_log`, `sys_activity`, `sys_comment` and `sys_comment_reaction` now carry Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, and so do the activity-feed summaries ("Created … ", "Updated …: fields", "… mentioned you"). A reader in one of those languages sees the audit trail, the activity feed and the mention notice in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion and its messages, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512.
16 changes: 15 additions & 1 deletion packages/plugins/plugin-audit/scripts/i18n-extract.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* strings were seeded from @objectstack/platform-objects.)
*
* os i18n extract packages/plugins/plugin-audit/scripts/i18n-extract.config.ts \
* --locales=zh-CN,ja-JP,es-ES --fill=default --objects-only --no-metadata-forms \
* --locales=zh-CN,ja-JP,es-ES,zh-Hant,ar,fr,pt-BR,ru,de,ko --fill=default --objects-only --no-metadata-forms \
* --source-hashes \
* --out=packages/plugins/plugin-audit/src/translations
*
Expand Down Expand Up @@ -45,6 +45,13 @@ import { enObjects } from '../src/translations/en.objects.generated.js';
import { zhCNObjects } from '../src/translations/zh-CN.objects.generated.js';
import { jaJPObjects } from '../src/translations/ja-JP.objects.generated.js';
import { esESObjects } from '../src/translations/es-ES.objects.generated.js';
import { zhHantObjects } from '../src/translations/zh-Hant.objects.generated.js';
import { arObjects } from '../src/translations/ar.objects.generated.js';
import { frObjects } from '../src/translations/fr.objects.generated.js';
import { ptBRObjects } from '../src/translations/pt-BR.objects.generated.js';
import { ruObjects } from '../src/translations/ru.objects.generated.js';
import { deObjects } from '../src/translations/de.objects.generated.js';
import { koObjects } from '../src/translations/ko.objects.generated.js';

/**
* The annotation is load-bearing, not decoration (#10868). `defineStack`
Expand All @@ -67,6 +74,13 @@ const config: ObjectStackDefinition = defineStack({
{ 'zh-CN': { objects: zhCNObjects } },
{ 'ja-JP': { objects: jaJPObjects } },
{ 'es-ES': { objects: esESObjects } },
{ 'zh-Hant': { objects: zhHantObjects } },
{ 'ar': { objects: arObjects } },
{ 'fr': { objects: frObjects } },
{ 'pt-BR': { objects: ptBRObjects } },
{ 'ru': { objects: ruObjects } },
{ 'de': { objects: deObjects } },
{ 'ko': { objects: koObjects } },
],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,13 @@ describe('sys_activity summary — rendered in the reader\'s language (#26167)',
expect((await summaries(reader('zh')))[0]).toBe('创建了 合同 "NDA"');
});

it('a regional reader locale reads its platform-table pack (`fr-FR` reads the `fr` pack, #30512)', async () => {
expect((await summaries(reader('fr-FR')))[0]).toBe('A créé Contract « NDA »');
});

it('a reader locale with no template keeps the stored sentence', async () => {
expect((await summaries(reader('fr-FR')))[0]).toBe('Created Contract "NDA"');
// Italian is not a platform-table language, so no pack carries the template.
expect((await summaries(reader('it-IT')))[0]).toBe('Created Contract "NDA"');
});

it('serves the code to no caller but the system, and composes when only `summary` is projected', async () => {
Expand Down
290 changes: 290 additions & 0 deletions packages/plugins/plugin-audit/src/translations/ar.objects.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
// 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<TranslationData['objects']> = {
sys_audit_log: {
label: "سجل التدقيق",
pluralLabel: "سجلات التدقيق",
description: "سجل تدقيق غير قابل للتعديل لأحداث المنصّة",
fields: {
created_at: {
label: "الطابع الزمني"
},
action: {
label: "الإجراء",
help: "نوع الإجراء (snake_case)",
options: {
create: "إنشاء",
read: "قراءة",
update: "تحديث",
delete: "حذف",
login: "تسجيل الدخول",
logout: "تسجيل الخروج",
login_failed: "فشل تسجيل الدخول",
account_locked: "الحساب مقفل",
config_change: "تغيير الإعدادات",
import: "استيراد",
platform_admin_standing_change: "تغيير صلاحية مسؤول المنصّة",
action_invocation: "استدعاء إجراء"
}
},
user_id: {
label: "المستخدم",
help: "المستخدم الذي نفّذ الإجراء (null للإجراءات غير الصادرة عن مستخدم أو عن خدمة — انظر المنفِّذ)"
},
actor: {
label: "المنفِّذ",
help: "الجهة التي نفّذت الإجراء: معرّف مستخدم، أو svc:<name>، أو null"
},
object_name: {
label: "الكائن",
help: "الكائن المستهدف (مثل sys_user وproject_task)"
},
record_id: {
label: "السجل",
help: "معرّف السجل المتأثّر"
},
old_value: {
label: "القيمة القديمة",
help: "الحالة السابقة مُسلسَلة بصيغة JSON"
},
new_value: {
label: "القيمة الجديدة",
help: "الحالة الجديدة مُسلسَلة بصيغة JSON"
},
ip_address: {
label: "عنوان IP"
},
user_agent: {
label: "وكيل المستخدم"
},
tenant_id: {
label: "المستأجر",
help: "المؤسسة التي يخصّها هذا الحدث؛ فارغ للإجراءات على مستوى النشر"
},
metadata: {
label: "البيانات الوصفية",
help: "سياق إضافي مُسلسَل بصيغة JSON"
},
id: {
label: "معرّف سجل التدقيق"
}
},
_views: {
recent: {
label: "الأحدث",
emptyState: {
title: "لا توجد أحداث تدقيق",
message: "سيظهر النشاط هنا عندما يتفاعل المستخدمون مع المنصّة."
}
},
writes_only: {
label: "عمليات الكتابة"
},
auth_events: {
label: "المصادقة"
},
record_views: {
label: "عرض السجلات",
emptyState: {
title: "لا توجد عمليات عرض سجلات مسجّلة",
message: "تدقيق عرض السجلات اختياري لكل كائن. تظهر الصفوف هنا بعد إضافة كائن إلى قائمة readAudit.objects في إضافة التدقيق وفتح أحدهم أحد سجلاته."
}
},
config_changes: {
label: "الإعدادات"
},
all_events: {
label: "الكل"
}
}
},
sys_activity: {
label: "موجز النشاط",
pluralLabel: "موجز النشاط",
description: "ما فعله الأشخاص بالسجلات مؤخرًا، الأحدث أولًا: من أنشأ أو غيّر أو حذف ماذا. تُحفظ الإدخالات لمدة 14 يومًا.",
fields: {
id: {
label: "معرّف النشاط"
},
timestamp: {
label: "الطابع الزمني"
},
type: {
label: "النوع",
help: "نوع النشاط. الخيارات المُعرَّفة هي المجموعة المضمّنة في المنصّة من مفردات مفتوحة، وليست تعدادًا مغلقًا: يمكن لمؤلفي البيانات الوصفية إضافة قيمهم الخاصة (القناة المعتمدة: `activityMilestones[].type`، ADR-0052 §5b.2)، وتُخزَّن القيمة غير المُعرَّفة حرفيًا بدل رفضها. على المستهلكين عرض القيمة غير المعروفة بدل افتراض أن هذه القائمة شاملة: المفردات مفتوحة بقرار، وليست ثغرة تنتظر الفرض.",
options: {
created: "تم الإنشاء",
updated: "تم التحديث",
deleted: "تم الحذف",
commented: "تم التعليق",
mentioned: "تمت الإشارة",
shared: "تمت المشاركة",
assigned: "تم الإسناد",
completed: "مكتمل",
scheduled: "مجدول",
login: "تسجيل الدخول",
logout: "تسجيل الخروج",
system: "النظام"
}
},
summary: {
label: "الملخّص",
help: "ملخّص مقروء من سطر واحد"
},
actor_id: {
label: "المنفِّذ"
},
actor_name: {
label: "اسم المنفِّذ"
},
actor_avatar_url: {
label: "الصورة الرمزية للمنفِّذ"
},
object_name: {
label: "الكائن",
help: "الاسم المختصر للكائن المستهدف (مثل account وsys_user)"
},
record_id: {
label: "السجل"
},
record_label: {
label: "تسمية السجل",
help: "التسمية المعروضة للسجل المستهدف وقت الكتابة"
},
source_object: {
label: "الكائن المصدر",
help: "اسم كائن الكيان المصدر الغني الذي اشتُقّ منه هذا النشاط (مثل \"sys_email\"). يكون null عندما يخصّ النشاط السجل المستهدف نفسه."
},
source_id: {
label: "معرّف المصدر",
help: "معرّف سجل الكيان المصدر الغني (يقترن بـ source_object) — يتيح للخط الزمني الانتقال إلى سجل البريد/المكالمة/الاجتماع الكامل."
},
url: {
label: "URL",
help: "رابط عميق اختياري إلى هدف النشاط"
},
environment_id: {
label: "البيئة",
help: "سياق البيئة (عمليات النشر متعددة البيئات)"
},
metadata: {
label: "البيانات الوصفية",
help: "سياق إضافي مُسلسَل بصيغة JSON"
},
changed_fields: {
label: "الحقول المتغيّرة",
help: "مجموعة مفاتيح التغيير المسجَّل: في التحديث، كل حقل تذكره القيم القديمة/الجديدة المسجَّلة؛ وفارغة لكل صف آخر. تختمها المنصّة وقت الكتابة؛ وتُقدَّم مقصورة على الحقول المتاحة للقارئ. تعني NULL أن الصف لم يُختم بعد (شغّل `os migrate activity-changed-fields`)."
},
milestone_key: {
label: "مفتاح المعلَم",
help: "لمعلَم نشاط مُطلَق: مفتاح واحد مرتّب ومفصول بفواصل لكل حقل كان تعريف activityMilestones للكائن يراقبه عند الإطلاق؛ وNULL لكل صف آخر. تختمه المنصّة وقت الكتابة ولا يُقدَّم أبدًا: تقارنه قاعدة القراءة بالتعريف الحالي."
}
},
_views: {
recent: {
label: "النشاط الأخير",
emptyState: {
title: "لا يوجد نشاط حديث",
message: "تظهر هنا التغييرات التي يجريها الأشخاص على السجلات، الأحدث أولًا."
}
},
all: {
label: "كل الإدخالات"
}
}
},
sys_comment: {
label: "تعليق",
pluralLabel: "التعليقات",
description: "تعليقات متسلسلة مرفقة بالسجلات عبر thread_id",
fields: {
id: {
label: "معرّف التعليق"
},
thread_id: {
label: "سلسلة المحادثة",
help: "معرّف سلسلة المحادثة — بالاصطلاح `{object}:{record_id}` (مثل `account:9001`)"
},
object_name: {
label: "الكائن الأصل",
help: "كائن السجل الذي يحدده `thread_id` — تختمه المنصّة ولا يكتبه المؤلفون أبدًا"
},
record_id: {
label: "السجل الأصل",
help: "معرّف السجل الذي يحدده `thread_id` — تختمه المنصّة ولا يكتبه المؤلفون أبدًا"
},
parent_id: {
label: "التعليق الأصل",
help: "تعليق أصل اختياري للردود المتداخلة"
},
author_id: {
label: "الكاتب"
},
author_name: {
label: "اسم الكاتب"
},
author_avatar_url: {
label: "الصورة الرمزية للكاتب"
},
body: {
label: "النص",
help: "نص التعليق (يدعم Markdown)"
},
mentions: {
label: "الإشارات",
help: "مصفوفة JSON لمعرّفات المستخدمين المُشار إليهم بـ @ في التعليق"
},
is_edited: {
label: "تم التعديل"
},
edited_at: {
label: "تاريخ التعديل"
},
created_at: {
label: "تاريخ الإنشاء"
},
updated_at: {
label: "تاريخ التحديث"
}
}
},
sys_comment_reaction: {
label: "تفاعل مع تعليق",
pluralLabel: "التفاعلات مع التعليقات",
description: "تفاعل برمز تعبيري مع تعليق، يملكه المستخدم الذي تفاعل",
fields: {
id: {
label: "معرّف التفاعل"
},
comment_id: {
label: "التعليق",
help: "معرّف sys_comment الذي يعود إليه هذا التفاعل. يمكن قراءة التفاعل عندما يمكن قراءة ذلك التعليق تحديدًا."
},
emoji: {
label: "الرمز التعبيري",
help: "الرمز التعبيري، بصفته تسلسل الأحرف الذي يعرضه العميل (مثل 👍)"
},
user_id: {
label: "المستخدم",
help: "المستخدم الذي تفاعل. يُختم من الجلسة عند الإنشاء؛ وتُستبدل القيمة التي يقدّمها العميل."
},
created_at: {
label: "تاريخ الإنشاء"
}
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 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
* `<locale>.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<Record<string, string>> = {
"objects.sys_activity.fields.url.label": "2203b0b9f72534f7",
};
Loading
Loading