diff --git a/.changeset/profile-card-i18n.md b/.changeset/profile-card-i18n.md new file mode 100644 index 000000000..0bac55fae --- /dev/null +++ b/.changeset/profile-card-i18n.md @@ -0,0 +1,32 @@ +--- +'@objectstack/console': patch +'@object-ui/i18n': patch +'@object-ui/app-shell': patch +'@object-ui/plugin-detail': patch +'@object-ui/plugin-tree': patch +--- + +i18n: translate the Profile page, honor inline i18n label objects under bare +base-language codes, and localize managed-by badges / record quick actions. + +- `pickLocalized` now upgrades a bare base language (`zh`) to any + region-qualified key sharing the base (`zh-CN`) — runtime language is + normalized to the base code while metadata authors write full BCP-47 tags, + so inline `{ en, 'zh-CN', ... }` label objects previously fell back to + English. +- ProfilePage (`account:profile_card` / `/system/profile`): every hardcoded + string — page title/subtitle, avatar Upload/Replace/Remove, Personal + Information card, Change/Set Password card — now goes through + `useObjectTranslation()` with `profile.*` keys (new namespace in all ten + locale bundles); the lazy-load fallback reuses `common.loading`. +- `ManagedByBadge` chips/tooltips (Config/System/Append-only/Identity) now + resolve through new `managedByBadge.*` keys with `{{provider}}` + interpolation. +- `record:quick_actions` resolves action labels via the + `objects.{object}._actions.{action}.label` convention plus `pickLocalized`, + so object action buttons (Change Password, Enable 2FA, …) localize. +- `record:details` / `record:related_list` / `record:alert` / `ObjectTree` + pass inline label objects through `pickLocalized`. +- Locale bundles: added `managedByBadge` namespace to all ten locales and + backfilled `list.inlineEditShort` / `inlineEditLabel` / + `recordEditingTitle` for ja/es/ko/de/fr/pt/ru/ar. diff --git a/apps/console/src/pages/system/ProfilePage.tsx b/apps/console/src/pages/system/ProfilePage.tsx index 79d2f8a54..b1db0d927 100644 --- a/apps/console/src/pages/system/ProfilePage.tsx +++ b/apps/console/src/pages/system/ProfilePage.tsx @@ -24,9 +24,11 @@ import { AlertDescription, } from '@object-ui/components'; import { useUpload } from '@object-ui/providers'; +import { useObjectTranslation } from '@object-ui/i18n'; import { CheckCircle2, AlertCircle, User, Lock, Upload, Loader2, X } from 'lucide-react'; export function ProfilePage() { + const { t } = useObjectTranslation(); const { user, updateUser, isLoading, changePassword, setInitialPassword, hasLocalPassword } = useAuth(); const { upload } = useUpload(); const [name, setName] = useState(user?.name ?? ''); @@ -91,8 +93,12 @@ export function ProfilePage() { return (
-

Profile

-

Manage your account settings

+

+ {t('profile.title', { defaultValue: 'Profile' })} +

+

+ {t('profile.subtitle', { defaultValue: 'Manage your account settings' })} +

{/* Avatar & Identity */} @@ -132,7 +138,9 @@ export function ProfilePage() { ) : ( )} - {user.image ? 'Replace' : 'Upload'} + {user.image + ? t('profile.avatar.replace', { defaultValue: 'Replace' }) + : t('profile.avatar.upload', { defaultValue: 'Upload' })} {user.image && ( )}
@@ -163,9 +171,13 @@ export function ProfilePage() {
- Personal Information + + {t('profile.info.title', { defaultValue: 'Personal Information' })} +
- Update your name and view account details + + {t('profile.info.description', { defaultValue: 'Update your name and view account details' })} +
@@ -179,13 +191,13 @@ export function ProfilePage() { - Profile updated successfully. + {t('profile.info.saved', { defaultValue: 'Profile updated successfully.' })} )}
- +
- + -

Email cannot be changed.

+

+ {t('profile.info.emailImmutable', { defaultValue: 'Email cannot be changed.' })} +

- + @@ -245,6 +261,7 @@ interface PasswordCardProps { } function PasswordCard({ changePassword, setInitialPassword, hasLocalPassword, highlight, onPasswordSet }: PasswordCardProps) { + const { t } = useObjectTranslation(); const [hasPassword, setHasPassword] = useState(null); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); @@ -273,11 +290,11 @@ function PasswordCard({ changePassword, setInitialPassword, hasLocalPassword, hi setSuccess(null); if (newPassword.length < 8) { - setError('Password must be at least 8 characters'); + setError(t('profile.password.tooShort', { defaultValue: 'Password must be at least 8 characters' })); return; } if (newPassword !== confirmPassword) { - setError('Passwords do not match'); + setError(t('profile.password.mismatch', { defaultValue: 'Passwords do not match' })); return; } @@ -285,15 +302,17 @@ function PasswordCard({ changePassword, setInitialPassword, hasLocalPassword, hi try { if (hasPassword) { if (!currentPassword) { - setError('Enter your current password'); + setError(t('profile.password.enterCurrent', { defaultValue: 'Enter your current password' })); setSubmitting(false); return; } await changePassword(currentPassword, newPassword); - setSuccess('Password changed.'); + setSuccess(t('profile.password.changed', { defaultValue: 'Password changed.' })); } else { await setInitialPassword(newPassword); - setSuccess('Local password set. You can now sign in with email and password on this environment.'); + setSuccess(t('profile.password.localSet', { + defaultValue: 'Local password set. You can now sign in with email and password on this environment.', + })); setHasPassword(true); onPasswordSet?.(); } @@ -314,20 +333,29 @@ function PasswordCard({ changePassword, setInitialPassword, hasLocalPassword, hi
- {hasPassword ? 'Change Password' : 'Set Local Password'} + {hasPassword + ? t('profile.password.changeTitle', { defaultValue: 'Change Password' }) + : t('profile.password.setTitle', { defaultValue: 'Set Local Password' })}
{hasPassword - ? 'Update the password you use to sign in to this environment.' - : 'You signed in via single sign-on. Set a local password to also sign in with email and password on this environment.'} + ? t('profile.password.changeDescription', { + defaultValue: 'Update the password you use to sign in to this environment.', + }) + : t('profile.password.setDescription', { + defaultValue: + 'You signed in via single sign-on. Set a local password to also sign in with email and password on this environment.', + })}
{hasPassword && (
- + )}
- +
- + - {submitting ? 'Saving…' : hasPassword ? 'Change password' : 'Set password'} + {submitting + ? t('profile.saving', { defaultValue: 'Saving…' }) + : hasPassword + ? t('profile.password.changeAction', { defaultValue: 'Change password' }) + : t('profile.password.setAction', { defaultValue: 'Set password' })} diff --git a/apps/console/src/registerAccountComponents.tsx b/apps/console/src/registerAccountComponents.tsx index 30a6fe8f3..738ad59a0 100644 --- a/apps/console/src/registerAccountComponents.tsx +++ b/apps/console/src/registerAccountComponents.tsx @@ -28,13 +28,19 @@ import { lazy, Suspense } from 'react'; import { registerAppComponent } from '@object-ui/app-shell'; +import { useObjectTranslation } from '@object-ui/i18n'; const ProfilePage = lazy(() => import('./pages/system/ProfilePage').then((m) => ({ default: m.ProfilePage })), ); -function AccountFallback({ label }: { label: string }) { - return
Loading {label}…
; +function AccountFallback() { + const { t } = useObjectTranslation(); + return ( +
+ {t('common.loading', { defaultValue: 'Loading...' })} +
+ ); } registerAppComponent({ @@ -42,7 +48,7 @@ registerAppComponent({ label: 'My Profile', source: '@objectstack/console', component: (props: any) => ( - }> + }> ), diff --git a/packages/app-shell/src/components/ManagedByBadge.tsx b/packages/app-shell/src/components/ManagedByBadge.tsx index 9ac5d140a..ce65d01c8 100644 --- a/packages/app-shell/src/components/ManagedByBadge.tsx +++ b/packages/app-shell/src/components/ManagedByBadge.tsx @@ -7,6 +7,7 @@ import { TooltipProvider, cn, } from '@object-ui/components'; +import { useObjectTranslation } from '@object-ui/i18n'; /** * ManagedByBadge — replaces the verbose, full-width `ManagedByBanner` with @@ -52,11 +53,11 @@ export interface ManagedByBadgeProps { interface Variant { icon: typeof ShieldAlert; - /** Short label shown inside the badge itself. */ + /** Key under the `managedByBadge.*` locale namespace. */ + i18nKey: string; + /** English fallbacks when the locale bundle misses a key. */ short: string; - /** Longer one-line title rendered in the tooltip heading. */ title: string; - /** Tooltip body. Receives the human-readable identity label (used only by better-auth). */ body: (display: string) => string; /** Tailwind classes for the badge surface. */ tone: string; @@ -65,6 +66,7 @@ interface Variant { const VARIANTS: Record, Variant> = { config: { icon: Settings2, + i18nKey: 'config', short: 'Admin config', title: 'Administrator configuration', body: () => @@ -73,6 +75,7 @@ const VARIANTS: Record, Variant> = { }, system: { icon: Lock, + i18nKey: 'system', short: 'System-managed', title: 'Managed by the platform', body: () => @@ -81,6 +84,7 @@ const VARIANTS: Record, Variant> = { }, 'append-only': { icon: Archive, + i18nKey: 'appendOnly', short: 'Read-only · Audit log', title: 'Read-only historical record', body: () => @@ -89,6 +93,7 @@ const VARIANTS: Record, Variant> = { }, 'better-auth': { icon: ShieldAlert, + i18nKey: 'betterAuth', short: 'Identity', title: 'Managed by the identity provider', body: (display) => @@ -98,11 +103,18 @@ const VARIANTS: Record, Variant> = { }; export function ManagedByBadge({ managedBy, label, className }: ManagedByBadgeProps) { + const { t } = useObjectTranslation(); if (!managedBy || managedBy === 'platform') return null; const variant = VARIANTS[managedBy as Exclude]; if (!variant) return null; const Icon = variant.icon; const display = label ?? 'better-auth'; + const short = t(`managedByBadge.${variant.i18nKey}.short`, { defaultValue: variant.short }); + const title = t(`managedByBadge.${variant.i18nKey}.title`, { defaultValue: variant.title }); + const body = t(`managedByBadge.${variant.i18nKey}.body`, { + defaultValue: variant.body(display), + provider: display, + }); return ( @@ -118,12 +130,12 @@ export function ManagedByBadge({ managedBy, label, className }: ManagedByBadgePr )} > diff --git a/packages/i18n/src/__tests__/pickLocalized.test.ts b/packages/i18n/src/__tests__/pickLocalized.test.ts index 662b677c4..b6d1d0524 100644 --- a/packages/i18n/src/__tests__/pickLocalized.test.ts +++ b/packages/i18n/src/__tests__/pickLocalized.test.ts @@ -11,6 +11,12 @@ describe('pickLocalized', () => { it('falls back from region to base language (zh-CN -> zh)', () => { expect(pickLocalized({ en: 'Pricing', zh: '定价' }, 'zh-CN')).toBe('定价'); }); + it('upgrades from base language to a region-qualified key (zh -> zh-CN)', () => { + expect(pickLocalized({ en: 'Pricing', 'zh-CN': '定价' }, 'zh')).toBe('定价'); + expect(pickLocalized({ en: 'Pricing', 'ja-JP': '価格' }, 'ja')).toBe('価格'); + // exact/base keys still win over the regional upgrade + expect(pickLocalized({ zh: '基础', 'zh-CN': '区域' }, 'zh')).toBe('基础'); + }); it('falls back to default then en then first', () => { expect(pickLocalized({ default: 'D', en: 'E' }, 'fr')).toBe('D'); expect(pickLocalized({ en: 'E', ja: 'J' }, 'fr')).toBe('E'); diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 60e357f65..3143c6c7b 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -280,6 +280,31 @@ const ar = { addGroup: "إضافة حقل مجموعة", collapsedByDefault: "مطوي افتراضياً", removeGroup: "إزالة", + inlineEditShort: "تحرير مضمّن", + inlineEditLabel: "حرّر السجلات مباشرة (انقر خلية للتحرير)", + recordEditingTitle: "تحرير السجلات", + }, + managedByBadge: { + config: { + short: "إعدادات المسؤول", + title: "تكوين المسؤول", + body: "تحدد هذه الصفوف سلوك المنصة أثناء التشغيل. أنشئها هنا؛ وتُخزَّن بيانات التشغيل الناتجة في جدول منفصل.", + }, + system: { + short: "يديره النظام", + title: "تديره المنصة", + body: "تُنشأ الصفوف تلقائيًا عند تشغيل الإجراءات على السجل المصدر. القائمة أدناه واجهة مراقبة للقراءة فقط — وتوجد إجراءات مستوى الصف (اعتماد، استرجاع، إعادة إرسال، …) على كل صف.", + }, + appendOnly: { + short: "للقراءة فقط · سجل التدقيق", + title: "سجل تاريخي للقراءة فقط", + body: "سجل تدقيق غير قابل للتغيير. لا يمكن إنشاء الصفوف أو تعديلها أو حذفها من الواجهة — تكتبها المنصة عند وقوع الأحداث. استخدم التصدير للتنزيل لمراجعة الامتثال.", + }, + betterAuth: { + short: "الهوية", + title: "يديره مزوّد الهوية", + body: "مخطط هذا الكائن مملوك لـ {{provider}}. التعديلات المباشرة تتجاوز تجزئة كلمات المرور والتحقق من الجلسات وفحوصات المصادقة الثنائية وخطافات التدقيق. أدر هذه السجلات عبر تدفقات تسجيل الدخول والدعوة والأمان لدى مزوّد المصادقة.", + }, }, kanban: { addCard: "إضافة بطاقة", @@ -1222,6 +1247,44 @@ const ar = { tenantHostHint: "تسجل الدخول إلى مساحة العمل هذه", }, }, + profile: { + title: "الملف الشخصي", + subtitle: "إدارة إعدادات حسابك", + saving: "جارٍ الحفظ…", + avatar: { + upload: "رفع", + replace: "استبدال", + remove: "إزالة", + }, + info: { + title: "المعلومات الشخصية", + description: "قم بتحديث اسمك وعرض تفاصيل حسابك", + saved: "تم تحديث الملف الشخصي بنجاح.", + name: "الاسم", + email: "البريد الإلكتروني", + emailImmutable: "لا يمكن تغيير البريد الإلكتروني.", + role: "الدور", + save: "حفظ التغييرات", + }, + password: { + changeTitle: "تغيير كلمة المرور", + setTitle: "تعيين كلمة مرور محلية", + changeDescription: "قم بتحديث كلمة المرور التي تستخدمها لتسجيل الدخول إلى هذه البيئة.", + setDescription: + "لقد سجّلت الدخول عبر تسجيل الدخول الموحّد. عيّن كلمة مرور محلية لتتمكن أيضاً من تسجيل الدخول بالبريد الإلكتروني وكلمة المرور في هذه البيئة.", + current: "كلمة المرور الحالية", + new: "كلمة المرور الجديدة", + password: "كلمة المرور", + confirm: "تأكيد كلمة المرور", + tooShort: "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل", + mismatch: "كلمتا المرور غير متطابقتين", + enterCurrent: "أدخل كلمة المرور الحالية", + changed: "تم تغيير كلمة المرور.", + localSet: "تم تعيين كلمة المرور المحلية. يمكنك الآن تسجيل الدخول بالبريد الإلكتروني وكلمة المرور في هذه البيئة.", + changeAction: "تغيير كلمة المرور", + setAction: "تعيين كلمة المرور", + }, + }, errors: { networkError: "خطأ في الشبكة. يرجى التحقق من اتصالك.", serverError: "خطأ في الخادم. يرجى المحاولة مرة أخرى لاحقاً.", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 4a7d1d4ed..87b1a4a5c 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -280,6 +280,31 @@ const de = { addGroup: "Gruppenfeld hinzufügen", collapsedByDefault: "Standardmäßig eingeklappt", removeGroup: "Entfernen", + inlineEditShort: "Inline bearbeiten", + inlineEditLabel: "Datensätze inline bearbeiten (Zelle anklicken zum Bearbeiten)", + recordEditingTitle: "Datensatzbearbeitung", + }, + managedByBadge: { + config: { + short: "Admin-Konfiguration", + title: "Administrator-Konfiguration", + body: "Diese Zeilen definieren das Laufzeitverhalten der Plattform. Erstellen Sie sie hier; die erzeugten Laufzeitdaten liegen in einer separaten Tabelle.", + }, + system: { + short: "Systemverwaltet", + title: "Von der Plattform verwaltet", + body: "Zeilen werden automatisch erstellt, wenn Aktionen auf dem Quelldatensatz ausgeführt werden. Die Liste ist eine schreibgeschützte Überwachungsansicht – Aktionen auf Zeilenebene (Genehmigen, Zurückrufen, Erneut senden, …) befinden sich an jeder Zeile.", + }, + appendOnly: { + short: "Schreibgeschützt · Audit-Log", + title: "Schreibgeschützter Verlauf", + body: "Unveränderliches Audit-Log. Zeilen können in der UI nicht erstellt, bearbeitet oder gelöscht werden – die Plattform schreibt sie bei Ereignissen. Nutzen Sie Export für Compliance-Prüfungen.", + }, + betterAuth: { + short: "Identität", + title: "Vom Identitätsanbieter verwaltet", + body: "Das Schema dieses Objekts gehört {{provider}}. Direkte Änderungen umgehen Passwort-Hashing, Sitzungsvalidierung, Zwei-Faktor-Prüfungen und Audit-Hooks. Verwalten Sie diese Datensätze über die Anmelde-, Einladungs- und Sicherheitsabläufe Ihres Authentifizierungsanbieters.", + }, }, kanban: { addCard: "Karte hinzufügen", @@ -1222,6 +1247,44 @@ const de = { tenantHostHint: "Sie melden sich bei diesem Arbeitsbereich an", }, }, + profile: { + title: "Profil", + subtitle: "Verwalten Sie Ihre Kontoeinstellungen", + saving: "Speichern…", + avatar: { + upload: "Hochladen", + replace: "Ersetzen", + remove: "Entfernen", + }, + info: { + title: "Persönliche Informationen", + description: "Aktualisieren Sie Ihren Namen und sehen Sie Ihre Kontodetails ein", + saved: "Profil erfolgreich aktualisiert.", + name: "Name", + email: "E-Mail", + emailImmutable: "Die E-Mail-Adresse kann nicht geändert werden.", + role: "Rolle", + save: "Änderungen speichern", + }, + password: { + changeTitle: "Passwort ändern", + setTitle: "Lokales Passwort festlegen", + changeDescription: "Aktualisieren Sie das Passwort, mit dem Sie sich in dieser Umgebung anmelden.", + setDescription: + "Sie haben sich per Single Sign-On angemeldet. Legen Sie ein lokales Passwort fest, um sich in dieser Umgebung auch mit E-Mail und Passwort anmelden zu können.", + current: "Aktuelles Passwort", + new: "Neues Passwort", + password: "Passwort", + confirm: "Passwort bestätigen", + tooShort: "Das Passwort muss mindestens 8 Zeichen lang sein", + mismatch: "Die Passwörter stimmen nicht überein", + enterCurrent: "Geben Sie Ihr aktuelles Passwort ein", + changed: "Passwort geändert.", + localSet: "Lokales Passwort festgelegt. Sie können sich in dieser Umgebung jetzt auch mit E-Mail und Passwort anmelden.", + changeAction: "Passwort ändern", + setAction: "Passwort festlegen", + }, + }, errors: { networkError: "Netzwerkfehler. Bitte überprüfen Sie Ihre Verbindung.", serverError: "Serverfehler. Bitte versuchen Sie es später erneut.", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index ade1706b2..051ccfe1c 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -356,6 +356,28 @@ const en = { inlineEditLabel: 'Edit records inline (click a cell to edit)', recordEditingTitle: 'Record editing', }, + managedByBadge: { + config: { + short: 'Admin config', + title: 'Administrator configuration', + body: 'These rows define how the platform behaves at runtime. Author them here; the runtime data they produce lives in a separate table.', + }, + system: { + short: 'System-managed', + title: 'Managed by the platform', + body: 'Rows here are created automatically when actions run on the source record. The list below is a read-only monitoring surface — row-level actions (Approve, Recall, Resend, …) live on each row.', + }, + appendOnly: { + short: 'Read-only · Audit log', + title: 'Read-only historical record', + body: "Immutable audit log. Rows cannot be created, edited, or deleted from the UI — they're written by the platform when events occur. Use Export to download for compliance review.", + }, + betterAuth: { + short: 'Identity', + title: 'Managed by the identity provider', + body: "This object's schema is owned by {{provider}}. Direct edits bypass password hashing, session validation, two-factor checks, and audit hooks. Manage these records through your authentication provider's sign-in, invitation, and security flows instead.", + }, + }, kanban: { addCard: 'Add card', addColumn: 'Add column', @@ -1680,6 +1702,44 @@ const en = { tenantHostHint: 'You are signing in to this workspace', }, }, + profile: { + title: 'Profile', + subtitle: 'Manage your account settings', + saving: 'Saving…', + avatar: { + upload: 'Upload', + replace: 'Replace', + remove: 'Remove', + }, + info: { + title: 'Personal Information', + description: 'Update your name and view account details', + saved: 'Profile updated successfully.', + name: 'Name', + email: 'Email', + emailImmutable: 'Email cannot be changed.', + role: 'Role', + save: 'Save Changes', + }, + password: { + changeTitle: 'Change Password', + setTitle: 'Set Local Password', + changeDescription: 'Update the password you use to sign in to this environment.', + setDescription: + 'You signed in via single sign-on. Set a local password to also sign in with email and password on this environment.', + current: 'Current password', + new: 'New password', + password: 'Password', + confirm: 'Confirm password', + tooShort: 'Password must be at least 8 characters', + mismatch: 'Passwords do not match', + enterCurrent: 'Enter your current password', + changed: 'Password changed.', + localSet: 'Local password set. You can now sign in with email and password on this environment.', + changeAction: 'Change password', + setAction: 'Set password', + }, + }, errors: { networkError: 'Network error. Please check your connection.', serverError: 'Server error. Please try again later.', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index bddd7cb53..eaf0a2a50 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -280,6 +280,31 @@ const es = { addGroup: "Agregar campo de grupo", collapsedByDefault: "Contraído por defecto", removeGroup: "Eliminar", + inlineEditShort: "Edición en línea", + inlineEditLabel: "Editar registros en línea (haga clic en una celda para editar)", + recordEditingTitle: "Edición de registros", + }, + managedByBadge: { + config: { + short: "Configuración", + title: "Configuración del administrador", + body: "Estas filas definen el comportamiento de la plataforma en tiempo de ejecución. Créelas aquí; los datos de tiempo de ejecución que producen viven en una tabla separada.", + }, + system: { + short: "Gestionado por el sistema", + title: "Gestionado por la plataforma", + body: "Las filas se crean automáticamente cuando se ejecutan acciones en el registro de origen. La lista es una superficie de supervisión de solo lectura; las acciones por fila (Aprobar, Retirar, Reenviar, …) están en cada fila.", + }, + appendOnly: { + short: "Solo lectura · Registro de auditoría", + title: "Registro histórico de solo lectura", + body: "Registro de auditoría inmutable. Las filas no se pueden crear, editar ni eliminar desde la interfaz: las escribe la plataforma cuando ocurren eventos. Use Exportar para descargarlas para revisión de cumplimiento.", + }, + betterAuth: { + short: "Identidad", + title: "Gestionado por el proveedor de identidad", + body: "El esquema de este objeto es propiedad de {{provider}}. Las ediciones directas omiten el hash de contraseñas, la validación de sesiones, las comprobaciones de dos factores y los hooks de auditoría. Gestione estos registros mediante los flujos de inicio de sesión, invitación y seguridad de su proveedor de autenticación.", + }, }, kanban: { addCard: "Añadir tarjeta", @@ -1222,6 +1247,44 @@ const es = { tenantHostHint: "Está iniciando sesión en este espacio de trabajo", }, }, + profile: { + title: "Perfil", + subtitle: "Gestione la configuración de su cuenta", + saving: "Guardando…", + avatar: { + upload: "Subir", + replace: "Reemplazar", + remove: "Quitar", + }, + info: { + title: "Información personal", + description: "Actualice su nombre y consulte los detalles de la cuenta", + saved: "Perfil actualizado correctamente.", + name: "Nombre", + email: "Correo electrónico", + emailImmutable: "El correo electrónico no se puede cambiar.", + role: "Rol", + save: "Guardar cambios", + }, + password: { + changeTitle: "Cambiar contraseña", + setTitle: "Establecer contraseña local", + changeDescription: "Actualice la contraseña que usa para iniciar sesión en este entorno.", + setDescription: + "Inició sesión mediante inicio de sesión único. Establezca una contraseña local para poder iniciar sesión también con correo y contraseña en este entorno.", + current: "Contraseña actual", + new: "Nueva contraseña", + password: "Contraseña", + confirm: "Confirmar contraseña", + tooShort: "La contraseña debe tener al menos 8 caracteres", + mismatch: "Las contraseñas no coinciden", + enterCurrent: "Introduzca su contraseña actual", + changed: "Contraseña cambiada.", + localSet: "Contraseña local establecida. Ahora puede iniciar sesión con correo y contraseña en este entorno.", + changeAction: "Cambiar contraseña", + setAction: "Establecer contraseña", + }, + }, errors: { networkError: "Error de red. Por favor, compruebe su conexión.", serverError: "Error del servidor. Por favor, inténtelo de nuevo más tarde.", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 1fc63660e..8c0c39122 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -280,6 +280,31 @@ const fr = { addGroup: "Ajouter un champ de groupe", collapsedByDefault: "Réduit par défaut", removeGroup: "Supprimer", + inlineEditShort: "Édition en ligne", + inlineEditLabel: "Modifier les enregistrements en ligne (cliquez sur une cellule pour modifier)", + recordEditingTitle: "Modification des enregistrements", + }, + managedByBadge: { + config: { + short: "Config admin", + title: "Configuration administrateur", + body: "Ces lignes définissent le comportement de la plateforme à l'exécution. Créez-les ici ; les données d'exécution produites résident dans une table séparée.", + }, + system: { + short: "Géré par le système", + title: "Géré par la plateforme", + body: "Les lignes sont créées automatiquement lorsque des actions s'exécutent sur l'enregistrement source. La liste est une surface de supervision en lecture seule — les actions par ligne (Approuver, Rappeler, Renvoyer, …) se trouvent sur chaque ligne.", + }, + appendOnly: { + short: "Lecture seule · Journal d'audit", + title: "Historique en lecture seule", + body: "Journal d'audit immuable. Les lignes ne peuvent pas être créées, modifiées ni supprimées depuis l'interface : la plateforme les écrit lors des événements. Utilisez Exporter pour les télécharger à des fins de conformité.", + }, + betterAuth: { + short: "Identité", + title: "Géré par le fournisseur d'identité", + body: "Le schéma de cet objet appartient à {{provider}}. Les modifications directes contournent le hachage des mots de passe, la validation des sessions, les contrôles à deux facteurs et les hooks d'audit. Gérez ces enregistrements via les flux de connexion, d'invitation et de sécurité de votre fournisseur d'authentification.", + }, }, kanban: { addCard: "Ajouter une carte", @@ -1222,6 +1247,44 @@ const fr = { tenantHostHint: "Vous vous connectez à cet espace de travail", }, }, + profile: { + title: "Profil", + subtitle: "Gérez les paramètres de votre compte", + saving: "Enregistrement…", + avatar: { + upload: "Téléverser", + replace: "Remplacer", + remove: "Supprimer", + }, + info: { + title: "Informations personnelles", + description: "Mettez à jour votre nom et consultez les détails de votre compte", + saved: "Profil mis à jour avec succès.", + name: "Nom", + email: "E-mail", + emailImmutable: "L'adresse e-mail ne peut pas être modifiée.", + role: "Rôle", + save: "Enregistrer les modifications", + }, + password: { + changeTitle: "Changer le mot de passe", + setTitle: "Définir un mot de passe local", + changeDescription: "Mettez à jour le mot de passe que vous utilisez pour vous connecter à cet environnement.", + setDescription: + "Vous vous êtes connecté via l'authentification unique. Définissez un mot de passe local pour pouvoir également vous connecter avec votre e-mail et un mot de passe sur cet environnement.", + current: "Mot de passe actuel", + new: "Nouveau mot de passe", + password: "Mot de passe", + confirm: "Confirmer le mot de passe", + tooShort: "Le mot de passe doit contenir au moins 8 caractères", + mismatch: "Les mots de passe ne correspondent pas", + enterCurrent: "Saisissez votre mot de passe actuel", + changed: "Mot de passe modifié.", + localSet: "Mot de passe local défini. Vous pouvez désormais vous connecter avec votre e-mail et votre mot de passe sur cet environnement.", + changeAction: "Changer le mot de passe", + setAction: "Définir le mot de passe", + }, + }, errors: { networkError: "Erreur réseau. Veuillez vérifier votre connexion.", serverError: "Erreur serveur. Veuillez réessayer plus tard.", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index bd8215c80..e7b7cb99b 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -280,6 +280,31 @@ const ja = { addGroup: "グループフィールドを追加", collapsedByDefault: "デフォルトで折りたたむ", removeGroup: "削除", + inlineEditShort: "インライン編集", + inlineEditLabel: "レコードをインラインで編集(セルをクリックして編集)", + recordEditingTitle: "レコード編集", + }, + managedByBadge: { + config: { + short: "管理設定", + title: "管理者設定", + body: "これらの行はプラットフォームの実行時の動作を定義します。ここで設定を作成してください。生成される実行時データは別のテーブルに保存されます。", + }, + system: { + short: "システム管理", + title: "プラットフォームが管理", + body: "ソースレコードでアクションが実行されると、行が自動的に作成されます。以下のリストは読み取り専用の監視画面です。行レベルのアクション(承認、取り消し、再送など)は各行にあります。", + }, + appendOnly: { + short: "読み取り専用 · 監査ログ", + title: "読み取り専用の履歴レコード", + body: "変更不可の監査ログです。行はイベント発生時にプラットフォームによって書き込まれ、UI から作成・編集・削除はできません。コンプライアンスレビューにはエクスポートをご利用ください。", + }, + betterAuth: { + short: "アイデンティティ", + title: "ID プロバイダーが管理", + body: "このオブジェクトのスキーマは {{provider}} が所有しています。直接編集するとパスワードハッシュ、セッション検証、二要素チェック、監査フックがバイパスされます。認証プロバイダーのサインイン、招待、セキュリティフローで管理してください。", + }, }, kanban: { addCard: "カードを追加", @@ -1222,6 +1247,44 @@ const ja = { tenantHostHint: "このワークスペースにサインインしています", }, }, + profile: { + title: "プロフィール", + subtitle: "アカウント設定を管理します", + saving: "保存中…", + avatar: { + upload: "アップロード", + replace: "変更", + remove: "削除", + }, + info: { + title: "個人情報", + description: "名前を更新し、アカウントの詳細を確認します", + saved: "プロフィールを更新しました。", + name: "名前", + email: "メールアドレス", + emailImmutable: "メールアドレスは変更できません。", + role: "ロール", + save: "変更を保存", + }, + password: { + changeTitle: "パスワードを変更", + setTitle: "ローカルパスワードを設定", + changeDescription: "この環境へのサインインに使用するパスワードを更新します。", + setDescription: + "シングルサインオンでサインインしています。ローカルパスワードを設定すると、この環境でメールアドレスとパスワードでもサインインできます。", + current: "現在のパスワード", + new: "新しいパスワード", + password: "パスワード", + confirm: "パスワードの確認", + tooShort: "パスワードは 8 文字以上で入力してください", + mismatch: "パスワードが一致しません", + enterCurrent: "現在のパスワードを入力してください", + changed: "パスワードを変更しました。", + localSet: "ローカルパスワードを設定しました。この環境でメールアドレスとパスワードでサインインできます。", + changeAction: "パスワードを変更", + setAction: "パスワードを設定", + }, + }, errors: { networkError: "ネットワークエラーです。接続を確認してください。", serverError: "サーバーエラーです。後でもう一度お試しください。", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 1b3de9e01..0127de792 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -280,6 +280,31 @@ const ko = { addGroup: "그룹 필드 추가", collapsedByDefault: "기본적으로 접힘", removeGroup: "제거", + inlineEditShort: "인라인 편집", + inlineEditLabel: "레코드를 인라인으로 편집(셀을 클릭하여 편집)", + recordEditingTitle: "레코드 편집", + }, + managedByBadge: { + config: { + short: "관리 구성", + title: "관리자 구성", + body: "이 행들은 플랫폼의 런타임 동작을 정의합니다. 여기에서 작성하세요. 생성되는 런타임 데이터는 별도의 테이블에 저장됩니다.", + }, + system: { + short: "시스템 관리", + title: "플랫폼에서 관리", + body: "원본 레코드에서 작업이 실행되면 행이 자동으로 생성됩니다. 아래 목록은 읽기 전용 모니터링 화면이며, 행 수준 작업(승인, 회수, 재전송 등)은 각 행에 있습니다.", + }, + appendOnly: { + short: "읽기 전용 · 감사 로그", + title: "읽기 전용 기록", + body: "변경할 수 없는 감사 로그입니다. 행은 이벤트 발생 시 플랫폼이 기록하며 UI에서 생성·편집·삭제할 수 없습니다. 규정 준수 검토를 위해 내보내기를 사용하세요.", + }, + betterAuth: { + short: "아이덴티티", + title: "ID 공급자가 관리", + body: "이 개체의 스키마는 {{provider}}가 소유합니다. 직접 편집하면 비밀번호 해싱, 세션 검증, 2단계 인증 확인 및 감사 후크를 우회하게 됩니다. 인증 공급자의 로그인, 초대, 보안 흐름을 통해 이 레코드를 관리하세요.", + }, }, kanban: { addCard: "카드 추가", @@ -1222,6 +1247,44 @@ const ko = { tenantHostHint: "이 작업 공간에 로그인하는 중입니다", }, }, + profile: { + title: "프로필", + subtitle: "계정 설정을 관리하세요", + saving: "저장 중…", + avatar: { + upload: "업로드", + replace: "변경", + remove: "제거", + }, + info: { + title: "개인 정보", + description: "이름을 업데이트하고 계정 정보를 확인하세요", + saved: "프로필이 성공적으로 업데이트되었습니다.", + name: "이름", + email: "이메일", + emailImmutable: "이메일은 변경할 수 없습니다.", + role: "역할", + save: "변경 사항 저장", + }, + password: { + changeTitle: "비밀번호 변경", + setTitle: "로컬 비밀번호 설정", + changeDescription: "이 환경에 로그인할 때 사용하는 비밀번호를 업데이트하세요.", + setDescription: + "통합 인증(SSO)으로 로그인하셨습니다. 로컬 비밀번호를 설정하면 이 환경에서 이메일과 비밀번호로도 로그인할 수 있습니다.", + current: "현재 비밀번호", + new: "새 비밀번호", + password: "비밀번호", + confirm: "비밀번호 확인", + tooShort: "비밀번호는 8자 이상이어야 합니다", + mismatch: "비밀번호가 일치하지 않습니다", + enterCurrent: "현재 비밀번호를 입력하세요", + changed: "비밀번호가 변경되었습니다.", + localSet: "로컬 비밀번호가 설정되었습니다. 이제 이 환경에서 이메일과 비밀번호로 로그인할 수 있습니다.", + changeAction: "비밀번호 변경", + setAction: "비밀번호 설정", + }, + }, errors: { networkError: "네트워크 오류입니다. 연결을 확인해주세요.", serverError: "서버 오류입니다. 나중에 다시 시도해주세요.", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 3c4293562..a9f0c0797 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -280,6 +280,31 @@ const pt = { addGroup: "Adicionar campo de grupo", collapsedByDefault: "Recolhido por padrão", removeGroup: "Remover", + inlineEditShort: "Edição em linha", + inlineEditLabel: "Editar registros em linha (clique em uma célula para editar)", + recordEditingTitle: "Edição de registros", + }, + managedByBadge: { + config: { + short: "Config. de admin", + title: "Configuração do administrador", + body: "Estas linhas definem o comportamento da plataforma em tempo de execução. Crie-as aqui; os dados de tempo de execução produzidos ficam em uma tabela separada.", + }, + system: { + short: "Gerenciado pelo sistema", + title: "Gerenciado pela plataforma", + body: "As linhas são criadas automaticamente quando ações são executadas no registro de origem. A lista é uma superfície de monitoramento somente leitura — as ações por linha (Aprovar, Retirar, Reenviar, …) ficam em cada linha.", + }, + appendOnly: { + short: "Somente leitura · Log de auditoria", + title: "Registro histórico somente leitura", + body: "Log de auditoria imutável. As linhas não podem ser criadas, editadas ou excluídas pela interface: a plataforma as grava quando os eventos ocorrem. Use Exportar para baixar para revisão de conformidade.", + }, + betterAuth: { + short: "Identidade", + title: "Gerenciado pelo provedor de identidade", + body: "O esquema deste objeto pertence a {{provider}}. Edições diretas ignoram o hash de senhas, a validação de sessões, as verificações de dois fatores e os hooks de auditoria. Gerencie esses registros pelos fluxos de login, convite e segurança do seu provedor de autenticação.", + }, }, kanban: { addCard: "Adicionar cartão", @@ -1222,6 +1247,44 @@ const pt = { tenantHostHint: "Você está fazendo login neste espaço de trabalho", }, }, + profile: { + title: "Perfil", + subtitle: "Gerencie as configurações da sua conta", + saving: "Salvando…", + avatar: { + upload: "Enviar", + replace: "Substituir", + remove: "Remover", + }, + info: { + title: "Informações pessoais", + description: "Atualize seu nome e veja os detalhes da conta", + saved: "Perfil atualizado com sucesso.", + name: "Nome", + email: "E-mail", + emailImmutable: "O e-mail não pode ser alterado.", + role: "Função", + save: "Salvar alterações", + }, + password: { + changeTitle: "Alterar senha", + setTitle: "Definir senha local", + changeDescription: "Atualize a senha que você usa para entrar neste ambiente.", + setDescription: + "Você entrou via logon único (SSO). Defina uma senha local para também poder entrar com e-mail e senha neste ambiente.", + current: "Senha atual", + new: "Nova senha", + password: "Senha", + confirm: "Confirmar senha", + tooShort: "A senha deve ter pelo menos 8 caracteres", + mismatch: "As senhas não coincidem", + enterCurrent: "Digite sua senha atual", + changed: "Senha alterada.", + localSet: "Senha local definida. Agora você pode entrar com e-mail e senha neste ambiente.", + changeAction: "Alterar senha", + setAction: "Definir senha", + }, + }, errors: { networkError: "Erro de rede. Por favor, verifique sua conexão.", serverError: "Erro no servidor. Por favor, tente novamente mais tarde.", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 7b079b271..ed291c65c 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -280,6 +280,31 @@ const ru = { addGroup: "Добавить поле группировки", collapsedByDefault: "Свёрнуто по умолчанию", removeGroup: "Удалить", + inlineEditShort: "Встроенное редактирование", + inlineEditLabel: "Редактировать записи в таблице (щёлкните ячейку для редактирования)", + recordEditingTitle: "Редактирование записей", + }, + managedByBadge: { + config: { + short: "Конфигурация", + title: "Конфигурация администратора", + body: "Эти строки определяют поведение платформы во время выполнения. Создавайте их здесь; создаваемые данные времени выполнения хранятся в отдельной таблице.", + }, + system: { + short: "Управляется системой", + title: "Управляется платформой", + body: "Строки создаются автоматически при выполнении действий над исходной записью. Список — это панель мониторинга только для чтения; действия на уровне строк (Утвердить, Отозвать, Отправить повторно и т. д.) находятся у каждой строки.", + }, + appendOnly: { + short: "Только чтение · Журнал аудита", + title: "Историческая запись только для чтения", + body: "Неизменяемый журнал аудита. Строки нельзя создавать, изменять или удалять из интерфейса — платформа записывает их при наступлении событий. Используйте экспорт для проверки соответствия.", + }, + betterAuth: { + short: "Идентификация", + title: "Управляется поставщиком удостоверений", + body: "Схема этого объекта принадлежит {{provider}}. Прямые изменения обходят хеширование паролей, проверку сессий, двухфакторные проверки и хуки аудита. Управляйте этими записями через процессы входа, приглашения и безопасности вашего поставщика аутентификации.", + }, }, kanban: { addCard: "Добавить карточку", @@ -1222,6 +1247,44 @@ const ru = { tenantHostHint: "Вы входите в это рабочее пространство", }, }, + profile: { + title: "Профиль", + subtitle: "Управляйте настройками своей учётной записи", + saving: "Сохранение…", + avatar: { + upload: "Загрузить", + replace: "Заменить", + remove: "Удалить", + }, + info: { + title: "Личная информация", + description: "Обновите имя и просмотрите данные учётной записи", + saved: "Профиль успешно обновлён.", + name: "Имя", + email: "Эл. почта", + emailImmutable: "Адрес электронной почты изменить нельзя.", + role: "Роль", + save: "Сохранить изменения", + }, + password: { + changeTitle: "Изменить пароль", + setTitle: "Задать локальный пароль", + changeDescription: "Обновите пароль, который вы используете для входа в эту среду.", + setDescription: + "Вы вошли через единый вход (SSO). Задайте локальный пароль, чтобы также входить в эту среду по электронной почте и паролю.", + current: "Текущий пароль", + new: "Новый пароль", + password: "Пароль", + confirm: "Подтвердите пароль", + tooShort: "Пароль должен содержать не менее 8 символов", + mismatch: "Пароли не совпадают", + enterCurrent: "Введите текущий пароль", + changed: "Пароль изменён.", + localSet: "Локальный пароль задан. Теперь вы можете входить в эту среду по электронной почте и паролю.", + changeAction: "Изменить пароль", + setAction: "Задать пароль", + }, + }, errors: { networkError: "Ошибка сети. Проверьте подключение к интернету.", serverError: "Ошибка сервера. Попробуйте позже.", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index c0b3673aa..818ee7883 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -432,6 +432,28 @@ const zh = { inlineEditLabel: '行内编辑记录(点击单元格进行编辑)', recordEditingTitle: '记录编辑', }, + managedByBadge: { + config: { + short: '管理配置', + title: '管理员配置', + body: '这些记录定义平台的运行时行为。请在此处编写配置;其产生的运行时数据存放在单独的表中。', + }, + system: { + short: '系统管理', + title: '由平台管理', + body: '当源记录上的操作运行时,这里的行会自动创建。下方列表是只读监控界面——行级操作(批准、撤回、重发等)在每一行上。', + }, + appendOnly: { + short: '只读 · 审计日志', + title: '只读历史记录', + body: '不可变审计日志。行由平台在事件发生时写入,不能在界面上创建、编辑或删除。可使用导出功能下载以供合规审查。', + }, + betterAuth: { + short: '身份', + title: '由身份提供方管理', + body: '此对象的架构由 {{provider}} 管理。直接编辑会绕过密码哈希、会话校验、两步验证检查和审计钩子。请通过身份认证提供方的登录、邀请和安全流程管理这些记录。', + }, + }, kanban: { addCard: '添加卡片', addColumn: '添加列', @@ -1766,6 +1788,43 @@ const zh = { tenantHostHint: '您正在登录此工作区', }, }, + profile: { + title: '个人资料', + subtitle: '管理您的账户设置', + saving: '保存中…', + avatar: { + upload: '上传', + replace: '更换', + remove: '移除', + }, + info: { + title: '个人信息', + description: '更新您的姓名并查看账户详情', + saved: '个人资料已更新。', + name: '姓名', + email: '邮箱', + emailImmutable: '邮箱不可修改。', + role: '角色', + save: '保存更改', + }, + password: { + changeTitle: '修改密码', + setTitle: '设置本地密码', + changeDescription: '更新您在此环境登录所用的密码。', + setDescription: '您通过单点登录进入。设置本地密码后,也可以在此环境使用邮箱和密码登录。', + current: '当前密码', + new: '新密码', + password: '密码', + confirm: '确认密码', + tooShort: '密码长度至少为 8 个字符', + mismatch: '两次输入的密码不一致', + enterCurrent: '请输入当前密码', + changed: '密码已修改。', + localSet: '本地密码已设置。现在您可以在此环境使用邮箱和密码登录。', + changeAction: '修改密码', + setAction: '设置密码', + }, + }, errors: { networkError: '网络错误,请检查网络连接。', serverError: '服务器错误,请稍后重试。', diff --git a/packages/i18n/src/pickLocalized.ts b/packages/i18n/src/pickLocalized.ts index c88c4cee4..9e5d92ae2 100644 --- a/packages/i18n/src/pickLocalized.ts +++ b/packages/i18n/src/pickLocalized.ts @@ -3,8 +3,9 @@ * * Accepts a plain string (passed through) or an i18n object keyed by language * code — e.g. `{ en: 'Pricing', 'zh-CN': '定价', zh: '定价', default: 'Pricing' }`. - * Resolution order: exact language -> base language (`zh-CN` -> `zh`) -> `default` - * -> `en` -> first value. Unlike a `default`/`en`-only resolver this is genuinely + * Resolution order: exact language -> base language (`zh-CN` -> `zh`) -> any + * region-qualified key sharing the base (`zh` -> `zh-CN`) -> `default` -> `en` + * -> first value. Unlike a `default`/`en`-only resolver this is genuinely * locale-aware, so server-driven metadata (page text, action param labels) can * carry inline translations instead of rendering "[object Object]" or English. * @@ -19,9 +20,13 @@ export function pickLocalized(value: unknown, language: string | undefined | nul const o = value as Record; const lang = (language || 'en').trim(); const base = lang.split('-')[0]; + // Runtime language is often a bare base code ('zh') while metadata authors + // write full BCP-47 tags ('zh-CN') — upgrade to any key sharing the base. + const regional = Object.keys(o).find((k) => k.split('-')[0] === base && typeof o[k] === 'string'); const pick = o[lang] ?? o[base] ?? + (regional !== undefined ? o[regional] : undefined) ?? o.default ?? o.en ?? Object.values(o).find((v) => typeof v === 'string'); diff --git a/packages/i18n/src/useObjectLabel.ts b/packages/i18n/src/useObjectLabel.ts index 74f717686..275e5a8d9 100644 --- a/packages/i18n/src/useObjectLabel.ts +++ b/packages/i18n/src/useObjectLabel.ts @@ -462,6 +462,7 @@ const SAFE_FIELD_LABEL_FALLBACK = { ) => options, fieldOptionLabel: (_objectName: string, _fieldName: string, _optionValue: string, fallback: string) => fallback, sectionLabel: (_objectName: string, _sectionName: string, fallback: string) => fallback, + actionLabel: (_objectName: string | undefined, _actionName: string, fallback: string) => fallback, }; /** diff --git a/packages/plugin-detail/src/renderers/record-alert.tsx b/packages/plugin-detail/src/renderers/record-alert.tsx index a2c9e1fd4..c42adec75 100644 --- a/packages/plugin-detail/src/renderers/record-alert.tsx +++ b/packages/plugin-detail/src/renderers/record-alert.tsx @@ -54,6 +54,7 @@ import { useActionEngine, } from '@object-ui/react'; import { Alert, AlertTitle, AlertDescription, Button, cn, LazyIcon } from '@object-ui/components'; +import { useObjectTranslation, pickLocalized } from '@object-ui/i18n'; import type { ActionDef } from '@object-ui/core'; type Severity = 'info' | 'warning' | 'error' | 'success'; @@ -118,6 +119,13 @@ export const RecordAlertRenderer: React.FC = ({ schema = {}, c const record = recordCtx?.data; const objectName = recordCtx?.objectName || ''; const recordId = (recordCtx?.recordId as any) ?? record?.id; + const { language } = useObjectTranslation(); + + // Authored copy may carry inline translations (`{ en, 'zh-CN', … }`) — + // resolve to the current language before rendering / keying. + const title = pickLocalized(props.title, language); + const body = pickLocalized(props.body, language); + const ctaLabel = pickLocalized(props.action?.label, language); const severity: Severity = (['info', 'warning', 'error', 'success'] as const).includes(props.severity) ? (props.severity as Severity) @@ -137,7 +145,9 @@ export const RecordAlertRenderer: React.FC = ({ schema = {}, c // dismissing on one record does not silence the same alert on another. const storageKey = React.useMemo(() => { if (!props.dismissible) return null; - const k = props.dismissKey || props.title || severity; + // Key by the language-independent resolution ('en'/default) so dismissing + // the alert in one locale keeps it dismissed after a language switch. + const k = props.dismissKey || pickLocalized(props.title, 'en') || severity; return `os.record-alert:${objectName}:${recordId ?? '_'}:${k}`; }, [props.dismissible, props.dismissKey, props.title, severity, objectName, recordId]); @@ -205,17 +215,17 @@ export const RecordAlertRenderer: React.FC = ({ schema = {}, c aria-live={severity === 'error' ? 'assertive' : 'polite'} >