diff --git a/packages/services/service-settings/src/translations/es-ES.ts b/packages/services/service-settings/src/translations/es-ES.ts index 2f170cd5a2..2868bf4b14 100644 --- a/packages/services/service-settings/src/translations/es-ES.ts +++ b/packages/services/service-settings/src/translations/es-ES.ts @@ -76,6 +76,26 @@ export const esES: TranslationData = { title: 'Correo y contraseña', description: 'Controla el inicio de sesión local con correo/contraseña y el registro de autoservicio.', }, + password_policy: { + title: 'Política de contraseñas', + description: 'Límites de longitud que el proveedor de autenticación exige en el registro y el restablecimiento de contraseña.', + }, + anti_abuse: { + title: 'Antiabuso', + description: 'Protección contra fuerza bruta: bloqueo de cuenta por identidad y límite de tasa por IP en los endpoints de autenticación.', + }, + multi_factor: { + title: 'Multifactor', + description: 'Exige a los miembros proteger su cuenta con una aplicación de autenticación (TOTP).', + }, + sessions: { + title: 'Sesiones', + description: 'Cuánto tiempo permanece válida una sesión iniciada.', + }, + network: { + title: 'Red', + description: 'Restringe desde dónde pueden autenticarse los usuarios.', + }, social: { title: 'Inicio de sesión social', description: @@ -86,6 +106,70 @@ export const esES: TranslationData = { email_password_enabled: { label: 'Habilitar inicio de sesión con correo/contraseña' }, signup_enabled: { label: 'Permitir registro de autoservicio' }, require_email_verification: { label: 'Requerir verificación de correo' }, + password_min_length: { label: 'Longitud mínima de contraseña' }, + password_max_length: { label: 'Longitud máxima de contraseña', help: 'Un límite superior protege frente a la denegación de servicio por el hasheo de contraseñas muy largas.' }, + password_reject_breached: { + label: 'Rechazar contraseñas filtradas', + help: 'Bloquea contraseñas presentes en corpus públicos de filtraciones mediante Have I Been Pwned (comprobación de rango con k-anonimato; la contraseña nunca se envía completa).', + }, + password_require_complexity: { + label: 'Requerir contraseñas complejas', + help: 'Exige que las contraseñas combinen clases de caracteres (mayúsculas, minúsculas, dígitos, símbolos) en el registro y al cambiar/restablecer la contraseña.', + }, + password_min_classes: { + label: 'Clases de caracteres mínimas', + help: 'Cuántas de las cuatro clases (mayúscula / minúscula / dígito / símbolo) debe incluir una contraseña.', + }, + password_history_count: { + label: 'Historial de contraseñas (sin reutilización)', + help: 'Impide reutilizar este número de contraseñas anteriores al cambiar/restablecer. 0 desactiva la comprobación.', + }, + password_expiry_days: { + label: 'Caducidad de contraseña (días)', + help: 'Fuerza un cambio de contraseña tras este número de días. 0 desactiva la caducidad. Mientras está caducada, el usuario no puede acceder a los datos hasta que la cambie.', + }, + lockout_threshold: { + label: 'Umbral de bloqueo de cuenta', + help: 'Bloquea una cuenta tras este número de inicios de sesión fallidos consecutivos. 0 desactiva el bloqueo. Mientras está bloqueada, el inicio de sesión se rechaza incluso con la contraseña correcta.', + }, + lockout_duration_minutes: { + label: 'Duración del bloqueo (minutos)', + help: 'Cuánto permanece bloqueada una cuenta una vez superado el umbral.', + }, + rate_limit_max: { + label: 'Límite de tasa de autenticación: máx. solicitudes', + help: 'Máximo de solicitudes por IP y ventana a los endpoints de inicio de sesión / registro / restablecimiento de contraseña.', + }, + rate_limit_window_seconds: { + label: 'Límite de tasa de autenticación: ventana (segundos)', + help: 'Ventana deslizante durante la cual se cuenta el tope de solicitudes anterior.', + }, + mfa_required: { + label: 'Requerir autenticación multifactor', + help: 'Los usuarios sin un autenticador registrado quedan bloqueados del acceso a los datos al terminar su periodo de gracia. Activarlo también habilita la función de dos factores para que los usuarios puedan registrarse.', + }, + mfa_grace_period_days: { + label: 'Periodo de gracia de MFA (días)', + help: 'Cuánto pueden aplazar el registro los usuarios antes del bloqueo definitivo. 0 bloquea de inmediato.', + }, + session_expiry_days: { label: 'Duración de la sesión (días)', help: 'Una sesión caduca este número de días después del inicio de sesión.' }, + session_refresh_days: { label: 'Umbral de renovación (días)', help: 'Una sesión activa se extiende cuando es más antigua que esto.' }, + session_idle_timeout_minutes: { + label: 'Tiempo de inactividad (minutos)', + help: 'Cierra la sesión del usuario tras este número de minutos de inactividad. 0 desactiva.', + }, + session_absolute_max_hours: { + label: 'Duración absoluta de la sesión (horas)', + help: 'Fuerza una reautenticación este número de horas después del inicio de sesión, independientemente de la actividad. 0 desactiva.', + }, + max_concurrent_sessions_per_user: { + label: 'Máx. sesiones simultáneas por usuario', + help: 'Limita las sesiones iniciadas simultáneas por usuario; las más antiguas se cierran al superar el límite. 0 = ilimitado.', + }, + allowed_ip_ranges: { + label: 'Rangos de IP permitidos', + help: 'Rangos CIDR o IP exactas (una por línea o separadas por comas), p. ej. 203.0.113.0/24. Cuando se define, se rechaza el inicio de sesión desde fuera de estos rangos. Vacío = sin restricción. Requiere un proxy de confianza que defina X-Forwarded-For.', + }, google_enabled: { label: 'Habilitar inicio de sesión con Google', help: 'Requiere un ID de cliente y un secreto de OAuth de Google desde Google Cloud Console.', @@ -101,6 +185,83 @@ export const esES: TranslationData = { }, }, + sms: { + title: 'Envío de SMS', + description: 'Configuración del proveedor de SMS para inicio de sesión con OTP, invitaciones y notificaciones.', + groups: { + provider: { title: 'Proveedor', description: 'Elige cómo envía este espacio de trabajo los SMS salientes.' }, + aliyun: { title: 'Aliyun SMS' }, + twilio: { title: 'Twilio' }, + }, + keys: { + provider: { + label: 'Proveedor', + options: { log: 'Ninguno (solo registro — sin envío real)', aliyun: 'Aliyun SMS', twilio: 'Twilio' }, + }, + aliyun_access_key_id: { label: 'AccessKey ID' }, + aliyun_access_key_secret: { label: 'AccessKey Secret' }, + aliyun_sign_name: { label: 'Nombre de firma (短信签名)' }, + aliyun_template_code: { + label: 'Código de plantilla predeterminado', + help: 'Se usa cuando un envío no lleva una plantilla explícita. Una plantilla genérica con una sola variable ${content} permite enviar SMS de notificación genéricos.', + }, + twilio_account_sid: { label: 'Account SID' }, + twilio_auth_token: { label: 'Token de autenticación' }, + twilio_from_number: { + label: 'Número de origen', + help: 'Remitente en formato E.164, p. ej. +15005550006. Se necesita esto o un Messaging Service SID.', + }, + twilio_messaging_service_sid: { label: 'Messaging Service SID' }, + }, + actions: { test: { label: 'Enviar SMS de prueba' } }, + }, + + company: { + title: 'Empresa', + description: 'Identidad de la entidad legal: nombre registrado, dirección, identificadores fiscales y contacto principal.', + groups: { + identity: { title: 'Identidad' }, + address: { title: 'Domicilio registrado' }, + contact: { title: 'Contacto' }, + }, + keys: { + legal_name: { label: 'Razón social', help: 'Nombre legal registrado de la organización (puede diferir del nombre del espacio de trabajo).' }, + registration_number: { label: 'Número de registro', help: 'Número de registro / constitución de la empresa (p. ej. EIN, n.º de sociedad).' }, + tax_id: { label: 'ID fiscal / IVA', help: 'Identificador fiscal que aparece en las facturas (p. ej. IVA, GST, ABN).' }, + address_line1: { label: 'Dirección línea 1' }, + address_line2: { label: 'Dirección línea 2' }, + city: { label: 'Ciudad' }, + state: { label: 'Provincia / Estado' }, + postal_code: { label: 'Código postal' }, + country: { label: 'País', help: 'Código ISO 3166-1 alfa-2 (p. ej. US, GB, CN).' }, + phone: { label: 'Teléfono', help: 'Teléfono principal de la empresa (se recomienda E.164, p. ej. +1 415 555 0100).' }, + website: { label: 'Sitio web', help: 'Ejemplo: https://example.com' }, + primary_contact_name: { label: 'Nombre del contacto principal' }, + primary_contact_email: { label: 'Correo del contacto principal', help: 'Ejemplo: ops@example.com' }, + }, + }, + + localization: { + title: 'Localización', + description: 'Zona horaria, idioma, moneda y formatos de fecha/número predeterminados.', + groups: { + region: { title: 'Región' }, + formats: { title: 'Formatos' }, + finance: { title: 'Finanzas' }, + }, + keys: { + timezone: { label: 'Zona horaria predeterminada', help: 'Zona IANA usada para resolver today()/daysFromNow, los buckets de fecha de analítica y las fechas/horas renderizadas.' }, + locale: { label: 'Idioma predeterminado', help: 'Configuración regional BCP-47 para los catálogos de mensajes y el formato de números/fechas.' }, + default_country: { label: 'País predeterminado', help: 'Código ISO 3166-1 alfa-2 (p. ej. US, GB, CN). Se usa para los valores por defecto de dirección y teléfono.' }, + date_format: { label: 'Formato de fecha' }, + time_format: { label: 'Formato de hora' }, + number_format: { label: 'Formato de número', help: 'Separadores de miles y decimales para los números mostrados.' }, + first_day_of_week: { label: 'Primer día de la semana', help: 'Ancla los buckets de analítica semanal y las cuadrículas de calendario.' }, + currency: { label: 'Moneda predeterminada', help: 'Código ISO 4217 aplicado cuando un campo de moneda no indica la suya.' }, + fiscal_year_start: { label: 'Inicio del año fiscal', help: 'Primer mes del año fiscal: determina «este trimestre / año fiscal» en los informes.' }, + }, + }, + feature_flags: { title: 'Indicadores de función', description: 'Activa funciones experimentales y en beta para este espacio de trabajo.', @@ -171,6 +332,12 @@ export const esES: TranslationData = { openai: { title: 'OpenAI' }, anthropic: { title: 'Anthropic' }, google: { title: 'Google' }, + deepseek: { title: 'DeepSeek', description: 'API compatible con OpenAI en https://api.deepseek.com. La Base URL se rellena automáticamente.' }, + dashscope: { title: '阿里通义 DashScope', description: 'Endpoint compatible con OpenAI en dashscope.aliyuncs.com/compatible-mode/v1. La Base URL se rellena automáticamente.' }, + cloudflare: { title: 'Cloudflare AI Gateway', description: 'Proxy de modelos compatibles con OpenAI a través de Cloudflare AI Gateway.' }, + siliconflow: { title: '硅基流动 SiliconFlow', description: 'Endpoint compatible con OpenAI en api.siliconflow.cn/v1. La Base URL se rellena automáticamente.' }, + openrouter: { title: 'OpenRouter', description: 'Enrutador multiproveedor en openrouter.ai/api/v1. La Base URL se rellena automáticamente.' }, + titles: { title: 'Títulos de conversación', description: 'Genera automáticamente un título de resumen breve para las conversaciones nuevas.' }, defaults: { title: 'Valores predeterminados de generación', description: 'Se aplican cuando un agente o una solicitud de chat no especifica su propio valor.' }, observability: { title: 'Observabilidad' }, @@ -188,6 +355,11 @@ export const esES: TranslationData = { openai: 'OpenAI', anthropic: 'Anthropic', google: 'Google Generative AI', + deepseek: 'DeepSeek (compatible con OpenAI)', + dashscope: '阿里通义 DashScope (compatible con OpenAI)', + cloudflare: 'Cloudflare AI Gateway (compatible con OpenAI)', + siliconflow: '硅基流动 SiliconFlow (compatible con OpenAI)', + openrouter: 'OpenRouter (compatible con OpenAI)', }, }, gateway_model: { label: 'Modelo de Gateway', @@ -206,6 +378,20 @@ export const esES: TranslationData = { google_api_key: { label: 'Clave de API de Google', help: 'Se reenvía como GOOGLE_GENERATIVE_AI_API_KEY. Se almacena cifrada en reposo.' }, google_model: { label: 'Modelo' }, + deepseek_api_key: { label: 'Clave de API de DeepSeek', help: 'sk-…, emitida en platform.deepseek.com.' }, + deepseek_model: { label: 'Modelo', help: 'Ejemplos: deepseek-chat (V3), deepseek-reasoner (razonamiento R1).' }, + dashscope_api_key: { label: 'Clave de API de DashScope', help: 'sk-…, emitida en dashscope.console.aliyun.com.' }, + dashscope_model: { label: 'Modelo' }, + cloudflare_account_id: { label: 'ID de cuenta de Cloudflare', help: 'El ID hexadecimal de 32 caracteres de la URL de tu panel de Cloudflare.' }, + cloudflare_gateway_id: { label: 'ID de gateway', help: 'Nombre del gateway configurado en Cloudflare → AI Gateway. Por defecto `default`.' }, + cloudflare_api_key: { label: 'Token de Cloudflare AI Gateway', help: 'Emitido en AI Gateway → pestaña «API tokens» (cfut_… o sk_…).' }, + cloudflare_model: { label: 'Modelo' }, + siliconflow_api_key: { label: 'Clave de API de SiliconFlow' }, + siliconflow_model: { label: 'Modelo', help: 'Ejemplos: Qwen/Qwen2.5-72B-Instruct, deepseek-ai/DeepSeek-V3, meta-llama/Meta-Llama-3.1-8B-Instruct.' }, + openrouter_api_key: { label: 'Clave de API de OpenRouter', help: 'sk-or-…' }, + openrouter_model: { label: 'Modelo', help: 'Formato: provider/model (p. ej. anthropic/claude-3.5-sonnet, deepseek/deepseek-chat).' }, + title_generation_enabled: { label: 'Resumir automáticamente los títulos de conversación' }, + title_max_length: { label: 'Longitud máx. del título (caracteres)', help: 'Tope estricto del título generado. Todo lo que exceda se trunca en el servidor.' }, temperature: { label: 'Temperatura', help: '0 = determinista, 2 = muy creativo.' }, max_tokens: { label: 'Máximo de tokens de salida', diff --git a/packages/services/service-settings/src/translations/ja-JP.ts b/packages/services/service-settings/src/translations/ja-JP.ts index d83f4d3ee9..03b70b8b6a 100644 --- a/packages/services/service-settings/src/translations/ja-JP.ts +++ b/packages/services/service-settings/src/translations/ja-JP.ts @@ -76,6 +76,26 @@ export const jaJP: TranslationData = { title: 'メールとパスワード', description: 'ローカルのメール/パスワードサインインとセルフサービス登録を制御します。', }, + password_policy: { + title: 'パスワードポリシー', + description: 'サインアップおよびパスワードリセット時に認証プロバイダーが強制する長さ制限。', + }, + anti_abuse: { + title: '不正利用対策', + description: 'ブルートフォース対策:ID ごとのアカウントロックと、認証エンドポイントへの IP ごとのレート制限。', + }, + multi_factor: { + title: '多要素', + description: 'メンバーに認証アプリ(TOTP)でのアカウント保護を必須にします。', + }, + sessions: { + title: 'セッション', + description: 'サインイン済みセッションの有効期間。', + }, + network: { + title: 'ネットワーク', + description: 'ユーザーがどこから認証できるかを制限します。', + }, social: { title: 'ソーシャルサインイン', description: @@ -86,6 +106,70 @@ export const jaJP: TranslationData = { email_password_enabled: { label: 'メール/パスワードログインを有効化' }, signup_enabled: { label: 'セルフサービス登録を許可' }, require_email_verification: { label: 'メール確認を必須にする' }, + password_min_length: { label: 'パスワードの最小文字数' }, + password_max_length: { label: 'パスワードの最大文字数', help: '非常に長いパスワードのハッシュ化によるサービス拒否を防ぎます。' }, + password_reject_breached: { + label: '漏洩済みパスワードを拒否', + help: 'Have I Been Pwned(k-匿名レンジチェック。パスワード全体は送信しません)により、公開漏洩コーパスに含まれるパスワードをブロックします。', + }, + password_require_complexity: { + label: '複雑なパスワードを要求', + help: 'サインアップおよびパスワード変更/リセット時に、複数の文字種(大文字・小文字・数字・記号)の組み合わせを要求します。', + }, + password_min_classes: { + label: '最小文字種数', + help: 'パスワードに含める必要がある文字種(大文字/小文字/数字/記号の4種)の数。', + }, + password_history_count: { + label: 'パスワード履歴(再利用禁止)', + help: '変更/リセット時に、直近この数のパスワードの再利用を禁止します。0 で無効。', + }, + password_expiry_days: { + label: 'パスワード有効期限(日)', + help: 'この日数を過ぎるとパスワード変更を強制します。0 で無効。期限切れの間、ユーザーはパスワードを変更するまでデータにアクセスできません。', + }, + lockout_threshold: { + label: 'アカウントロックのしきい値', + help: '連続してこの回数サインインに失敗するとアカウントをロックします。0 でロック無効。ロック中は正しいパスワードでもサインインを拒否します。', + }, + lockout_duration_minutes: { + label: 'ロック時間(分)', + help: 'しきい値を超えた後、アカウントがロックされ続ける時間。', + }, + rate_limit_max: { + label: '認証レート制限:最大リクエスト数', + help: 'サインイン / サインアップ / パスワードリセットの各エンドポイントに対する、IP ごと・時間枠ごとの最大リクエスト数。', + }, + rate_limit_window_seconds: { + label: '認証レート制限:時間枠(秒)', + help: '上記のリクエスト上限を数える移動時間枠。', + }, + mfa_required: { + label: '多要素認証を必須にする', + help: '認証アプリを登録していないユーザーは、猶予期間の終了後にデータへのアクセスをブロックされます。これを有効にすると、ユーザーが登録できるよう二要素機能もオンになります。', + }, + mfa_grace_period_days: { + label: 'MFA 猶予期間(日)', + help: 'ハードブロックまでに登録を延期できる期間。0 で即時ブロック。', + }, + session_expiry_days: { label: 'セッション有効期間(日)', help: 'サインインからこの日数でセッションが失効します。' }, + session_refresh_days: { label: '更新しきい値(日)', help: 'アクティブなセッションがこの期間を超えると延長されます。' }, + session_idle_timeout_minutes: { + label: 'アイドルタイムアウト(分)', + help: 'この分数、無操作が続くとユーザーをサインアウトします。0 で無効。', + }, + session_absolute_max_hours: { + label: 'セッションの絶対有効期間(時間)', + help: 'アクティビティに関係なく、サインインからこの時間で再認証を強制します。0 で無効。', + }, + max_concurrent_sessions_per_user: { + label: 'ユーザーあたりの最大同時セッション数', + help: 'ユーザーごとの同時サインインセッション数を制限します。上限を超えると古いものからサインアウトされます。0 で無制限。', + }, + allowed_ip_ranges: { + label: '許可 IP レンジ', + help: 'CIDR レンジまたは正確な IP(1行に1つ、またはカンマ区切り)。例:203.0.113.0/24。設定すると、この範囲外からのサインインを拒否します。空欄で制限なし。X-Forwarded-For を設定する信頼できるプロキシが必要です。', + }, google_enabled: { label: 'Google ログインを有効化', help: 'Google Cloud Console の Google OAuth クライアント ID とシークレットが必要です。', @@ -101,6 +185,83 @@ export const jaJP: TranslationData = { }, }, + sms: { + title: 'SMS 配信', + description: 'OTP サインイン、招待、通知に使う SMS プロバイダー設定。', + groups: { + provider: { title: 'プロバイダー', description: 'このワークスペースが送信 SMS をどう送るかを選択します。' }, + aliyun: { title: 'Aliyun SMS' }, + twilio: { title: 'Twilio' }, + }, + keys: { + provider: { + label: 'プロバイダー', + options: { log: 'なし(ログのみ — 実送信しない)', aliyun: 'Aliyun SMS', twilio: 'Twilio' }, + }, + aliyun_access_key_id: { label: 'AccessKey ID' }, + aliyun_access_key_secret: { label: 'AccessKey Secret' }, + aliyun_sign_name: { label: '署名(短信签名)' }, + aliyun_template_code: { + label: '既定テンプレートコード', + help: '送信時にテンプレートが指定されない場合に使用します。単一の ${content} 変数を持つ汎用テンプレートで、一般的な通知 SMS を送信できます。', + }, + twilio_account_sid: { label: 'Account SID' }, + twilio_auth_token: { label: 'Auth トークン' }, + twilio_from_number: { + label: '送信元番号', + help: 'E.164 形式の送信者。例:+15005550006。これか Messaging Service SID のいずれかが必要です。', + }, + twilio_messaging_service_sid: { label: 'Messaging Service SID' }, + }, + actions: { test: { label: 'テスト SMS を送信' } }, + }, + + company: { + title: '会社', + description: '法人としてのアイデンティティ — 登記名、住所、税務 ID、主担当者。', + groups: { + identity: { title: 'アイデンティティ' }, + address: { title: '登記住所' }, + contact: { title: '連絡先' }, + }, + keys: { + legal_name: { label: '登記名', help: '組織の登記上の正式名称(ワークスペース名と異なる場合があります)。' }, + registration_number: { label: '登記番号', help: '会社の登記 / 設立番号(例:EIN、会社番号)。' }, + tax_id: { label: '税務 / VAT ID', help: '請求書に表示される税務識別番号(例:VAT、GST、ABN)。' }, + address_line1: { label: '住所 1' }, + address_line2: { label: '住所 2' }, + city: { label: '市区町村' }, + state: { label: '都道府県 / 州' }, + postal_code: { label: '郵便番号' }, + country: { label: '国', help: 'ISO 3166-1 alpha-2 コード(例:US、GB、CN)。' }, + phone: { label: '電話', help: '主要な業務用電話(E.164 推奨。例:+1 415 555 0100)。' }, + website: { label: 'ウェブサイト', help: '例:https://example.com' }, + primary_contact_name: { label: '主担当者名' }, + primary_contact_email: { label: '主担当者メール', help: '例:ops@example.com' }, + }, + }, + + localization: { + title: 'ローカリゼーション', + description: '既定のタイムゾーン、言語、通貨、日付/数値フォーマット。', + groups: { + region: { title: '地域' }, + formats: { title: 'フォーマット' }, + finance: { title: '財務' }, + }, + keys: { + timezone: { label: '既定のタイムゾーン', help: 'today()/daysFromNow の解決、分析の日付バケット、日時レンダリングに使う IANA タイムゾーン。' }, + locale: { label: '既定の言語', help: 'メッセージカタログと数値/日付フォーマットに使う BCP-47 ロケール。' }, + default_country: { label: '既定の国', help: 'ISO 3166-1 alpha-2 コード(例:US、GB、CN)。住所と電話の既定に使用します。' }, + date_format: { label: '日付フォーマット' }, + time_format: { label: '時刻フォーマット' }, + number_format: { label: '数値フォーマット', help: '表示する数値の桁区切りと小数点の記号。' }, + first_day_of_week: { label: '週の開始日', help: '週次分析のバケットとカレンダーグリッドの基準になります。' }, + currency: { label: '既定の通貨', help: '通貨フィールドが独自の通貨を指定しない場合に適用される ISO 4217 コード。' }, + fiscal_year_start: { label: '会計年度の開始月', help: '会計年度の最初の月 — レポートの「今四半期 / 会計年度」を決定します。' }, + }, + }, + feature_flags: { title: '機能フラグ', description: 'このワークスペースで実験的・ベータ機能を切替えます。', @@ -168,6 +329,12 @@ export const jaJP: TranslationData = { openai: { title: 'OpenAI' }, anthropic: { title: 'Anthropic' }, google: { title: 'Google' }, + deepseek: { title: 'DeepSeek', description: 'https://api.deepseek.com の OpenAI 互換 API。Base URL は自動入力されます。' }, + dashscope: { title: '阿里通义 DashScope', description: 'dashscope.aliyuncs.com/compatible-mode/v1 の OpenAI 互換エンドポイント。Base URL は自動入力されます。' }, + cloudflare: { title: 'Cloudflare AI Gateway', description: 'Cloudflare AI Gateway 経由で OpenAI 互換モデルをプロキシします。' }, + siliconflow: { title: '硅基流动 SiliconFlow', description: 'api.siliconflow.cn/v1 の OpenAI 互換エンドポイント。Base URL は自動入力されます。' }, + openrouter: { title: 'OpenRouter', description: 'openrouter.ai/api/v1 のマルチプロバイダールーター。Base URL は自動入力されます。' }, + titles: { title: '会話タイトル', description: '新しい会話に短い要約タイトルを自動生成します。' }, defaults: { title: '生成のデフォルト値', description: 'エージェントまたはチャットリクエストが独自の値を指定しない場合に適用されます。' }, observability: { title: '可観測性' }, @@ -185,6 +352,11 @@ export const jaJP: TranslationData = { openai: 'OpenAI', anthropic: 'Anthropic', google: 'Google Generative AI', + deepseek: 'DeepSeek(OpenAI 互換)', + dashscope: '阿里通义 DashScope(OpenAI 互換)', + cloudflare: 'Cloudflare AI Gateway(OpenAI 互換)', + siliconflow: '硅基流动 SiliconFlow(OpenAI 互換)', + openrouter: 'OpenRouter(OpenAI 互換)', }, }, gateway_model: { label: 'Gateway モデル', @@ -203,6 +375,20 @@ export const jaJP: TranslationData = { google_api_key: { label: 'Google API キー', help: 'GOOGLE_GENERATIVE_AI_API_KEY として転送されます。保存時に暗号化されます。' }, google_model: { label: 'モデル' }, + deepseek_api_key: { label: 'DeepSeek API キー', help: 'sk-…。platform.deepseek.com で発行。' }, + deepseek_model: { label: 'モデル', help: '例:deepseek-chat(V3)、deepseek-reasoner(R1 推論)。' }, + dashscope_api_key: { label: 'DashScope API キー', help: 'sk-…。dashscope.console.aliyun.com で発行。' }, + dashscope_model: { label: 'モデル' }, + cloudflare_account_id: { label: 'Cloudflare アカウント ID', help: 'Cloudflare ダッシュボード URL に含まれる 32 桁の 16 進数 ID。' }, + cloudflare_gateway_id: { label: 'Gateway ID', help: 'Cloudflare → AI Gateway で設定したゲートウェイ名。既定は `default`。' }, + cloudflare_api_key: { label: 'Cloudflare AI Gateway トークン', help: 'AI Gateway →「API tokens」タブで発行(cfut_… または sk_…)。' }, + cloudflare_model: { label: 'モデル' }, + siliconflow_api_key: { label: 'SiliconFlow API キー' }, + siliconflow_model: { label: 'モデル', help: '例:Qwen/Qwen2.5-72B-Instruct、deepseek-ai/DeepSeek-V3、meta-llama/Meta-Llama-3.1-8B-Instruct。' }, + openrouter_api_key: { label: 'OpenRouter API キー', help: 'sk-or-…' }, + openrouter_model: { label: 'モデル', help: '形式:provider/model(例:anthropic/claude-3.5-sonnet、deepseek/deepseek-chat)。' }, + title_generation_enabled: { label: '会話タイトルを自動要約' }, + title_max_length: { label: 'タイトルの最大長(文字)', help: '生成タイトルの上限。超過分はサーバー側で切り詰められます。' }, temperature: { label: 'Temperature', help: '0 = 決定的、2 = 非常に創造的。' }, max_tokens: { label: '最大出力トークン数', diff --git a/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts b/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts new file mode 100644 index 0000000000..9aaecd8ddb --- /dev/null +++ b/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Settings-translation coverage guard (zh-CN / ja-JP / es-ES). + * + * Every built-in settings manifest ships English labels inline; the console + * localizes them by resolving `settings..…` against the shipped + * translation bundles (see `useSettingsLabel`). When a manifest gains a new + * group or field but a locale bundle isn't updated, the setting silently + * renders English under a translated UI (objectui#2851). + * + * This test pins each shipped non-English locale to full coverage of every + * manifest's structural strings — namespace title/description, each group + * title, and each field label. Dropdown *option* labels are intentionally + * excluded: several are codes or format specimens (e.g. `date_format` = + * "YYYY-MM-DD", `number_format` = "1,234.56") that must not be "translated", + * and option-level parity is filled per-locale on a best-effort basis. + */ + +import { describe, it, expect } from 'vitest'; +import * as manifestsModule from '../manifests/index'; +import { zhCN, jaJP, esES } from './index'; + +type Specifier = { type?: string; id?: string; key?: string; label?: string; description?: string }; +type Manifest = { namespace: string; description?: string; specifiers: Specifier[] }; + +const manifests = Object.values(manifestsModule).filter( + (v): v is Manifest => + !!v && typeof v === 'object' && 'namespace' in v && Array.isArray((v as Manifest).specifiers), +); + +const LOCALES: Array<[string, { settings?: Record }]> = [ + ['zh-CN', zhCN], + ['ja-JP', jaJP], + ['es-ES', esES], +]; + +function missingFor(data: { settings?: Record }, m: Manifest): string[] { + const tr = (data.settings ?? {})[m.namespace]; + const missing: string[] = []; + if (tr?.title == null) missing.push('title'); + if (m.description && tr?.description == null) missing.push('description'); + for (const s of m.specifiers) { + if (s.type === 'group') { + if (s.id && tr?.groups?.[s.id]?.title == null) missing.push(`group:${s.id}`); + } else if (s.key) { + if (tr?.keys?.[s.key]?.label == null) missing.push(`key:${s.key}`); + } + } + return missing; +} + +describe('settings translation coverage', () => { + it('covers every built-in settings manifest', () => { + expect(manifests.length).toBeGreaterThanOrEqual(10); + }); + + for (const [locale, data] of LOCALES) { + for (const m of manifests) { + it(`${locale} covers settings.${m.namespace} (title / groups / keys)`, () => { + const missing = missingFor(data, m); + expect(missing, `${locale} missing for ${m.namespace}: ${missing.join(', ')}`).toEqual([]); + }); + } + } +}); diff --git a/packages/services/service-settings/src/translations/zh-CN.ts b/packages/services/service-settings/src/translations/zh-CN.ts index 2e4873d265..b171ce7e6f 100644 --- a/packages/services/service-settings/src/translations/zh-CN.ts +++ b/packages/services/service-settings/src/translations/zh-CN.ts @@ -49,6 +49,43 @@ export const zhCN: TranslationData = { }, }, + sms: { + title: '短信投递', + description: '用于 OTP 登录、邀请与通知的短信服务商配置。', + groups: { + provider: { title: '服务商', description: '选择此工作区如何发送外发短信。' }, + aliyun: { title: '阿里云短信' }, + twilio: { title: 'Twilio' }, + }, + keys: { + provider: { + label: '服务商', + options: { + log: '无(仅记录日志 — 不真实发送)', + aliyun: '阿里云短信', + twilio: 'Twilio', + }, + }, + aliyun_access_key_id: { label: 'AccessKey ID' }, + aliyun_access_key_secret: { label: 'AccessKey Secret' }, + aliyun_sign_name: { label: '短信签名' }, + aliyun_template_code: { + label: '默认短信模板', + help: '当发送未指定模板时使用。含单个 ${content} 变量的通用模板可用于发送通用通知短信。', + }, + twilio_account_sid: { label: 'Account SID' }, + twilio_auth_token: { label: 'Auth Token' }, + twilio_from_number: { + label: '发信号码', + help: 'E.164 格式的发信方,例如 +15005550006。此项与 Messaging Service SID 二选一。', + }, + twilio_messaging_service_sid: { label: 'Messaging Service SID' }, + }, + actions: { + test: { label: '发送测试短信' }, + }, + }, + branding: { title: '品牌', description: '工作区名称、Logo 与主题色。', @@ -102,15 +139,95 @@ export const zhCN: TranslationData = { finance: { title: '财务' }, }, keys: { - timezone: { label: '默认时区', help: '用于 today()/daysFromNow、分析日期分桶和 datetime 渲染的 IANA 时区。' }, - locale: { label: '默认语言', help: '用于消息文案和数字/日期格式的 BCP-47 语言。' }, + timezone: { + label: '默认时区', + help: '用于 today()/daysFromNow、分析日期分桶和 datetime 渲染的 IANA 时区。', + options: { + UTC: 'UTC', + 'America/Los_Angeles': '(UTC−08/−07) 洛杉矶', + 'America/Denver': '(UTC−07/−06) 丹佛', + 'America/Chicago': '(UTC−06/−05) 芝加哥', + 'America/New_York': '(UTC−05/−04) 纽约', + 'America/Sao_Paulo': '(UTC−03) 圣保罗', + 'Europe/London': '(UTC±00/+01) 伦敦', + 'Europe/Paris': '(UTC+01/+02) 巴黎', + 'Europe/Berlin': '(UTC+01/+02) 柏林', + 'Europe/Moscow': '(UTC+03) 莫斯科', + 'Asia/Dubai': '(UTC+04) 迪拜', + 'Asia/Kolkata': '(UTC+05:30) 加尔各答', + 'Asia/Singapore': '(UTC+08) 新加坡', + 'Asia/Shanghai': '(UTC+08) 上海', + 'Asia/Tokyo': '(UTC+09) 东京', + 'Australia/Sydney': '(UTC+10/+11) 悉尼', + 'Pacific/Auckland': '(UTC+12/+13) 奥克兰', + }, + }, + locale: { + label: '默认语言', + help: '用于消息文案和数字/日期格式的 BCP-47 语言。', + options: { + 'en-US': '英语(美国)', + 'zh-CN': '简体中文', + 'ja-JP': '日语', + 'es-ES': '西班牙语(西班牙)', + }, + }, default_country: { label: '默认国家/地区', help: 'ISO 3166-1 二位代码(如 US、GB、CN)。' }, - date_format: { label: '日期格式' }, + date_format: { + label: '日期格式', + options: { + 'YYYY-MM-DD': '2026-06-17(ISO)', + 'MM/DD/YYYY': '06/17/2026(美国)', + 'DD/MM/YYYY': '17/06/2026(欧洲)', + 'DD.MM.YYYY': '17.06.2026', + 'DD-MMM-YYYY': '17-Jun-2026', + }, + }, time_format: { label: '时间格式', options: { '24h': '24 小时制(14:30)', '12h': '12 小时制(2:30 PM)' } }, - number_format: { label: '数字格式', help: '用于显示数字的千分位与小数分隔符。' }, + number_format: { + label: '数字格式', + help: '用于显示数字的千分位与小数分隔符。', + options: { + '1,234.56': '1,234.56(逗号 / 句点)', + '1.234,56': '1.234,56(句点 / 逗号)', + '1 234,56': '1 234,56(空格 / 逗号)', + '1,23,456.78': '1,23,456.78(印度)', + }, + }, first_day_of_week: { label: '每周起始日', help: '用作周度分析分桶与日历网格的起始基准。', options: { monday: '周一(ISO)', sunday: '周日', saturday: '周六' } }, - currency: { label: '默认货币', help: '当货币字段未指定币种时套用的 ISO 4217 代码。' }, - fiscal_year_start: { label: '财年起始月', help: '财年的起始月份——决定报表中的"本季度/本财年"。' }, + currency: { + label: '默认货币', + help: '当货币字段未指定币种时套用的 ISO 4217 代码。', + options: { + USD: 'USD — 美元', + EUR: 'EUR — 欧元', + GBP: 'GBP — 英镑', + JPY: 'JPY — 日元', + CNY: 'CNY — 人民币', + INR: 'INR — 印度卢比', + AUD: 'AUD — 澳大利亚元', + CAD: 'CAD — 加拿大元', + BRL: 'BRL — 巴西雷亚尔', + }, + }, + fiscal_year_start: { + label: '财年起始月', + help: '财年的起始月份——决定报表中的"本季度/本财年"。', + options: { + january: '一月', + february: '二月', + march: '三月', + april: '四月', + may: '五月', + june: '六月', + july: '七月', + august: '八月', + september: '九月', + october: '十月', + november: '十一月', + december: '十二月', + }, + }, }, }, @@ -126,10 +243,22 @@ export const zhCN: TranslationData = { title: '密码策略', description: '由认证提供商在注册和重置密码时强制的长度限制。', }, + anti_abuse: { + title: '防滥用', + description: '暴力破解防护:按身份的账户锁定,以及按 IP 对认证端点的限流。', + }, + multi_factor: { + title: '多因素', + description: '要求成员使用身份验证器应用(TOTP)保护账户。', + }, sessions: { title: '会话', description: '登录会话的有效时长。', }, + network: { + title: '网络', + description: '限制用户可以从哪里登录。', + }, social: { title: '社交登录', description: '配置内置的 Google 登录提供商。部署环境变量仍优先生效。', @@ -141,8 +270,68 @@ export const zhCN: TranslationData = { require_email_verification: { label: '要求邮箱验证' }, password_min_length: { label: '密码最小长度' }, password_max_length: { label: '密码最大长度', help: '防止超长密码哈希导致的拒绝服务。' }, + password_reject_breached: { + label: '拒绝已泄露的密码', + help: '通过 Have I Been Pwned 拦截出现在公开泄露库中的密码(k-匿名区间校验,绝不发送完整密码)。', + }, + password_require_complexity: { + label: '要求复杂密码', + help: '在注册及修改/重置密码时,要求密码混合多种字符类别(大写、小写、数字、符号)。', + }, + password_min_classes: { + label: '最少字符类别数', + help: '密码至少需包含四类(大写/小写/数字/符号)中的几类。', + }, + password_history_count: { + label: '密码历史(禁止重用)', + help: '在修改/重置时禁止重用最近这么多个旧密码。0 表示关闭该检查。', + }, + password_expiry_days: { + label: '密码有效期(天)', + help: '超过这么多天后强制修改密码。0 表示不过期。密码过期期间,用户在修改密码前将被阻止访问数据。', + }, + lockout_threshold: { + label: '账户锁定阈值', + help: '连续登录失败达到此次数后锁定账户。0 表示关闭锁定。锁定期间即使密码正确也会拒绝登录。', + }, + lockout_duration_minutes: { + label: '锁定时长(分钟)', + help: '越过阈值后账户保持锁定的时长。', + }, + rate_limit_max: { + label: '认证限流:最大请求数', + help: '每个 IP、每个时间窗内对登录 / 注册 / 重置密码端点的最大请求数。', + }, + rate_limit_window_seconds: { + label: '认证限流:时间窗(秒)', + help: '统计上述请求上限所用的滑动时间窗。', + }, + mfa_required: { + label: '要求多因素认证', + help: '未注册身份验证器的用户在宽限期结束后将被阻止访问数据。启用此项也会开启两步验证功能,以便用户注册。', + }, + mfa_grace_period_days: { + label: 'MFA 宽限期(天)', + help: '用户在被硬阻断前可延迟注册的时长。0 表示立即阻断。', + }, session_expiry_days: { label: '会话有效期(天)', help: '登录后会话在此天数后过期。' }, session_refresh_days: { label: '刷新阈值(天)', help: '活跃会话在超过此时长后自动续期。' }, + session_idle_timeout_minutes: { + label: '空闲超时(分钟)', + help: '在这么多分钟无活动后将用户登出。0 表示关闭。', + }, + session_absolute_max_hours: { + label: '会话绝对有效期(小时)', + help: '登录后经过这么多小时强制重新认证,与是否活跃无关。0 表示关闭。', + }, + max_concurrent_sessions_per_user: { + label: '每用户最大并发会话数', + help: '限制每个用户同时在线的会话数;超出上限后最旧的会话会被登出。0 表示不限制。', + }, + allowed_ip_ranges: { + label: '允许的 IP 段', + help: 'CIDR 段或精确 IP(每行一个,或用逗号分隔),例如 203.0.113.0/24。设置后,来自这些范围之外的登录将被拒绝。留空表示不限制。需要可信代理设置 X-Forwarded-For。', + }, google_enabled: { label: '启用 Google 登录', help: '需要在 Google Cloud Console 中创建的 Google OAuth 客户端 ID 与密钥。', @@ -222,6 +411,12 @@ export const zhCN: TranslationData = { openai: { title: 'OpenAI' }, anthropic: { title: 'Anthropic' }, google: { title: 'Google' }, + deepseek: { title: 'DeepSeek', description: 'https://api.deepseek.com 上的 OpenAI 兼容 API,Base URL 自动填充。' }, + dashscope: { title: '阿里通义 DashScope', description: 'dashscope.aliyuncs.com/compatible-mode/v1 上的 OpenAI 兼容端点,Base URL 自动填充。' }, + cloudflare: { title: 'Cloudflare AI Gateway', description: '通过 Cloudflare AI Gateway 代理 OpenAI 兼容模型。' }, + siliconflow: { title: '硅基流动 SiliconFlow', description: 'api.siliconflow.cn/v1 上的 OpenAI 兼容端点,Base URL 自动填充。' }, + openrouter: { title: 'OpenRouter', description: 'openrouter.ai/api/v1 上的多提供商路由,Base URL 自动填充。' }, + titles: { title: '会话标题', description: '为新会话自动生成简短的摘要标题。' }, defaults: { title: '生成默认值', description: '当 Agent 或聊天请求未指定时使用。' }, observability: { title: '可观测性' }, embedder: { title: 'Embedder', description: '知识库和 RAG 使用的文本→向量提供商,与上方聊天提供商相互独立。' }, @@ -235,6 +430,11 @@ export const zhCN: TranslationData = { openai: 'OpenAI', anthropic: 'Anthropic', google: 'Google Generative AI', + deepseek: 'DeepSeek(OpenAI 兼容)', + dashscope: '阿里通义 DashScope(OpenAI 兼容)', + cloudflare: 'Cloudflare AI Gateway(OpenAI 兼容)', + siliconflow: '硅基流动 SiliconFlow(OpenAI 兼容)', + openrouter: 'OpenRouter(OpenAI 兼容)', }, }, gateway_model: { label: 'Gateway 模型', help: '作为 AI_GATEWAY_MODEL 转发。示例:openai/gpt-4o' }, @@ -246,6 +446,20 @@ export const zhCN: TranslationData = { anthropic_model: { label: '模型' }, google_api_key: { label: 'Google API Key', help: '作为 GOOGLE_GENERATIVE_AI_API_KEY 转发,加密存储。' }, google_model: { label: '模型' }, + deepseek_api_key: { label: 'DeepSeek API Key', help: 'sk-…,在 platform.deepseek.com 签发。' }, + deepseek_model: { label: '模型', help: '示例:deepseek-chat(V3)、deepseek-reasoner(R1 推理)。' }, + dashscope_api_key: { label: 'DashScope API Key', help: 'sk-…,在 dashscope.console.aliyun.com 签发。' }, + dashscope_model: { label: '模型' }, + cloudflare_account_id: { label: 'Cloudflare Account ID', help: '来自 Cloudflare 控制台 URL 的 32 位十六进制 ID。' }, + cloudflare_gateway_id: { label: 'Gateway ID', help: '在 Cloudflare → AI Gateway 中配置的网关名称,默认为 `default`。' }, + cloudflare_api_key: { label: 'Cloudflare AI Gateway Token', help: '在 AI Gateway →「API tokens」标签页签发(cfut_… 或 sk_…)。' }, + cloudflare_model: { label: '模型' }, + siliconflow_api_key: { label: 'SiliconFlow API Key' }, + siliconflow_model: { label: '模型', help: '示例:Qwen/Qwen2.5-72B-Instruct、deepseek-ai/DeepSeek-V3、meta-llama/Meta-Llama-3.1-8B-Instruct。' }, + openrouter_api_key: { label: 'OpenRouter API Key' }, + openrouter_model: { label: '模型', help: '格式:provider/model(例如 anthropic/claude-3.5-sonnet、deepseek/deepseek-chat)。' }, + title_generation_enabled: { label: '自动为会话生成摘要标题' }, + title_max_length: { label: '标题最大长度(字符)', help: '生成标题的硬上限,超出部分在服务端截断。' }, temperature: { label: '温度', help: '0 = 确定性,2 = 高度发散。' }, max_tokens: { label: '最大输出 tokens', help: '单次响应生成的硬上限。' }, request_timeout_ms: { label: '请求超时(毫秒)' },