diff --git a/src/i18n/registry.test.ts b/src/i18n/registry.test.ts index e05ba99e738..a2fa23a0d0b 100644 --- a/src/i18n/registry.test.ts +++ b/src/i18n/registry.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "vitest"; +import type { TranslationMap } from "../../ui/src/i18n/lib/types.ts"; import { DEFAULT_LOCALE, SUPPORTED_LOCALES, loadLazyLocaleTranslation, resolveNavigatorLocale, } from "../../ui/src/i18n/lib/registry.ts"; -import type { TranslationMap } from "../../ui/src/i18n/lib/types.ts"; function getNestedTranslation(map: TranslationMap | null, ...path: string[]): string | undefined { let value: string | TranslationMap | undefined = map ?? undefined; @@ -20,12 +20,14 @@ function getNestedTranslation(map: TranslationMap | null, ...path: string[]): st describe("ui i18n locale registry", () => { it("lists supported locales", () => { - expect(SUPPORTED_LOCALES).toEqual(["en", "zh-CN", "zh-TW", "pt-BR", "de"]); + expect(SUPPORTED_LOCALES).toEqual(["en", "zh-CN", "zh-TW", "pt-BR", "de", "es"]); expect(DEFAULT_LOCALE).toBe("en"); }); it("resolves browser locale fallbacks", () => { expect(resolveNavigatorLocale("de-DE")).toBe("de"); + expect(resolveNavigatorLocale("es-ES")).toBe("es"); + expect(resolveNavigatorLocale("es-MX")).toBe("es"); expect(resolveNavigatorLocale("pt-PT")).toBe("pt-BR"); expect(resolveNavigatorLocale("zh-HK")).toBe("zh-TW"); expect(resolveNavigatorLocale("en-US")).toBe("en"); @@ -33,9 +35,14 @@ describe("ui i18n locale registry", () => { it("loads lazy locale translations from the registry", async () => { const de = await loadLazyLocaleTranslation("de"); + const es = await loadLazyLocaleTranslation("es"); + const ptBR = await loadLazyLocaleTranslation("pt-BR"); const zhCN = await loadLazyLocaleTranslation("zh-CN"); expect(getNestedTranslation(de, "common", "health")).toBe("Status"); + expect(getNestedTranslation(es, "common", "health")).toBe("Estado"); + expect(getNestedTranslation(es, "languages", "de")).toBe("Deutsch (Alemán)"); + expect(getNestedTranslation(ptBR, "languages", "es")).toBe("Español (Espanhol)"); expect(getNestedTranslation(zhCN, "common", "health")).toBe("健康状况"); expect(await loadLazyLocaleTranslation("en")).toBeNull(); }); diff --git a/ui/src/i18n/lib/registry.ts b/ui/src/i18n/lib/registry.ts index 341f27e213c..d61911053bf 100644 --- a/ui/src/i18n/lib/registry.ts +++ b/ui/src/i18n/lib/registry.ts @@ -10,7 +10,7 @@ type LazyLocaleRegistration = { export const DEFAULT_LOCALE: Locale = "en"; -const LAZY_LOCALES: readonly LazyLocale[] = ["zh-CN", "zh-TW", "pt-BR", "de"]; +const LAZY_LOCALES: readonly LazyLocale[] = ["zh-CN", "zh-TW", "pt-BR", "de", "es"]; const LAZY_LOCALE_REGISTRY: Record = { "zh-CN": { @@ -29,6 +29,10 @@ const LAZY_LOCALE_REGISTRY: Record = { exportName: "de", loader: () => import("../locales/de.ts"), }, + es: { + exportName: "es", + loader: () => import("../locales/es.ts"), + }, }; export const SUPPORTED_LOCALES: ReadonlyArray = [DEFAULT_LOCALE, ...LAZY_LOCALES]; @@ -51,6 +55,9 @@ export function resolveNavigatorLocale(navLang: string): Locale { if (navLang.startsWith("de")) { return "de"; } + if (navLang.startsWith("es")) { + return "es"; + } return DEFAULT_LOCALE; } diff --git a/ui/src/i18n/lib/types.ts b/ui/src/i18n/lib/types.ts index 9578d0ff7a9..8b25ecbc6da 100644 --- a/ui/src/i18n/lib/types.ts +++ b/ui/src/i18n/lib/types.ts @@ -1,6 +1,6 @@ export type TranslationMap = { [key: string]: string | TranslationMap }; -export type Locale = "en" | "zh-CN" | "zh-TW" | "pt-BR" | "de"; +export type Locale = "en" | "zh-CN" | "zh-TW" | "pt-BR" | "de" | "es"; export interface I18nConfig { locale: Locale; diff --git a/ui/src/i18n/locales/de.ts b/ui/src/i18n/locales/de.ts index bbdf2bdb3b5..633bdeb12d8 100644 --- a/ui/src/i18n/locales/de.ts +++ b/ui/src/i18n/locales/de.ts @@ -125,5 +125,6 @@ export const de: TranslationMap = { zhTW: "繁體中文 (Traditionelles Chinesisch)", ptBR: "Português (Brasilianisches Portugiesisch)", de: "Deutsch", + es: "Spanisch (Español)", }, }; diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 8d3ef85a44b..c4a83017c19 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -122,6 +122,7 @@ export const en: TranslationMap = { zhTW: "繁體中文 (Traditional Chinese)", ptBR: "Português (Brazilian Portuguese)", de: "Deutsch (German)", + es: "Español (Spanish)", }, cron: { summary: { diff --git a/ui/src/i18n/locales/es.ts b/ui/src/i18n/locales/es.ts new file mode 100644 index 00000000000..0a77e447a0f --- /dev/null +++ b/ui/src/i18n/locales/es.ts @@ -0,0 +1,347 @@ +import type { TranslationMap } from "../lib/types.ts"; + +export const es: TranslationMap = { + common: { + version: "Versión", + health: "Estado", + ok: "Correcto", + offline: "Desconectado", + connect: "Conectar", + refresh: "Actualizar", + enabled: "Habilitado", + disabled: "Deshabilitado", + na: "n/a", + docs: "Docs", + resources: "Recursos", + }, + nav: { + chat: "Chat", + control: "Control", + agent: "Agente", + settings: "Ajustes", + expand: "Expandir barra lateral", + collapse: "Contraer barra lateral", + }, + tabs: { + agents: "Agentes", + overview: "Resumen", + channels: "Canales", + instances: "Instancias", + sessions: "Sesiones", + usage: "Uso", + cron: "Tareas Cron", + skills: "Habilidades", + nodes: "Nodos", + chat: "Chat", + config: "Configuración", + debug: "Depuración", + logs: "Registros", + }, + subtitles: { + agents: "Gestionar espacios de trabajo, herramientas e identidades de agentes.", + overview: "Estado de la puerta de enlace, puntos de entrada y lectura rápida de salud.", + channels: "Gestionar canales y ajustes.", + instances: "Balizas de presencia de clientes y nodos conectados.", + sessions: "Inspeccionar sesiones activas y ajustar valores predeterminados por sesión.", + usage: "Monitorear uso de API y costes.", + cron: "Programar despertares y ejecuciones recurrentes de agentes.", + skills: "Gestionar disponibilidad de habilidades e inyección de claves API.", + nodes: "Dispositivos emparejados, capacidades y exposición de comandos.", + chat: "Sesión de chat directa con la puerta de enlace para intervenciones rápidas.", + config: "Editar ~/.openclaw/openclaw.json de forma segura.", + debug: "Instantáneas de la puerta de enlace, eventos y llamadas RPC manuales.", + logs: "Seguimiento en vivo de los registros de la puerta de enlace.", + }, + overview: { + access: { + title: "Acceso a la puerta de enlace", + subtitle: "Dónde se conecta el panel y cómo se autentica.", + wsUrl: "URL de WebSocket", + token: "Token de la puerta de enlace", + password: "Contraseña (no se guarda)", + sessionKey: "Clave de sesión predeterminada", + language: "Idioma", + connectHint: "Haz clic en Conectar para aplicar los cambios de conexión.", + trustedProxy: "Autenticado mediante proxy de confianza.", + }, + snapshot: { + title: "Instantánea", + subtitle: "Información más reciente del saludo con la puerta de enlace.", + status: "Estado", + uptime: "Tiempo de actividad", + tickInterval: "Intervalo de tick", + lastChannelsRefresh: "Última actualización de canales", + channelsHint: "Usa Canales para vincular WhatsApp, Telegram, Discord, Signal o iMessage.", + }, + stats: { + instances: "Instancias", + instancesHint: "Balizas de presencia en los últimos 5 minutos.", + sessions: "Sesiones", + sessionsHint: "Claves de sesión recientes rastreadas por la puerta de enlace.", + cron: "Cron", + cronNext: "Próximo despertar {time}", + }, + notes: { + title: "Notas", + subtitle: "Recordatorios rápidos para configuraciones de control remoto.", + tailscaleTitle: "Tailscale serve", + tailscaleText: + "Prefiere el modo serve para mantener la puerta de enlace en loopback con autenticación tailnet.", + sessionTitle: "Higiene de sesión", + sessionText: "Usa /new o sessions.patch para reiniciar el contexto.", + cronTitle: "Recordatorios de Cron", + cronText: "Usa sesiones aisladas para ejecuciones recurrentes.", + }, + auth: { + required: + "Esta puerta de enlace requiere autenticación. Añade un token o contraseña y haz clic en Conectar.", + failed: + "Autenticación fallida. Vuelve a copiar una URL con token mediante {command}, o actualiza el token y haz clic en Conectar.", + }, + pairing: { + hint: "Este dispositivo necesita aprobación de emparejamiento del host de la puerta de enlace.", + mobileHint: + "¿En el móvil? Copia la URL completa (incluyendo #token=...) desde openclaw dashboard --no-open en tu escritorio.", + }, + insecure: { + hint: "Esta página es HTTP, por lo que el navegador bloquea el acceso a la identidad del dispositivo. Usa HTTPS (Tailscale Serve) o abre {url} en el equipo host.", + stayHttp: "Si debes permanecer en HTTP, utiliza {config} (solo token).", + }, + }, + chat: { + disconnected: "Desconectado de la puerta de enlace.", + refreshTitle: "Actualizar datos del chat", + thinkingToggle: "Alternar salida de pensamiento/trabajo del asistente", + focusToggle: "Alternar modo de enfoque (ocultar barra lateral + cabecera)", + hideCronSessions: "Ocultar sesiones de cron", + showCronSessions: "Mostrar sesiones de cron", + showCronSessionsHidden: "Mostrar sesiones de cron ({count} ocultas)", + onboardingDisabled: "Deshabilitado durante el inicio guiado", + }, + languages: { + en: "Inglés (English)", + zhCN: "Chino simplificado (简体中文)", + zhTW: "Chino tradicional (繁體中文)", + ptBR: "Portugués brasileño (Português)", + de: "Deutsch (Alemán)", + es: "Español", + }, + cron: { + summary: { + enabled: "Habilitado", + yes: "Sí", + no: "No", + jobs: "Tareas", + nextWake: "Próxima activación", + refreshing: "Actualizando...", + refresh: "Actualizar", + }, + jobs: { + title: "Tareas", + subtitle: "Todas las tareas programadas almacenadas en la puerta de enlace.", + shownOf: "{shown} mostradas de {total}", + searchJobs: "Buscar tareas", + searchPlaceholder: "Nombre, descripción o agente", + enabled: "Habilitado", + schedule: "Programación", + lastRun: "Última ejecución", + all: "Todas", + sort: "Ordenar", + nextRun: "Próxima ejecución", + recentlyUpdated: "Actualizadas recientemente", + name: "Nombre", + direction: "Dirección", + ascending: "Ascendente", + descending: "Descendente", + reset: "Restablecer", + noMatching: "No hay tareas coincidentes.", + loading: "Cargando...", + loadMore: "Cargar más tareas", + }, + runs: { + title: "Historial de ejecuciones", + subtitleAll: "Últimas ejecuciones de todas las tareas.", + subtitleJob: "Últimas ejecuciones de {title}.", + scope: "Alcance", + allJobs: "Todas las tareas", + selectedJob: "Tarea seleccionada", + searchRuns: "Buscar ejecuciones", + searchPlaceholder: "Resumen, error o tarea", + newestFirst: "Más recientes primero", + oldestFirst: "Más antiguas primero", + status: "Estado", + delivery: "Entrega", + clear: "Limpiar", + allStatuses: "Todos los estados", + allDelivery: "Todas las entregas", + selectJobHint: "Selecciona una tarea para ver su historial de ejecuciones.", + noMatching: "No hay ejecuciones coincidentes.", + loadMore: "Cargar más ejecuciones", + runStatusOk: "OK", + runStatusError: "Error", + runStatusSkipped: "Omitida", + runStatusUnknown: "Desconocido", + deliveryDelivered: "Entregado", + deliveryNotDelivered: "No entregado", + deliveryUnknown: "Desconocido", + deliveryNotRequested: "No solicitado", + }, + form: { + editJob: "Editar tarea", + newJob: "Nueva tarea", + updateSubtitle: "Actualiza la tarea programada seleccionada.", + createSubtitle: "Crea una activación programada o ejecución de agente.", + required: "Requerido", + requiredSr: "requerido", + basics: "Básico", + basicsSub: "Asigna un nombre, elige el asistente y define si está habilitada.", + fieldName: "Nombre", + description: "Descripción", + agentId: "ID de agente", + namePlaceholder: "Resumen matutino", + descriptionPlaceholder: "Contexto opcional para esta tarea", + agentPlaceholder: "main u ops", + agentHelp: + "Comienza a escribir para seleccionar un agente conocido o ingresa uno personalizado.", + schedule: "Programación", + scheduleSub: "Controla cuándo se ejecuta esta tarea.", + every: "Cada", + at: "A las", + cronOption: "Cron", + runAt: "Ejecutar a las", + unit: "Unidad", + minutes: "Minutos", + hours: "Horas", + days: "Días", + expression: "Expresión", + expressionPlaceholder: "0 7 * * *", + everyAmountPlaceholder: "30", + timezoneOptional: "Zona horaria (opcional)", + timezonePlaceholder: "America/Los_Angeles", + timezoneHelp: "Selecciona una zona horaria común o ingresa cualquier zona IANA válida.", + jitterHelp: + "¿Necesitas variación? Usa Avanzado → Ventana de escalonamiento / Unidad de escalonamiento.", + execution: "Ejecución", + executionSub: "Elige cuándo activar y qué debe hacer esta tarea.", + session: "Sesión", + main: "Principal", + isolated: "Aislada", + sessionHelp: + "Principal publica un evento del sistema. Aislada ejecuta un turno dedicado del agente.", + wakeMode: "Modo de activación", + now: "Ahora", + nextHeartbeat: "Próximo latido", + wakeModeHelp: "Ahora se activa inmediatamente. Próximo latido espera el siguiente ciclo.", + payloadKind: "¿Qué debe ejecutarse?", + systemEvent: "Publicar mensaje en la línea de tiempo principal", + agentTurn: "Ejecutar tarea del asistente (aislada)", + systemEventHelp: + "Envía tu texto a la línea de tiempo principal de la puerta de enlace (ideal para recordatorios/activadores).", + agentTurnHelp: "Inicia una ejecución del asistente en su propia sesión usando tu indicación.", + timeoutSeconds: "Tiempo de espera (segundos)", + timeoutPlaceholder: "Opcional, ej. 90", + timeoutHelp: + "Opcional. Déjalo en blanco para usar el comportamiento de tiempo de espera predeterminado de la puerta de enlace para esta ejecución.", + mainTimelineMessage: "Mensaje de la línea de tiempo principal", + assistantTaskPrompt: "Indicación para la tarea del asistente", + deliverySection: "Entrega", + deliverySub: "Elige dónde se envían los resúmenes de ejecución.", + resultDelivery: "Entrega de resultados", + announceDefault: "Anunciar resumen (predeterminado)", + webhookPost: "Webhook POST", + noneInternal: "Ninguna (interno)", + deliveryHelp: + "Anunciar publica un resumen en el chat. Ninguna mantiene la ejecución interna.", + webhookUrl: "URL del webhook", + channel: "Canal", + webhookPlaceholder: "https://example.com/cron", + channelHelp: "Elige qué canal conectado recibe el resumen.", + webhookHelp: "Envía resúmenes de ejecución a un endpoint webhook.", + to: "Para", + toPlaceholder: "+1555... o ID de chat", + toHelp: "Anulación opcional del destinatario (ID de chat, teléfono o ID de usuario).", + advanced: "Avanzado", + advancedHelp: + "Anulaciones opcionales para garantías de entrega, variación de programación y controles del modelo.", + deleteAfterRun: "Eliminar después de ejecutar", + deleteAfterRunHelp: + "Ideal para recordatorios de un solo uso que deben limpiarse automáticamente.", + clearAgentOverride: "Limpiar anulación de agente", + clearAgentHelp: + "Forza a esta tarea a usar el asistente predeterminado de la puerta de enlace.", + exactTiming: "Tiempo exacto (sin escalonamiento)", + exactTimingHelp: "Ejecutar en límites exactos de cron sin dispersión.", + staggerWindow: "Ventana de escalonamiento", + staggerUnit: "Unidad de escalonamiento", + staggerPlaceholder: "30", + seconds: "Segundos", + model: "Modelo", + modelPlaceholder: "openai/gpt-5.2", + modelHelp: + "Comienza a escribir para seleccionar un modelo conocido o ingresa uno personalizado.", + thinking: "Pensamiento", + thinkingPlaceholder: "bajo", + thinkingHelp: "Usa un nivel sugerido o ingresa un valor específico del proveedor.", + bestEffortDelivery: "Entrega de mejor esfuerzo", + bestEffortHelp: "No fallar la tarea si la entrega misma falla.", + cantAddYet: "Aún no se puede agregar la tarea", + fillRequired: "Completa los campos requeridos a continuación para habilitar el envío.", + fixFields: "Corrige {count} campo para continuar.", + fixFieldsPlural: "Corrige {count} campos para continuar.", + saving: "Guardando...", + saveChanges: "Guardar cambios", + addJob: "Agregar tarea", + cancel: "Cancelar", + }, + jobList: { + allJobs: "todas las tareas", + selectJob: "(selecciona una tarea)", + enabled: "habilitada", + disabled: "deshabilitada", + edit: "Editar", + clone: "Clonar", + disable: "Deshabilitar", + enable: "Habilitar", + run: "Ejecutar", + history: "Historial", + remove: "Eliminar", + }, + jobDetail: { + system: "Sistema", + prompt: "Indicación", + delivery: "Entrega", + agent: "Agente", + }, + jobState: { + status: "Estado", + next: "Próxima", + last: "Última", + }, + runEntry: { + noSummary: "Sin resumen.", + runAt: "Ejecutada a las", + openRunChat: "Abrir chat de ejecución", + next: "Próxima {rel}", + due: "Programada {rel}", + }, + errors: { + nameRequired: "El nombre es requerido.", + scheduleAtInvalid: "Ingresa una fecha/hora válida.", + everyAmountInvalid: "El intervalo debe ser mayor a 0.", + cronExprRequired: "La expresión Cron es requerida.", + staggerAmountInvalid: "El escalonamiento debe ser mayor a 0.", + systemTextRequired: "El texto del sistema es requerido.", + agentMessageRequired: "El mensaje del agente es requerido.", + timeoutInvalid: "Si se establece, el tiempo de espera debe ser mayor a 0 segundos.", + webhookUrlRequired: "La URL del webhook es requerida.", + webhookUrlInvalid: "La URL del webhook debe comenzar con http:// o https://.", + invalidRunTime: "Tiempo de ejecución inválido.", + invalidIntervalAmount: "Cantidad de intervalo inválida.", + cronExprRequiredShort: "Expresión Cron requerida.", + invalidStaggerAmount: "Cantidad de escalonamiento inválida.", + systemEventTextRequired: "Texto de evento del sistema requerido.", + agentMessageRequiredShort: "Mensaje del agente requerido.", + nameRequiredShort: "Nombre requerido.", + }, + }, +}; diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts index 7a973a13992..d763ca04217 100644 --- a/ui/src/i18n/locales/pt-BR.ts +++ b/ui/src/i18n/locales/pt-BR.ts @@ -124,5 +124,6 @@ export const pt_BR: TranslationMap = { zhTW: "繁體中文 (Chinês Tradicional)", ptBR: "Português (Português Brasileiro)", de: "Deutsch (Alemão)", + es: "Español (Espanhol)", }, }; diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index aad258d8bf4..2cf8ca35ec2 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -121,6 +121,7 @@ export const zh_CN: TranslationMap = { zhTW: "繁體中文 (繁体中文)", ptBR: "Português (巴西葡萄牙语)", de: "Deutsch (德语)", + es: "Español (西班牙语)", }, cron: { summary: { diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts index 1165d56fe4e..6fb48680e75 100644 --- a/ui/src/i18n/locales/zh-TW.ts +++ b/ui/src/i18n/locales/zh-TW.ts @@ -121,5 +121,6 @@ export const zh_TW: TranslationMap = { zhTW: "繁體中文 (繁體中文)", ptBR: "Português (巴西葡萄牙語)", de: "Deutsch (德語)", + es: "Español (西班牙語)", }, };