Skip to content

Commit c5101a0

Browse files
i18n(plugin-audit): ship the audit objects and activity sentences in all eleven platform languages (#30625)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgBxjikPH2bUin9o95gbAxsx)_ domain:server#2 · os-bill · session_01Utp6Cai5CRdAs4u9gTipWU Refs #30512 — package 4 of 11 (`@objectstack/plugin-audit`). The card stays open: the last PR of the series flips the bundle check to the platform locale table. Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 审计、动态、评论四个对象(`sys_audit_log`、`sys_activity`、`sys_comment`、`sys_comment_reaction`)的名称、字段、选项、帮助和视图名,以及动态流的句子(「创建了…」「更新了…」「…提到了你」),补上繁中、阿语、法语、巴葡、俄语、德语、韩语 7 种,与平台语言表的 11 种一致。 - **为什么改:** 平台语言表(#29428 裁决 F)要求每个界面带齐 11 种语言;这是第 3 期按包补齐的第 4 个包(对象 128 条 + 句子 8 条)。 - **风险与回滚:** 只加翻译数据和注册行,不动逻辑、schema、接口;原有 4 种语言一字未改。改了一条测试:它原来用 `fr-FR` 当「没有语言包」的例子,现在法语包已有,**是测试的前提变了、不是放宽断言**——「没有语言包时保留原句」改用 `it-IT` 验证,另加一条钉住 `fr-FR` 读到 `fr` 包。回滚即撤 PR。 - **先看:** `src/translations/index.ts`(按表标签注册,zh-CN 在 zh-Hant 前)、`messages.ts`,再看 `activity-summary-reader-locale.integration.test.ts` 的那处改动。 - **两分钟验证:** `node scripts/check-i18n-bundles.mjs --filter=audit` 输出 `plugins/plugin-audit in sync`;`pnpm --filter @objectstack/plugin-audit test` 全绿。 - **需要你做的:** 无,席位落地。 ## What changes - Seven new locales for the audit package under the platform locale table's tags (`zh-Hant`, `ar`, `fr`, `pt-BR`, `ru`, `de`, `ko`): each a `TAG.objects.generated.ts` with all 128 object leaves plus its `TAG.source-hashes.generated.ts`, written by the repo's own `os i18n extract --fill=default --source-hashes` and served through `withSourceFallback` like the existing three. `zh-CN` stays ahead of `zh-Hant` in `index.ts`. - `messages.ts` gains the seven locales' activity-feed sentences (8 ids each, the same `{{holes}}` as `en`), registered as each locale's `messages`. - The extract command in `scripts/i18n-extract.config.ts` lists the seven tags and its merge baseline imports the seven bundles, so `check:i18n` covers them from this PR on. ## The one test change — the premise moved, the assertion did not loosen `activity-summary-reader-locale.integration.test.ts` used `fr-FR` as its example of a reader locale with no pack ("a reader locale with no template keeps the stored sentence"). Once `fr` ships, `fr-FR` resolves to it, so that premise is no longer true of French. The no-pack case now uses `it-IT` (Italian is not a platform-table language) and still asserts the stored English sentence byte for byte; a new case pins that `fr-FR` reads the `fr` pack (`A créé Contract « NDA »`). Nothing is asserted less strictly than before. ## Acceptance notes - **Quality bar (#17508 / PR #19345):** every leaf written for the reader; placeholders, code spans and identifiers byte-identical, checked mechanically per leaf against `en`. Arabic is RTL prose with identifiers left in Latin script. `t()` has no plural selection, so the `ru`/`ar` "{{count}} more" sentence uses a counting form that reads correctly for every number. - **Leaves equal to English, on purpose:** `URL` in every locale; beyond that native words only: `fr` `Action`, `Emoji`, `Import`, `Mentions`, `Type`; `pt-BR` `Emoji`; `de` `Emoji`, `Import`, `Thread`. The extractor records each as a source copy. `pt-BR` writes sign-in/sign-out as `Acesso` / `Encerramento de sessão` rather than the English loanwords. - No cross-locale coincidence: `check:i18n-stale-fill` passes with the baseline untouched. ## Verification <details><summary>Package suite, typecheck, build (verify lock)</summary> ``` turbo build --filter=@objectstack/plugin-audit...: 16 tasks successful, exit 0 pnpm --filter @objectstack/plugin-audit test && typecheck (at head fedf6bf) Test Files 71 passed (71) Tests 1138 passed (1138) os-verify-lock: VERDICT command-exit 0 ``` </details> <details><summary>i18n gates</summary> ``` node scripts/check-i18n-bundles.mjs --filter=audit plugins/plugin-audit in sync (21 bundle(s)) EXIT 0 node scripts/check-i18n-stale-fill.mjs check-i18n-stale-fill: OK (11 bundle set(s) — no new stale fills, 1 baselined). EXIT 0 eslint --no-inline-config on the changed .ts files: exit 0 changed lines 2331 (+2328 / -3), 19 files: under the 3000-line human-merge threshold ``` </details> Derived gate families (dispatch-gates --commands, reconciled with --ran) are recorded in the os-dev-report on #30512. Dogfood: not run — content-only package PR, no `!` in the title and no `(narrowing)` arm. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6Cai5CRdAs4u9gTipWU Co-authored-by: Claude <noreply@anthropic.com>
1 parent ffeda13 commit c5101a0

19 files changed

Lines changed: 2328 additions & 3 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/plugin-audit': patch
3+
---
4+
5+
i18n(plugin-audit): the audit, activity and comment objects and the activity-feed sentences ship in all eleven platform languages
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`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.

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

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

4956
/**
5057
* The annotation is load-bearing, not decoration (#10868). `defineStack`
@@ -67,6 +74,13 @@ const config: ObjectStackDefinition = defineStack({
6774
{ 'zh-CN': { objects: zhCNObjects } },
6875
{ 'ja-JP': { objects: jaJPObjects } },
6976
{ 'es-ES': { objects: esESObjects } },
77+
{ 'zh-Hant': { objects: zhHantObjects } },
78+
{ 'ar': { objects: arObjects } },
79+
{ 'fr': { objects: frObjects } },
80+
{ 'pt-BR': { objects: ptBRObjects } },
81+
{ 'ru': { objects: ruObjects } },
82+
{ 'de': { objects: deObjects } },
83+
{ 'ko': { objects: koObjects } },
7084
],
7185
});
7286

‎packages/plugins/plugin-audit/src/activity-summary-reader-locale.integration.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,13 @@ describe('sys_activity summary — rendered in the reader\'s language (#26167)',
161161
expect((await summaries(reader('zh')))[0]).toBe('创建了 合同 "NDA"');
162162
});
163163

164+
it('a regional reader locale reads its platform-table pack (`fr-FR` reads the `fr` pack, #30512)', async () => {
165+
expect((await summaries(reader('fr-FR')))[0]).toBe('A créé Contract « NDA »');
166+
});
167+
164168
it('a reader locale with no template keeps the stored sentence', async () => {
165-
expect((await summaries(reader('fr-FR')))[0]).toBe('Created Contract "NDA"');
169+
// Italian is not a platform-table language, so no pack carries the template.
170+
expect((await summaries(reader('it-IT')))[0]).toBe('Created Contract "NDA"');
166171
});
167172

168173
it('serves the code to no caller but the system, and composes when only `summary` is projected', async () => {
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Auto-generated by 'os i18n extract' for locale 'ar'.
5+
* Edit translations in place; re-run extract (with --merge) to fill new gaps.
6+
* Do not hand-edit the structure — only the leaf string values.
7+
* Merge only fills gaps: correcting a source label/description does not
8+
* push the correction into a leaf here that already holds a translation —
9+
* a present-but-stale string is not a gap, so it is left as-is. Re-translate
10+
* it by hand when its source changes; nothing here or in `os i18n check`
11+
* tells a leaf a translator updated on purpose from one nobody has looked
12+
* at since the source moved.
13+
*/
14+
15+
import type { TranslationData } from '@objectstack/spec/system';
16+
17+
export const arObjects: NonNullable<TranslationData['objects']> = {
18+
sys_audit_log: {
19+
label: "سجل التدقيق",
20+
pluralLabel: "سجلات التدقيق",
21+
description: "سجل تدقيق غير قابل للتعديل لأحداث المنصّة",
22+
fields: {
23+
created_at: {
24+
label: "الطابع الزمني"
25+
},
26+
action: {
27+
label: "الإجراء",
28+
help: "نوع الإجراء (snake_case)",
29+
options: {
30+
create: "إنشاء",
31+
read: "قراءة",
32+
update: "تحديث",
33+
delete: "حذف",
34+
login: "تسجيل الدخول",
35+
logout: "تسجيل الخروج",
36+
login_failed: "فشل تسجيل الدخول",
37+
account_locked: "الحساب مقفل",
38+
config_change: "تغيير الإعدادات",
39+
import: "استيراد",
40+
platform_admin_standing_change: "تغيير صلاحية مسؤول المنصّة",
41+
action_invocation: "استدعاء إجراء"
42+
}
43+
},
44+
user_id: {
45+
label: "المستخدم",
46+
help: "المستخدم الذي نفّذ الإجراء (null للإجراءات غير الصادرة عن مستخدم أو عن خدمة — انظر المنفِّذ)"
47+
},
48+
actor: {
49+
label: "المنفِّذ",
50+
help: "الجهة التي نفّذت الإجراء: معرّف مستخدم، أو svc:<name>، أو null"
51+
},
52+
object_name: {
53+
label: "الكائن",
54+
help: "الكائن المستهدف (مثل sys_user وproject_task)"
55+
},
56+
record_id: {
57+
label: "السجل",
58+
help: "معرّف السجل المتأثّر"
59+
},
60+
old_value: {
61+
label: "القيمة القديمة",
62+
help: "الحالة السابقة مُسلسَلة بصيغة JSON"
63+
},
64+
new_value: {
65+
label: "القيمة الجديدة",
66+
help: "الحالة الجديدة مُسلسَلة بصيغة JSON"
67+
},
68+
ip_address: {
69+
label: "عنوان IP"
70+
},
71+
user_agent: {
72+
label: "وكيل المستخدم"
73+
},
74+
tenant_id: {
75+
label: "المستأجر",
76+
help: "المؤسسة التي يخصّها هذا الحدث؛ فارغ للإجراءات على مستوى النشر"
77+
},
78+
metadata: {
79+
label: "البيانات الوصفية",
80+
help: "سياق إضافي مُسلسَل بصيغة JSON"
81+
},
82+
id: {
83+
label: "معرّف سجل التدقيق"
84+
}
85+
},
86+
_views: {
87+
recent: {
88+
label: "الأحدث",
89+
emptyState: {
90+
title: "لا توجد أحداث تدقيق",
91+
message: "سيظهر النشاط هنا عندما يتفاعل المستخدمون مع المنصّة."
92+
}
93+
},
94+
writes_only: {
95+
label: "عمليات الكتابة"
96+
},
97+
auth_events: {
98+
label: "المصادقة"
99+
},
100+
record_views: {
101+
label: "عرض السجلات",
102+
emptyState: {
103+
title: "لا توجد عمليات عرض سجلات مسجّلة",
104+
message: "تدقيق عرض السجلات اختياري لكل كائن. تظهر الصفوف هنا بعد إضافة كائن إلى قائمة readAudit.objects في إضافة التدقيق وفتح أحدهم أحد سجلاته."
105+
}
106+
},
107+
config_changes: {
108+
label: "الإعدادات"
109+
},
110+
all_events: {
111+
label: "الكل"
112+
}
113+
}
114+
},
115+
sys_activity: {
116+
label: "موجز النشاط",
117+
pluralLabel: "موجز النشاط",
118+
description: "ما فعله الأشخاص بالسجلات مؤخرًا، الأحدث أولًا: من أنشأ أو غيّر أو حذف ماذا. تُحفظ الإدخالات لمدة 14 يومًا.",
119+
fields: {
120+
id: {
121+
label: "معرّف النشاط"
122+
},
123+
timestamp: {
124+
label: "الطابع الزمني"
125+
},
126+
type: {
127+
label: "النوع",
128+
help: "نوع النشاط. الخيارات المُعرَّفة هي المجموعة المضمّنة في المنصّة من مفردات مفتوحة، وليست تعدادًا مغلقًا: يمكن لمؤلفي البيانات الوصفية إضافة قيمهم الخاصة (القناة المعتمدة: `activityMilestones[].type`، ADR-0052 §5b.2)، وتُخزَّن القيمة غير المُعرَّفة حرفيًا بدل رفضها. على المستهلكين عرض القيمة غير المعروفة بدل افتراض أن هذه القائمة شاملة: المفردات مفتوحة بقرار، وليست ثغرة تنتظر الفرض.",
129+
options: {
130+
created: "تم الإنشاء",
131+
updated: "تم التحديث",
132+
deleted: "تم الحذف",
133+
commented: "تم التعليق",
134+
mentioned: "تمت الإشارة",
135+
shared: "تمت المشاركة",
136+
assigned: "تم الإسناد",
137+
completed: "مكتمل",
138+
scheduled: "مجدول",
139+
login: "تسجيل الدخول",
140+
logout: "تسجيل الخروج",
141+
system: "النظام"
142+
}
143+
},
144+
summary: {
145+
label: "الملخّص",
146+
help: "ملخّص مقروء من سطر واحد"
147+
},
148+
actor_id: {
149+
label: "المنفِّذ"
150+
},
151+
actor_name: {
152+
label: "اسم المنفِّذ"
153+
},
154+
actor_avatar_url: {
155+
label: "الصورة الرمزية للمنفِّذ"
156+
},
157+
object_name: {
158+
label: "الكائن",
159+
help: "الاسم المختصر للكائن المستهدف (مثل account وsys_user)"
160+
},
161+
record_id: {
162+
label: "السجل"
163+
},
164+
record_label: {
165+
label: "تسمية السجل",
166+
help: "التسمية المعروضة للسجل المستهدف وقت الكتابة"
167+
},
168+
source_object: {
169+
label: "الكائن المصدر",
170+
help: "اسم كائن الكيان المصدر الغني الذي اشتُقّ منه هذا النشاط (مثل \"sys_email\"). يكون null عندما يخصّ النشاط السجل المستهدف نفسه."
171+
},
172+
source_id: {
173+
label: "معرّف المصدر",
174+
help: "معرّف سجل الكيان المصدر الغني (يقترن بـ source_object) — يتيح للخط الزمني الانتقال إلى سجل البريد/المكالمة/الاجتماع الكامل."
175+
},
176+
url: {
177+
label: "URL",
178+
help: "رابط عميق اختياري إلى هدف النشاط"
179+
},
180+
environment_id: {
181+
label: "البيئة",
182+
help: "سياق البيئة (عمليات النشر متعددة البيئات)"
183+
},
184+
metadata: {
185+
label: "البيانات الوصفية",
186+
help: "سياق إضافي مُسلسَل بصيغة JSON"
187+
},
188+
changed_fields: {
189+
label: "الحقول المتغيّرة",
190+
help: "مجموعة مفاتيح التغيير المسجَّل: في التحديث، كل حقل تذكره القيم القديمة/الجديدة المسجَّلة؛ وفارغة لكل صف آخر. تختمها المنصّة وقت الكتابة؛ وتُقدَّم مقصورة على الحقول المتاحة للقارئ. تعني NULL أن الصف لم يُختم بعد (شغّل `os migrate activity-changed-fields`)."
191+
},
192+
milestone_key: {
193+
label: "مفتاح المعلَم",
194+
help: "لمعلَم نشاط مُطلَق: مفتاح واحد مرتّب ومفصول بفواصل لكل حقل كان تعريف activityMilestones للكائن يراقبه عند الإطلاق؛ وNULL لكل صف آخر. تختمه المنصّة وقت الكتابة ولا يُقدَّم أبدًا: تقارنه قاعدة القراءة بالتعريف الحالي."
195+
}
196+
},
197+
_views: {
198+
recent: {
199+
label: "النشاط الأخير",
200+
emptyState: {
201+
title: "لا يوجد نشاط حديث",
202+
message: "تظهر هنا التغييرات التي يجريها الأشخاص على السجلات، الأحدث أولًا."
203+
}
204+
},
205+
all: {
206+
label: "كل الإدخالات"
207+
}
208+
}
209+
},
210+
sys_comment: {
211+
label: "تعليق",
212+
pluralLabel: "التعليقات",
213+
description: "تعليقات متسلسلة مرفقة بالسجلات عبر thread_id",
214+
fields: {
215+
id: {
216+
label: "معرّف التعليق"
217+
},
218+
thread_id: {
219+
label: "سلسلة المحادثة",
220+
help: "معرّف سلسلة المحادثة — بالاصطلاح `{object}:{record_id}` (مثل `account:9001`)"
221+
},
222+
object_name: {
223+
label: "الكائن الأصل",
224+
help: "كائن السجل الذي يحدده `thread_id` — تختمه المنصّة ولا يكتبه المؤلفون أبدًا"
225+
},
226+
record_id: {
227+
label: "السجل الأصل",
228+
help: "معرّف السجل الذي يحدده `thread_id` — تختمه المنصّة ولا يكتبه المؤلفون أبدًا"
229+
},
230+
parent_id: {
231+
label: "التعليق الأصل",
232+
help: "تعليق أصل اختياري للردود المتداخلة"
233+
},
234+
author_id: {
235+
label: "الكاتب"
236+
},
237+
author_name: {
238+
label: "اسم الكاتب"
239+
},
240+
author_avatar_url: {
241+
label: "الصورة الرمزية للكاتب"
242+
},
243+
body: {
244+
label: "النص",
245+
help: "نص التعليق (يدعم Markdown)"
246+
},
247+
mentions: {
248+
label: "الإشارات",
249+
help: "مصفوفة JSON لمعرّفات المستخدمين المُشار إليهم بـ @ في التعليق"
250+
},
251+
is_edited: {
252+
label: "تم التعديل"
253+
},
254+
edited_at: {
255+
label: "تاريخ التعديل"
256+
},
257+
created_at: {
258+
label: "تاريخ الإنشاء"
259+
},
260+
updated_at: {
261+
label: "تاريخ التحديث"
262+
}
263+
}
264+
},
265+
sys_comment_reaction: {
266+
label: "تفاعل مع تعليق",
267+
pluralLabel: "التفاعلات مع التعليقات",
268+
description: "تفاعل برمز تعبيري مع تعليق، يملكه المستخدم الذي تفاعل",
269+
fields: {
270+
id: {
271+
label: "معرّف التفاعل"
272+
},
273+
comment_id: {
274+
label: "التعليق",
275+
help: "معرّف sys_comment الذي يعود إليه هذا التفاعل. يمكن قراءة التفاعل عندما يمكن قراءة ذلك التعليق تحديدًا."
276+
},
277+
emoji: {
278+
label: "الرمز التعبيري",
279+
help: "الرمز التعبيري، بصفته تسلسل الأحرف الذي يعرضه العميل (مثل 👍)"
280+
},
281+
user_id: {
282+
label: "المستخدم",
283+
help: "المستخدم الذي تفاعل. يُختم من الجلسة عند الإنشاء؛ وتُستبدل القيمة التي يقدّمها العميل."
284+
},
285+
created_at: {
286+
label: "تاريخ الإنشاء"
287+
}
288+
}
289+
}
290+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Auto-generated by 'os i18n extract' for locale 'ar'. Do not hand-edit.
5+
*
6+
* Each entry is the digest of the SOURCE REVISION that this locale's leaf at
7+
* that path is still a byte copy of — provenance for the generated half of the
8+
* bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead.
9+
*
10+
* An entry exists only while the leaf IS such a copy. Re-translate the leaf in
11+
* `<locale>.objects.generated.ts` and the next extract drops its entry by
12+
* itself — the table makes no claim about text a translator wrote. A path with
13+
* no entry is LEGACY-TRUSTED and never reported stale.
14+
*
15+
* ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest
16+
* records that the current text was copied from the current source, which is
17+
* the false claim the mechanism exists to detect. Fix the TRANSLATION.
18+
*/
19+
20+
export const arGeneratedSourceHashes: Readonly<Record<string, string>> = {
21+
"objects.sys_activity.fields.url.label": "2203b0b9f72534f7",
22+
};

0 commit comments

Comments
 (0)