Skip to content

Commit bb71321

Browse files
authored
feat(i18n): translate system account & messaging surfaces (zh-CN/ja-JP/es-ES) (#2906)
* feat(i18n): translate system account & messaging surfaces (zh-CN/ja-JP/es-ES) - spec: allow _views.<view>.emptyState.{title,message} in object translation schema - cli: extract emptyState keys in 'os i18n extract' - platform-objects: fill missing zh-CN/ja-JP/es-ES for sys_user/sys_organization/sys_business_unit - service-messaging: new ADR-0029 D8 MessagingTranslations bundle (7 sys_* objects), registered on kernel:ready * feat(platform-objects): inline i18n labels on sys account detail pages - sys-user.page.ts: tab/section/alert/add-button labels become inline { en, zh-CN, ja-JP, es-ES } objects; Security tab rebuilt on record:quick_actions with actionNames so the change-password / change-email / 2FA / delete-account buttons resolve object action translations. - sys-organization.page.ts / sys-position.page.ts: remaining hardcoded tab titles and add labels get the same inline i18n objects.
1 parent a199626 commit bb71321

16 files changed

Lines changed: 1952 additions & 174 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': minor
4+
'@objectstack/platform-objects': patch
5+
'@objectstack/service-messaging': minor
6+
---
7+
8+
i18n: translate the system account/messaging surfaces end to end.
9+
10+
- **spec**: `ObjectTranslationDataSchema` / `ObjectTranslationNodeSchema` now
11+
accept `_views.<view>.emptyState.{title,message}` so list-view empty states
12+
are translatable (contract-first for the extractor below).
13+
- **cli**: `os i18n extract` emits `_views.<view>.emptyState` keys when a view
14+
declares an empty state.
15+
- **platform-objects**: fill every missing zh-CN/ja-JP/es-ES translation for
16+
`sys_user`, `sys_organization` and `sys_business_unit` (fields, options,
17+
views, actions); replace the hardcoded English tab/section/action labels in
18+
the `sys_user`, `sys_organization` and `sys_position` detail pages with
19+
inline i18n label objects, and route the user Security tab through
20+
`record:quick_actions` so object action labels localize.
21+
- **service-messaging**: new ADR-0029 D8 translation bundle
22+
(`MessagingTranslations`) covering the seven `sys_*` messaging objects
23+
(inbox message, receipts, deliveries, preferences, subscriptions, templates,
24+
HTTP deliveries), registered on `kernel:ready`; zh-CN is fully translated
25+
and ja-JP/es-ES cover `sys_inbox_message` (incl. the `mine` view empty
26+
state).

packages/cli/src/utils/i18n-extract.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* objects.<name>.fields.<field>.options.<value>
2222
* objects.<name>._views.<view>.label
2323
* objects.<name>._views.<view>.description
24+
* objects.<name>._views.<view>.emptyState.title / .message
2425
* objects.<name>._actions.<action>.label
2526
* objects.<name>._actions.<action>.confirmText
2627
* objects.<name>._actions.<action>.successMessage
@@ -119,6 +120,22 @@ function viewObjectName(view: any): string | undefined {
119120
return view?.objectName ?? view?.object ?? view?.data?.object;
120121
}
121122

123+
/**
124+
* Emit `_views.<view>.emptyState.{title,message}` entries when the view
125+
* declares empty-state copy. Mirrors the client-side resolver convention
126+
* (`viewEmptyState` in @object-ui/i18n) and `ObjectTranslationDataSchema`.
127+
*/
128+
function pushViewEmptyState(out: ExpectedEntry[], viewPath: string[], view: any, objectName: string): void {
129+
const emptyState = view?.emptyState;
130+
if (!emptyState || typeof emptyState !== 'object') return;
131+
if (typeof emptyState.title === 'string' && emptyState.title.length > 0) {
132+
pushEntry(out, [...viewPath, 'emptyState', 'title'], emptyState.title, 'view', { objectName });
133+
}
134+
if (typeof emptyState.message === 'string' && emptyState.message.length > 0) {
135+
pushEntry(out, [...viewPath, 'emptyState', 'message'], emptyState.message, 'view', { objectName });
136+
}
137+
}
138+
122139
function pushEntry(
123140
out: ExpectedEntry[],
124141
path: string[],
@@ -200,6 +217,7 @@ export function collectExpectedEntries(config: any): ExpectedEntry[] {
200217
if (view.description) {
201218
pushEntry(out, ['objects', objectName, '_views', viewName, 'description'], view.description, 'view', { objectName });
202219
}
220+
pushViewEmptyState(out, ['objects', objectName, '_views', viewName], view, objectName);
203221
}
204222
}
205223

@@ -229,6 +247,7 @@ export function collectExpectedEntries(config: any): ExpectedEntry[] {
229247
if (view.description) {
230248
pushEntry(out, ['objects', objectName, '_views', view.name, 'description'], view.description, 'view', { objectName });
231249
}
250+
pushViewEmptyState(out, ['objects', objectName, '_views', view.name], view, objectName);
232251
}
233252

234253
// ── Top-level actions ────────────────────────────────────────────

packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -43,44 +43,44 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
4343
help: "Si se establece, el bloqueo se elimina automáticamente en ese momento."
4444
},
4545
failed_login_count: {
46-
label: "Failed Login Count",
47-
help: "Consecutive failed sign-in attempts; reset to 0 on success. Maintained by the auth manager."
46+
label: "Intentos fallidos consecutivos",
47+
help: "Intentos de inicio de sesión fallidos consecutivos; se restablece a 0 tras un inicio correcto. Lo mantiene el gestor de autenticación."
4848
},
4949
locked_until: {
50-
label: "Locked Until",
51-
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."
50+
label: "Bloqueado hasta",
51+
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»."
5252
},
5353
password_changed_at: {
54-
label: "Password Changed At",
55-
help: "Timestamp of the last password change. Backs password_expiry_days; system-managed."
54+
label: "Contraseña cambiada el",
55+
help: "Fecha y hora del último cambio de contraseña. Sirve de base a password_expiry_days; gestionado por el sistema."
5656
},
5757
phone_number: {
58-
label: "Phone Number",
59-
help: "Sign-in phone number (E.164 recommended). Unique per user; managed by better-auth when the phoneNumber plugin is enabled."
58+
label: "Número de teléfono",
59+
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."
6060
},
6161
phone_number_verified: {
62-
label: "Phone Verified",
63-
help: "Whether the phone number has been verified (OTP verification requires SMS infrastructure; false until that ships). System-managed."
62+
label: "Teléfono verificado",
63+
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."
6464
},
6565
must_change_password: {
66-
label: "Must Change Password",
67-
help: "When true, the user is blocked (403 PASSWORD_EXPIRED) until they change their password. Stamped by the admin user-management routes; system-managed."
66+
label: "Debe cambiar la contraseña",
67+
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."
6868
},
6969
mfa_required_at: {
70-
label: "MFA Required At",
71-
help: "When enforced MFA first applied to this user (grace-period clock). Backs mfa_required; system-managed."
70+
label: "MFA exigido desde",
71+
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."
7272
},
7373
last_login_at: {
74-
label: "Last Login At",
75-
help: "Timestamp of the last successful sign-in. Stamped by the auth manager; system-managed."
74+
label: "Último inicio de sesión",
75+
help: "Fecha y hora del último inicio de sesión correcto. Lo registra el gestor de autenticación; gestionado por el sistema."
7676
},
7777
last_login_ip: {
78-
label: "Last Login IP",
79-
help: "Client IP of the last successful sign-in (from the trusted proxy forwarded header). System-managed."
78+
label: "IP del último inicio de sesión",
79+
help: "IP del cliente del último inicio de sesión correcto (de la cabecera reenviada por el proxy de confianza). Gestionado por el sistema."
8080
},
8181
ai_access: {
82-
label: "AI Access",
83-
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)."
82+
label: "Acceso a IA",
83+
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)."
8484
},
8585
image: {
8686
label: "Imagen de perfil"
@@ -94,11 +94,11 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
9494
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."
9595
},
9696
source: {
97-
label: "Identity Source",
98-
help: "How this identity was created — idp_provisioned (federated SSO JIT) or env_native (local signup / app end-user). System-managed; do not edit.",
97+
label: "Origen de la identidad",
98+
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.",
9999
options: {
100-
idp_provisioned: "IdP-Provisioned",
101-
env_native: "Env-Native"
100+
idp_provisioned: "Aprovisionada por IdP",
101+
env_native: "Nativa del entorno"
102102
}
103103
},
104104
id: {
@@ -113,7 +113,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
113113
},
114114
_views: {
115115
me: {
116-
label: "My Profile"
116+
label: "Mi perfil"
117117
},
118118
all_users: {
119119
label: "Todos los usuarios"
@@ -143,12 +143,12 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
143143
successMessage: "Usuario desbloqueado"
144144
},
145145
unlock_user: {
146-
label: "Unlock Account",
147-
successMessage: "Account unlocked"
146+
label: "Desbloquear cuenta",
147+
successMessage: "Cuenta desbloqueada"
148148
},
149149
create_user: {
150-
label: "Create User",
151-
successMessage: "User created"
150+
label: "Crear usuario",
151+
successMessage: "Usuario creado"
152152
},
153153
set_user_password: {
154154
label: "Establecer contraseña",
@@ -176,27 +176,27 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
176176
successMessage: "Correo de verificación enviado; revisa la nueva dirección para confirmar."
177177
},
178178
resend_verification_email: {
179-
label: "Resend Verification Email",
180-
successMessage: "Verification email sent — check your inbox."
179+
label: "Reenviar correo de verificación",
180+
successMessage: "Correo de verificación enviado; revisa tu bandeja de entrada."
181181
},
182182
delete_my_account: {
183183
label: "Eliminar mi cuenta",
184184
confirmText: "¿Eliminar tu cuenta de forma permanente? Esta acción no se puede deshacer: se cerrarán todas tus sesiones y se eliminarán todos los datos de tu propiedad según la política de retención configurada.",
185185
successMessage: "Cuenta eliminada"
186186
},
187187
enable_two_factor: {
188-
label: "Enable Two-Factor Auth",
189-
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."
188+
label: "Habilitar autenticación de dos factores",
189+
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."
190190
},
191191
disable_two_factor: {
192-
label: "Disable Two-Factor Auth",
193-
confirmText: "Turn off two-factor authentication? Your account will be less secure.",
194-
successMessage: "Two-factor authentication disabled."
192+
label: "Deshabilitar autenticación de dos factores",
193+
confirmText: "¿Desactivar la autenticación de dos factores? Tu cuenta será menos segura.",
194+
successMessage: "Autenticación de dos factores deshabilitada."
195195
},
196196
generate_backup_codes: {
197-
label: "Regenerate Backup Codes",
198-
confirmText: "Generate a new set of backup codes? Any previously generated codes will stop working.",
199-
successMessage: "New backup codes generated — save them somewhere safe."
197+
label: "Regenerar códigos de respaldo",
198+
confirmText: "¿Generar un nuevo juego de códigos de respaldo? Los códigos generados anteriormente dejarán de funcionar.",
199+
successMessage: "Nuevos códigos de respaldo generados; guárdalos en un lugar seguro."
200200
}
201201
}
202202
},
@@ -396,8 +396,8 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
396396
help: "Metadatos de la organización serializados en JSON."
397397
},
398398
require_mfa: {
399-
label: "Require Multi-Factor Auth",
400-
help: "When true, every member of this organization must enroll an authenticator app to access data."
399+
label: "Exigir autenticación multifactor",
400+
help: "Si es true, todos los miembros de esta organización deben registrar una aplicación de autenticación para acceder a los datos."
401401
},
402402
id: {
403403
label: "ID de organización"
@@ -438,9 +438,9 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
438438
successMessage: "Ha abandonado la organización"
439439
},
440440
change_slug: {
441-
label: "Change Slug",
442-
confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?",
443-
successMessage: "Organization slug changed"
441+
label: "Cambiar slug",
442+
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?",
443+
successMessage: "Slug de la organización cambiado"
444444
}
445445
}
446446
},
@@ -724,7 +724,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
724724
},
725725
_views: {
726726
org_chart: {
727-
label: "Org Chart"
727+
label: "Organigrama"
728728
},
729729
active: {
730730
label: "Activo"

0 commit comments

Comments
 (0)