fix: finalize spanish locale support

This commit is contained in:
Darshil
2026-03-04 15:27:06 -08:00
committed by Darshil
parent 9c6847074d
commit b3fb881a73
9 changed files with 370 additions and 4 deletions

View File

@@ -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();
});

View File

@@ -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<LazyLocale, LazyLocaleRegistration> = {
"zh-CN": {
@@ -29,6 +29,10 @@ const LAZY_LOCALE_REGISTRY: Record<LazyLocale, LazyLocaleRegistration> = {
exportName: "de",
loader: () => import("../locales/de.ts"),
},
es: {
exportName: "es",
loader: () => import("../locales/es.ts"),
},
};
export const SUPPORTED_LOCALES: ReadonlyArray<Locale> = [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;
}

View File

@@ -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;

View File

@@ -125,5 +125,6 @@ export const de: TranslationMap = {
zhTW: "繁體中文 (Traditionelles Chinesisch)",
ptBR: "Português (Brasilianisches Portugiesisch)",
de: "Deutsch",
es: "Spanisch (Español)",
},
};

View File

@@ -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: {

347
ui/src/i18n/locales/es.ts Normal file
View File

@@ -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.",
},
},
};

View File

@@ -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)",
},
};

View File

@@ -121,6 +121,7 @@ export const zh_CN: TranslationMap = {
zhTW: "繁體中文 (繁体中文)",
ptBR: "Português (巴西葡萄牙语)",
de: "Deutsch (德语)",
es: "Español (西班牙语)",
},
cron: {
summary: {

View File

@@ -121,5 +121,6 @@ export const zh_TW: TranslationMap = {
zhTW: "繁體中文 (繁體中文)",
ptBR: "Português (巴西葡萄牙語)",
de: "Deutsch (德語)",
es: "Español (西班牙語)",
},
};