From b7566263639e7dc5b607c5fe6fb402fbb057b6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Thu, 16 Jul 2026 04:02:12 -0700 Subject: [PATCH 1/2] feat(cli): extract action param i18n keys + fill platform-objects translations (#3030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os i18n extract` never walked `actions[].params`, so the `o.._actions..params.*` keys the console client already resolves were absent from generated bundles — action dialogs (Setup → Create User, Set Password, Ban…) rendered raw English under any locale. - collectExpectedEntries now emits inline-param label/helpText/placeholder/ options (field-backed params only when they carry a literal override), for object actions and top-level actions alike. - Regenerate the en/zh-CN/ja-JP/es-ES bundles in platform-objects and fill the new key groups; a second `--merge` pass is idempotent. --- .changeset/i18n-extract-action-params.md | 21 +++ packages/cli/src/utils/i18n-extract.ts | 54 ++++++ packages/cli/test/i18n-extract.test.ts | 53 +++++- .../en.metadata-forms.generated.ts | 16 ++ .../apps/translations/en.objects.generated.ts | 157 ++++++++++++++++-- .../es-ES.metadata-forms.generated.ts | 16 ++ .../translations/es-ES.objects.generated.ts | 157 ++++++++++++++++-- .../ja-JP.metadata-forms.generated.ts | 16 ++ .../translations/ja-JP.objects.generated.ts | 157 ++++++++++++++++-- .../zh-CN.metadata-forms.generated.ts | 16 ++ .../translations/zh-CN.objects.generated.ts | 157 ++++++++++++++++-- 11 files changed, 759 insertions(+), 61 deletions(-) create mode 100644 .changeset/i18n-extract-action-params.md diff --git a/.changeset/i18n-extract-action-params.md b/.changeset/i18n-extract-action-params.md new file mode 100644 index 0000000000..1497fab834 --- /dev/null +++ b/.changeset/i18n-extract-action-params.md @@ -0,0 +1,21 @@ +--- +'@objectstack/cli': minor +'@objectstack/platform-objects': patch +--- + +feat(cli): `os i18n extract` now emits action param keys (`o.._actions..params..*`) so action-dialog forms are translatable (#3030) + +The console client already resolves param labels, help text, placeholders and +option labels from `o.._actions..params.*`, but the extractor +never walked `actions[].params`, so those keys were absent from generated +bundles and dialogs like Setup → Create User rendered raw English under any +locale. The extractor now emits: + +- inline params → `label` / `helpText` / `placeholder` / `options.`; +- field-backed params (`{ field: '…' }`) → only when they carry a literal + override (field translations already cover them at runtime); +- both object actions and top-level (global) actions. + +`@objectstack/platform-objects` regenerates its en/zh-CN/ja-JP/es-ES bundles +with the new keys filled (user admin actions, sys_jwks fields, page variable +forms). Re-running extract with `--merge` stays idempotent. diff --git a/packages/cli/src/utils/i18n-extract.ts b/packages/cli/src/utils/i18n-extract.ts index 02c031a407..870457db48 100644 --- a/packages/cli/src/utils/i18n-extract.ts +++ b/packages/cli/src/utils/i18n-extract.ts @@ -25,7 +25,10 @@ * objects.._actions..label * objects.._actions..confirmText * objects.._actions..successMessage + * objects.._actions..params..label / .helpText / .placeholder + * objects.._actions..params..options. * globalActions..label / .confirmText / .successMessage + * globalActions..params..* (same shape as object actions) * apps..label / .description * apps..navigation..label * dashboards..label / .description @@ -147,6 +150,55 @@ function pushEntry( out.push({ path, sourceValue, source, ...extra }); } +/** + * Emit `params..{label,helpText,placeholder}` and + * `params..options.` entries under an action's translation root. + * Mirrors the client-side resolver convention (`actionParamText` / + * `actionParamOptionLabel` in @object-ui/i18n) and the `params` slot on + * `ObjectTranslationDataSchema._actions`. + * + * Field-backed params (`{ field: 'email' }`) inherit the referenced field's + * translated label/help at runtime, so a label entry is emitted only when the + * author overrode it with a literal string. Inline params (name-based) always + * emit a label — falling back to the param name, matching the dialog render. + * Localized-map labels (`{ en, 'zh-CN' }`) are already multilingual and are + * skipped. + */ +function pushActionParams( + out: ExpectedEntry[], + actionRoot: string[], + action: any, + kind: ExpectedEntry['source'], + objectName?: string, +): void { + if (!Array.isArray(action?.params)) return; + for (const param of action.params) { + if (!param || typeof param !== 'object') continue; + const pname = param.name ?? param.field; + if (typeof pname !== 'string' || pname.length === 0) continue; + const base = [...actionRoot, 'params', pname]; + const literalLabel = typeof param.label === 'string' ? param.label : undefined; + if (param.field) { + if (literalLabel) pushEntry(out, [...base, 'label'], literalLabel, kind, { objectName }); + } else { + pushEntry(out, [...base, 'label'], literalLabel ?? pname, kind, { objectName }); + } + if (typeof param.helpText === 'string' && param.helpText.length > 0) { + pushEntry(out, [...base, 'helpText'], param.helpText, kind, { objectName }); + } + if (typeof param.placeholder === 'string' && param.placeholder.length > 0) { + pushEntry(out, [...base, 'placeholder'], param.placeholder, kind, { objectName }); + } + if (Array.isArray(param.options)) { + for (const opt of param.options) { + if (opt && typeof opt === 'object' && 'value' in opt && typeof opt.label === 'string') { + pushEntry(out, [...base, 'options', String(opt.value)], opt.label, kind, { objectName }); + } + } + } + } +} + /** Collect every translatable entry from a normalized stack config. */ export function collectExpectedEntries(config: any): ExpectedEntry[] { const out: ExpectedEntry[] = []; @@ -233,6 +285,7 @@ export function collectExpectedEntries(config: any): ExpectedEntry[] { if (action.successMessage) { pushEntry(out, ['objects', objectName, '_actions', aname, 'successMessage'], action.successMessage, 'action', { objectName }); } + pushActionParams(out, ['objects', objectName, '_actions', aname], action, 'action', objectName); } } } @@ -266,6 +319,7 @@ export function collectExpectedEntries(config: any): ExpectedEntry[] { if (action.successMessage) { pushEntry(out, [...root, 'successMessage'], action.successMessage, kind, { objectName }); } + pushActionParams(out, root, action, kind, objectName); } // ── Apps + navigation ──────────────────────────────────────────── diff --git a/packages/cli/test/i18n-extract.test.ts b/packages/cli/test/i18n-extract.test.ts index 0dc28a5577..4b0c8380c3 100644 --- a/packages/cli/test/i18n-extract.test.ts +++ b/packages/cli/test/i18n-extract.test.ts @@ -33,6 +33,31 @@ const config: any = { active: { label: 'Active', name: 'active' }, all: { label: 'All' }, }, + actions: [ + { + name: 'set_password', + label: 'Set Password', + params: [ + { field: 'label' }, + { field: 'active', label: 'Enabled Override' }, + { + name: 'generatePassword', + label: 'Generate Temporary Password', + type: 'boolean', + helpText: 'Leave checked to auto-generate.', + }, + { + name: 'mode', + type: 'select', + placeholder: 'Pick a mode', + options: [ + { value: 'auto', label: 'Auto' }, + { value: 'manual', label: 'Manual' }, + ], + }, + ], + }, + ], }, ], actions: [ @@ -43,7 +68,12 @@ const config: any = { confirmText: 'Merge?', successMessage: 'Merged.', }, - { name: 'export_csv', label: 'Export CSV', successMessage: 'Done.' }, + { + name: 'export_csv', + label: 'Export CSV', + successMessage: 'Done.', + params: [{ name: 'delimiter', label: 'Delimiter' }], + }, ], translations: [ { @@ -92,6 +122,27 @@ describe('collectExpectedEntries', () => { expect(byPath['objects.sys_position._actions.merge.label']).toBe('Merge'); expect(byPath['metadataForms.flow.fields.name.label']).toBe('Name'); }); + + it('emits action param entries (inline + top-level), skipping field-backed labels without overrides', () => { + const entries = collectExpectedEntries(config); + const byPath = Object.fromEntries(entries.map((e) => [e.path.join('.'), e.sourceValue])); + const base = 'objects.sys_position._actions.set_password.params'; + + // Field-backed param with no literal override → no label entry (field + // translations cover it at runtime). + expect(byPath[`${base}.label.label`]).toBeUndefined(); + // Field-backed param WITH a literal override → entry under the field name. + expect(byPath[`${base}.active.label`]).toBe('Enabled Override'); + // Inline params emit label / helpText / placeholder / options. + expect(byPath[`${base}.generatePassword.label`]).toBe('Generate Temporary Password'); + expect(byPath[`${base}.generatePassword.helpText`]).toBe('Leave checked to auto-generate.'); + expect(byPath[`${base}.mode.label`]).toBe('mode'); // no label → falls back to name + expect(byPath[`${base}.mode.placeholder`]).toBe('Pick a mode'); + expect(byPath[`${base}.mode.options.auto`]).toBe('Auto'); + expect(byPath[`${base}.mode.options.manual`]).toBe('Manual'); + // Top-level (global) actions get the same treatment. + expect(byPath['globalActions.export_csv.params.delimiter.label']).toBe('Delimiter'); + }); }); describe('extractTranslations', () => { diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index 787798368e..7bc5e8fa2f 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -793,6 +793,22 @@ export const enMetadataForms: NonNullable = { label: "Variables", helpText: "Local page state variables" }, + "variables.name": { + label: "Name", + helpText: "Variable name — exposed to expressions as `page.`" + }, + "variables.type": { + label: "Type", + helpText: "Value type" + }, + "variables.defaultValue": { + label: "Default Value", + helpText: "Initial value (defaults to a type-appropriate empty value)" + }, + "variables.source": { + label: "Source", + helpText: "Component (by id) that writes this variable — e.g. an element:record_picker" + }, regions: { label: "Regions", helpText: "Layout regions (header, main, sidebar, footer) with components" diff --git a/packages/platform-objects/src/apps/translations/en.objects.generated.ts b/packages/platform-objects/src/apps/translations/en.objects.generated.ts index c24594fbad..75e19805f8 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -136,7 +136,12 @@ export const enObjects: NonNullable = { ban_user: { label: "Ban User", confirmText: "Ban this user? They will be signed out and unable to sign in until unbanned.", - successMessage: "User banned" + successMessage: "User banned", + params: { + banReason: { + label: "Ban Reason" + } + } }, unban_user: { label: "Unban User", @@ -148,15 +153,46 @@ export const enObjects: NonNullable = { }, create_user: { label: "Create User", - successMessage: "User created" + successMessage: "User created", + params: { + phoneNumber: { + label: "Phone Number", + helpText: "Sign-in phone number (E.164, e.g. +8613800000000). Required when no email is given." + }, + generatePassword: { + label: "Generate Temporary Password" + }, + password: { + label: "Password (leave empty to generate)" + }, + mustChangePassword: { + label: "Require Password Change On First Login" + } + } }, set_user_password: { label: "Set Password", - successMessage: "Password updated" + successMessage: "Password updated", + params: { + generatePassword: { + label: "Generate Temporary Password" + }, + newPassword: { + label: "New Password (leave empty to generate)" + }, + mustChangePassword: { + label: "Require Password Change On Next Login" + } + } }, set_user_role: { label: "Set Platform Role", - successMessage: "Role updated" + successMessage: "Role updated", + params: { + role: { + label: "Platform Role" + } + } }, impersonate_user: { label: "Impersonate User", @@ -169,11 +205,27 @@ export const enObjects: NonNullable = { }, change_my_password: { label: "Change Password", - successMessage: "Password changed" + successMessage: "Password changed", + params: { + currentPassword: { + label: "Current Password" + }, + newPassword: { + label: "New Password" + }, + revokeOtherSessions: { + label: "Sign out other devices" + } + } }, change_my_email: { label: "Change Email", - successMessage: "Verification email sent — check the new address to confirm." + successMessage: "Verification email sent — check the new address to confirm.", + params: { + newEmail: { + label: "New Email" + } + } }, resend_verification_email: { label: "Resend Verification Email", @@ -182,21 +234,41 @@ export const enObjects: NonNullable = { delete_my_account: { label: "Delete My Account", confirmText: "Permanently delete your account? This cannot be undone — all your sessions will be terminated and all data you own will be removed per the configured retention policy.", - successMessage: "Account deleted" + successMessage: "Account deleted", + params: { + password: { + label: "Current Password" + } + } }, 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." + 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.", + params: { + password: { + label: "Current Password" + } + } }, 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." + successMessage: "Two-factor authentication disabled.", + params: { + password: { + label: "Current Password" + } + } }, 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." + successMessage: "New backup codes generated — save them somewhere safe.", + params: { + password: { + label: "Current Password" + } + } } } }, @@ -340,7 +412,21 @@ export const enObjects: NonNullable = { }, _actions: { link_social: { - label: "Link Social Account" + label: "Link Social Account", + params: { + provider: { + label: "Provider", + options: { + google: "Google", + github: "GitHub", + microsoft: "Microsoft", + apple: "Apple", + facebook: "Facebook", + gitlab: "GitLab", + discord: "Discord" + } + } + } }, unlink_account: { label: "Unlink Account", @@ -897,16 +983,31 @@ export const enObjects: NonNullable = { }, _actions: { enable_two_factor: { - label: "Enable 2FA" + label: "Enable 2FA", + params: { + password: { + label: "Current Password" + } + } }, disable_two_factor: { label: "Disable 2FA", confirmText: "Disable two-factor authentication on your account?", - successMessage: "2FA disabled" + successMessage: "2FA disabled", + params: { + password: { + label: "Current Password" + } + } }, regenerate_backup_codes: { label: "Regenerate Backup Codes", - confirmText: "Regenerate backup codes? All previous backup codes will stop working immediately." + confirmText: "Regenerate backup codes? All previous backup codes will stop working immediately.", + params: { + password: { + label: "Current Password" + } + } } } }, @@ -1148,7 +1249,25 @@ export const enObjects: NonNullable = { successMessage: "OAuth application enabled" }, create_oauth_application: { - label: "Register OAuth Application" + label: "Register OAuth Application", + params: { + name: { + label: "Application Name" + }, + redirectURLs: { + label: "Redirect URLs", + helpText: "One URL per line. Must use https:// in production." + }, + type: { + label: "Application Type", + options: { + web: "Web", + native: "Native", + "user-agent-based": "User-agent based", + public: "Public" + } + } + } }, rotate_client_secret: { label: "Rotate Client Secret", @@ -1302,6 +1421,14 @@ export const enObjects: NonNullable = { label: "Private Key", help: "JSON-serialized JWK private key (encrypted at rest)" }, + alg: { + label: "Algorithm", + help: "JWK signing algorithm, e.g. `EdDSA` (better-auth 1.7+)" + }, + crv: { + label: "Curve", + help: "JWK curve for EdDSA/EC keys, e.g. `Ed25519` (better-auth 1.7+)" + }, created_at: { label: "Created At" }, diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index 5fba5af574..b6b8a995c4 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -793,6 +793,22 @@ export const esESMetadataForms: NonNullable = label: "Variables de página", helpText: "Variables de estado local de página" }, + "variables.name": { + label: "Nombre", + helpText: "Nombre de la variable — expuesta en expresiones como `page.`" + }, + "variables.type": { + label: "Tipo", + helpText: "Tipo de valor" + }, + "variables.defaultValue": { + label: "Valor predeterminado", + helpText: "Valor inicial (por defecto, un valor vacío según el tipo)" + }, + "variables.source": { + label: "Origen", + helpText: "Componente (por id) que escribe esta variable — p. ej. un element:record_picker" + }, regions: { label: "Regiones", helpText: "Regiones de diseño (header, main, sidebar, footer) con componentes" 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 704198f948..4a16e3b4a6 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 @@ -136,7 +136,12 @@ export const esESObjects: NonNullable = { ban_user: { label: "Bloquear usuario", confirmText: "¿Bloquear a este usuario? Cerrará sesión y no podrá volver a iniciarla hasta que se desbloquee.", - successMessage: "Usuario bloqueado" + successMessage: "Usuario bloqueado", + params: { + banReason: { + label: "Motivo del bloqueo" + } + } }, unban_user: { label: "Desbloquear usuario", @@ -148,15 +153,46 @@ export const esESObjects: NonNullable = { }, create_user: { label: "Crear usuario", - successMessage: "Usuario creado" + successMessage: "Usuario creado", + params: { + phoneNumber: { + label: "Número de teléfono", + helpText: "Número de teléfono para iniciar sesión (E.164, p. ej. +8613800000000). Obligatorio si no se indica un correo." + }, + generatePassword: { + label: "Generar contraseña temporal" + }, + password: { + label: "Contraseña (dejar en blanco para generarla)" + }, + mustChangePassword: { + label: "Exigir cambio de contraseña en el primer inicio de sesión" + } + } }, set_user_password: { label: "Establecer contraseña", - successMessage: "Contraseña actualizada" + successMessage: "Contraseña actualizada", + params: { + generatePassword: { + label: "Generar contraseña temporal" + }, + newPassword: { + label: "Nueva contraseña (dejar en blanco para generarla)" + }, + mustChangePassword: { + label: "Exigir cambio de contraseña en el próximo inicio de sesión" + } + } }, set_user_role: { label: "Establecer rol de plataforma", - successMessage: "Rol actualizado" + successMessage: "Rol actualizado", + params: { + role: { + label: "Rol de plataforma" + } + } }, impersonate_user: { label: "Suplantar usuario", @@ -169,11 +205,27 @@ export const esESObjects: NonNullable = { }, change_my_password: { label: "Cambiar contraseña", - successMessage: "Contraseña cambiada" + successMessage: "Contraseña cambiada", + params: { + currentPassword: { + label: "Contraseña actual" + }, + newPassword: { + label: "Nueva contraseña" + }, + revokeOtherSessions: { + label: "Cerrar sesión en los demás dispositivos" + } + } }, change_my_email: { label: "Cambiar correo", - successMessage: "Correo de verificación enviado; revisa la nueva dirección para confirmar." + successMessage: "Correo de verificación enviado; revisa la nueva dirección para confirmar.", + params: { + newEmail: { + label: "Nuevo correo" + } + } }, resend_verification_email: { label: "Reenviar correo de verificación", @@ -182,21 +234,41 @@ export const esESObjects: NonNullable = { delete_my_account: { label: "Eliminar mi cuenta", 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.", - successMessage: "Cuenta eliminada" + successMessage: "Cuenta eliminada", + params: { + password: { + label: "Contraseña actual" + } + } }, enable_two_factor: { 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." + 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.", + params: { + password: { + label: "Contraseña actual" + } + } }, disable_two_factor: { 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." + successMessage: "Autenticación de dos factores deshabilitada.", + params: { + password: { + label: "Contraseña actual" + } + } }, generate_backup_codes: { 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." + successMessage: "Nuevos códigos de respaldo generados; guárdalos en un lugar seguro.", + params: { + password: { + label: "Contraseña actual" + } + } } } }, @@ -340,7 +412,21 @@ export const esESObjects: NonNullable = { }, _actions: { link_social: { - label: "Vincular cuenta social" + label: "Vincular cuenta social", + params: { + provider: { + label: "Proveedor", + options: { + google: "Google", + github: "GitHub", + microsoft: "Microsoft", + apple: "Apple", + facebook: "Facebook", + gitlab: "GitLab", + discord: "Discord" + } + } + } }, unlink_account: { label: "Desvincular cuenta", @@ -897,16 +983,31 @@ export const esESObjects: NonNullable = { }, _actions: { enable_two_factor: { - label: "Habilitar 2FA" + label: "Habilitar 2FA", + params: { + password: { + label: "Contraseña actual" + } + } }, disable_two_factor: { label: "Deshabilitar 2FA", confirmText: "¿Deshabilitar la autenticación de doble factor en su cuenta?", - successMessage: "2FA deshabilitado" + successMessage: "2FA deshabilitado", + params: { + password: { + label: "Contraseña actual" + } + } }, regenerate_backup_codes: { label: "Regenerar códigos de respaldo", - confirmText: "¿Regenerar los códigos de respaldo? Todos los códigos de respaldo anteriores dejarán de funcionar de inmediato." + confirmText: "¿Regenerar los códigos de respaldo? Todos los códigos de respaldo anteriores dejarán de funcionar de inmediato.", + params: { + password: { + label: "Contraseña actual" + } + } } } }, @@ -1148,7 +1249,25 @@ export const esESObjects: NonNullable = { successMessage: "Aplicación OAuth habilitada" }, create_oauth_application: { - label: "Registrar aplicación OAuth" + label: "Registrar aplicación OAuth", + params: { + name: { + label: "Nombre de la aplicación" + }, + redirectURLs: { + label: "URLs de redirección", + helpText: "Una URL por línea. Debe usar https:// en producción." + }, + type: { + label: "Tipo de aplicación", + options: { + web: "Web", + native: "Nativa", + "user-agent-based": "Basada en user-agent", + public: "Pública" + } + } + } }, rotate_client_secret: { label: "Rotar Client Secret", @@ -1302,6 +1421,14 @@ export const esESObjects: NonNullable = { label: "Clave privada", help: "Clave privada JWK serializada en JSON (cifrada en reposo)." }, + alg: { + label: "Algoritmo", + help: "Algoritmo de firma JWK, p. ej. `EdDSA` (better-auth 1.7+)" + }, + crv: { + label: "Curva", + help: "Curva JWK para claves EdDSA/EC, p. ej. `Ed25519` (better-auth 1.7+)" + }, created_at: { label: "Creado el" }, diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index 15e7c5a399..24e10a242f 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -793,6 +793,22 @@ export const jaJPMetadataForms: NonNullable = label: "変数", helpText: "ページローカル状態変数" }, + "variables.name": { + label: "名前", + helpText: "変数名 — 式では `page.` として参照できます" + }, + "variables.type": { + label: "型", + helpText: "値の型" + }, + "variables.defaultValue": { + label: "デフォルト値", + helpText: "初期値(未指定の場合は型に応じた空の値)" + }, + "variables.source": { + label: "ソース", + helpText: "この変数に書き込むコンポーネント(id 指定)— 例: element:record_picker" + }, regions: { label: "リージョン", helpText: "コンポーネントを含むレイアウト領域(header, main, sidebar, footer)" 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 f2598f3117..f805f902c4 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 @@ -136,7 +136,12 @@ export const jaJPObjects: NonNullable = { ban_user: { label: "利用停止", confirmText: "このユーザーを利用停止にしますか?利用停止になるとサインアウトされ、解除されるまでサインインできなくなります。", - successMessage: "ユーザーを利用停止にしました" + successMessage: "ユーザーを利用停止にしました", + params: { + banReason: { + label: "利用停止の理由" + } + } }, unban_user: { label: "利用停止を解除", @@ -148,15 +153,46 @@ export const jaJPObjects: NonNullable = { }, create_user: { label: "ユーザーを作成", - successMessage: "ユーザーを作成しました" + successMessage: "ユーザーを作成しました", + params: { + phoneNumber: { + label: "電話番号", + helpText: "サインイン用の電話番号(E.164 形式、例: +8613800000000)。メールアドレスを指定しない場合は必須です。" + }, + generatePassword: { + label: "一時パスワードを生成" + }, + password: { + label: "パスワード(空欄の場合は自動生成)" + }, + mustChangePassword: { + label: "初回サインイン時にパスワード変更を必須にする" + } + } }, set_user_password: { label: "パスワードを設定", - successMessage: "パスワードを更新しました" + successMessage: "パスワードを更新しました", + params: { + generatePassword: { + label: "一時パスワードを生成" + }, + newPassword: { + label: "新しいパスワード(空欄の場合は自動生成)" + }, + mustChangePassword: { + label: "次回サインイン時にパスワード変更を必須にする" + } + } }, set_user_role: { label: "プラットフォームロールを設定", - successMessage: "ロールを更新しました" + successMessage: "ロールを更新しました", + params: { + role: { + label: "プラットフォームロール" + } + } }, impersonate_user: { label: "代理ログイン", @@ -169,11 +205,27 @@ export const jaJPObjects: NonNullable = { }, change_my_password: { label: "パスワード変更", - successMessage: "パスワードを変更しました" + successMessage: "パスワードを変更しました", + params: { + currentPassword: { + label: "現在のパスワード" + }, + newPassword: { + label: "新しいパスワード" + }, + revokeOtherSessions: { + label: "他のデバイスからサインアウト" + } + } }, change_my_email: { label: "メールアドレス変更", - successMessage: "確認メールを送信しました。新しいメールアドレスで確認してください。" + successMessage: "確認メールを送信しました。新しいメールアドレスで確認してください。", + params: { + newEmail: { + label: "新しいメールアドレス" + } + } }, resend_verification_email: { label: "確認メールを再送", @@ -182,21 +234,41 @@ export const jaJPObjects: NonNullable = { delete_my_account: { label: "アカウント削除", confirmText: "アカウントを完全に削除しますか?この操作は元に戻せません。すべてのセッションが終了され、設定された保持ポリシーに従って所有するすべてのデータが削除されます。", - successMessage: "アカウントを削除しました" + successMessage: "アカウントを削除しました", + params: { + password: { + label: "現在のパスワード" + } + } }, enable_two_factor: { label: "二要素認証を有効化", - successMessage: "二要素認証を有効にしました。認証アプリで QR コードをスキャンするか otpauth URI を貼り付け、コードを検証して設定を完了してください。" + successMessage: "二要素認証を有効にしました。認証アプリで QR コードをスキャンするか otpauth URI を貼り付け、コードを検証して設定を完了してください。", + params: { + password: { + label: "現在のパスワード" + } + } }, disable_two_factor: { label: "二要素認証を無効化", confirmText: "二要素認証をオフにしますか?アカウントの安全性が低下します。", - successMessage: "二要素認証を無効にしました。" + successMessage: "二要素認証を無効にしました。", + params: { + password: { + label: "現在のパスワード" + } + } }, generate_backup_codes: { label: "バックアップコードを再生成", confirmText: "新しいバックアップコードを生成しますか?以前に生成されたコードはすべて使用できなくなります。", - successMessage: "新しいバックアップコードを生成しました。安全な場所に保管してください。" + successMessage: "新しいバックアップコードを生成しました。安全な場所に保管してください。", + params: { + password: { + label: "現在のパスワード" + } + } } } }, @@ -340,7 +412,21 @@ export const jaJPObjects: NonNullable = { }, _actions: { link_social: { - label: "ソーシャルアカウント連携" + label: "ソーシャルアカウント連携", + params: { + provider: { + label: "プロバイダー", + options: { + google: "Google", + github: "GitHub", + microsoft: "Microsoft", + apple: "Apple", + facebook: "Facebook", + gitlab: "GitLab", + discord: "Discord" + } + } + } }, unlink_account: { label: "連携解除", @@ -897,16 +983,31 @@ export const jaJPObjects: NonNullable = { }, _actions: { enable_two_factor: { - label: "2FA を有効化" + label: "2FA を有効化", + params: { + password: { + label: "現在のパスワード" + } + } }, disable_two_factor: { label: "2FA を無効化", confirmText: "アカウントの二要素認証を無効化しますか?", - successMessage: "2FA を無効化しました" + successMessage: "2FA を無効化しました", + params: { + password: { + label: "現在のパスワード" + } + } }, regenerate_backup_codes: { label: "バックアップコード再生成", - confirmText: "バックアップコードを再生成しますか?以前のバックアップコードはすべて直ちに使用できなくなります。" + confirmText: "バックアップコードを再生成しますか?以前のバックアップコードはすべて直ちに使用できなくなります。", + params: { + password: { + label: "現在のパスワード" + } + } } } }, @@ -1148,7 +1249,25 @@ export const jaJPObjects: NonNullable = { successMessage: "OAuthアプリケーションを有効化しました" }, create_oauth_application: { - label: "OAuthアプリケーションを登録" + label: "OAuthアプリケーションを登録", + params: { + name: { + label: "アプリケーション名" + }, + redirectURLs: { + label: "リダイレクト URL", + helpText: "1 行につき 1 つの URL。本番環境では https:// が必須です。" + }, + type: { + label: "アプリケーションタイプ", + options: { + web: "Web", + native: "ネイティブ", + "user-agent-based": "ユーザーエージェント型", + public: "パブリック" + } + } + } }, rotate_client_secret: { label: "クライアントシークレット更新", @@ -1302,6 +1421,14 @@ export const jaJPObjects: NonNullable = { label: "秘密鍵", help: "JSON シリアライズされた JWK 秘密鍵(保存時に暗号化済み)" }, + alg: { + label: "アルゴリズム", + help: "JWK 署名アルゴリズム(例: `EdDSA`、better-auth 1.7+)" + }, + crv: { + label: "曲線", + help: "EdDSA/EC 鍵で使う JWK 曲線(例: `Ed25519`、better-auth 1.7+)" + }, created_at: { label: "作成日時" }, diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index 260a791ae0..58860d8449 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -793,6 +793,22 @@ export const zhCNMetadataForms: NonNullable = label: "变量", helpText: "页面本地状态变量" }, + "variables.name": { + label: "名称", + helpText: "变量名——在表达式中以 `page.` 访问" + }, + "variables.type": { + label: "类型", + helpText: "值类型" + }, + "variables.defaultValue": { + label: "默认值", + helpText: "初始值(默认为对应类型的空值)" + }, + "variables.source": { + label: "来源", + helpText: "写入该变量的组件(按 id 指定),如 element:record_picker" + }, regions: { label: "区域", helpText: "布局区域(header、main、sidebar、footer)及其组件" 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 60dff24e01..67764e9e16 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 @@ -136,7 +136,12 @@ export const zhCNObjects: NonNullable = { ban_user: { label: "封禁用户", confirmText: "要封禁该用户吗?封禁后会立即登出,且在解除封禁前无法再次登录。", - successMessage: "用户已封禁" + successMessage: "用户已封禁", + params: { + banReason: { + label: "封禁原因" + } + } }, unban_user: { label: "解除封禁", @@ -148,15 +153,46 @@ export const zhCNObjects: NonNullable = { }, create_user: { label: "创建用户", - successMessage: "用户已创建" + successMessage: "用户已创建", + params: { + phoneNumber: { + label: "手机号", + helpText: "登录手机号(E.164 格式,如 +8613800000000)。未填写邮箱时必填。" + }, + generatePassword: { + label: "生成临时密码" + }, + password: { + label: "密码(留空则自动生成)" + }, + mustChangePassword: { + label: "首次登录时必须修改密码" + } + } }, set_user_password: { label: "设置密码", - successMessage: "密码已更新" + successMessage: "密码已更新", + params: { + generatePassword: { + label: "生成临时密码" + }, + newPassword: { + label: "新密码(留空则自动生成)" + }, + mustChangePassword: { + label: "下次登录时必须修改密码" + } + } }, set_user_role: { label: "设置平台角色", - successMessage: "角色已更新" + successMessage: "角色已更新", + params: { + role: { + label: "平台角色" + } + } }, impersonate_user: { label: "模拟用户", @@ -169,11 +205,27 @@ export const zhCNObjects: NonNullable = { }, change_my_password: { label: "修改密码", - successMessage: "已修改密码" + successMessage: "已修改密码", + params: { + currentPassword: { + label: "当前密码" + }, + newPassword: { + label: "新密码" + }, + revokeOtherSessions: { + label: "登出其他设备" + } + } }, change_my_email: { label: "修改邮箱", - successMessage: "已发送验证邮件,请前往新邮箱确认。" + successMessage: "已发送验证邮件,请前往新邮箱确认。", + params: { + newEmail: { + label: "新邮箱" + } + } }, resend_verification_email: { label: "重发验证邮件", @@ -182,21 +234,41 @@ export const zhCNObjects: NonNullable = { delete_my_account: { label: "删除我的账号", confirmText: "确定要永久删除您的账户吗?此操作无法撤销——您的所有会话都将被终止,并将按照配置的保留策略移除您拥有的所有数据。", - successMessage: "已删除账号" + successMessage: "已删除账号", + params: { + password: { + label: "当前密码" + } + } }, enable_two_factor: { label: "启用双因素认证", - successMessage: "双因素认证已启用。用身份验证器 App 扫描二维码或粘贴 otpauth URI,然后验证一次动态码以完成设置。" + successMessage: "双因素认证已启用。用身份验证器 App 扫描二维码或粘贴 otpauth URI,然后验证一次动态码以完成设置。", + params: { + password: { + label: "当前密码" + } + } }, disable_two_factor: { label: "停用双因素认证", confirmText: "要关闭双因素认证吗?您的账户安全性将降低。", - successMessage: "双因素认证已停用。" + successMessage: "双因素认证已停用。", + params: { + password: { + label: "当前密码" + } + } }, generate_backup_codes: { label: "重新生成备用码", confirmText: "要生成一组新的备用码吗?之前生成的备用码将全部失效。", - successMessage: "新备用码已生成——请妥善保存。" + successMessage: "新备用码已生成——请妥善保存。", + params: { + password: { + label: "当前密码" + } + } } } }, @@ -340,7 +412,21 @@ export const zhCNObjects: NonNullable = { }, _actions: { link_social: { - label: "关联社交账号" + label: "关联社交账号", + params: { + provider: { + label: "服务提供商", + options: { + google: "Google", + github: "GitHub", + microsoft: "Microsoft", + apple: "Apple", + facebook: "Facebook", + gitlab: "GitLab", + discord: "Discord" + } + } + } }, unlink_account: { label: "解除关联", @@ -897,16 +983,31 @@ export const zhCNObjects: NonNullable = { }, _actions: { enable_two_factor: { - label: "启用 2FA" + label: "启用 2FA", + params: { + password: { + label: "当前密码" + } + } }, disable_two_factor: { label: "停用 2FA", confirmText: "要停用你账号上的双因素认证吗?", - successMessage: "2FA 已停用" + successMessage: "2FA 已停用", + params: { + password: { + label: "当前密码" + } + } }, regenerate_backup_codes: { label: "重新生成备用码", - confirmText: "确定要重新生成备份码吗?此前的所有备份码将立即失效。" + confirmText: "确定要重新生成备份码吗?此前的所有备份码将立即失效。", + params: { + password: { + label: "当前密码" + } + } } } }, @@ -1148,7 +1249,25 @@ export const zhCNObjects: NonNullable = { successMessage: "OAuth 应用已启用" }, create_oauth_application: { - label: "注册 OAuth 应用" + label: "注册 OAuth 应用", + params: { + name: { + label: "应用名称" + }, + redirectURLs: { + label: "回调 URL", + helpText: "每行一个 URL。生产环境必须使用 https://。" + }, + type: { + label: "应用类型", + options: { + web: "Web", + native: "原生应用", + "user-agent-based": "基于 User-Agent", + public: "公共客户端" + } + } + } }, rotate_client_secret: { label: "轮换 Client Secret", @@ -1302,6 +1421,14 @@ export const zhCNObjects: NonNullable = { label: "私钥", help: "JWK 私钥的 JSON 序列化内容(静态存储时加密)" }, + alg: { + label: "算法", + help: "JWK 签名算法,如 `EdDSA`(better-auth 1.7+)" + }, + crv: { + label: "曲线", + help: "EdDSA/EC 密钥使用的 JWK 曲线,如 `Ed25519`(better-auth 1.7+)" + }, created_at: { label: "创建时间" }, From b27c5e134a047a4efe70dd07bab694713dcf7bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Thu, 16 Jul 2026 04:02:23 -0700 Subject: [PATCH 2/2] fix(auth): explicit password overrides generatePassword on create-user/set-password (#3031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console Create User dialog defaults `generatePassword: true` and labels the password input "leave empty to generate" — so typing a password sent both and the endpoint 400'd ("Provide either password or generatePassword, not both"), making the most natural flow always fail. The param `visible` CEL can't see other params' live values, so the dialog can't uncheck the box client-side. resolvePassword now treats an explicit non-empty password as an unambiguous override: use it (complexity-checked, no temporaryPassword in the response); generate only when the password is empty and generatePassword is true. Applies to create-user and set-password alike. --- .../src/admin-user-endpoints.test.ts | 18 ++++++++++++++++-- .../plugin-auth/src/admin-user-endpoints.ts | 9 +++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/plugins/plugin-auth/src/admin-user-endpoints.test.ts b/packages/plugins/plugin-auth/src/admin-user-endpoints.test.ts index f836b80959..ead4d919f1 100644 --- a/packages/plugins/plugin-auth/src/admin-user-endpoints.test.ts +++ b/packages/plugins/plugin-auth/src/admin-user-endpoints.test.ts @@ -129,14 +129,28 @@ describe('runAdminCreateUser', () => { expect(m.createUser).not.toHaveBeenCalled(); }); - it('rejects when both password and generatePassword are provided', async () => { + it('prefers an explicit password over generatePassword (console dialog sends both)', async () => { const m = makeDeps(); const res = await runAdminCreateUser( m.deps, makeRequest({ email: 'a@b.co', password: 'Explicit1!', generatePassword: true }), ACTOR, ); - expect(res.status).toBe(400); + expect(res.status).toBe(200); + expect(m.createUser.mock.calls[0][0].body.password).toBe('Explicit1!'); + // Explicit password → nothing to hand back once; no temporaryPassword leak. + expect((res.body.data as any).temporaryPassword).toBeUndefined(); + }); + + it('still generates when generatePassword is true and the password is empty', async () => { + const m = makeDeps(); + const res = await runAdminCreateUser( + m.deps, + makeRequest({ email: 'a@b.co', password: '', generatePassword: true }), + ACTOR, + ); + expect(res.status).toBe(200); + expect(typeof (res.body.data as any).temporaryPassword).toBe('string'); }); it('creates a user via better-auth, stamps must_change_password, audits, and returns the temp password once', async () => { diff --git a/packages/plugins/plugin-auth/src/admin-user-endpoints.ts b/packages/plugins/plugin-auth/src/admin-user-endpoints.ts index 419dae77b5..cd32589b47 100644 --- a/packages/plugins/plugin-auth/src/admin-user-endpoints.ts +++ b/packages/plugins/plugin-auth/src/admin-user-endpoints.ts @@ -197,6 +197,10 @@ export function isLikelyEmail(value: string): boolean { /** * Resolve the password for a create/set request: an explicit `password` / * `newPassword`, or a generated temporary one when `generatePassword` is true. + * An explicit non-empty password always wins over `generatePassword` — the + * console action dialog defaults `generatePassword: true` and labels the + * password input "leave empty to generate", so typing a password is an + * unambiguous override, not a conflict. * Returns an EndpointResult on validation failure. */ async function resolvePassword( @@ -206,11 +210,8 @@ async function resolvePassword( ): Promise<{ password: string; generated: boolean } | EndpointResult> { const explicit = body[explicitKey]; const generate = body.generatePassword === true; - if (generate && explicit !== undefined) { - return badRequest(`Provide either ${explicitKey} or generatePassword, not both`); - } - if (generate) return { password: generateTemporaryPassword(), generated: true }; if (typeof explicit !== 'string' || explicit.length === 0) { + if (generate) return { password: generateTemporaryPassword(), generated: true }; return badRequest(`${explicitKey} is required (or set generatePassword: true)`); } try {