diff --git a/.changeset/sys-tables-account-i18n.md b/.changeset/sys-tables-account-i18n.md new file mode 100644 index 0000000000..8ada8162f4 --- /dev/null +++ b/.changeset/sys-tables-account-i18n.md @@ -0,0 +1,26 @@ +--- +'@objectstack/spec': minor +'@objectstack/cli': minor +'@objectstack/platform-objects': patch +'@objectstack/service-messaging': minor +--- + +i18n: translate the system account/messaging surfaces end to end. + +- **spec**: `ObjectTranslationDataSchema` / `ObjectTranslationNodeSchema` now + accept `_views..emptyState.{title,message}` so list-view empty states + are translatable (contract-first for the extractor below). +- **cli**: `os i18n extract` emits `_views..emptyState` keys when a view + declares an empty state. +- **platform-objects**: fill every missing zh-CN/ja-JP/es-ES translation for + `sys_user`, `sys_organization` and `sys_business_unit` (fields, options, + views, actions); replace the hardcoded English tab/section/action labels in + the `sys_user`, `sys_organization` and `sys_position` detail pages with + inline i18n label objects, and route the user Security tab through + `record:quick_actions` so object action labels localize. +- **service-messaging**: new ADR-0029 D8 translation bundle + (`MessagingTranslations`) covering the seven `sys_*` messaging objects + (inbox message, receipts, deliveries, preferences, subscriptions, templates, + HTTP deliveries), registered on `kernel:ready`; zh-CN is fully translated + and ja-JP/es-ES cover `sys_inbox_message` (incl. the `mine` view empty + state). diff --git a/packages/cli/src/utils/i18n-extract.ts b/packages/cli/src/utils/i18n-extract.ts index c33d0ba313..02c031a407 100644 --- a/packages/cli/src/utils/i18n-extract.ts +++ b/packages/cli/src/utils/i18n-extract.ts @@ -21,6 +21,7 @@ * objects..fields..options. * objects.._views..label * objects.._views..description + * objects.._views..emptyState.title / .message * objects.._actions..label * objects.._actions..confirmText * objects.._actions..successMessage @@ -119,6 +120,22 @@ function viewObjectName(view: any): string | undefined { return view?.objectName ?? view?.object ?? view?.data?.object; } +/** + * Emit `_views..emptyState.{title,message}` entries when the view + * declares empty-state copy. Mirrors the client-side resolver convention + * (`viewEmptyState` in @object-ui/i18n) and `ObjectTranslationDataSchema`. + */ +function pushViewEmptyState(out: ExpectedEntry[], viewPath: string[], view: any, objectName: string): void { + const emptyState = view?.emptyState; + if (!emptyState || typeof emptyState !== 'object') return; + if (typeof emptyState.title === 'string' && emptyState.title.length > 0) { + pushEntry(out, [...viewPath, 'emptyState', 'title'], emptyState.title, 'view', { objectName }); + } + if (typeof emptyState.message === 'string' && emptyState.message.length > 0) { + pushEntry(out, [...viewPath, 'emptyState', 'message'], emptyState.message, 'view', { objectName }); + } +} + function pushEntry( out: ExpectedEntry[], path: string[], @@ -200,6 +217,7 @@ export function collectExpectedEntries(config: any): ExpectedEntry[] { if (view.description) { pushEntry(out, ['objects', objectName, '_views', viewName, 'description'], view.description, 'view', { objectName }); } + pushViewEmptyState(out, ['objects', objectName, '_views', viewName], view, objectName); } } @@ -229,6 +247,7 @@ export function collectExpectedEntries(config: any): ExpectedEntry[] { if (view.description) { pushEntry(out, ['objects', objectName, '_views', view.name, 'description'], view.description, 'view', { objectName }); } + pushViewEmptyState(out, ['objects', objectName, '_views', view.name], view, objectName); } // ── Top-level actions ──────────────────────────────────────────── diff --git a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts index bf53f97302..95abc11e68 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts @@ -43,44 +43,44 @@ export const esESObjects: NonNullable = { help: "Si se establece, el bloqueo se elimina automáticamente en ese momento." }, failed_login_count: { - label: "Failed Login Count", - help: "Consecutive failed sign-in attempts; reset to 0 on success. Maintained by the auth manager." + label: "Intentos fallidos consecutivos", + help: "Intentos de inicio de sesión fallidos consecutivos; se restablece a 0 tras un inicio correcto. Lo mantiene el gestor de autenticación." }, locked_until: { - label: "Locked Until", - help: "When set and in the future, sign-in is rejected (brute-force lockout). Auto-clears past this time; an admin can clear it early via Unlock." + label: "Bloqueado hasta", + help: "Si está establecido y es una fecha futura, el inicio de sesión se rechaza (bloqueo por fuerza bruta). Se limpia automáticamente pasado ese momento; un administrador puede levantarlo antes con «Desbloquear cuenta»." }, password_changed_at: { - label: "Password Changed At", - help: "Timestamp of the last password change. Backs password_expiry_days; system-managed." + label: "Contraseña cambiada el", + help: "Fecha y hora del último cambio de contraseña. Sirve de base a password_expiry_days; gestionado por el sistema." }, phone_number: { - label: "Phone Number", - help: "Sign-in phone number (E.164 recommended). Unique per user; managed by better-auth when the phoneNumber plugin is enabled." + label: "Número de teléfono", + help: "Número de teléfono para iniciar sesión (se recomienda E.164). Único por usuario; lo gestiona better-auth cuando el plugin phoneNumber está habilitado." }, phone_number_verified: { - label: "Phone Verified", - help: "Whether the phone number has been verified (OTP verification requires SMS infrastructure; false until that ships). System-managed." + label: "Teléfono verificado", + help: "Indica si el número de teléfono se ha verificado (la verificación OTP requiere infraestructura de SMS; false hasta que esté disponible). Gestionado por el sistema." }, must_change_password: { - label: "Must Change Password", - help: "When true, the user is blocked (403 PASSWORD_EXPIRED) until they change their password. Stamped by the admin user-management routes; system-managed." + label: "Debe cambiar la contraseña", + help: "Si es true, el usuario queda bloqueado (403 PASSWORD_EXPIRED) hasta que cambie su contraseña. Lo establecen las rutas de administración de usuarios; gestionado por el sistema." }, mfa_required_at: { - label: "MFA Required At", - help: "When enforced MFA first applied to this user (grace-period clock). Backs mfa_required; system-managed." + label: "MFA exigido desde", + help: "Cuándo se aplicó por primera vez el MFA obligatorio a este usuario (inicio del período de gracia). Sirve de base a mfa_required; gestionado por el sistema." }, last_login_at: { - label: "Last Login At", - help: "Timestamp of the last successful sign-in. Stamped by the auth manager; system-managed." + label: "Último inicio de sesión", + help: "Fecha y hora del último inicio de sesión correcto. Lo registra el gestor de autenticación; gestionado por el sistema." }, last_login_ip: { - label: "Last Login IP", - help: "Client IP of the last successful sign-in (from the trusted proxy forwarded header). System-managed." + label: "IP del último inicio de sesión", + help: "IP del cliente del último inicio de sesión correcto (de la cabecera reenviada por el proxy de confianza). Gestionado por el sistema." }, ai_access: { - label: "AI Access", - help: "Whether this user holds an AI seat — grants access to the in-UI AI agents (build / ask). The framework synthesizes the `ai_seat` capability from this flag (plugin-hono-server resolveCtx). Assignment is capped by the licensed / purchased seat count (enforced by @objectstack/security-enterprise AiSeatPlugin). Owned by objectql (better-auth is oblivious to this column)." + label: "Acceso a IA", + help: "Indica si este usuario dispone de una plaza de IA: otorga acceso a los agentes de IA integrados en la interfaz (build / ask). El framework sintetiza la capacidad `ai_seat` a partir de este indicador (plugin-hono-server resolveCtx). La asignación está limitada por el número de plazas licenciadas o compradas (aplicado por AiSeatPlugin de @objectstack/security-enterprise). Propiedad de objectql (better-auth ignora esta columna)." }, image: { label: "Imagen de perfil" @@ -94,11 +94,11 @@ export const esESObjects: NonNullable = { help: "The user's primary business unit — a denormalised projection of sys_business_unit_member.is_primary, maintained by plugin-sharing (ADR-0057 addendum D12). Lets a user-lookup filter candidates by business unit without traversing the membership junction. Do not edit directly; set it via business-unit membership." }, source: { - label: "Identity Source", - help: "How this identity was created — idp_provisioned (federated SSO JIT) or env_native (local signup / app end-user). System-managed; do not edit.", + label: "Origen de la identidad", + help: "Cómo se creó esta identidad: idp_provisioned (JIT de SSO federado) o env_native (registro local / usuario final de la aplicación). Gestionado por el sistema; no editar.", options: { - idp_provisioned: "IdP-Provisioned", - env_native: "Env-Native" + idp_provisioned: "Aprovisionada por IdP", + env_native: "Nativa del entorno" } }, id: { @@ -113,7 +113,7 @@ export const esESObjects: NonNullable = { }, _views: { me: { - label: "My Profile" + label: "Mi perfil" }, all_users: { label: "Todos los usuarios" @@ -143,12 +143,12 @@ export const esESObjects: NonNullable = { successMessage: "Usuario desbloqueado" }, unlock_user: { - label: "Unlock Account", - successMessage: "Account unlocked" + label: "Desbloquear cuenta", + successMessage: "Cuenta desbloqueada" }, create_user: { - label: "Create User", - successMessage: "User created" + label: "Crear usuario", + successMessage: "Usuario creado" }, set_user_password: { label: "Establecer contraseña", @@ -176,8 +176,8 @@ export const esESObjects: NonNullable = { successMessage: "Correo de verificación enviado; revisa la nueva dirección para confirmar." }, resend_verification_email: { - label: "Resend Verification Email", - successMessage: "Verification email sent — check your inbox." + label: "Reenviar correo de verificación", + successMessage: "Correo de verificación enviado; revisa tu bandeja de entrada." }, delete_my_account: { label: "Eliminar mi cuenta", @@ -185,18 +185,18 @@ export const esESObjects: NonNullable = { successMessage: "Cuenta eliminada" }, enable_two_factor: { - label: "Enable Two-Factor Auth", - successMessage: "Two-factor authentication enabled. Scan the QR code or paste the otpauth URI into your authenticator app, then verify a code to complete setup." + label: "Habilitar autenticación de dos factores", + successMessage: "Autenticación de dos factores habilitada. Escanea el código QR o pega el URI otpauth en tu aplicación de autenticación y verifica un código para completar la configuración." }, disable_two_factor: { - label: "Disable Two-Factor Auth", - confirmText: "Turn off two-factor authentication? Your account will be less secure.", - successMessage: "Two-factor authentication disabled." + label: "Deshabilitar autenticación de dos factores", + confirmText: "¿Desactivar la autenticación de dos factores? Tu cuenta será menos segura.", + successMessage: "Autenticación de dos factores deshabilitada." }, generate_backup_codes: { - label: "Regenerate Backup Codes", - confirmText: "Generate a new set of backup codes? Any previously generated codes will stop working.", - successMessage: "New backup codes generated — save them somewhere safe." + label: "Regenerar códigos de respaldo", + confirmText: "¿Generar un nuevo juego de códigos de respaldo? Los códigos generados anteriormente dejarán de funcionar.", + successMessage: "Nuevos códigos de respaldo generados; guárdalos en un lugar seguro." } } }, @@ -396,8 +396,8 @@ export const esESObjects: NonNullable = { help: "Metadatos de la organización serializados en JSON." }, require_mfa: { - label: "Require Multi-Factor Auth", - help: "When true, every member of this organization must enroll an authenticator app to access data." + label: "Exigir autenticación multifactor", + help: "Si es true, todos los miembros de esta organización deben registrar una aplicación de autenticación para acceder a los datos." }, id: { label: "ID de organización" @@ -438,9 +438,9 @@ export const esESObjects: NonNullable = { successMessage: "Ha abandonado la organización" }, change_slug: { - label: "Change Slug", - confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?", - successMessage: "Organization slug changed" + label: "Cambiar slug", + confirmText: "Renombrar el slug reescribe todos los subdominios de la plataforma para esta organización y reserva el slug anterior durante 90 días. ¿Continuar?", + successMessage: "Slug de la organización cambiado" } } }, @@ -724,7 +724,7 @@ export const esESObjects: NonNullable = { }, _views: { org_chart: { - label: "Org Chart" + label: "Organigrama" }, active: { label: "Activo" diff --git a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts index 052c010adf..5ab58ee234 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts @@ -43,44 +43,44 @@ export const jaJPObjects: NonNullable = { help: "設定されている場合、この日時に利用停止が自動解除されます。" }, failed_login_count: { - label: "Failed Login Count", - help: "Consecutive failed sign-in attempts; reset to 0 on success. Maintained by the auth manager." + label: "連続ログイン失敗回数", + help: "連続してサインインに失敗した回数。成功すると 0 にリセットされます。認証マネージャーが管理します。" }, locked_until: { - label: "Locked Until", - help: "When set and in the future, sign-in is rejected (brute-force lockout). Auto-clears past this time; an admin can clear it early via Unlock." + label: "ロック解除日時", + help: "設定されており未来の日時の場合、サインインは拒否されます(ブルートフォース対策ロック)。この日時を過ぎると自動解除され、管理者は「アカウントのロック解除」で早期解除できます。" }, password_changed_at: { - label: "Password Changed At", - help: "Timestamp of the last password change. Backs password_expiry_days; system-managed." + label: "パスワード変更日時", + help: "最後にパスワードを変更した日時。password_expiry_days の基準になります。システム管理。" }, phone_number: { - label: "Phone Number", - help: "Sign-in phone number (E.164 recommended). Unique per user; managed by better-auth when the phoneNumber plugin is enabled." + label: "電話番号", + help: "サインイン用の電話番号(E.164 形式を推奨)。ユーザーごとに一意。phoneNumber プラグイン有効時は better-auth が管理します。" }, phone_number_verified: { - label: "Phone Verified", - help: "Whether the phone number has been verified (OTP verification requires SMS infrastructure; false until that ships). System-managed." + label: "電話番号確認済み", + help: "電話番号が確認済みかどうか(OTP 確認には SMS 基盤が必要で、提供されるまでは false)。システム管理。" }, must_change_password: { - label: "Must Change Password", - help: "When true, the user is blocked (403 PASSWORD_EXPIRED) until they change their password. Stamped by the admin user-management routes; system-managed." + label: "パスワード変更必須", + help: "true の場合、パスワードを変更するまでユーザーはブロックされます(403 PASSWORD_EXPIRED)。管理者のユーザー管理ルートが設定します。システム管理。" }, mfa_required_at: { - label: "MFA Required At", - help: "When enforced MFA first applied to this user (grace-period clock). Backs mfa_required; system-managed." + label: "MFA 強制適用日時", + help: "強制 MFA がこのユーザーに最初に適用された日時(猶予期間の起点)。mfa_required の基準になります。システム管理。" }, last_login_at: { - label: "Last Login At", - help: "Timestamp of the last successful sign-in. Stamped by the auth manager; system-managed." + label: "最終ログイン日時", + help: "最後に成功したサインインの日時。認証マネージャーが記録します。システム管理。" }, last_login_ip: { - label: "Last Login IP", - help: "Client IP of the last successful sign-in (from the trusted proxy forwarded header). System-managed." + label: "最終ログイン IP", + help: "最後に成功したサインインのクライアント IP(信頼済みプロキシの転送ヘッダー由来)。システム管理。" }, ai_access: { - label: "AI Access", - help: "Whether this user holds an AI seat — grants access to the in-UI AI agents (build / ask). The framework synthesizes the `ai_seat` capability from this flag (plugin-hono-server resolveCtx). Assignment is capped by the licensed / purchased seat count (enforced by @objectstack/security-enterprise AiSeatPlugin). Owned by objectql (better-auth is oblivious to this column)." + label: "AI アクセス", + help: "このユーザーが AI シートを保有しているかどうか。UI 内 AI エージェント(build / ask)へのアクセスを付与します。フレームワークはこのフラグから `ai_seat` ケイパビリティを合成します(plugin-hono-server resolveCtx)。割り当て数はライセンス/購入済みシート数が上限です(@objectstack/security-enterprise AiSeatPlugin が強制)。objectql が所有します(better-auth はこの列を認識しません)。" }, image: { label: "プロフィール画像" @@ -94,11 +94,11 @@ export const jaJPObjects: NonNullable = { help: "The user's primary business unit — a denormalised projection of sys_business_unit_member.is_primary, maintained by plugin-sharing (ADR-0057 addendum D12). Lets a user-lookup filter candidates by business unit without traversing the membership junction. Do not edit directly; set it via business-unit membership." }, source: { - label: "Identity Source", - help: "How this identity was created — idp_provisioned (federated SSO JIT) or env_native (local signup / app end-user). System-managed; do not edit.", + label: "ID ソース", + help: "この ID の作成方法 — idp_provisioned(フェデレーション SSO の JIT)または env_native(ローカルサインアップ/アプリのエンドユーザー)。システム管理のため編集しないでください。", options: { - idp_provisioned: "IdP-Provisioned", - env_native: "Env-Native" + idp_provisioned: "IdP プロビジョニング", + env_native: "環境ネイティブ" } }, id: { @@ -113,7 +113,7 @@ export const jaJPObjects: NonNullable = { }, _views: { me: { - label: "My Profile" + label: "マイプロフィール" }, all_users: { label: "すべてのユーザー" @@ -143,12 +143,12 @@ export const jaJPObjects: NonNullable = { successMessage: "ユーザーの利用停止を解除しました" }, unlock_user: { - label: "Unlock Account", - successMessage: "Account unlocked" + label: "アカウントのロック解除", + successMessage: "アカウントのロックを解除しました" }, create_user: { - label: "Create User", - successMessage: "User created" + label: "ユーザーを作成", + successMessage: "ユーザーを作成しました" }, set_user_password: { label: "パスワードを設定", @@ -176,8 +176,8 @@ export const jaJPObjects: NonNullable = { successMessage: "確認メールを送信しました。新しいメールアドレスで確認してください。" }, resend_verification_email: { - label: "Resend Verification Email", - successMessage: "Verification email sent — check your inbox." + label: "確認メールを再送", + successMessage: "確認メールを送信しました。受信トレイをご確認ください。" }, delete_my_account: { label: "アカウント削除", @@ -185,18 +185,18 @@ export const jaJPObjects: NonNullable = { successMessage: "アカウントを削除しました" }, enable_two_factor: { - label: "Enable Two-Factor Auth", - successMessage: "Two-factor authentication enabled. Scan the QR code or paste the otpauth URI into your authenticator app, then verify a code to complete setup." + label: "二要素認証を有効化", + successMessage: "二要素認証を有効にしました。認証アプリで QR コードをスキャンするか otpauth URI を貼り付け、コードを検証して設定を完了してください。" }, disable_two_factor: { - label: "Disable Two-Factor Auth", - confirmText: "Turn off two-factor authentication? Your account will be less secure.", - successMessage: "Two-factor authentication disabled." + label: "二要素認証を無効化", + confirmText: "二要素認証をオフにしますか?アカウントの安全性が低下します。", + successMessage: "二要素認証を無効にしました。" }, generate_backup_codes: { - label: "Regenerate Backup Codes", - confirmText: "Generate a new set of backup codes? Any previously generated codes will stop working.", - successMessage: "New backup codes generated — save them somewhere safe." + label: "バックアップコードを再生成", + confirmText: "新しいバックアップコードを生成しますか?以前に生成されたコードはすべて使用できなくなります。", + successMessage: "新しいバックアップコードを生成しました。安全な場所に保管してください。" } } }, @@ -396,8 +396,8 @@ export const jaJPObjects: NonNullable = { help: "JSON シリアライズされた組織メタデータ" }, require_mfa: { - label: "Require Multi-Factor Auth", - help: "When true, every member of this organization must enroll an authenticator app to access data." + label: "多要素認証を必須化", + help: "true の場合、この組織のすべてのメンバーはデータにアクセスするために認証アプリの登録が必要になります。" }, id: { label: "組織 ID" @@ -438,9 +438,9 @@ export const jaJPObjects: NonNullable = { successMessage: "組織から退出しました" }, change_slug: { - label: "Change Slug", - confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?", - successMessage: "Organization slug changed" + label: "スラッグを変更", + confirmText: "スラッグを変更すると、この組織のすべてのプラットフォームサブドメインが書き換えられ、旧スラッグは 90 日間確保されます。続行しますか?", + successMessage: "組織のスラッグを変更しました" } } }, @@ -724,7 +724,7 @@ export const jaJPObjects: NonNullable = { }, _views: { org_chart: { - label: "Org Chart" + label: "組織図" }, active: { label: "有効" diff --git a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts index 3cea691914..9ac86ede06 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts @@ -43,44 +43,44 @@ export const zhCNObjects: NonNullable = { help: "设置后,到达该时间会自动解除封禁。" }, failed_login_count: { - label: "Failed Login Count", - help: "Consecutive failed sign-in attempts; reset to 0 on success. Maintained by the auth manager." + label: "连续登录失败次数", + help: "连续登录失败的次数;登录成功后重置为 0。由认证管理器维护。" }, locked_until: { - label: "Locked Until", - help: "When set and in the future, sign-in is rejected (brute-force lockout). Auto-clears past this time; an admin can clear it early via Unlock." + label: "锁定至", + help: "设置且时间在未来时,登录会被拒绝(防暴力破解锁定)。超过该时间自动解除;管理员也可通过“解锁账号”提前解除。" }, password_changed_at: { - label: "Password Changed At", - help: "Timestamp of the last password change. Backs password_expiry_days; system-managed." + label: "密码修改时间", + help: "最近一次修改密码的时间。支撑 password_expiry_days(密码有效期);系统维护。" }, phone_number: { - label: "Phone Number", - help: "Sign-in phone number (E.164 recommended). Unique per user; managed by better-auth when the phoneNumber plugin is enabled." + label: "手机号码", + help: "用于登录的手机号(建议 E.164 格式)。每个用户唯一;启用 phoneNumber 插件时由 better-auth 管理。" }, phone_number_verified: { - label: "Phone Verified", - help: "Whether the phone number has been verified (OTP verification requires SMS infrastructure; false until that ships). System-managed." + label: "手机已验证", + help: "手机号是否已验证(OTP 验证依赖短信基础设施,在其上线前保持 false)。系统维护。" }, must_change_password: { - label: "Must Change Password", - help: "When true, the user is blocked (403 PASSWORD_EXPIRED) until they change their password. Stamped by the admin user-management routes; system-managed." + label: "须修改密码", + help: "为 true 时,用户会被阻止访问(403 PASSWORD_EXPIRED),直到修改密码。由管理员用户管理接口写入;系统维护。" }, mfa_required_at: { - label: "MFA Required At", - help: "When enforced MFA first applied to this user (grace-period clock). Backs mfa_required; system-managed." + label: "MFA 强制生效时间", + help: "强制 MFA 首次对该用户生效的时间(宽限期计时起点)。支撑 mfa_required;系统维护。" }, last_login_at: { - label: "Last Login At", - help: "Timestamp of the last successful sign-in. Stamped by the auth manager; system-managed." + label: "最近登录时间", + help: "最近一次成功登录的时间。由认证管理器写入;系统维护。" }, last_login_ip: { - label: "Last Login IP", - help: "Client IP of the last successful sign-in (from the trusted proxy forwarded header). System-managed." + label: "最近登录 IP", + help: "最近一次成功登录的客户端 IP(取自可信代理转发头)。系统维护。" }, ai_access: { - label: "AI Access", - help: "Whether this user holds an AI seat — grants access to the in-UI AI agents (build / ask). The framework synthesizes the `ai_seat` capability from this flag (plugin-hono-server resolveCtx). Assignment is capped by the licensed / purchased seat count (enforced by @objectstack/security-enterprise AiSeatPlugin). Owned by objectql (better-auth is oblivious to this column)." + label: "AI 访问权限", + help: "该用户是否占用一个 AI 席位——授予界面内 AI 智能体(build / ask)的使用权限。框架会根据此标志合成 `ai_seat` 能力(plugin-hono-server resolveCtx)。可分配数量受许可/已购席位数限制(由 @objectstack/security-enterprise AiSeatPlugin 强制执行)。归 objectql 所有(better-auth 不感知此列)。" }, image: { label: "头像" @@ -94,11 +94,11 @@ export const zhCNObjects: NonNullable = { help: "The user's primary business unit — a denormalised projection of sys_business_unit_member.is_primary, maintained by plugin-sharing (ADR-0057 addendum D12). Lets a user-lookup filter candidates by business unit without traversing the membership junction. Do not edit directly; set it via business-unit membership." }, source: { - label: "Identity Source", - help: "How this identity was created — idp_provisioned (federated SSO JIT) or env_native (local signup / app end-user). System-managed; do not edit.", + label: "身份来源", + help: "该身份的创建方式——idp_provisioned(联合 SSO 即时开通)或 env_native(本地注册 / 应用最终用户)。系统维护,请勿编辑。", options: { - idp_provisioned: "IdP-Provisioned", - env_native: "Env-Native" + idp_provisioned: "IdP 开通", + env_native: "环境本地" } }, id: { @@ -113,7 +113,7 @@ export const zhCNObjects: NonNullable = { }, _views: { me: { - label: "My Profile" + label: "我的资料" }, all_users: { label: "全部用户" @@ -143,12 +143,12 @@ export const zhCNObjects: NonNullable = { successMessage: "用户已解除封禁" }, unlock_user: { - label: "Unlock Account", - successMessage: "Account unlocked" + label: "解锁账号", + successMessage: "账号已解锁" }, create_user: { - label: "Create User", - successMessage: "User created" + label: "创建用户", + successMessage: "用户已创建" }, set_user_password: { label: "设置密码", @@ -176,8 +176,8 @@ export const zhCNObjects: NonNullable = { successMessage: "已发送验证邮件,请前往新邮箱确认。" }, resend_verification_email: { - label: "Resend Verification Email", - successMessage: "Verification email sent — check your inbox." + label: "重发验证邮件", + successMessage: "验证邮件已发送,请查收。" }, delete_my_account: { label: "删除我的账号", @@ -185,18 +185,18 @@ export const zhCNObjects: NonNullable = { successMessage: "已删除账号" }, enable_two_factor: { - label: "Enable Two-Factor Auth", - successMessage: "Two-factor authentication enabled. Scan the QR code or paste the otpauth URI into your authenticator app, then verify a code to complete setup." + label: "启用双因素认证", + successMessage: "双因素认证已启用。用身份验证器 App 扫描二维码或粘贴 otpauth URI,然后验证一次动态码以完成设置。" }, disable_two_factor: { - label: "Disable Two-Factor Auth", - confirmText: "Turn off two-factor authentication? Your account will be less secure.", - successMessage: "Two-factor authentication disabled." + label: "停用双因素认证", + confirmText: "要关闭双因素认证吗?您的账户安全性将降低。", + successMessage: "双因素认证已停用。" }, generate_backup_codes: { - label: "Regenerate Backup Codes", - confirmText: "Generate a new set of backup codes? Any previously generated codes will stop working.", - successMessage: "New backup codes generated — save them somewhere safe." + label: "重新生成备用码", + confirmText: "要生成一组新的备用码吗?之前生成的备用码将全部失效。", + successMessage: "新备用码已生成——请妥善保存。" } } }, @@ -396,8 +396,8 @@ export const zhCNObjects: NonNullable = { help: "JSON 序列化的组织元数据" }, require_mfa: { - label: "Require Multi-Factor Auth", - help: "When true, every member of this organization must enroll an authenticator app to access data." + label: "强制多因素认证", + help: "为 true 时,该组织的每位成员都必须注册身份验证器 App 才能访问数据。" }, id: { label: "组织 ID" @@ -438,9 +438,9 @@ export const zhCNObjects: NonNullable = { successMessage: "你已退出该组织" }, change_slug: { - label: "Change Slug", - confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?", - successMessage: "Organization slug changed" + label: "修改标识符", + confirmText: "重命名标识符会重写该组织所有平台子域名,旧标识符将保留占用 90 天。继续吗?", + successMessage: "组织标识符已修改" } } }, @@ -724,7 +724,7 @@ export const zhCNObjects: NonNullable = { }, _views: { org_chart: { - label: "Org Chart" + label: "组织架构" }, active: { label: "启用" diff --git a/packages/platform-objects/src/pages/sys-organization.page.ts b/packages/platform-objects/src/pages/sys-organization.page.ts index d2baf3cf1d..c5514ff084 100644 --- a/packages/platform-objects/src/pages/sys-organization.page.ts +++ b/packages/platform-objects/src/pages/sys-organization.page.ts @@ -52,7 +52,7 @@ export const SysOrganizationDetailPage: Page = { position: 'top', items: [ { - label: 'Members', + label: { en: 'Members', 'zh-CN': '成员', 'ja-JP': 'メンバー', 'es-ES': 'Miembros' }, icon: 'users', children: [ { @@ -64,13 +64,13 @@ export const SysOrganizationDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'Members', + title: { en: 'Members', 'zh-CN': '成员', 'ja-JP': 'メンバー', 'es-ES': 'Miembros' }, }, }, ], }, { - label: 'Invitations', + label: { en: 'Invitations', 'zh-CN': '邀请', 'ja-JP': '招待', 'es-ES': 'Invitaciones' }, icon: 'mail', children: [ { @@ -82,13 +82,13 @@ export const SysOrganizationDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'Invitations', + title: { en: 'Invitations', 'zh-CN': '邀请', 'ja-JP': '招待', 'es-ES': 'Invitaciones' }, }, }, ], }, { - label: 'Teams', + label: { en: 'Teams', 'zh-CN': '团队', 'ja-JP': 'チーム', 'es-ES': 'Equipos' }, icon: 'users-round', children: [ { @@ -100,7 +100,7 @@ export const SysOrganizationDetailPage: Page = { sort: [{ field: 'name', order: 'asc' }], limit: 25, showViewAll: true, - title: 'Teams', + title: { en: 'Teams', 'zh-CN': '团队', 'ja-JP': 'チーム', 'es-ES': 'Equipos' }, }, }, ], diff --git a/packages/platform-objects/src/pages/sys-position.page.ts b/packages/platform-objects/src/pages/sys-position.page.ts index 9c41a16b71..dd06659d16 100644 --- a/packages/platform-objects/src/pages/sys-position.page.ts +++ b/packages/platform-objects/src/pages/sys-position.page.ts @@ -50,7 +50,7 @@ export const SysPositionDetailPage: Page = { position: 'top', items: [ { - label: 'Holders', + label: { en: 'Holders', 'zh-CN': '任职人员', 'ja-JP': '在任者', 'es-ES': 'Titulares' }, icon: 'users', children: [ { @@ -63,21 +63,21 @@ export const SysPositionDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'Holders', + title: { en: 'Holders', 'zh-CN': '任职人员', 'ja-JP': '在任者', 'es-ES': 'Titulares' }, add: { picker: { object: 'sys_user', labelField: 'name', }, linkField: 'user_id', - label: 'Assign user', + label: { en: 'Assign user', 'zh-CN': '分配用户', 'ja-JP': 'ユーザーを割り当て', 'es-ES': 'Asignar usuario' }, }, }, }, ], }, { - label: 'Permission Sets', + label: { en: 'Permission Sets', 'zh-CN': '权限集', 'ja-JP': '権限セット', 'es-ES': 'Conjuntos de permisos' }, icon: 'lock', children: [ { @@ -89,14 +89,14 @@ export const SysPositionDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'Permission Sets', + title: { en: 'Permission Sets', 'zh-CN': '权限集', 'ja-JP': '権限セット', 'es-ES': 'Conjuntos de permisos' }, add: { picker: { object: 'sys_permission_set', labelField: 'label', }, linkField: 'permission_set_id', - label: 'Bind permission set', + label: { en: 'Bind permission set', 'zh-CN': '绑定权限集', 'ja-JP': '権限セットをバインド', 'es-ES': 'Vincular conjunto de permisos' }, }, }, }, diff --git a/packages/platform-objects/src/pages/sys-user.page.ts b/packages/platform-objects/src/pages/sys-user.page.ts index 06fc1eda39..6d8c238489 100644 --- a/packages/platform-objects/src/pages/sys-user.page.ts +++ b/packages/platform-objects/src/pages/sys-user.page.ts @@ -62,13 +62,30 @@ export const SysUserDetailPage: Page = { properties: { severity: 'warning', icon: 'mail', - title: '邮箱未验证', - body: '验证你的邮箱以接收密码重置和重要的系统通知。', + // Inline-i18n object labels: the renderer resolves them with + // pickLocalized (exact → base → default → en → first). + title: { + en: 'Email not verified', + 'zh-CN': '邮箱未验证', + 'ja-JP': 'メールが未認証です', + 'es-ES': 'Correo no verificado', + }, + body: { + en: 'Verify your email to receive password resets and important system notifications.', + 'zh-CN': '验证你的邮箱以接收密码重置和重要的系统通知。', + 'ja-JP': 'パスワードリセットや重要なシステム通知を受け取るには、メールアドレスを認証してください。', + 'es-ES': 'Verifica tu correo para recibir restablecimientos de contraseña y notificaciones importantes del sistema.', + }, visible: 'record.id == ctx.user.id && record.email_verified == false', dismissible: false, action: { actionName: 'resend_verification_email', - label: '重新发送验证邮件', + label: { + en: 'Resend verification email', + 'zh-CN': '重新发送验证邮件', + 'ja-JP': '認証メールを再送信', + 'es-ES': 'Reenviar correo de verificación', + }, }, }, }, @@ -99,11 +116,11 @@ export const SysUserDetailPage: Page = { ], sections: [ { - label: 'Identity', + label: { en: 'Identity', 'zh-CN': '身份', 'ja-JP': 'アイデンティティ', 'es-ES': 'Identidad' }, fields: ['name', 'image'], }, { - label: 'Audit', + label: { en: 'Audit', 'zh-CN': '审计', 'ja-JP': '監査', 'es-ES': 'Auditoría' }, fields: ['created_at', 'updated_at'], }, ], @@ -122,7 +139,7 @@ export const SysUserDetailPage: Page = { position: 'top', items: [ { - label: 'Positions', + label: { en: 'Positions', 'zh-CN': '岗位', 'ja-JP': 'ポジション', 'es-ES': 'Puestos' }, icon: 'shield-check', children: [ { @@ -139,7 +156,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'Positions', + title: { en: 'Positions', 'zh-CN': '岗位', 'ja-JP': 'ポジション', 'es-ES': 'Puestos' }, add: { picker: { object: 'sys_position', @@ -147,14 +164,19 @@ export const SysUserDetailPage: Page = { labelField: 'label', }, linkField: 'position', - label: 'Assign position', + label: { + en: 'Assign position', + 'zh-CN': '分配岗位', + 'ja-JP': 'ポジションを割り当て', + 'es-ES': 'Asignar puesto', + }, }, }, }, ], }, { - label: 'Sessions', + label: { en: 'Sessions', 'zh-CN': '会话', 'ja-JP': 'セッション', 'es-ES': 'Sesiones' }, icon: 'monitor', children: [ { @@ -166,13 +188,13 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'Sessions', + title: { en: 'Sessions', 'zh-CN': '会话', 'ja-JP': 'セッション', 'es-ES': 'Sesiones' }, }, }, ], }, { - label: 'Linked Accounts', + label: { en: 'Linked Accounts', 'zh-CN': '关联账号', 'ja-JP': '連携アカウント', 'es-ES': 'Cuentas vinculadas' }, icon: 'link', children: [ { @@ -184,13 +206,13 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'Linked Accounts', + title: { en: 'Linked Accounts', 'zh-CN': '关联账号', 'ja-JP': '連携アカウント', 'es-ES': 'Cuentas vinculadas' }, }, }, ], }, { - label: 'Organizations', + label: { en: 'Organizations', 'zh-CN': '组织', 'ja-JP': '組織', 'es-ES': 'Organizaciones' }, icon: 'building-2', children: [ { @@ -202,13 +224,13 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'Organizations', + title: { en: 'Organizations', 'zh-CN': '组织', 'ja-JP': '組織', 'es-ES': 'Organizaciones' }, }, }, ], }, { - label: 'OAuth Apps', + label: { en: 'OAuth Apps', 'zh-CN': 'OAuth 应用', 'ja-JP': 'OAuth アプリ', 'es-ES': 'Aplicaciones OAuth' }, icon: 'key-square', children: [ { @@ -220,7 +242,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'OAuth Apps', + title: { en: 'OAuth Apps', 'zh-CN': 'OAuth 应用', 'ja-JP': 'OAuth アプリ', 'es-ES': 'Aplicaciones OAuth' }, }, }, ], @@ -232,21 +254,31 @@ export const SysUserDetailPage: Page = { // `location: 'record_section'` so they only render here, not // in the global header row. { - label: 'Security', + label: { en: 'Security', 'zh-CN': '安全', 'ja-JP': 'セキュリティ', 'es-ES': 'Seguridad' }, icon: 'shield', children: [ { type: 'element:text', properties: { variant: 'subheading', - content: 'Password & Sign-in', + content: { + en: 'Password & Sign-in', + 'zh-CN': '密码与登录', + 'ja-JP': 'パスワードとサインイン', + 'es-ES': 'Contraseña e inicio de sesión', + }, }, }, { type: 'element:text', properties: { variant: 'caption', - content: 'Change your password or the email address associated with this account.', + content: { + en: 'Change your password or the email address associated with this account.', + 'zh-CN': '修改密码或此账号绑定的邮箱地址。', + 'ja-JP': 'パスワード、またはこのアカウントに関連付けられたメールアドレスを変更します。', + 'es-ES': 'Cambia tu contraseña o la dirección de correo asociada a esta cuenta.', + }, }, }, { @@ -262,14 +294,24 @@ export const SysUserDetailPage: Page = { type: 'element:text', properties: { variant: 'subheading', - content: 'Two-Factor Authentication', + content: { + en: 'Two-Factor Authentication', + 'zh-CN': '两步验证', + 'ja-JP': '二要素認証', + 'es-ES': 'Autenticación de dos factores', + }, }, }, { type: 'element:text', properties: { variant: 'caption', - content: 'Add a second layer of security using a TOTP authenticator app. Backup codes let you sign in if you lose your device.', + content: { + en: 'Add a second layer of security using a TOTP authenticator app. Backup codes let you sign in if you lose your device.', + 'zh-CN': '使用 TOTP 验证器应用添加第二层安全防护。备用验证码可在设备丢失时用于登录。', + 'ja-JP': 'TOTP 認証アプリでセキュリティをもう一段強化します。バックアップコードがあれば、デバイスを紛失してもサインインできます。', + 'es-ES': 'Añade una segunda capa de seguridad con una app de autenticación TOTP. Los códigos de respaldo te permiten iniciar sesión si pierdes tu dispositivo.', + }, }, }, { @@ -285,14 +327,24 @@ export const SysUserDetailPage: Page = { type: 'element:text', properties: { variant: 'subheading', - content: 'Email Verification', + content: { + en: 'Email Verification', + 'zh-CN': '邮箱验证', + 'ja-JP': 'メール認証', + 'es-ES': 'Verificación de correo', + }, }, }, { type: 'element:text', properties: { variant: 'caption', - content: 'Verify your email so password resets and notifications reach you. The button appears only while verification is pending.', + content: { + en: 'Verify your email so password resets and notifications reach you. The button appears only while verification is pending.', + 'zh-CN': '验证你的邮箱,以便接收密码重置和系统通知。按钮仅在邮箱待验证时显示。', + 'ja-JP': 'パスワードリセットや通知を受け取れるよう、メールアドレスを認証してください。ボタンは未認証の間のみ表示されます。', + 'es-ES': 'Verifica tu correo para recibir restablecimientos de contraseña y notificaciones. El botón solo aparece mientras la verificación está pendiente.', + }, }, }, { @@ -308,7 +360,12 @@ export const SysUserDetailPage: Page = { type: 'element:text', properties: { variant: 'subheading', - content: 'Danger Zone', + content: { + en: 'Danger Zone', + 'zh-CN': '危险操作', + 'ja-JP': '危険な操作', + 'es-ES': 'Zona de peligro', + }, }, className: 'text-destructive', }, @@ -316,7 +373,12 @@ export const SysUserDetailPage: Page = { type: 'element:text', properties: { variant: 'caption', - content: 'Permanent. Once deleted, your account cannot be recovered.', + content: { + en: 'Permanent. Once deleted, your account cannot be recovered.', + 'zh-CN': '此操作不可逆,账号一经删除将无法恢复。', + 'ja-JP': 'この操作は取り消せません。削除されたアカウントは復元できません。', + 'es-ES': 'Permanente. Una vez eliminada, tu cuenta no se puede recuperar.', + }, }, }, { @@ -334,7 +396,7 @@ export const SysUserDetailPage: Page = { // user_id FK; the sys_api_key object's own list-item actions // (revoke / restore) handle row operations. { - label: 'API Keys', + label: { en: 'API Keys', 'zh-CN': 'API 密钥', 'ja-JP': 'API キー', 'es-ES': 'Claves de API' }, icon: 'key-round', children: [ { @@ -346,7 +408,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: 'API Keys', + title: { en: 'API Keys', 'zh-CN': 'API 密钥', 'ja-JP': 'API キー', 'es-ES': 'Claves de API' }, }, }, ], diff --git a/packages/services/service-messaging/scripts/i18n-extract.config.ts b/packages/services/service-messaging/scripts/i18n-extract.config.ts new file mode 100644 index 0000000000..175a3dab69 --- /dev/null +++ b/packages/services/service-messaging/scripts/i18n-extract.config.ts @@ -0,0 +1,46 @@ +// 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 service owns the i18n extraction for the objects it owns; the + * `translations` baseline is this service's OWN generated bundles so re-running + * `--merge` preserves every hand-translated string. + * + * os i18n extract packages/services/service-messaging/scripts/i18n-extract.config.ts \ + * --locales=zh-CN,ja-JP,es-ES --fill=default --objects-only \ + * --out=packages/services/service-messaging/src/translations + */ + +import { defineStack } from '@objectstack/spec'; +import { + InboxMessage, + NotificationReceipt, + NotificationDelivery, + NotificationPreference, + NotificationSubscription, + NotificationTemplate, + HttpDelivery, +} from '../src/objects/index.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-messaging-i18n-extract', + objects: [ + InboxMessage, + NotificationReceipt, + NotificationDelivery, + NotificationPreference, + NotificationSubscription, + NotificationTemplate, + HttpDelivery, + ] as any, + translations: [ + { en: { objects: enObjects } }, + { 'zh-CN': { objects: zhCNObjects } }, + { 'ja-JP': { objects: jaJPObjects } }, + { 'es-ES': { objects: esESObjects } }, + ], +}); diff --git a/packages/services/service-messaging/src/messaging-service-plugin.ts b/packages/services/service-messaging/src/messaging-service-plugin.ts index f38e3a21e2..da0488c3a1 100644 --- a/packages/services/service-messaging/src/messaging-service-plugin.ts +++ b/packages/services/service-messaging/src/messaging-service-plugin.ts @@ -157,6 +157,23 @@ export class MessagingServicePlugin implements Plugin { ], }); + // ADR-0029 D8 — contribute this service's object translations to the + // i18n service on kernel:ready (the i18n plugin may register after + // this one). + if (typeof ctx.hook === 'function') { + ctx.hook('kernel:ready', async () => { + try { + const i18n = ctx.getService('i18n'); + if (i18n && typeof i18n.loadTranslations === 'function') { + const { MessagingTranslations } = await import('./translations/index.js'); + for (const [locale, data] of Object.entries(MessagingTranslations)) { + i18n.loadTranslations(locale, data as Record); + } + } + } catch { /* i18n optional */ } + }); + } + // Provision the physical tables for this service's system objects // up-front, once the engine is ready. The inbox channel materializes // sys_inbox_message + sys_notification_receipt rows on first delivery, diff --git a/packages/services/service-messaging/src/translations/en.objects.generated.ts b/packages/services/service-messaging/src/translations/en.objects.generated.ts new file mode 100644 index 0000000000..61135398cd --- /dev/null +++ b/packages/services/service-messaging/src/translations/en.objects.generated.ts @@ -0,0 +1,393 @@ +// 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_inbox_message: { + label: "Inbox Message", + pluralLabel: "Inbox Messages", + description: "User-facing in-app notification rows materialized by the inbox messaging channel.", + fields: { + id: { + label: "Inbox Message ID" + }, + user_id: { + label: "Recipient User" + }, + notification_id: { + label: "Notification Event", + help: "FK → sys_notification (the L2 event this row materializes)" + }, + delivery_id: { + label: "Delivery", + help: "FK → sys_notification_delivery (outbox row); null until P1" + }, + topic: { + label: "Topic" + }, + title: { + label: "Title" + }, + body_md: { + label: "Body" + }, + severity: { + label: "Severity", + options: { + info: "Info", + warning: "Warning", + critical: "Critical" + } + }, + action_url: { + label: "Action URL" + }, + created_at: { + label: "Created At" + } + }, + _views: { + mine: { + label: "Notifications", + emptyState: { + title: "Inbox zero", + message: "No notifications." + } + } + } + }, + sys_notification_receipt: { + label: "Notification Receipt", + pluralLabel: "Notification Receipts", + description: "Per-recipient × channel receipt; the source of truth for notification read-state.", + fields: { + id: { + label: "Receipt ID" + }, + notification_id: { + label: "Notification Event", + help: "FK → sys_notification (L2 event)" + }, + delivery_id: { + label: "Delivery", + help: "FK → sys_notification_delivery (outbox row); null until P1" + }, + user_id: { + label: "Recipient User" + }, + channel: { + label: "Channel", + help: "Channel id this receipt is for (inbox / email / push / …)" + }, + state: { + label: "State", + options: { + delivered: "delivered", + read: "read", + clicked: "clicked", + dismissed: "dismissed" + } + }, + at: { + label: "At", + help: "When the receipt reached its current state" + }, + created_at: { + label: "Created At" + } + } + }, + sys_notification_delivery: { + label: "Notification Delivery", + pluralLabel: "Notification Deliveries", + description: "Durable per-recipient × channel delivery outbox (ADR-0030 Layer 4).", + fields: { + id: { + label: "Delivery ID" + }, + notification_id: { + label: "Notification Event", + help: "FK → sys_notification (L2 event)" + }, + recipient_id: { + label: "Recipient User" + }, + channel: { + label: "Channel" + }, + topic: { + label: "Topic" + }, + digest_key: { + label: "Digest Key", + help: "recipient|channel|window grouping key for batched (digest) deliveries; null for normal sends." + }, + payload: { + label: "Payload", + help: "Snapshot of the rendered notification content for dispatch." + }, + status: { + label: "Status", + options: { + pending: "pending", + in_flight: "in_flight", + success: "success", + failed: "failed", + dead: "dead", + suppressed: "suppressed" + } + }, + attempts: { + label: "Attempts" + }, + partition_key: { + label: "Partition Key" + }, + claimed_by: { + label: "Claimed By", + help: "Node id while in_flight" + }, + claimed_at: { + label: "Claimed At (ms)" + }, + next_attempt_at: { + label: "Next Attempt At (ms)" + }, + last_attempted_at: { + label: "Last Attempted At (ms)" + }, + error: { + label: "Error" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_notification_preference: { + label: "Notification Preference", + pluralLabel: "Notification Preferences", + description: "Per-user × topic × channel notification toggle (mute/allow), with admin-global defaults.", + fields: { + id: { + label: "Preference ID" + }, + user_id: { + label: "User", + help: "Recipient user id, or '*' for the admin-global default." + }, + topic: { + label: "Topic", + help: "Notification topic, or '*' for all topics." + }, + channel: { + label: "Channel", + help: "Channel id (inbox/email/push/…), or '*' for all channels." + }, + enabled: { + label: "Enabled", + help: "When false, this (user, topic, channel) is muted." + }, + digest: { + label: "Digest", + help: "Batch cadence (P3 digest middleware).", + options: { + none: "none", + daily: "daily", + weekly: "weekly" + } + }, + quiet_hours: { + label: "Quiet Hours", + help: "Optional { tz, start, end } window (P3 quiet-hours middleware)." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_notification_subscription: { + label: "Notification Subscription", + pluralLabel: "Notification Subscriptions", + description: "Standing subscription of a principal (role/team/user) to a notification topic.", + fields: { + id: { + label: "Subscription ID" + }, + topic: { + label: "Topic", + help: "Notification topic this principal subscribes to." + }, + principal: { + label: "Principal", + help: "Subscriber selector: 'role:x' | 'team:x' | 'user:id' | bare user id." + }, + enabled: { + label: "Enabled", + help: "When false, the subscription is inactive." + }, + created_at: { + label: "Created At" + } + } + }, + sys_notification_template: { + label: "Notification Template", + pluralLabel: "Notification Templates", + description: "Per (topic × channel × locale) render template for notifications.", + fields: { + id: { + label: "Template ID" + }, + topic: { + label: "Topic" + }, + channel: { + label: "Channel", + help: "Channel id this template renders for (email/inbox/push/…)." + }, + locale: { + label: "Locale", + help: "BCP-47 locale, e.g. 'en' / 'en-US' / 'zh-CN'." + }, + version: { + label: "Version" + }, + subject: { + label: "Subject / Title", + help: "Rendered into the email subject / inbox title. Supports {{ payload.x }}." + }, + body: { + label: "Body", + help: "Template body. Supports {{ payload.x }}. Interpreted per `format`." + }, + format: { + label: "Body Format", + options: { + markdown: "markdown", + html: "html", + text: "text", + mjml: "mjml" + } + }, + is_active: { + label: "Active", + help: "Only active templates are selected at render time." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_http_delivery: { + label: "HTTP Delivery", + pluralLabel: "HTTP Deliveries", + description: "Durable outbox row for one outbound-HTTP attempt (ADR-0018). Managed by @objectstack/service-messaging; do not write directly.", + fields: { + id: { + label: "Delivery ID", + help: "UUID — also doubles as the receiver-side idempotency key" + }, + source: { + label: "Source", + help: "Provenance domain, e.g. 'webhook' | 'flow'. UNIQUE(source, dedup_key)." + }, + ref_id: { + label: "Ref ID", + help: "Partition/ordering anchor within source (webhook id, flow id, …)" + }, + dedup_key: { + label: "Dedup Key", + help: "UNIQUE(source, dedup_key) for at-most-once enqueue" + }, + label: { + label: "Label", + help: "Diagnostic label / event type — surfaced on X-Objectstack-Event" + }, + url: { + label: "Target URL", + help: "Snapshotted at enqueue so config edits do not rewrite live rows" + }, + method: { + label: "Method" + }, + headers_json: { + label: "Headers JSON" + }, + signing_secret: { + label: "HMAC Secret" + }, + timeout_ms: { + label: "Timeout (ms)" + }, + payload_json: { + label: "Payload JSON" + }, + partition_key: { + label: "Partition", + help: "hash(ref_id) mod partitionCount — precomputed for cheap WHERE" + }, + status: { + label: "Status", + help: "pending | in_flight | success | failed | dead" + }, + attempts: { + label: "Attempts", + help: "Number of attempts made so far" + }, + claimed_by: { + label: "Claimed By" + }, + claimed_at: { + label: "Claimed At (ms)" + }, + next_retry_at: { + label: "Next Retry At (ms)" + }, + last_attempted_at: { + label: "Last Attempted At (ms)" + }, + response_code: { + label: "HTTP Status" + }, + response_body: { + label: "Response Body (capped)" + }, + error: { + label: "Error" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + recent: { + label: "Recent" + }, + failures: { + label: "Failures" + }, + pending: { + label: "Pending" + } + } + } +}; diff --git a/packages/services/service-messaging/src/translations/es-ES.objects.generated.ts b/packages/services/service-messaging/src/translations/es-ES.objects.generated.ts new file mode 100644 index 0000000000..d574a3f64b --- /dev/null +++ b/packages/services/service-messaging/src/translations/es-ES.objects.generated.ts @@ -0,0 +1,393 @@ +// 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_inbox_message: { + label: "Mensaje de bandeja de entrada", + pluralLabel: "Mensajes de bandeja de entrada", + description: "Filas de notificación en la aplicación, orientadas al usuario final, materializadas por el canal de bandeja de entrada.", + fields: { + id: { + label: "ID del mensaje" + }, + user_id: { + label: "Usuario destinatario" + }, + notification_id: { + label: "Evento de notificación", + help: "FK → sys_notification (el evento L2 que materializa esta fila)" + }, + delivery_id: { + label: "Entrega", + help: "FK → sys_notification_delivery (fila de outbox); nulo hasta P1" + }, + topic: { + label: "Tema" + }, + title: { + label: "Título" + }, + body_md: { + label: "Cuerpo" + }, + severity: { + label: "Severidad", + options: { + info: "Información", + warning: "Advertencia", + critical: "Crítica" + } + }, + action_url: { + label: "URL de acción" + }, + created_at: { + label: "Fecha de creación" + } + }, + _views: { + mine: { + label: "Notificaciones", + emptyState: { + title: "Sin notificaciones", + message: "No hay notificaciones." + } + } + } + }, + sys_notification_receipt: { + label: "Notification Receipt", + pluralLabel: "Notification Receipts", + description: "Per-recipient × channel receipt; the source of truth for notification read-state.", + fields: { + id: { + label: "Receipt ID" + }, + notification_id: { + label: "Notification Event", + help: "FK → sys_notification (L2 event)" + }, + delivery_id: { + label: "Delivery", + help: "FK → sys_notification_delivery (outbox row); null until P1" + }, + user_id: { + label: "Recipient User" + }, + channel: { + label: "Channel", + help: "Channel id this receipt is for (inbox / email / push / …)" + }, + state: { + label: "State", + options: { + delivered: "delivered", + read: "read", + clicked: "clicked", + dismissed: "dismissed" + } + }, + at: { + label: "At", + help: "When the receipt reached its current state" + }, + created_at: { + label: "Created At" + } + } + }, + sys_notification_delivery: { + label: "Notification Delivery", + pluralLabel: "Notification Deliveries", + description: "Durable per-recipient × channel delivery outbox (ADR-0030 Layer 4).", + fields: { + id: { + label: "Delivery ID" + }, + notification_id: { + label: "Notification Event", + help: "FK → sys_notification (L2 event)" + }, + recipient_id: { + label: "Recipient User" + }, + channel: { + label: "Channel" + }, + topic: { + label: "Topic" + }, + digest_key: { + label: "Digest Key", + help: "recipient|channel|window grouping key for batched (digest) deliveries; null for normal sends." + }, + payload: { + label: "Payload", + help: "Snapshot of the rendered notification content for dispatch." + }, + status: { + label: "Status", + options: { + pending: "pending", + in_flight: "in_flight", + success: "success", + failed: "failed", + dead: "dead", + suppressed: "suppressed" + } + }, + attempts: { + label: "Attempts" + }, + partition_key: { + label: "Partition Key" + }, + claimed_by: { + label: "Claimed By", + help: "Node id while in_flight" + }, + claimed_at: { + label: "Claimed At (ms)" + }, + next_attempt_at: { + label: "Next Attempt At (ms)" + }, + last_attempted_at: { + label: "Last Attempted At (ms)" + }, + error: { + label: "Error" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_notification_preference: { + label: "Notification Preference", + pluralLabel: "Notification Preferences", + description: "Per-user × topic × channel notification toggle (mute/allow), with admin-global defaults.", + fields: { + id: { + label: "Preference ID" + }, + user_id: { + label: "User", + help: "Recipient user id, or '*' for the admin-global default." + }, + topic: { + label: "Topic", + help: "Notification topic, or '*' for all topics." + }, + channel: { + label: "Channel", + help: "Channel id (inbox/email/push/…), or '*' for all channels." + }, + enabled: { + label: "Enabled", + help: "When false, this (user, topic, channel) is muted." + }, + digest: { + label: "Digest", + help: "Batch cadence (P3 digest middleware).", + options: { + none: "none", + daily: "daily", + weekly: "weekly" + } + }, + quiet_hours: { + label: "Quiet Hours", + help: "Optional { tz, start, end } window (P3 quiet-hours middleware)." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_notification_subscription: { + label: "Notification Subscription", + pluralLabel: "Notification Subscriptions", + description: "Standing subscription of a principal (role/team/user) to a notification topic.", + fields: { + id: { + label: "Subscription ID" + }, + topic: { + label: "Topic", + help: "Notification topic this principal subscribes to." + }, + principal: { + label: "Principal", + help: "Subscriber selector: 'role:x' | 'team:x' | 'user:id' | bare user id." + }, + enabled: { + label: "Enabled", + help: "When false, the subscription is inactive." + }, + created_at: { + label: "Created At" + } + } + }, + sys_notification_template: { + label: "Notification Template", + pluralLabel: "Notification Templates", + description: "Per (topic × channel × locale) render template for notifications.", + fields: { + id: { + label: "Template ID" + }, + topic: { + label: "Topic" + }, + channel: { + label: "Channel", + help: "Channel id this template renders for (email/inbox/push/…)." + }, + locale: { + label: "Locale", + help: "BCP-47 locale, e.g. 'en' / 'en-US' / 'zh-CN'." + }, + version: { + label: "Version" + }, + subject: { + label: "Subject / Title", + help: "Rendered into the email subject / inbox title. Supports {{ payload.x }}." + }, + body: { + label: "Body", + help: "Template body. Supports {{ payload.x }}. Interpreted per `format`." + }, + format: { + label: "Body Format", + options: { + markdown: "markdown", + html: "html", + text: "text", + mjml: "mjml" + } + }, + is_active: { + label: "Active", + help: "Only active templates are selected at render time." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_http_delivery: { + label: "HTTP Delivery", + pluralLabel: "HTTP Deliveries", + description: "Durable outbox row for one outbound-HTTP attempt (ADR-0018). Managed by @objectstack/service-messaging; do not write directly.", + fields: { + id: { + label: "Delivery ID", + help: "UUID — also doubles as the receiver-side idempotency key" + }, + source: { + label: "Source", + help: "Provenance domain, e.g. 'webhook' | 'flow'. UNIQUE(source, dedup_key)." + }, + ref_id: { + label: "Ref ID", + help: "Partition/ordering anchor within source (webhook id, flow id, …)" + }, + dedup_key: { + label: "Dedup Key", + help: "UNIQUE(source, dedup_key) for at-most-once enqueue" + }, + label: { + label: "Label", + help: "Diagnostic label / event type — surfaced on X-Objectstack-Event" + }, + url: { + label: "Target URL", + help: "Snapshotted at enqueue so config edits do not rewrite live rows" + }, + method: { + label: "Method" + }, + headers_json: { + label: "Headers JSON" + }, + signing_secret: { + label: "HMAC Secret" + }, + timeout_ms: { + label: "Timeout (ms)" + }, + payload_json: { + label: "Payload JSON" + }, + partition_key: { + label: "Partition", + help: "hash(ref_id) mod partitionCount — precomputed for cheap WHERE" + }, + status: { + label: "Status", + help: "pending | in_flight | success | failed | dead" + }, + attempts: { + label: "Attempts", + help: "Number of attempts made so far" + }, + claimed_by: { + label: "Claimed By" + }, + claimed_at: { + label: "Claimed At (ms)" + }, + next_retry_at: { + label: "Next Retry At (ms)" + }, + last_attempted_at: { + label: "Last Attempted At (ms)" + }, + response_code: { + label: "HTTP Status" + }, + response_body: { + label: "Response Body (capped)" + }, + error: { + label: "Error" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + recent: { + label: "Recent" + }, + failures: { + label: "Failures" + }, + pending: { + label: "Pending" + } + } + } +}; diff --git a/packages/services/service-messaging/src/translations/index.ts b/packages/services/service-messaging/src/translations/index.ts new file mode 100644 index 0000000000..5f4e71d7fa --- /dev/null +++ b/packages/services/service-messaging/src/translations/index.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * MessagingTranslations — i18n bundle owned by this service (ADR-0029 D8). + * + * Object label/field/view/action translations for the sys_* objects this + * service owns (sys_inbox_message / sys_notification_receipt / + * sys_notification_delivery / sys_notification_preference / + * sys_notification_subscription / sys_notification_template / + * sys_http_delivery). 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 MessagingTranslations: TranslationBundle = { + en: { objects: enObjects }, + 'zh-CN': { objects: zhCNObjects }, + 'ja-JP': { objects: jaJPObjects }, + 'es-ES': { objects: esESObjects }, +}; diff --git a/packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts b/packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts new file mode 100644 index 0000000000..a422a8c6b0 --- /dev/null +++ b/packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts @@ -0,0 +1,393 @@ +// 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_inbox_message: { + label: "受信メッセージ", + pluralLabel: "受信メッセージ", + description: "受信トレイ通知チャネルによって生成される、エンドユーザー向けのアプリ内通知行。", + fields: { + id: { + label: "受信メッセージ ID" + }, + user_id: { + label: "受信ユーザー" + }, + notification_id: { + label: "通知イベント", + help: "外部キー → sys_notification(この行が具現化する L2 イベント)" + }, + delivery_id: { + label: "配信レコード", + help: "外部キー → sys_notification_delivery(アウトボックス行);P1 までは null" + }, + topic: { + label: "トピック" + }, + title: { + label: "タイトル" + }, + body_md: { + label: "本文" + }, + severity: { + label: "重大度", + options: { + info: "情報", + warning: "警告", + critical: "重大" + } + }, + action_url: { + label: "アクション URL" + }, + created_at: { + label: "作成日時" + } + }, + _views: { + mine: { + label: "通知", + emptyState: { + title: "通知はありません", + message: "現在、通知はありません。" + } + } + } + }, + sys_notification_receipt: { + label: "Notification Receipt", + pluralLabel: "Notification Receipts", + description: "Per-recipient × channel receipt; the source of truth for notification read-state.", + fields: { + id: { + label: "Receipt ID" + }, + notification_id: { + label: "Notification Event", + help: "FK → sys_notification (L2 event)" + }, + delivery_id: { + label: "Delivery", + help: "FK → sys_notification_delivery (outbox row); null until P1" + }, + user_id: { + label: "Recipient User" + }, + channel: { + label: "Channel", + help: "Channel id this receipt is for (inbox / email / push / …)" + }, + state: { + label: "State", + options: { + delivered: "delivered", + read: "read", + clicked: "clicked", + dismissed: "dismissed" + } + }, + at: { + label: "At", + help: "When the receipt reached its current state" + }, + created_at: { + label: "Created At" + } + } + }, + sys_notification_delivery: { + label: "Notification Delivery", + pluralLabel: "Notification Deliveries", + description: "Durable per-recipient × channel delivery outbox (ADR-0030 Layer 4).", + fields: { + id: { + label: "Delivery ID" + }, + notification_id: { + label: "Notification Event", + help: "FK → sys_notification (L2 event)" + }, + recipient_id: { + label: "Recipient User" + }, + channel: { + label: "Channel" + }, + topic: { + label: "Topic" + }, + digest_key: { + label: "Digest Key", + help: "recipient|channel|window grouping key for batched (digest) deliveries; null for normal sends." + }, + payload: { + label: "Payload", + help: "Snapshot of the rendered notification content for dispatch." + }, + status: { + label: "Status", + options: { + pending: "pending", + in_flight: "in_flight", + success: "success", + failed: "failed", + dead: "dead", + suppressed: "suppressed" + } + }, + attempts: { + label: "Attempts" + }, + partition_key: { + label: "Partition Key" + }, + claimed_by: { + label: "Claimed By", + help: "Node id while in_flight" + }, + claimed_at: { + label: "Claimed At (ms)" + }, + next_attempt_at: { + label: "Next Attempt At (ms)" + }, + last_attempted_at: { + label: "Last Attempted At (ms)" + }, + error: { + label: "Error" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_notification_preference: { + label: "Notification Preference", + pluralLabel: "Notification Preferences", + description: "Per-user × topic × channel notification toggle (mute/allow), with admin-global defaults.", + fields: { + id: { + label: "Preference ID" + }, + user_id: { + label: "User", + help: "Recipient user id, or '*' for the admin-global default." + }, + topic: { + label: "Topic", + help: "Notification topic, or '*' for all topics." + }, + channel: { + label: "Channel", + help: "Channel id (inbox/email/push/…), or '*' for all channels." + }, + enabled: { + label: "Enabled", + help: "When false, this (user, topic, channel) is muted." + }, + digest: { + label: "Digest", + help: "Batch cadence (P3 digest middleware).", + options: { + none: "none", + daily: "daily", + weekly: "weekly" + } + }, + quiet_hours: { + label: "Quiet Hours", + help: "Optional { tz, start, end } window (P3 quiet-hours middleware)." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_notification_subscription: { + label: "Notification Subscription", + pluralLabel: "Notification Subscriptions", + description: "Standing subscription of a principal (role/team/user) to a notification topic.", + fields: { + id: { + label: "Subscription ID" + }, + topic: { + label: "Topic", + help: "Notification topic this principal subscribes to." + }, + principal: { + label: "Principal", + help: "Subscriber selector: 'role:x' | 'team:x' | 'user:id' | bare user id." + }, + enabled: { + label: "Enabled", + help: "When false, the subscription is inactive." + }, + created_at: { + label: "Created At" + } + } + }, + sys_notification_template: { + label: "Notification Template", + pluralLabel: "Notification Templates", + description: "Per (topic × channel × locale) render template for notifications.", + fields: { + id: { + label: "Template ID" + }, + topic: { + label: "Topic" + }, + channel: { + label: "Channel", + help: "Channel id this template renders for (email/inbox/push/…)." + }, + locale: { + label: "Locale", + help: "BCP-47 locale, e.g. 'en' / 'en-US' / 'zh-CN'." + }, + version: { + label: "Version" + }, + subject: { + label: "Subject / Title", + help: "Rendered into the email subject / inbox title. Supports {{ payload.x }}." + }, + body: { + label: "Body", + help: "Template body. Supports {{ payload.x }}. Interpreted per `format`." + }, + format: { + label: "Body Format", + options: { + markdown: "markdown", + html: "html", + text: "text", + mjml: "mjml" + } + }, + is_active: { + label: "Active", + help: "Only active templates are selected at render time." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_http_delivery: { + label: "HTTP Delivery", + pluralLabel: "HTTP Deliveries", + description: "Durable outbox row for one outbound-HTTP attempt (ADR-0018). Managed by @objectstack/service-messaging; do not write directly.", + fields: { + id: { + label: "Delivery ID", + help: "UUID — also doubles as the receiver-side idempotency key" + }, + source: { + label: "Source", + help: "Provenance domain, e.g. 'webhook' | 'flow'. UNIQUE(source, dedup_key)." + }, + ref_id: { + label: "Ref ID", + help: "Partition/ordering anchor within source (webhook id, flow id, …)" + }, + dedup_key: { + label: "Dedup Key", + help: "UNIQUE(source, dedup_key) for at-most-once enqueue" + }, + label: { + label: "Label", + help: "Diagnostic label / event type — surfaced on X-Objectstack-Event" + }, + url: { + label: "Target URL", + help: "Snapshotted at enqueue so config edits do not rewrite live rows" + }, + method: { + label: "Method" + }, + headers_json: { + label: "Headers JSON" + }, + signing_secret: { + label: "HMAC Secret" + }, + timeout_ms: { + label: "Timeout (ms)" + }, + payload_json: { + label: "Payload JSON" + }, + partition_key: { + label: "Partition", + help: "hash(ref_id) mod partitionCount — precomputed for cheap WHERE" + }, + status: { + label: "Status", + help: "pending | in_flight | success | failed | dead" + }, + attempts: { + label: "Attempts", + help: "Number of attempts made so far" + }, + claimed_by: { + label: "Claimed By" + }, + claimed_at: { + label: "Claimed At (ms)" + }, + next_retry_at: { + label: "Next Retry At (ms)" + }, + last_attempted_at: { + label: "Last Attempted At (ms)" + }, + response_code: { + label: "HTTP Status" + }, + response_body: { + label: "Response Body (capped)" + }, + error: { + label: "Error" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + recent: { + label: "Recent" + }, + failures: { + label: "Failures" + }, + pending: { + label: "Pending" + } + } + } +}; diff --git a/packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts b/packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts new file mode 100644 index 0000000000..01d7219a3c --- /dev/null +++ b/packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts @@ -0,0 +1,393 @@ +// 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_inbox_message: { + label: "站内消息", + pluralLabel: "站内消息", + description: "由站内信通道物化的应用内通知记录,面向最终用户。", + fields: { + id: { + label: "站内消息 ID" + }, + user_id: { + label: "接收用户" + }, + notification_id: { + label: "通知事件", + help: "外键 → sys_notification(本行物化的 L2 事件)" + }, + delivery_id: { + label: "投递记录", + help: "外键 → sys_notification_delivery(发件箱行);P1 之前为空" + }, + topic: { + label: "主题" + }, + title: { + label: "标题" + }, + body_md: { + label: "正文" + }, + severity: { + label: "严重程度", + options: { + info: "信息", + warning: "警告", + critical: "严重" + } + }, + action_url: { + label: "操作链接" + }, + created_at: { + label: "创建时间" + } + }, + _views: { + mine: { + label: "通知", + emptyState: { + title: "暂无通知", + message: "目前没有任何通知。" + } + } + } + }, + sys_notification_receipt: { + label: "通知回执", + pluralLabel: "通知回执", + description: "按接收人 × 通道的回执;通知已读状态的唯一权威来源。", + fields: { + id: { + label: "回执 ID" + }, + notification_id: { + label: "通知事件", + help: "外键 → sys_notification(L2 事件)" + }, + delivery_id: { + label: "投递记录", + help: "外键 → sys_notification_delivery(发件箱行);P1 之前为空" + }, + user_id: { + label: "接收用户" + }, + channel: { + label: "通道", + help: "本回执对应的通道 ID(inbox / email / push / …)" + }, + state: { + label: "状态", + options: { + delivered: "已送达", + read: "已读", + clicked: "已点击", + dismissed: "已忽略" + } + }, + at: { + label: "状态时间", + help: "回执到达当前状态的时间" + }, + created_at: { + label: "创建时间" + } + } + }, + sys_notification_delivery: { + label: "通知投递", + pluralLabel: "通知投递", + description: "持久化的按接收人 × 通道投递发件箱(ADR-0030 第 4 层)。", + fields: { + id: { + label: "投递 ID" + }, + notification_id: { + label: "通知事件", + help: "外键 → sys_notification(L2 事件)" + }, + recipient_id: { + label: "接收用户" + }, + channel: { + label: "通道" + }, + topic: { + label: "主题" + }, + digest_key: { + label: "摘要键", + help: "批量(摘要)投递的 接收人|通道|窗口 分组键;普通发送为空。" + }, + payload: { + label: "负载", + help: "用于分发的通知内容渲染快照。" + }, + status: { + label: "状态", + options: { + pending: "待处理", + in_flight: "投递中", + success: "成功", + failed: "失败", + dead: "死信", + suppressed: "已抑制" + } + }, + attempts: { + label: "尝试次数" + }, + partition_key: { + label: "分区键" + }, + claimed_by: { + label: "认领节点", + help: "投递中时的节点 ID" + }, + claimed_at: { + label: "认领时间(毫秒)" + }, + next_attempt_at: { + label: "下次尝试时间(毫秒)" + }, + last_attempted_at: { + label: "上次尝试时间(毫秒)" + }, + error: { + label: "错误" + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + } + } + }, + sys_notification_preference: { + label: "通知偏好", + pluralLabel: "通知偏好", + description: "按用户 × 主题 × 通道的通知开关(静音/允许),支持管理员全局默认值。", + fields: { + id: { + label: "偏好 ID" + }, + user_id: { + label: "用户", + help: "接收用户 ID,'*' 表示管理员全局默认值。" + }, + topic: { + label: "主题", + help: "通知主题,'*' 表示所有主题。" + }, + channel: { + label: "通道", + help: "通道 ID(inbox/email/push/…),'*' 表示所有通道。" + }, + enabled: { + label: "启用", + help: "为 false 时,该(用户、主题、通道)组合被静音。" + }, + digest: { + label: "摘要", + help: "批量发送节奏(P3 摘要中间件)。", + options: { + none: "不摘要", + daily: "每日", + weekly: "每周" + } + }, + quiet_hours: { + label: "免打扰时段", + help: "可选的 { tz, start, end } 时间窗(P3 免打扰中间件)。" + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + } + } + }, + sys_notification_subscription: { + label: "通知订阅", + pluralLabel: "通知订阅", + description: "主体(角色/团队/用户)对通知主题的长期订阅。", + fields: { + id: { + label: "订阅 ID" + }, + topic: { + label: "主题", + help: "该主体订阅的通知主题。" + }, + principal: { + label: "主体", + help: "订阅者选择器:'role:x' | 'team:x' | 'user:id' | 裸用户 ID。" + }, + enabled: { + label: "启用", + help: "为 false 时,订阅不生效。" + }, + created_at: { + label: "创建时间" + } + } + }, + sys_notification_template: { + label: "通知模板", + pluralLabel: "通知模板", + description: "按(主题 × 通道 × 语言)的通知渲染模板。", + fields: { + id: { + label: "模板 ID" + }, + topic: { + label: "主题" + }, + channel: { + label: "通道", + help: "本模板渲染的通道 ID(email/inbox/push/…)。" + }, + locale: { + label: "语言", + help: "BCP-47 语言代码,如 'en' / 'en-US' / 'zh-CN'。" + }, + version: { + label: "版本" + }, + subject: { + label: "主题 / 标题", + help: "渲染为邮件主题 / 站内信标题。支持 {{ payload.x }}。" + }, + body: { + label: "正文", + help: "模板正文。支持 {{ payload.x }},按 `format` 解释。" + }, + format: { + label: "正文格式", + options: { + markdown: "Markdown", + html: "HTML", + text: "纯文本", + mjml: "MJML" + } + }, + is_active: { + label: "启用", + help: "渲染时仅选用启用中的模板。" + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + } + } + }, + sys_http_delivery: { + label: "HTTP 投递", + pluralLabel: "HTTP 投递", + description: "单次出站 HTTP 调用的持久化发件箱行(ADR-0018)。由 @objectstack/service-messaging 管理,请勿直接写入。", + fields: { + id: { + label: "投递 ID", + help: "UUID——同时作为接收方的幂等键" + }, + source: { + label: "来源", + help: "来源域,如 'webhook' | 'flow'。UNIQUE(source, dedup_key)。" + }, + ref_id: { + label: "引用 ID", + help: "来源内的分区/排序锚点(webhook ID、flow ID 等)" + }, + dedup_key: { + label: "去重键", + help: "UNIQUE(source, dedup_key) 保证至多入队一次" + }, + label: { + label: "标签", + help: "诊断标签 / 事件类型——通过 X-Objectstack-Event 头暴露" + }, + url: { + label: "目标 URL", + help: "入队时快照,配置修改不会改写已存在的行" + }, + method: { + label: "请求方法" + }, + headers_json: { + label: "请求头 JSON" + }, + signing_secret: { + label: "HMAC 密钥" + }, + timeout_ms: { + label: "超时(毫秒)" + }, + payload_json: { + label: "负载 JSON" + }, + partition_key: { + label: "分区", + help: "hash(ref_id) mod partitionCount——预计算以便低成本过滤" + }, + status: { + label: "状态", + help: "pending | in_flight | success | failed | dead" + }, + attempts: { + label: "尝试次数", + help: "迄今已尝试的次数" + }, + claimed_by: { + label: "认领节点" + }, + claimed_at: { + label: "认领时间(毫秒)" + }, + next_retry_at: { + label: "下次重试时间(毫秒)" + }, + last_attempted_at: { + label: "上次尝试时间(毫秒)" + }, + response_code: { + label: "HTTP 状态码" + }, + response_body: { + label: "响应体(截断)" + }, + error: { + label: "错误" + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + } + }, + _views: { + recent: { + label: "最近" + }, + failures: { + label: "失败" + }, + pending: { + label: "待处理" + } + } + } +}; diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index a7d2ec11e8..80e5126623 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -62,10 +62,16 @@ export const ObjectTranslationDataSchema = lazySchema(() => z.object({ * Convention (auto-resolved by `resolveViewLabel`): * objects.._views..label * objects.._views..description + * objects.._views..emptyState.title + * objects.._views..emptyState.message */ _views: z.record(z.string(), z.object({ label: z.string().optional().describe('Translated view label'), description: z.string().optional().describe('Translated view description'), + emptyState: z.object({ + title: z.string().optional().describe('Translated empty-state title'), + message: z.string().optional().describe('Translated empty-state message'), + }).optional().describe('Translated empty-state copy shown when the view has no rows'), })).optional().describe('View translations keyed by view name'), /** @@ -478,6 +484,10 @@ export const ObjectTranslationNodeSchema = lazySchema(() => z.object({ _views: z.record(z.string(), z.object({ label: z.string().optional().describe('Translated view label'), description: z.string().optional().describe('Translated view description'), + emptyState: z.object({ + title: z.string().optional().describe('Translated empty-state title'), + message: z.string().optional().describe('Translated empty-state message'), + }).optional().describe('Translated empty-state copy shown when the view has no rows'), })).optional().describe('View translations keyed by view name'), /** Section (form section / tab) translations keyed by section name */