/* ───────────────────────────────────────────────────────────────────────────
   Wivexx UI i18n — translate the rendered control room into the chosen language.
   Same approach as icons.js: a TreeWalker + a guarded MutationObserver replace
   visible English text (and title/placeholder/aria-label) using a dictionary,
   caching each node's ORIGINAL English so switching back is lossless.
   English ("en") is a pure no-op, so English users are completely unaffected.
   Exposes window.WivexxI18n = { lang, setLang, t, apply, add }.
   ─────────────────────────────────────────────────────────────────────────── */
(function () {
  "use strict";

  // ── dictionaries: english source string → translation ─────────────────────
  const DICT = {
    es: {
      // top strip / window
      "starting…": "iniciando…",
      "full power": "máxima potencia",
      "low power": "bajo consumo",
      "ready": "listo",
      "Settings": "Configuración",
      "Notifications": "Notificaciones",
      "Performance HUD — live CPU/GPU/RAM": "Panel de rendimiento — CPU/GPU/RAM en vivo",
      "Minimize": "Minimizar",
      "Maximize": "Maximizar",
      "Fullscreen (F11)": "Pantalla completa (F11)",
      "Close to tray": "Cerrar a la bandeja",
      "Toggle performance HUD": "Alternar panel de rendimiento",
      // hero / conversation
      "Talk to Wivexx — say “hey wivexx” or type below.": "Habla con Wivexx — di “hey wivexx” o escribe abajo.",
      "e.g. “what’s on my calendar?” · “take a screenshot” · “social media report”": "p. ej. “¿qué tengo en el calendario?” · “toma una captura” · “informe de redes sociales”",
      // starter chips (the visible text IS the command that gets sent — these
      // Spanish versions must stay valid Wivexx commands)
      "Try one of these": "Prueba uno de estos",
      "Connected to Wivexx on your PC. Ask anything, or try one:":
        "Conectado a Wivexx en tu PC. Pregunta lo que sea, o prueba uno:",
      "what can you do?": "¿qué puedes hacer?",
      "take a screenshot": "toma una captura",
      "set a timer for 5 minutes": "pon un temporizador de 5 minutos",
      "what’s the weather?": "¿cómo está el clima?",
      // first-run welcome tour
      "Welcome to Wivexx": "Bienvenido a Wivexx",
      "Hi, I’m Wivexx": "Hola, soy Wivexx",
      "Your voice assistant for this PC. Say “hey wivexx” and just talk — or type in the box below. That’s all you need to start.":
        "Tu asistente de voz para esta PC. Di “hey wivexx” y simplemente habla — o escribe en el cuadro de abajo. Es todo lo que necesitas para empezar.",
      "Chat and commands mix": "Chat y comandos se mezclan",
      "Ask me anything, or tell me to do things — even mid-sentence. Try “take a screenshot” or “open discord”. Type / to browse every command.":
        "Pregúntame lo que sea, o pídeme hacer cosas — incluso a mitad de frase. Prueba “toma una captura” o “abre discord”. Escribe / para ver todos los comandos.",
      "Explore at your own pace": "Explora a tu ritmo",
      "Goals, social and actions live at the bottom. Settings is the gear up top. Everything else shows up when you need it — no homework required.":
        "Objetivos, social y acciones viven abajo. Configuración es el engrane de arriba. Todo lo demás aparece cuando lo necesitas — sin tarea que estudiar.",
      "skip": "saltar",
      "next →": "siguiente →",
      "let’s go": "¡vamos!",
      "Ask anything, or type / to see all commands…": "Pregunta lo que sea, o escribe / para ver los comandos…",
      "send →": "enviar →",
      "Send": "Enviar",
      "Type a command or question": "Escribe un comando o pregunta",
      "Attach a file (image, video, audio, anything)": "Adjuntar un archivo (imagen, video, audio, lo que sea)",
      "Attach a file": "Adjuntar un archivo",
      "Drop a file to attach": "Suelta un archivo para adjuntarlo",
      // tools / hubs
      "goals": "objetivos",
      "social": "social",
      "actions": "acciones",
      "Tools": "Herramientas",
      "today's goals & to-dos": "objetivos y pendientes de hoy",
      "captions · tracking · deliver": "subtítulos · seguimiento · entregas",
      "autoclicker · macro recorder": "autoclicker · grabadora de macros",
      // behavior modes
      "memory": "memoria",
      "work mode": "modo trabajo",
      "call mode": "modo llamada",
      "performance mode": "modo rendimiento",
      "Behavior controls": "Controles de comportamiento",
      "semantic memory · open": "memoria semántica · abrir",
      "wake word off · push-to-talk": "palabra de activación apagada · pulsar para hablar",
      "freeze background apps + high-performance power": "congelar apps en segundo plano + energía de alto rendimiento",
      // transfer card
      "importing": "importando",
      "storage": "almacenamiento",
      "pause": "pausar",
      "cancel": "cancelar",
      "Hide transfer": "Ocultar transferencia",
      // panel / settings
      "settings": "configuración",
      "Close": "Cerrar",
      "Close panel": "Cerrar panel",
      "configure": "configurar",
      "microphone": "micrófono",
      "language": "idioma",
      "what I can do": "lo que puedo hacer",
      "everything Wivexx can do right now": "todo lo que Wivexx puede hacer ahora mismo",
      "commands": "comandos",
      "your custom voice & text commands": "tus comandos de voz y texto personalizados",
      "shortcuts": "atajos",
      "global keyboard hotkeys": "atajos de teclado globales",
      "phones & clients": "teléfonos y clientes",
      "devices connected to Wivexx": "dispositivos conectados a Wivexx",
      "welcome tour": "tour de bienvenida",
      "replay the quick 3-step intro": "vuelve a ver la intro rápida de 3 pasos",
      "More Wivexx settings will live here.": "Más ajustes de Wivexx estarán aquí.",
      "System default microphone": "Micrófono predeterminado del sistema",
      "System default": "Predeterminado del sistema",
      "system default": "predeterminado del sistema",
      "Choose which microphone Wivexx listens to. Takes effect on your next command (restart if the wake word still uses the old mic).":
        "Elige qué micrófono escucha Wivexx. Se aplica en tu próximo comando (reinicia si la palabra de activación sigue usando el micrófono anterior).",
      "Microphone updated": "Micrófono actualizado",
      "Choose Wivexx’s interface language. It automatically understands whatever language you speak.":
        "Elige el idioma de la interfaz de Wivexx. Entiende automáticamente cualquier idioma que hables.",
      "English": "Inglés",
      "Spanish": "Español",
      "Language updated": "Idioma actualizado",
      "Language updated — restart to change the voice input language": "Idioma actualizado — reinicia para cambiar el idioma de entrada de voz",
      // hero caption + status states (exact strings incl. the … suffix)
      "wivexx is thinking": "wivexx está pensando",
      "listening…": "escuchando…",
      "thinking…": "pensando…",
      "listening": "escuchando",
      "thinking": "pensando",
      "speaking": "hablando",
      "standby": "en espera",
      "processing": "procesando",
      "idle": "inactivo",
      "online": "en línea",
      "offline": "sin conexión",
      // common toasts / status / actions
      "loading…": "cargando…",
      "Working…": "Trabajando…",
      "Copied": "Copiado",
      "Saved": "Guardado",
      "Cancel": "Cancelar",
      "Save": "Guardar",
      "Delete": "Eliminar",
      "Remove": "Quitar",
      "Add": "Añadir",
      "Edit": "Editar",
      "Done": "Hecho",
      "Retry": "Reintentar",
      "Open": "Abrir",
      "Yes": "Sí",
      "No": "No",
      "goals": "objetivos",
      "activity": "actividad",
      "notifications": "notificaciones",
      "No notifications yet": "Aún no hay notificaciones",
      "no notifications": "sin notificaciones",
      "Check for updates": "Buscar actualizaciones",
      "report a problem": "reportar un problema",
      "send report": "enviar reporte",

      // ── deeper panels (goals · social · actions · captions · reel · memory · etc.) ──
      "(auto-named if blank)": "(nombre automático si se deja vacío)",
      "(none captured)": "(ninguno capturado)",
      "action recorder": "grabador de acciones",
      "active recording — fire it from a shortcut, even in-game": "grabación activa — actívala desde un atajo, incluso en el juego",
      "add": "añadir",
      "add 15s of timeline": "añadir 15s de línea de tiempo",
      "add a fact": "añadir un dato",
      "add a key input": "añadir una entrada de tecla",
      "add a wake word or phrase": "añadir una palabra o frase de activación",
      "add goal": "añadir objetivo",
      "add key input": "añadir entrada de tecla",
      "add media": "añadir medios",
      "add shortcut": "añadir atajo",
      "ALL CAPS": "MAYÚSCULAS",
      "applies next launch": "se aplica al próximo inicio",
      "audio outputs": "salidas de audio",
      "auto-brighten": "auto-brillo",
      "auto-clicker": "auto-clic",
      "auto-edit settings": "ajustes de auto-edición",
      "auto-import": "auto-importar",
      "autoclicker & inputs": "auto-clic y entradas",
      "Automatic": "Automático",
      "back to projects": "volver a proyectos",
      "background box": "caja de fondo",
      "bold": "negrita",
      "box color": "color de la caja",
      "brightness": "brillo",
      "browse…": "explorar…",
      "Builds the reel then opens the captions editor.": "Crea el reel y luego abre el editor de subtítulos.",
      "cancelling…": "cancelando…",
      "Capabilities catalog unavailable.": "Catálogo de capacidades no disponible.",
      "caption studio": "estudio de subtítulos",
      "captions editor": "editor de subtítulos",
      "capture keys": "capturar teclas",
      "change": "cambiar",
      "Checking…": "Comprobando…",
      "clear": "limpiar",
      "clear all": "limpiar todo",
      "clear selection": "limpiar selección",
      "clear timeline": "limpiar línea de tiempo",
      "click a caption above to keyframe its scale / rotation / opacity over time.": "haz clic en un subtítulo arriba para animar su escala / rotación / opacidad en el tiempo.",
      "Click a clip, overlay or music track on the timeline to edit just that item here.": "Haz clic en un clip, superposición o pista de música en la línea de tiempo para editar solo ese elemento aquí.",
      "click a colour to copy its hex": "haz clic en un color para copiar su hex",
      "click or drag to scrub": "haz clic o arrastra para desplazarte",
      "Click rebind, then press the new combo — keyboard keys and mouse side/middle buttons both work.": "Haz clic en reasignar, luego pulsa la nueva combinación — funcionan teclas del teclado y botones laterales/central del ratón.",
      "clicks / sec": "clics / seg",
      "client delivery": "entrega a clientes",
      "colour": "color",
      "command to run": "comando a ejecutar",
      "compress": "comprimir",
      "contact": "contacto",
      "convert": "convertir",
      "custom colour": "color personalizado",
      "custom position — drag on the preview": "posición personalizada — arrastra en la vista previa",
      "custom shortcuts — run any voice command from a combo": "atajos personalizados — ejecuta cualquier comando de voz desde una combinación",
      "cut tool — click the timeline to cut": "herramienta de corte — haz clic en la línea de tiempo para cortar",
      "danger": "peligro",
      "day streak": "días seguidos",
      "delete keyframe": "eliminar fotograma clave",
      "delete project": "eliminar proyecto",
      "deletes go to the Recycle Bin — recoverable.": "los borrados van a la Papelera de reciclaje — recuperables.",
      "deny — ignore this drive": "denegar — ignorar esta unidad",
      "desktop only — clicks land at exact positions": "solo escritorio — los clics caen en posiciones exactas",
      "discard": "descartar",
      "Dismiss": "Descartar",
      "done this week": "hecho esta semana",
      "drag files onto Wivexx to add them · click or drag a tile onto the timeline · drag a tile onto a folder to file it": "arrastra archivos a Wivexx para añadirlos · haz clic o arrastra una miniatura a la línea de tiempo · arrastra una miniatura a una carpeta para archivarla",
      "drag to change duration": "arrastra para cambiar la duración",
      "drag to reorder": "arrastra para reordenar",
      "drag to resize the inspector": "arrastra para redimensionar el inspector",
      "drag to resize the media panel": "arrastra para redimensionar el panel de medios",
      "drag to resize the timeline": "arrastra para redimensionar la línea de tiempo",
      "drag to resize this track": "arrastra para redimensionar esta pista",
      "drag to trim the end": "arrastra para recortar el final",
      "drag to trim the start": "arrastra para recortar el inicio",
      "drive cleanup": "limpieza de disco",
      "duck under speech": "atenuar bajo la voz",
      "e.g. jump spam": "p. ej. saltos repetidos",
      "e.g. Osmo Pocket, camera card": "p. ej. Osmo Pocket, tarjeta de cámara",
      "e.g. wybexx · hey vex · computer": "p. ej. wybexx · hey vex · computadora",
      "Each input rapid-fires the key(s) you capture (a single key like": "Cada entrada dispara rápidamente la(s) tecla(s) que captures (una sola tecla como",
      "ease": "suavizado",
      "edit fact": "editar dato",
      "edit goal": "editar objetivo",
      "email or @handle": "correo o @usuario",
      "emotion": "emoción",
      "enabled": "activado",
      "ends in": "termina en",
      "environments": "entornos",
      "export": "exportar",
      "Exports straight to a file — you can keep working while it renders in the background.": "Exporta directamente a un archivo — puedes seguir trabajando mientras se renderiza en segundo plano.",
      "external storage auto-import": "auto-importación de almacenamiento externo",
      "fade s": "fundido s",
      "fire": "disparar",
      "first run also fetches the speech model — hang tight": "el primer uso también descarga el modelo de voz — espera un momento",
      "forget": "olvidar",
      "Forget": "Olvidar",
      "format": "formato",
      "gallery / folder name": "nombre de galería / carpeta",
      "game mouse-look sensitivity — raise it if a recorded spin replays too small": "sensibilidad del ratón en juego — súbela si un giro grabado se reproduce muy pequeño",
      "Global shortcuts aren’t available on this system yet. Restart Wivexx and try again.": "Los atajos globales aún no están disponibles en este sistema. Reinicia Wivexx e inténtalo de nuevo.",
      "high": "alto",
      "highlight": "resaltado",
      "IG handle": "usuario de IG",
      "ignored": "ignorado",
      "imports land in": "las importaciones van a",
      "in progress": "en progreso",
      "italic": "cursiva",
      "JPEG quality": "calidad JPEG",
      "key (e.g. camera)": "clave (p. ej. cámara)",
      "key combo": "combinación de teclas",
      "key inputs — autoclick any key or combo": "entradas de tecla — auto-clic de cualquier tecla o combinación",
      "key(s) to press": "tecla(s) a pulsar",
      "keyboard · built-in actions": "teclado · acciones integradas",
      "label (optional)": "etiqueta (opcional)",
      "length": "duración",
      "letter spacing": "espaciado entre letras",
      "listening to the audio — this runs on your PC": "escuchando el audio — esto se ejecuta en tu PC",
      "loading devices…": "cargando dispositivos…",
      "loading microphones…": "cargando micrófonos…",
      "loading preview…": "cargando vista previa…",
      "looks": "estilos",
      "loop to fill": "repetir para rellenar",
      "magnet: snap clips edge-to-edge": "imán: ajustar clips borde con borde",
      "mascot studio": "estudio de mascota",
      "max": "máx",
      "max secs": "seg máx",
      "max words": "palabras máx",
      "media": "medios",
      "min": "mín",
      "mode": "modo",
      "motion": "movimiento",
      "motion keyframes": "fotogramas de movimiento",
      "mouse button": "botón del ratón",
      "mouse capture": "captura del ratón",
      "move down": "bajar",
      "move up": "subir",
      "music": "música",
      "music, tunes": "música, canciones",
      "mute this clip": "silenciar este clip",
      "name": "nombre",
      "name this recording": "nombra esta grabación",
      "new folder": "nueva carpeta",
      "new goal": "nuevo objetivo",
      "New reel project": "Nuevo proyecto de reel",
      "no analysis yet.": "aún no hay análisis.",
      "no clips yet — add a few to build a reel.": "aún no hay clips — añade algunos para crear un reel.",
      "No custom shortcuts yet.": "Aún no hay atajos personalizados.",
      "no custom words yet — Wivexx still wakes to the built-ins below": "aún no hay palabras personalizadas — Wivexx sigue despertando con las integradas de abajo",
      "no finished items yet.": "aún no hay elementos terminados.",
      "no folder chosen": "ninguna carpeta elegida",
      "No key inputs yet. Add one below to rapid-fire (or single-press) a key from its own shortcut.": "Aún no hay entradas de tecla. Añade una abajo para disparar rápido (o pulsar una vez) una tecla desde su propio atajo.",
      "no keyframes — add one at the playhead": "sin fotogramas clave — añade uno en el cabezal",
      "no matching command": "ningún comando coincidente",
      "No open goals. Add one above.": "No hay objetivos abiertos. Añade uno arriba.",
      "No goals yet — type one above, or just tell Wivexx: “add a goal to finish the report”.":
        "Aún no hay objetivos — escribe uno arriba, o simplemente dile a Wivexx: “agrega un objetivo de terminar el informe”.",
      "No output commands yet.": "Aún no hay comandos de salida.",
      "no saved projects yet — create one to get started.": "aún no hay proyectos guardados — crea uno para empezar.",
      "no speech": "sin voz",
      "no speech detected": "no se detectó voz",
      "none": "ninguno",
      "none attached": "ninguno adjunto",
      "Not tracking any accounts yet. Add an Instagram handle below — Wivexx checks it and graphs your growth over time.": "Aún no se sigue ninguna cuenta. Añade un usuario de Instagram abajo — Wivexx lo revisa y grafica tu crecimiento con el tiempo.",
      "nothing happening right now.": "no pasa nada ahora mismo.",
      "Notification settings unavailable. Restart Wivexx and try again.": "Ajustes de notificaciones no disponibles. Reinicia Wivexx e inténtalo de nuevo.",
      "number of times to play": "número de veces a reproducir",
      "one image was too large after resizing — skipped": "una imagen era demasiado grande tras redimensionar — omitida",
      "one-click style presets": "estilos predefinidos de un clic",
      "open": "abrir",
      "open / close": "abrir / cerrar",
      "open spotify": "abrir spotify",
      "opening…": "abriendo…",
      "outline": "contorno",
      "Output": "Salida",
      "output device this switches to": "dispositivo de salida al que cambia",
      "overlay": "superposición",
      "overlays": "superposiciones",
      "owns a Sony Alpha mirrorless": "tiene una Sony Alpha sin espejo",
      "photo name sequence": "secuencia de nombres de fotos",
      "play / pause": "reproducir / pausar",
      "play / pause preview": "reproducir / pausar vista previa",
      "points": "puntos",
      "Press": "Pulsa",
      "press keys…": "pulsa teclas…",
      "press play ▶ or scrub the timeline": "pulsa reproducir ▶ o desplázate por la línea de tiempo",
      "press play or scrub the timeline": "pulsa reproducir o desplázate por la línea de tiempo",
      "presses / sec": "pulsaciones / seg",
      "priority": "prioridad",
      "project name": "nombre del proyecto",
      "public share link": "enlace público para compartir",
      "punchier, add a question, less salesy…": "más impactante, añade una pregunta, menos comercial…",
      "RAW only — the untouched originals, no developing": "solo RAW — los originales sin tocar, sin revelar",
      "reading colours…": "leyendo colores…",
      "rebind": "reasignar",
      "recent": "recientes",
      "reclaimable": "recuperable",
      "recording": "grabando",
      "recurring notifications": "notificaciones recurrentes",
      "Reel exported ✓": "Reel exportado ✓",
      "reel maker": "creador de reels",
      "Reel ready ✓ — see notifications": "Reel listo ✓ — ver notificaciones",
      "reel studio": "estudio de reels",
      "Refresh now": "Actualizar ahora",
      "remind at": "recordar a las",
      "reminder": "recordatorio",
      "Remove attachment": "Quitar adjunto",
      "remove beat": "quitar beat",
      "remove from project": "quitar del proyecto",
      "repeat": "repetir",
      "repeat until you hit the panic key or stop": "repetir hasta que pulses la tecla de pánico o pares",
      "Report sent": "Reporte enviado",
      "reset position": "restablecer posición",
      "reset to defaults": "restablecer valores por defecto",
      "rotate": "rotar",
      "save": "guardar",
      "save as": "guardar como",
      "saved recordings": "grabaciones guardadas",
      "search": "buscar",
      "search facts…": "buscar datos…",
      "segments": "segmentos",
      "select all": "seleccionar todo",
      "Select clips above in the order you want them, then auto-edit cuts the silences and assembles a tight cut on V1.": "Selecciona los clips arriba en el orden que quieras, luego la auto-edición corta los silencios y arma un corte ajustado en V1.",
      "select for auto-edit (order matters)": "seleccionar para auto-edición (el orden importa)",
      "send a report": "enviar un reporte",
      "send error:": "error de envío:",
      "Send report": "Enviar reporte",
      "sending…": "enviando…",
      "session topics": "temas de la sesión",
      "set shortcut": "asignar atajo",
      "shortcut": "atajo",
      "shortcut to trigger it": "atajo para activarlo",
      "shortcuts & wake words": "atajos y palabras de activación",
      "show in Explorer": "mostrar en el Explorador",
      "shutting down in": "apagando en",
      "single": "único",
      "size": "tamaño",
      "social tracking": "seguimiento social",
      "sound": "sonido",
      "Spam a mouse button at a set speed. Press the shortcut to start/stop (toggle), or hold it (hold mode).": "Dispara un botón del ratón a una velocidad fija. Pulsa el atajo para iniciar/parar (alternar), o mantenlo pulsado (modo mantener).",
      "speakers": "altavoces",
      "split in half": "dividir por la mitad",
      "split on punctuation": "dividir en la puntuación",
      "spoken phrase → output device": "frase hablada → dispositivo de salida",
      "start / stop recording shortcut": "atajo de iniciar / parar grabación",
      "start a fresh edit in the studio": "empezar una edición nueva en el estudio",
      "still working — longer clips take a bit on CPU": "aún trabajando — los clips más largos tardan un poco en la CPU",
      "Stop tracking": "Dejar de seguir",
      "style": "estilo",
      "tab has transitions, format and export.": "la pestaña tiene transiciones, formato y exportación.",
      "talking": "hablando",
      "Tap a number to see its last-30-days graph.": "Toca un número para ver su gráfico de los últimos 30 días.",
      "text": "texto",
      "The action recorder isn’t available on this system (needs pynput). Restart Wivexx and try again.": "El grabador de acciones no está disponible en este sistema (necesita pynput). Reinicia Wivexx e inténtalo de nuevo.",
      "The autoclicker isn’t available on this system yet. Restart Wivexx and try again.": "El auto-clic aún no está disponible en este sistema. Reinicia Wivexx e inténtalo de nuevo.",
      "the pack": "el paquete",
      "timeline": "línea de tiempo",
      "timer": "temporizador",
      "to abort instantly.": "para abortar al instante.",
      "track": "pista",
      "track an Instagram account": "seguir una cuenta de Instagram",
      "transcribing the video…": "transcribiendo el video…",
      "transcription cancelled.": "transcripción cancelada.",
      "transitions": "transiciones",
      "translating…": "traduciendo…",
      "Try again": "Intentar de nuevo",
      "type": "tipo",
      "underline": "subrayado",
      "untitled reel": "reel sin título",
      "Update available": "Actualización disponible",
      "updates": "actualizaciones",
      "value": "valor",
      "video track": "pista de video",
      "voice · wake words": "voz · palabras de activación",
      "volume": "volumen",
      "What happened, or what would you love to see?": "¿Qué pasó, o qué te encantaría ver?",
      "what is the post about?": "¿de qué trata la publicación?",
      "what needs doing?": "¿qué hay que hacer?",
      "what to send": "qué enviar",
      "what Wivexx remembers about you": "lo que Wivexx recuerda de ti",
      "when this drive is connected": "cuando esta unidad esté conectada",
      "work": "trabajo",
      "write a message or attach a photo first.": "escribe un mensaje o adjunta una foto primero.",
      "you can close this — it keeps working in notifications": "puedes cerrar esto — sigue funcionando en las notificaciones",
      "your environments": "tus entornos",
      "your message": "tu mensaje",
      "your open / close commands": "tus comandos de abrir / cerrar",
      "your projects": "tus proyectos",
      "zoom in": "acercar",
      "zoom out": "alejar",
      "auto-editing…": "auto-editando…",
      "moved to Recycle Bin": "movido a la Papelera de reciclaje",
      "✨ auto-editing…": "✨ auto-editando…",
      "🗑 moved to Recycle Bin": "🗑 movido a la Papelera de reciclaje",
      "delete": "eliminar",
      "edit": "editar",
      "remove": "quitar",
      "cancel": "cancelar",
      "(.ARW → full-quality JPG)": "(.ARW → JPG de máxima calidad)",
      "(advanced — scale · rotation · opacity)": "(avanzado — escala · rotación · opacidad)",
      "(carousel ok)": "(carrusel permitido)",
      "(chain emotions into a story)": "(encadena emociones en una historia)",
      "(hashtags researched per platform)": "(hashtags investigados por plataforma)",
      "(hold any image — it bobs with Wivexx)": "(sostén cualquier imagen — se mueve con Wivexx)",
      "(logos · stickers · B-roll)": "(logos · stickers · B-roll)",
      "(multi-select ok)": "(selección múltiple permitida)",
      "(optional — I study the profile’s voice)": "(opcional — estudio el estilo del perfil)",
      "(optional — if you want a reply)": "(opcional — si quieres una respuesta)",
      "(up to 3)": "(hasta 3)",
      "@handle": "@usuario",
      "@username": "@usuario",
      "allow — auto-import its photos & videos": "permitir — auto-importar sus fotos y videos",
      "app path · .exe, file, folder or https:// URL": "ruta de la app · .exe, archivo, carpeta o URL https://",
      "behind-the-scenes from today’s shoot": "detrás de cámaras de la sesión de hoy",
      "Build social reels from your clips & photos — trim, add transitions, overlays, music and captions, then export. Open a project to launch the full editor.": "Crea reels sociales con tus clips y fotos — recorta, añade transiciones, superposiciones, música y subtítulos, luego exporta. Abre un proyecto para lanzar el editor completo.",
      "built-in: you can always say “switch output to <device name>” without setting anything up.": "integrado: siempre puedes decir “cambiar salida a <nombre del dispositivo>” sin configurar nada.",
      "captions language:": "idioma de subtítulos:",
      "due (optional)": "vence (opcional)",
      "environment name · say “activate <this> environment”": "nombre del entorno · di “activar entorno <este>”",
      "extra spoken aliases (comma separated, optional)": "alias hablados adicionales (separados por comas, opcional)",
      "Import clips & photos, trim / split / reorder, add transitions, overlays and music — then build one video and jump to captions.": "Importa clips y fotos, recorta / divide / reordena, añade transiciones, superposiciones y música — luego crea un video y salta a los subtítulos.",
      "importance (1–10)": "importancia (1–10)",
      "items to OPEN — one per line (app, path, folder or URL)": "elementos a ABRIR — uno por línea (app, ruta, carpeta o URL)",
      "key(s) to press · press any key or a combo": "tecla(s) a pulsar · pulsa cualquier tecla o combinación",
      "name (optional)": "nombre (opcional)",
      "nickname · shown in the import HUD": "apodo · se muestra en el HUD de importación",
      "No custom commands yet. Add one below — then say “open <name>”.": "Aún no hay comandos personalizados. Añade uno abajo — luego di “abrir <nombre>”.",
      "No drives known yet. Plug in an SD card or USB drive and Wivexx will ask whether to import — it then appears here.": "Aún no se conocen unidades. Conecta una tarjeta SD o USB y Wivexx preguntará si importar — luego aparece aquí.",
      "No environments yet. Build one below, then say “activate <name> environment”.": "Aún no hay entornos. Crea uno abajo, luego di “activar entorno <nombre>”.",
      "no photos — I’ll work from your description": "sin fotos — trabajaré con tu descripción",
      "No recordings yet. Hit record, do your thing, then stop & save.": "Aún no hay grabaciones. Pulsa grabar, haz lo tuyo, luego para y guarda.",
      "not quite right? tell me what to change — I’ll give 3 new options": "¿no está del todo bien? dime qué cambiar — te daré 3 nuevas opciones",
      "panic key — aborts a running loop (press any key or combo)": "tecla de pánico — aborta un bucle en marcha (pulsa cualquier tecla o combinación)",
      "quick-play shortcut — plays the active recording (press again to stop)": "atajo de reproducción rápida — reproduce la grabación activa (pulsa de nuevo para parar)",
      "single clip — tap “+ add” to chain beats into a story (e.g. 😐 → 😠 shake → 😮)": "clip único — toca “+ añadir” para encadenar momentos en una historia (p. ej. 😐 → 😠 temblar → 😮)",
      "spoken command · e.g. “change to output 1”": "comando hablado · p. ej. “cambiar a salida 1”",
      "spoken name · say “open <this>”": "nombre hablado · di “abrir <este>”",
      "tag (optional) · sorts into photos/<tag> & video/<tag>": "etiqueta (opcional) · ordena en photos/<tag> y video/<tag>",
      "to stop & save from in-game.": "para parar y guardar desde el juego.",
      "Turn Wivexx’s automatic spoken notifications on or off. Everything keeps working — Wivexx just won’t announce the ones you switch off.": "Activa o desactiva las notificaciones habladas automáticas de Wivexx. Todo sigue funcionando — Wivexx simplemente no anunciará las que desactives.",
      "Update & relaunch": "Actualizar y reiniciar",
      "· auto-detected": "· detectado automáticamente",
      "…or press": "…o pulsa",
      "custom": "personalizado",
      "1 · the content": "1 · el contenido",
      "2 · who is it for": "2 · para quién es",
      "3 · platforms": "3 · plataformas",
      "No stored facts match.": "No hay datos que coincidan.",
      "No stored facts yet. Add one below, or just tell Wivexx “remember that …”.": "Aún no hay datos guardados. Añade uno abajo, o simplemente dile a Wivexx “recuerda que …”.",
      "analysing the content…": "analizando el contenido…",
      "revising the pack…": "revisando el paquete…",
      "spoken name · say “open <this>”": "nombre hablado · di “abrir <este>”",
      "JPG / PNG — develop the RAWs into finished JPGs (recommended)": "JPG / PNG — revela los RAW en JPG terminados (recomendado)",
      "RAW + JPG — developed JPGs plus the original RAW files": "RAW + JPG — JPG revelados más los archivos RAW originales",
      "make the link public (anyone with the link)": "hacer el enlace público (cualquiera con el enlace)",
      "1 · source folder": "1 · carpeta de origen",
      "2 · develop": "2 · revelar",
      "3 · deliver to google drive": "3 · entregar a google drive",
      "Keep Wivexx current — updates install themselves and relaunch. Your settings, memory and projects are never touched.": "Mantén Wivexx al día — las actualizaciones se instalan solas y reinician. Tus ajustes, memoria y proyectos nunca se tocan.",
      "Up to date": "Al día",
      "You’re on the latest version.": "Tienes la última versión.",
      "revising the pack…": "revisando el paquete…",
      "analysing the content…": "analizando el contenido…",
      "+ add current as beat": "+ añadir actual como momento",
      // ── settings menu (v1.29) ──
      "general": "general",
      "voice & audio": "voz y audio",
      "performance": "rendimiento",
      "manage": "gestionar",
      "Launch on startup": "Abrir al iniciar",
      "Open Wivexx automatically when Windows starts": "Abre Wivexx automáticamente cuando arranca Windows",
      "The ✕ button hides Wivexx instead of quitting it": "El botón ✕ oculta Wivexx en vez de cerrarlo",
      "Floating orb when minimized": "Orbe flotante al minimizar",
      "Show the transparent orb overlay when you minimize": "Muestra el orbe transparente cuando minimizas",
      "Mute voice": "Silenciar voz",
      "Best-in-class wake detection": "Detección de activación de primer nivel",
      "Use a dedicated on-device model to hear the wake word — far fewer accidental activations (recommended)":
        "Usa un modelo dedicado en el dispositivo para oír la palabra de activación — muchas menos activaciones accidentales (recomendado)",
      "Only wake for my voice": "Solo activar con mi voz",
      "Ignore the wake word unless it’s you — say “enroll my voice” first to set this up":
        "Ignora la palabra de activación si no eres tú — di “registra mi voz” primero para configurarlo",
      "Reply on-screen only — don’t speak out loud": "Responde solo en pantalla — sin hablar en voz alta",
      "Voice volume": "Volumen de voz",
      "How loudly Wivexx speaks": "Qué tan alto habla Wivexx",
      "Low-resource mode": "Modo de bajos recursos",
      "Lighter speech model + paused background work": "Modelo de voz más ligero + trabajo en segundo plano en pausa",
      "Reduce animations": "Reducir animaciones",
      "A calmer orb for lower CPU/GPU use": "Un orbe más calmado para menos uso de CPU/GPU",
      "Startup greeting": "Saludo al iniciar",
      "Wivexx says hello when it launches": "Wivexx saluda cuando se inicia",
      "Disk-space warnings": "Avisos de espacio en disco",
      "Warn when a drive is running low on space": "Avisa cuando una unidad se está quedando sin espacio",
      "Choose which microphone Wivexx listens to.": "Elige qué micrófono escucha Wivexx.",
      "On": "Activado",
      "Off": "Desactivado",
      // ── settings hub + new toggles (v1.34) ──
      "🔄 updates": "🔄 actualizaciones",
      "⚙ settings": "⚙ configuración",
      "Auto-update on startup": "Actualización automática al iniciar",
      "Install new Wivexx versions automatically when it boots (your data is always kept)":
        "Instala nuevas versiones de Wivexx automáticamente al arrancar (tus datos siempre se conservan)",
      "Overlay in optimized mode": "Superposición en modo optimizado",
      "Auto-show the transparent performance overlay in optimized/gaming mode (Ctrl+Shift+O toggles it anytime)":
        "Muestra la superposición de rendimiento transparente en modo optimizado/juego (Ctrl+Shift+O la alterna en cualquier momento)",
      // ── update panel states ──
      "Couldn't check for updates.": "No se pudieron buscar actualizaciones.",
      "Couldn't check for updates right now.": "No se pudieron buscar actualizaciones ahora mismo.",
      "Downloading update…": "Descargando actualización…",
      "Update downloaded — Wivexx is relaunching…": "Actualización descargada — Wivexx se está reiniciando…",
      "Update failed.": "La actualización falló.",
      "✅ You're on the latest version": "✅ Tienes la última versión",
      "You're on the latest version.": "Tienes la última versión.",
    },
  };

  const norm = (s) => (s == null ? "" : String(s)).replace(/\s+/g, " ").trim();
  const orig = new WeakMap();          // node/attr-key → original English text
  let lang = "en";

  function dict() { return DICT[lang] || null; }

  function t(key) {
    const d = dict();
    if (!d) return key;
    return d[norm(key)] || key;
  }

  // translate one text node (cache original; restore for en)
  function doText(node) {
    if (!node || node.nodeType !== 3) return;
    const p = node.parentNode;
    if (!p) return;
    const tag = p.nodeName;
    if (tag === "SCRIPT" || tag === "STYLE" || tag === "TEXTAREA") return;
    if (p.closest && p.closest("[data-no-i18n]")) return;
    let en = orig.get(node);
    if (en === undefined) {
      if (!norm(node.nodeValue)) return;          // whitespace-only
      en = node.nodeValue;
      orig.set(node, en);
    }
    // IMPORTANT: only write when the value actually changes — writing the same
    // value still fires a MutationObserver record and would loop forever.
    let out = en;
    if (lang !== "en") {
      const d = dict();
      const tr = d && d[norm(en)];
      if (tr != null) {
        // preserve leading/trailing whitespace but swap the CORE for the
        // translation. (Can't use en.replace(norm(en), tr): norm collapses
        // internal   / newlines, so norm(en) isn't a literal substring of en
        // and the replace would silently no-op.)
        const lead = (en.match(/^\s*/) || [""])[0];
        const trail = (en.match(/\s*$/) || [""])[0];
        out = lead + tr + trail;
      }
    }
    if (node.nodeValue !== out) node.nodeValue = out;
  }

  const ATTRS = ["title", "placeholder", "aria-label"];
  function doAttrs(el) {
    if (!el || el.nodeType !== 1 || !el.getAttribute) return;
    if (el.closest && el.closest("[data-no-i18n]")) return;
    for (const a of ATTRS) {
      if (!el.hasAttribute(a)) continue;
      const kkey = a + "";
      let store = orig.get(el);
      if (!store) { store = {}; orig.set(el, store); }
      if (store[kkey] === undefined) store[kkey] = el.getAttribute(a);
      const en = store[kkey];
      let out = en;
      if (lang !== "en") {
        const d = dict();
        const tr = d && d[norm(en)];
        if (tr != null) out = tr;
      }
      if (el.getAttribute(a) !== out) el.setAttribute(a, out);   // guard: no-op if unchanged
    }
  }

  function walk(root) {
    if (!root) return;
    try {
      if (root.nodeType === 1) doAttrs(root);
      const tw = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null);
      let n;
      while ((n = tw.nextNode())) {
        if (n.nodeType === 3) doText(n);
        else doAttrs(n);
      }
    } catch (e) { /* never let translation break the UI */ }
  }

  let obs = null, busy = false;
  function apply() {
    if (busy) return;
    busy = true;
    try { walk(document.body); } finally { busy = false; }
  }

  function start() {
    apply();
    if (obs || !window.MutationObserver) return;
    obs = new MutationObserver((muts) => {
      if (busy) return;
      busy = true;
      try {
        for (const m of muts) {
          if (m.type === "childList") m.addedNodes.forEach((nd) => {
            if (nd.nodeType === 3) doText(nd);
            else if (nd.nodeType === 1) walk(nd);
          });
          else if (m.type === "characterData") doText(m.target);
          else if (m.type === "attributes" && m.target) doAttrs(m.target);
        }
      } finally { busy = false; }
    });
    // NOTE: we do NOT observe `attributes` — translating an attribute would fire
    // an attribute mutation and risk a loop. Attributes are handled in the initial
    // walk + whenever a subtree is added (childList), which covers dynamic panels.
    obs.observe(document.body, { childList: true, subtree: true, characterData: true });
  }

  const I18n = {
    get lang() { return lang; },
    t,
    apply,
    add(langCode, map) { DICT[langCode] = Object.assign(DICT[langCode] || {}, map || {}); },
    setLang(next) {
      next = (next || "en").toLowerCase();
      // v1.51: interface language is en/es only. A legacy "auto" falls through to
      // "en" (spoken-language detection is now always automatic, not a UI choice).
      lang = (next === "es") ? "es" : "en";
      apply();
      try { document.documentElement.setAttribute("lang", lang); } catch (e) {}
    },
  };
  window.WivexxI18n = I18n;

  // initial language: backend injects window.__WVX_LANG before this loads
  try {
    const boot = (window.__WVX_LANG || "en").toLowerCase();
    lang = (boot === "es") ? "es" : "en";   // v1.51: legacy "auto" → en (interface only)
  } catch (e) { lang = "en"; }

  if (document.readyState === "loading")
    document.addEventListener("DOMContentLoaded", start);
  else start();
})();
