diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index 1da213c79..31443a411 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -57,13 +57,14 @@ import { // bundles keep working until then. // ── Audit ───────────────────────────────────────────────────────────────── +// sys_audit_log / sys_activity / sys_comment moved to @objectstack/plugin-audit +// and sys_presence to @objectstack/service-realtime (ADR-0029 K2 / D8). Their +// i18n extraction now lives in those packages; the already-generated bundles +// here keep working until the next regeneration. sys_attachment stays here +// pending the storage-domain decomposition (it belongs with service-storage). import { - SysAuditLog, - SysPresence, - SysActivity, - SysComment, - SysAttachment, SysNotification, + SysAttachment, SysEmail, SysEmailTemplate, SysSavedReport, @@ -144,13 +145,11 @@ export default defineStack({ // Security: RBAC moved to @objectstack/plugin-security, sharing to // @objectstack/plugin-sharing (ADR-0029 K2 / D8). - // Audit - SysAuditLog, - SysPresence, - SysActivity, - SysComment, - SysAttachment, + // Audit (sys_audit_log / sys_activity / sys_comment moved to + // @objectstack/plugin-audit; sys_presence to @objectstack/service-realtime; + // sys_attachment stays pending storage-domain decomposition) SysNotification, + SysAttachment, SysEmail, SysEmailTemplate, SysSavedReport, diff --git a/packages/platform-objects/src/apps/setup-nav.contributions.ts b/packages/platform-objects/src/apps/setup-nav.contributions.ts index 85f3d4130..b1a2c3e55 100644 --- a/packages/platform-objects/src/apps/setup-nav.contributions.ts +++ b/packages/platform-objects/src/apps/setup-nav.contributions.ts @@ -92,8 +92,9 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [ group: 'group_diagnostics', priority: BASE_PRIORITY, items: [ + // Audit Logs (sys_audit_log) is contributed by @objectstack/plugin-audit + // which now owns it (ADR-0029 K2). { id: 'nav_sessions', type: 'object', label: 'Sessions', objectName: 'sys_session', icon: 'monitor' }, - { id: 'nav_audit_logs', type: 'object', label: 'Audit Logs', objectName: 'sys_audit_log', icon: 'scroll-text' }, { id: 'nav_notifications', type: 'object', label: 'Notification Events', objectName: 'sys_notification', viewName: 'recent', icon: 'bell', requiresObject: 'sys_notification' }, ], }, diff --git a/packages/platform-objects/src/audit/index.ts b/packages/platform-objects/src/audit/index.ts index d794cfb50..dccdb30dc 100644 --- a/packages/platform-objects/src/audit/index.ts +++ b/packages/platform-objects/src/audit/index.ts @@ -4,12 +4,13 @@ * platform-objects/audit — Audit & Realtime Platform Objects */ -export { SysAuditLog } from './sys-audit-log.object.js'; -export { SysPresence } from './sys-presence.object.js'; -export { SysActivity } from './sys-activity.object.js'; -export { SysComment } from './sys-comment.object.js'; -export { SysAttachment } from './sys-attachment.object.js'; +// sys_audit_log / sys_activity / sys_comment moved to @objectstack/plugin-audit +// and sys_presence to @objectstack/service-realtime (ADR-0029 K2). +// sys_notification stays here pending ADR-0030 messaging rework; sys_attachment +// stays here pending the storage-domain decomposition (it belongs with +// @objectstack/service-storage's sys_file, not the audit plugin). export { SysNotification } from './sys-notification.object.js'; +export { SysAttachment } from './sys-attachment.object.js'; export { SysEmail } from './sys-email.object.js'; export { SysEmailTemplate } from './sys-email-template.object.js'; export { SysSavedReport } from './sys-saved-report.object.js'; diff --git a/packages/platform-objects/src/platform-objects.test.ts b/packages/platform-objects/src/platform-objects.test.ts index afb96fd32..e8f64876f 100644 --- a/packages/platform-objects/src/platform-objects.test.ts +++ b/packages/platform-objects/src/platform-objects.test.ts @@ -19,7 +19,9 @@ import { // RBAC objects (SysRole/SysPermissionSet/… + defaultPermissionSets) moved to // @objectstack/plugin-security and the sharing objects to // @objectstack/plugin-sharing per ADR-0029 K2 — see their packages' tests. -import { SysAuditLog, SysPresence } from './audit/index.js'; +// sys_audit_log / sys_activity / sys_comment / sys_attachment moved to +// @objectstack/plugin-audit and sys_presence to @objectstack/service-realtime +// per ADR-0029 K2 — see their packages' tests. // sys_webhook moved to @objectstack/plugin-webhooks per ADR-0029 (K2.a). import { SysMetadata, @@ -42,8 +44,6 @@ const systemObjects = [ ['SysApiKey', SysApiKey, 'sys_api_key'], ['SysTwoFactor', SysTwoFactor, 'sys_two_factor'], ['SysUserPreference', SysUserPreference, 'sys_user_preference'], - ['SysAuditLog', SysAuditLog, 'sys_audit_log'], - ['SysPresence', SysPresence, 'sys_presence'], ['SysMetadata', SysMetadata, 'sys_metadata'], ['SysMetadataHistoryObject', SysMetadataHistoryObject, 'sys_metadata_history'], ['SysSetting', SysSetting, 'sys_setting'], diff --git a/packages/plugins/plugin-audit/scripts/i18n-extract.config.ts b/packages/plugins/plugin-audit/scripts/i18n-extract.config.ts new file mode 100644 index 000000000..dd3186edf --- /dev/null +++ b/packages/plugins/plugin-audit/scripts/i18n-extract.config.ts @@ -0,0 +1,33 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Build-time only config for `os i18n extract` (ADR-0029 D8). Not deployed. + * The plugin owns the i18n extraction for the objects it owns; the + * `translations` baseline is this plugin's OWN generated bundles so re-running + * `--merge` preserves every hand-translated string. (Initial zh-CN/ja-JP/es-ES + * 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 \ + * --out=packages/plugins/plugin-audit/src/translations + */ + +import { defineStack } from '@objectstack/spec'; +import { SysAuditLog } from '../src/objects/sys-audit-log.object.js'; +import { SysActivity } from '../src/objects/sys-activity.object.js'; +import { SysComment } from '../src/objects/sys-comment.object.js'; +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'; + +export default defineStack({ + name: 'plugin-audit-i18n-extract', + objects: [SysAuditLog, SysActivity, SysComment] as any, + translations: [ + { en: { objects: enObjects } }, + { 'zh-CN': { objects: zhCNObjects } }, + { 'ja-JP': { objects: jaJPObjects } }, + { 'es-ES': { objects: esESObjects } }, + ], +}); diff --git a/packages/plugins/plugin-audit/src/audit-plugin.ts b/packages/plugins/plugin-audit/src/audit-plugin.ts index be66b5c6c..d2a942ff7 100644 --- a/packages/plugins/plugin-audit/src/audit-plugin.ts +++ b/packages/plugins/plugin-audit/src/audit-plugin.ts @@ -2,7 +2,13 @@ import type { Plugin, PluginContext } from '@objectstack/core'; import type { IDataEngine } from '@objectstack/spec/contracts'; -import { SysAuditLog, SysActivity, SysComment, SysAttachment, SysNotification } from '@objectstack/platform-objects/audit'; +import { SysAuditLog, SysActivity, SysComment } from './objects/index.js'; +// Registered here but still owned by platform-objects (the plugin contributes +// them to the kernel without owning the definition yet): +// - sys_notification — reworked by ADR-0030 messaging (notification→event). +// - sys_attachment — a file↔record link belonging with service-storage's +// sys_file; moves in the storage-domain decomposition, not this audit move. +import { SysNotification, SysAttachment } from '@objectstack/platform-objects/audit'; import { installAuditWriters, type MessagingEmitSurface } from './audit-writers.js'; /** @@ -32,8 +38,36 @@ export class AuditPlugin implements Plugin { defaultDatasource: 'cloud', namespace: 'sys', objects: [SysAuditLog, SysActivity, SysComment, SysAttachment, SysNotification], + // ADR-0029 D7 — contribute the Audit Logs entry into the Setup app's + // `group_diagnostics` slot. The plugin owns sys_audit_log (K2). + navigationContributions: [ + { + app: 'setup', + group: 'group_diagnostics', + priority: 100, + items: [ + { id: 'nav_audit_logs', type: 'object', label: 'Audit Logs', objectName: 'sys_audit_log', icon: 'scroll-text' }, + ], + }, + ], }); + // ADR-0029 D8 — contribute this plugin's object translations to the i18n + // service on kernel:ready (the i18n plugin may register after this one). + if (typeof (ctx as any).hook === 'function') { + (ctx as any).hook('kernel:ready', async () => { + try { + const i18n = ctx.getService('i18n'); + if (i18n && typeof i18n.loadTranslations === 'function') { + const { AuditTranslations } = await import('./translations/index.js'); + for (const [locale, data] of Object.entries(AuditTranslations)) { + i18n.loadTranslations(locale, data as Record); + } + } + } catch { /* i18n optional */ } + }); + } + ctx.logger.info('Audit Plugin initialized'); } diff --git a/packages/plugins/plugin-audit/src/objects/index.ts b/packages/plugins/plugin-audit/src/objects/index.ts new file mode 100644 index 000000000..c5a7e10d8 --- /dev/null +++ b/packages/plugins/plugin-audit/src/objects/index.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Audit & collaboration objects owned by `@objectstack/plugin-audit` + * (ADR-0029 K2). Moved here from the `@objectstack/platform-objects` monolith + * so the plugin owns its data model + behavior — exactly the objects the audit + * writers produce/observe (sys_audit_log + sys_activity rows; sys_comment + * @mention hook). + * + * Intentionally NOT moved here: + * - `sys_notification` — reworked by ADR-0030 messaging. + * - `sys_attachment` — a file↔record link belonging with service-storage's + * sys_file; stays in platform-objects pending the storage-domain move. + */ + +export { SysAuditLog } from './sys-audit-log.object.js'; +export { SysActivity } from './sys-activity.object.js'; +export { SysComment } from './sys-comment.object.js'; diff --git a/packages/plugins/plugin-audit/src/objects/objects.test.ts b/packages/plugins/plugin-audit/src/objects/objects.test.ts new file mode 100644 index 000000000..a22228e29 --- /dev/null +++ b/packages/plugins/plugin-audit/src/objects/objects.test.ts @@ -0,0 +1,36 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { StorageNameMapping } from '@objectstack/spec/system'; +import { SysAuditLog, SysActivity, SysComment } from './index.js'; + +/** + * Canonical-identity coverage for the audit/collaboration objects this plugin + * owns after the ADR-0029 K2 move out of @objectstack/platform-objects. Locks + * in the short names, system flag, and the storage-name resolution so a future + * rename can't silently change the physical table. + */ +const ownedObjects = [ + ['SysAuditLog', SysAuditLog, 'sys_audit_log'], + ['SysActivity', SysActivity, 'sys_activity'], + ['SysComment', SysComment, 'sys_comment'], +] as const; + +describe('@objectstack/plugin-audit objects', () => { + it.each(ownedObjects)('%s uses a canonical sys_ short name', (_name, object, name) => { + expect(object.name).toBe(name); + }); + + it.each(ownedObjects)('%s resolves to the same physical table name', (_name, object, name) => { + expect(StorageNameMapping.resolveTableName(object)).toBe(name); + }); + + it.each(ownedObjects)('%s is marked as a system object', (_name, object) => { + expect(object.isSystem).toBe(true); + }); + + it.each(ownedObjects)('%s does not carry deprecated storage identity fields', (_name, object) => { + expect((object as any).namespace).toBeUndefined(); + expect((object as any).tableName).toBeUndefined(); + }); +}); diff --git a/packages/platform-objects/src/audit/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts similarity index 100% rename from packages/platform-objects/src/audit/sys-activity.object.ts rename to packages/plugins/plugin-audit/src/objects/sys-activity.object.ts diff --git a/packages/platform-objects/src/audit/sys-audit-log.object.ts b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts similarity index 100% rename from packages/platform-objects/src/audit/sys-audit-log.object.ts rename to packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts diff --git a/packages/platform-objects/src/audit/sys-comment.object.ts b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts similarity index 100% rename from packages/platform-objects/src/audit/sys-comment.object.ts rename to packages/plugins/plugin-audit/src/objects/sys-comment.object.ts diff --git a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts new file mode 100644 index 000000000..b98ccc62a --- /dev/null +++ b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts @@ -0,0 +1,219 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'en'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const enObjects: NonNullable = { + sys_audit_log: { + label: "Audit Log", + pluralLabel: "Audit Logs", + description: "Immutable audit trail for platform events", + fields: { + created_at: { + label: "Timestamp" + }, + action: { + label: "Action", + help: "Action type (snake_case)", + options: { + create: "create", + update: "update", + delete: "delete", + restore: "restore", + login: "login", + logout: "logout", + permission_change: "permission_change", + config_change: "config_change", + export: "export", + import: "import" + } + }, + user_id: { + label: "Actor", + help: "User who performed the action (null for system actions)" + }, + object_name: { + label: "Object", + help: "Target object (e.g. sys_user, project_task)" + }, + record_id: { + label: "Record ID", + help: "ID of the affected record" + }, + old_value: { + label: "Old Value", + help: "JSON-serialized previous state" + }, + new_value: { + label: "New Value", + help: "JSON-serialized new state" + }, + ip_address: { + label: "IP Address" + }, + user_agent: { + label: "User Agent" + }, + tenant_id: { + label: "Tenant", + help: "Tenant context for multi-tenant isolation" + }, + metadata: { + label: "Metadata", + help: "JSON-serialized additional context" + }, + id: { + label: "Audit Log ID" + } + }, + _views: { + recent: { + label: "Recent" + }, + writes_only: { + label: "Writes" + }, + auth_events: { + label: "Auth" + }, + config_changes: { + label: "Config" + }, + all_events: { + label: "All" + } + } + }, + sys_activity: { + label: "Activity", + pluralLabel: "Activities", + description: "Recent activity stream entries (lightweight, denormalized)", + fields: { + id: { + label: "Activity ID" + }, + timestamp: { + label: "Timestamp" + }, + type: { + label: "Type", + options: { + created: "created", + updated: "updated", + deleted: "deleted", + commented: "commented", + mentioned: "mentioned", + shared: "shared", + assigned: "assigned", + completed: "completed", + login: "login", + logout: "logout", + system: "system" + } + }, + summary: { + label: "Summary", + help: "Human-readable one-line summary" + }, + actor_id: { + label: "Actor" + }, + actor_name: { + label: "Actor Name" + }, + actor_avatar_url: { + label: "Actor Avatar" + }, + object_name: { + label: "Object", + help: "Target object short name (e.g. account, sys_user)" + }, + record_id: { + label: "Record ID" + }, + record_label: { + label: "Record Label", + help: "Display label of the target record at write time" + }, + url: { + label: "URL", + help: "Optional deep-link to the activity target" + }, + environment_id: { + label: "Project", + help: "Environment context (multi-environment deployments)" + }, + metadata: { + label: "Metadata", + help: "JSON-serialized additional context" + } + } + }, + sys_comment: { + label: "Comment", + pluralLabel: "Comments", + description: "Threaded comments attached to records via thread_id", + fields: { + id: { + label: "Comment ID" + }, + thread_id: { + label: "Thread", + help: "Thread identifier — conventionally `{object}:{record_id}` (e.g. `sys_user:abc123`)" + }, + parent_id: { + label: "Parent Comment", + help: "Optional parent comment for nested replies" + }, + reply_count: { + label: "Reply Count" + }, + author_id: { + label: "Author" + }, + author_name: { + label: "Author Name" + }, + author_avatar_url: { + label: "Author Avatar" + }, + body: { + label: "Body", + help: "Comment text (Markdown supported)" + }, + mentions: { + label: "Mentions", + help: "JSON array of @mention objects" + }, + reactions: { + label: "Reactions", + help: "JSON array of emoji reaction objects" + }, + is_edited: { + label: "Edited" + }, + edited_at: { + label: "Edited At" + }, + visibility: { + label: "Visibility", + options: { + public: "public", + internal: "internal", + private: "private" + } + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + } +}; diff --git a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts new file mode 100644 index 000000000..a7f0e0d5b --- /dev/null +++ b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts @@ -0,0 +1,219 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'es-ES'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const esESObjects: NonNullable = { + sys_audit_log: { + label: "Registro de auditoría", + pluralLabel: "Registros de auditoría", + description: "Registro de auditoría inmutable para eventos de la plataforma", + fields: { + created_at: { + label: "Marca temporal" + }, + action: { + label: "Acción", + help: "Tipo de acción (snake_case).", + options: { + create: "Crear", + update: "Actualizar", + delete: "Eliminar", + restore: "Restaurar", + login: "Inicio de sesión", + logout: "Cierre de sesión", + permission_change: "Cambio de permisos", + config_change: "Cambio de configuración", + export: "Exportar", + import: "Importar" + } + }, + user_id: { + label: "Actor", + help: "Usuario que realizó la acción (null para acciones del sistema)." + }, + object_name: { + label: "Objeto", + help: "Objeto de destino (p. ej. sys_user, project_task)." + }, + record_id: { + label: "ID de registro", + help: "ID del registro afectado." + }, + old_value: { + label: "Valor anterior", + help: "Estado anterior serializado en JSON." + }, + new_value: { + label: "Valor nuevo", + help: "Estado nuevo serializado en JSON." + }, + ip_address: { + label: "Dirección IP" + }, + user_agent: { + label: "Agente de usuario" + }, + tenant_id: { + label: "Inquilino", + help: "Contexto del tenant para el aislamiento multi-tenant." + }, + metadata: { + label: "Metadatos", + help: "Contexto adicional serializado en JSON." + }, + id: { + label: "ID de registro de auditoría" + } + }, + _views: { + recent: { + label: "Recientes" + }, + writes_only: { + label: "Escrituras" + }, + auth_events: { + label: "Autenticación" + }, + config_changes: { + label: "Configuración" + }, + all_events: { + label: "Todos" + } + } + }, + sys_activity: { + label: "Actividad", + pluralLabel: "Actividades", + description: "Entradas recientes del flujo de actividad (ligeras y desnormalizadas).", + fields: { + id: { + label: "ID de actividad" + }, + timestamp: { + label: "Marca temporal" + }, + type: { + label: "Tipo", + options: { + created: "Creado", + updated: "Actualizado", + deleted: "Eliminado", + commented: "Comentado", + mentioned: "Mencionado", + shared: "Compartido", + assigned: "Asignado", + completed: "Completado", + login: "Inicio de sesión", + logout: "Cierre de sesión", + system: "Sistema" + } + }, + summary: { + label: "Resumen", + help: "Resumen legible de una línea." + }, + actor_id: { + label: "Actor" + }, + actor_name: { + label: "Nombre del actor" + }, + actor_avatar_url: { + label: "Avatar del actor" + }, + object_name: { + label: "Objeto", + help: "Nombre corto del objeto de destino (p. ej. account, sys_user)." + }, + record_id: { + label: "ID de registro" + }, + record_label: { + label: "Nombre visible del registro", + help: "Nombre visible del registro de destino en el momento de escritura." + }, + url: { + label: "URL", + help: "Enlace profundo opcional al destino de la actividad." + }, + environment_id: { + label: "Proyecto", + help: "Contexto del proyecto (implementaciones multiproyecto)." + }, + metadata: { + label: "Metadatos", + help: "Contexto adicional serializado en JSON." + } + } + }, + sys_comment: { + label: "Comentario", + pluralLabel: "Comentarios", + description: "Comentarios en hilo adjuntos a registros mediante thread_id.", + fields: { + id: { + label: "ID de comentario" + }, + thread_id: { + label: "Hilo", + help: "Identificador del hilo; por convención `{object}:{record_id}` (p. ej. `sys_user:abc123`)." + }, + parent_id: { + label: "Comentario principal", + help: "Comentario principal opcional para respuestas anidadas." + }, + reply_count: { + label: "Número de respuestas" + }, + author_id: { + label: "Autor" + }, + author_name: { + label: "Nombre del autor" + }, + author_avatar_url: { + label: "Avatar del autor" + }, + body: { + label: "Contenido", + help: "Texto del comentario (compatible con Markdown)." + }, + mentions: { + label: "Menciones", + help: "Matriz JSON de objetos @mention." + }, + reactions: { + label: "Reacciones", + help: "Matriz JSON de objetos de reacción emoji." + }, + is_edited: { + label: "Editado" + }, + edited_at: { + label: "Editado el" + }, + visibility: { + label: "Visibilidad", + options: { + public: "Público", + internal: "Interno", + private: "Privado" + } + }, + created_at: { + label: "Creado el" + }, + updated_at: { + label: "Actualizado el" + } + } + } +}; diff --git a/packages/plugins/plugin-audit/src/translations/index.ts b/packages/plugins/plugin-audit/src/translations/index.ts new file mode 100644 index 000000000..a90f97e45 --- /dev/null +++ b/packages/plugins/plugin-audit/src/translations/index.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * AuditTranslations — i18n bundle owned by this plugin (ADR-0029 D8). + * + * Object label/field/view/action translations for the sys_* objects this + * plugin owns (sys_audit_log / sys_activity / sys_comment / sys_attachment). + * Loaded at runtime via the plugin's `kernel:ready` hook + * (`i18n.loadTranslations`). Regenerate with `os i18n extract` against + * `scripts/i18n-extract.config.ts`. + */ + +import type { TranslationBundle } from '@objectstack/spec/system'; +import { enObjects } from './en.objects.generated.js'; +import { zhCNObjects } from './zh-CN.objects.generated.js'; +import { jaJPObjects } from './ja-JP.objects.generated.js'; +import { esESObjects } from './es-ES.objects.generated.js'; + +export const AuditTranslations: TranslationBundle = { + en: { objects: enObjects }, + 'zh-CN': { objects: zhCNObjects }, + 'ja-JP': { objects: jaJPObjects }, + 'es-ES': { objects: esESObjects }, +}; diff --git a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts new file mode 100644 index 000000000..42d5ac7c4 --- /dev/null +++ b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts @@ -0,0 +1,219 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'ja-JP'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const jaJPObjects: NonNullable = { + sys_audit_log: { + label: "監査ログ", + pluralLabel: "監査ログ", + description: "プラットフォームイベントの不変の監査証跡", + fields: { + created_at: { + label: "タイムスタンプ" + }, + action: { + label: "アクション", + help: "アクションタイプ(snake_case)", + options: { + create: "作成", + update: "更新", + delete: "削除", + restore: "復元", + login: "ログイン", + logout: "ログアウト", + permission_change: "権限変更", + config_change: "構成変更", + export: "エクスポート", + import: "インポート" + } + }, + user_id: { + label: "操作者", + help: "アクションを実行したユーザー(システム操作の場合は null)" + }, + object_name: { + label: "オブジェクト", + help: "対象オブジェクト(例: sys_user、project_task)" + }, + record_id: { + label: "レコード ID", + help: "影響を受けたレコードの ID" + }, + 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: "監査ログ ID" + } + }, + _views: { + recent: { + label: "最近" + }, + writes_only: { + label: "書き込み" + }, + auth_events: { + label: "認証" + }, + config_changes: { + label: "構成変更" + }, + all_events: { + label: "すべて" + } + } + }, + sys_activity: { + label: "アクティビティ", + pluralLabel: "アクティビティ", + description: "最近のアクティビティストリームエントリ(軽量、非正規化)", + fields: { + id: { + label: "アクティビティ ID" + }, + timestamp: { + label: "タイムスタンプ" + }, + type: { + label: "タイプ", + options: { + created: "作成", + updated: "更新", + deleted: "削除", + commented: "コメント", + mentioned: "メンション", + shared: "共有", + assigned: "割り当て", + completed: "完了", + login: "ログイン", + logout: "ログアウト", + system: "システム" + } + }, + summary: { + label: "サマリー", + help: "判別しやすい 1 行サマリー" + }, + actor_id: { + label: "操作者" + }, + actor_name: { + label: "操作者名" + }, + actor_avatar_url: { + label: "操作者アバター" + }, + object_name: { + label: "オブジェクト", + help: "対象オブジェクトの短い名前(例: account、sys_user)" + }, + record_id: { + label: "レコード ID" + }, + record_label: { + label: "レコード表示名", + help: "書き込み時点の対象レコードの表示名" + }, + url: { + label: "URL", + help: "アクティビティターゲットへのオプションのディープリンク" + }, + environment_id: { + label: "プロジェクト", + help: "プロジェクトコンテキスト(マルチプロジェクトデプロイメント)" + }, + metadata: { + label: "メタデータ", + help: "JSON シリアライズされた追加コンテキスト" + } + } + }, + sys_comment: { + label: "コメント", + pluralLabel: "コメント", + description: "thread_id を介してレコードに添付されたスレッドコメント", + fields: { + id: { + label: "コメント ID" + }, + thread_id: { + label: "スレッド", + help: "スレッド識別子 — 通常は `{object}:{record_id}`(例: `sys_user:abc123`)" + }, + parent_id: { + label: "親コメント", + help: "ネストした返信用のオプションの親コメント" + }, + reply_count: { + label: "返信数" + }, + author_id: { + label: "投稿者" + }, + author_name: { + label: "投稿者名" + }, + author_avatar_url: { + label: "投稿者アバター" + }, + body: { + label: "本文", + help: "コメントテキスト(Markdown 対応)" + }, + mentions: { + label: "メンション", + help: "@メンションオブジェクトの JSON 配列" + }, + reactions: { + label: "リアクション", + help: "絵文字リアクションオブジェクトの JSON 配列" + }, + is_edited: { + label: "編集済み" + }, + edited_at: { + label: "編集日時" + }, + visibility: { + label: "公開範囲", + options: { + public: "公開", + internal: "内部", + private: "非公開" + } + }, + created_at: { + label: "作成日時" + }, + updated_at: { + label: "更新日時" + } + } + } +}; diff --git a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts new file mode 100644 index 000000000..5d23b04bd --- /dev/null +++ b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts @@ -0,0 +1,219 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'zh-CN'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const zhCNObjects: NonNullable = { + sys_audit_log: { + label: "审计日志", + pluralLabel: "审计日志", + description: "平台事件的不可变审计追踪", + fields: { + created_at: { + label: "时间戳" + }, + action: { + label: "操作", + help: "操作类型(snake_case)", + options: { + create: "创建", + update: "更新", + delete: "删除", + restore: "恢复", + login: "登录", + logout: "登出", + permission_change: "权限变更", + config_change: "配置变更", + export: "导出", + import: "导入" + } + }, + user_id: { + label: "执行人", + help: "执行该操作的用户(系统操作时为 null)" + }, + object_name: { + label: "对象", + help: "目标对象(例如 sys_user、project_task)" + }, + record_id: { + label: "记录 ID", + help: "受影响记录的 ID" + }, + 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: "审计日志 ID" + } + }, + _views: { + recent: { + label: "最近" + }, + writes_only: { + label: "写入" + }, + auth_events: { + label: "认证" + }, + config_changes: { + label: "配置" + }, + all_events: { + label: "全部" + } + } + }, + sys_activity: { + label: "活动", + pluralLabel: "活动", + description: "最近活动流条目(轻量、去规范化)", + fields: { + id: { + label: "活动 ID" + }, + timestamp: { + label: "时间戳" + }, + type: { + label: "类型", + options: { + created: "已创建", + updated: "已更新", + deleted: "已删除", + commented: "已评论", + mentioned: "被提及", + shared: "已共享", + assigned: "已分配", + completed: "已完成", + 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: "记录 ID" + }, + record_label: { + label: "记录标签", + help: "写入时目标记录的显示标签" + }, + url: { + label: "URL", + help: "指向活动目标的可选深度链接" + }, + environment_id: { + label: "项目", + help: "项目上下文(多项目部署)" + }, + metadata: { + label: "元数据", + help: "附加上下文的 JSON 序列化内容" + } + } + }, + sys_comment: { + label: "评论", + pluralLabel: "评论", + description: "通过 thread_id 附加到记录的线程化评论", + fields: { + id: { + label: "评论 ID" + }, + thread_id: { + label: "线程", + help: "线程标识——约定格式为 `{object}:{record_id}`(例如 `sys_user:abc123`)" + }, + parent_id: { + label: "父评论", + help: "可选的父评论,用于嵌套回复" + }, + reply_count: { + label: "回复数" + }, + author_id: { + label: "作者" + }, + author_name: { + label: "作者名称" + }, + author_avatar_url: { + label: "作者头像" + }, + body: { + label: "正文", + help: "评论文本(支持 Markdown)" + }, + mentions: { + label: "提及", + help: "@mention 对象的 JSON 数组" + }, + reactions: { + label: "回应", + help: "表情回应对象的 JSON 数组" + }, + is_edited: { + label: "已编辑" + }, + edited_at: { + label: "编辑时间" + }, + visibility: { + label: "可见性", + options: { + public: "公开", + internal: "内部", + private: "私有" + } + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + } + } + } +}; diff --git a/packages/services/service-realtime/scripts/i18n-extract.config.ts b/packages/services/service-realtime/scripts/i18n-extract.config.ts new file mode 100644 index 000000000..8c34095ef --- /dev/null +++ b/packages/services/service-realtime/scripts/i18n-extract.config.ts @@ -0,0 +1,31 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Build-time only config for `os i18n extract` (ADR-0029 D8). Not deployed. + * The plugin owns the i18n extraction for the objects it owns; the + * `translations` baseline is this plugin's OWN generated bundles so re-running + * `--merge` preserves every hand-translated string. (Initial zh-CN/ja-JP/es-ES + * strings were seeded from @objectstack/platform-objects.) + * + * os i18n extract packages/services/service-realtime/scripts/i18n-extract.config.ts \ + * --locales=zh-CN,ja-JP,es-ES --fill=default --objects-only \ + * --out=packages/services/service-realtime/src/translations + */ + +import { defineStack } from '@objectstack/spec'; +import { SysPresence } from '../src/objects/sys-presence.object.js'; +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'; + +export default defineStack({ + name: 'service-realtime-i18n-extract', + objects: [SysPresence] as any, + translations: [ + { en: { objects: enObjects } }, + { 'zh-CN': { objects: zhCNObjects } }, + { 'ja-JP': { objects: jaJPObjects } }, + { 'es-ES': { objects: esESObjects } }, + ], +}); diff --git a/packages/services/service-realtime/src/objects/index.ts b/packages/services/service-realtime/src/objects/index.ts new file mode 100644 index 000000000..6ad7e8118 --- /dev/null +++ b/packages/services/service-realtime/src/objects/index.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Realtime objects owned by `@objectstack/service-realtime` (ADR-0029 K2). + * `sys_presence` moved here from `@objectstack/platform-objects`. + */ + +export { SysPresence } from './sys-presence.object.js'; diff --git a/packages/services/service-realtime/src/objects/sys-presence.object.test.ts b/packages/services/service-realtime/src/objects/sys-presence.object.test.ts index 696452b19..029327f74 100644 --- a/packages/services/service-realtime/src/objects/sys-presence.object.test.ts +++ b/packages/services/service-realtime/src/objects/sys-presence.object.test.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; -import { SysPresence } from '@objectstack/platform-objects/audit'; +import { SysPresence } from './sys-presence.object.js'; import { StorageNameMapping } from '@objectstack/spec/system'; describe('SysPresence object definition', () => { diff --git a/packages/platform-objects/src/audit/sys-presence.object.ts b/packages/services/service-realtime/src/objects/sys-presence.object.ts similarity index 100% rename from packages/platform-objects/src/audit/sys-presence.object.ts rename to packages/services/service-realtime/src/objects/sys-presence.object.ts diff --git a/packages/services/service-realtime/src/realtime-service-plugin.ts b/packages/services/service-realtime/src/realtime-service-plugin.ts index fcb261287..ecbef1a3c 100644 --- a/packages/services/service-realtime/src/realtime-service-plugin.ts +++ b/packages/services/service-realtime/src/realtime-service-plugin.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import type { Plugin, PluginContext } from '@objectstack/core'; -import { SysPresence } from '@objectstack/platform-objects/audit'; +import { SysPresence } from './objects/index.js'; import { InMemoryRealtimeAdapter } from './in-memory-realtime-adapter.js'; import type { InMemoryRealtimeAdapterOptions } from './in-memory-realtime-adapter.js'; @@ -63,6 +63,21 @@ export class RealtimeServicePlugin implements Plugin { objects: [SysPresence], }); + // ADR-0029 D8 — contribute sys_presence translations on kernel:ready. + if (typeof (ctx as any).hook === 'function') { + (ctx as any).hook('kernel:ready', async () => { + try { + const i18n = ctx.getService('i18n'); + if (i18n && typeof i18n.loadTranslations === 'function') { + const { RealtimeTranslations } = await import('./translations/index.js'); + for (const [locale, data] of Object.entries(RealtimeTranslations)) { + i18n.loadTranslations(locale, data as Record); + } + } + } catch { /* i18n optional */ } + }); + } + ctx.logger.info('RealtimeServicePlugin: registered in-memory realtime adapter'); } } diff --git a/packages/services/service-realtime/src/translations/en.objects.generated.ts b/packages/services/service-realtime/src/translations/en.objects.generated.ts new file mode 100644 index 000000000..2cf3bc77c --- /dev/null +++ b/packages/services/service-realtime/src/translations/en.objects.generated.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'en'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const enObjects: NonNullable = { + sys_presence: { + label: "Presence", + pluralLabel: "Presences", + description: "Real-time user presence and activity tracking", + fields: { + id: { + label: "Presence ID" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + }, + user_id: { + label: "User" + }, + session_id: { + label: "Session" + }, + status: { + label: "Status", + options: { + online: "Online", + away: "Away", + busy: "Busy", + offline: "Offline" + } + }, + last_seen: { + label: "Last Seen" + }, + current_location: { + label: "Current Location" + }, + device: { + label: "Device", + options: { + desktop: "Desktop", + mobile: "Mobile", + tablet: "Tablet", + other: "Other" + } + }, + custom_status: { + label: "Custom Status" + }, + metadata: { + label: "Metadata", + help: "Arbitrary JSON metadata associated with the presence state (matches PresenceStateSchema.metadata)." + } + } + } +}; diff --git a/packages/services/service-realtime/src/translations/es-ES.objects.generated.ts b/packages/services/service-realtime/src/translations/es-ES.objects.generated.ts new file mode 100644 index 000000000..0c41f603a --- /dev/null +++ b/packages/services/service-realtime/src/translations/es-ES.objects.generated.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'es-ES'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const esESObjects: NonNullable = { + sys_presence: { + label: "Presencia", + pluralLabel: "Presencias", + description: "Seguimiento en tiempo real de presencia y actividad de usuarios", + fields: { + id: { + label: "ID de presencia" + }, + created_at: { + label: "Creado el" + }, + updated_at: { + label: "Actualizado el" + }, + user_id: { + label: "Usuario" + }, + session_id: { + label: "Sesión" + }, + status: { + label: "Estado", + options: { + online: "En línea", + away: "Ausente", + busy: "Ocupado", + offline: "Desconectado" + } + }, + last_seen: { + label: "Visto por última vez" + }, + current_location: { + label: "Ubicación actual" + }, + device: { + label: "Dispositivo", + options: { + desktop: "Escritorio", + mobile: "Móvil", + tablet: "Tableta", + other: "Otro" + } + }, + custom_status: { + label: "Estado personalizado" + }, + metadata: { + label: "Metadatos", + help: "Metadatos JSON arbitrarios asociados al estado de presencia (coincide con PresenceStateSchema.metadata)." + } + } + } +}; diff --git a/packages/services/service-realtime/src/translations/index.ts b/packages/services/service-realtime/src/translations/index.ts new file mode 100644 index 000000000..7e4624717 --- /dev/null +++ b/packages/services/service-realtime/src/translations/index.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * RealtimeTranslations — i18n bundle owned by this service plugin + * (ADR-0029 D8). + * + * Object label/field/view/action translations for sys_presence, the object + * this plugin owns. Loaded at runtime via the plugin's `kernel:ready` hook + * (`i18n.loadTranslations`). Regenerate with `os i18n extract` against + * `scripts/i18n-extract.config.ts`. + */ + +import type { TranslationBundle } from '@objectstack/spec/system'; +import { enObjects } from './en.objects.generated.js'; +import { zhCNObjects } from './zh-CN.objects.generated.js'; +import { jaJPObjects } from './ja-JP.objects.generated.js'; +import { esESObjects } from './es-ES.objects.generated.js'; + +export const RealtimeTranslations: TranslationBundle = { + en: { objects: enObjects }, + 'zh-CN': { objects: zhCNObjects }, + 'ja-JP': { objects: jaJPObjects }, + 'es-ES': { objects: esESObjects }, +}; diff --git a/packages/services/service-realtime/src/translations/ja-JP.objects.generated.ts b/packages/services/service-realtime/src/translations/ja-JP.objects.generated.ts new file mode 100644 index 000000000..935829b8a --- /dev/null +++ b/packages/services/service-realtime/src/translations/ja-JP.objects.generated.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'ja-JP'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const jaJPObjects: NonNullable = { + sys_presence: { + label: "在席状況", + pluralLabel: "在席状況", + description: "リアルタイムのユーザー在席状況とアクティビティ追跡", + fields: { + id: { + label: "在席 ID" + }, + created_at: { + label: "作成日時" + }, + updated_at: { + label: "更新日時" + }, + user_id: { + label: "ユーザー" + }, + session_id: { + label: "セッション" + }, + status: { + label: "ステータス", + options: { + online: "オンライン", + away: "離席中", + busy: "取り込み中", + offline: "オフライン" + } + }, + last_seen: { + label: "最終確認日時" + }, + current_location: { + label: "現在地" + }, + device: { + label: "デバイス", + options: { + desktop: "デスクトップ", + mobile: "モバイル", + tablet: "タブレット", + other: "その他" + } + }, + custom_status: { + label: "カスタムステータス" + }, + metadata: { + label: "メタデータ", + help: "在席状態に関連付けられた任意の JSON メタデータ(PresenceStateSchema.metadata に対応)。" + } + } + } +}; diff --git a/packages/services/service-realtime/src/translations/zh-CN.objects.generated.ts b/packages/services/service-realtime/src/translations/zh-CN.objects.generated.ts new file mode 100644 index 000000000..3662ed153 --- /dev/null +++ b/packages/services/service-realtime/src/translations/zh-CN.objects.generated.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'zh-CN'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const zhCNObjects: NonNullable = { + sys_presence: { + label: "在线状态", + pluralLabel: "在线状态", + description: "实时用户在线与活动跟踪", + fields: { + id: { + label: "在线状态 ID" + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + }, + user_id: { + label: "用户" + }, + session_id: { + label: "会话" + }, + status: { + label: "状态", + options: { + online: "在线", + away: "离开", + busy: "忙碌", + offline: "离线" + } + }, + last_seen: { + label: "最近在线时间" + }, + current_location: { + label: "当前位置" + }, + device: { + label: "设备", + options: { + desktop: "桌面端", + mobile: "移动端", + tablet: "平板端", + other: "其他" + } + }, + custom_status: { + label: "自定义状态" + }, + metadata: { + label: "元数据", + help: "与在线状态关联的任意 JSON 元数据(对应 PresenceStateSchema.metadata)。" + } + } + } +};