Browse Source

feat(i18n): add Spanish (es) locale

  Full European Spanish translation — frontend/src/i18n/locales/es.ts
  covers all 4899 keys with placeholders, plural forms, and inline
  markup preserved. Registered in i18n/index.ts (resources,
  supportedLngs, availableLanguages) and selectable as "Español".

  check-i18n-parity.mjs auto-discovers the new file; added an
  ES_COGNATES allow-list for genuine Spanish cognates and brand/format
  tokens so Check 4 does not flag them as untranslated leaks. Brings the
  supported-language count to 9.
maziggy 6 days ago
parent
commit
a51d59eabf

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
 ## [0.2.5b1] - Unreleased
 ## [0.2.5b1] - Unreleased
 
 
 ### Added
 ### Added
+- **Spanish (es) translation (#1243, requested by @MiguelAngelLV)** — Bambuddy now ships a full European Spanish locale. New `frontend/src/i18n/locales/es.ts` translates all 4899 keys with placeholders, plural forms, and inline markup preserved; registered in `frontend/src/i18n/index.ts` and selectable as "Español" in the language picker. The parity checker auto-discovers the file — `frontend/scripts/check-i18n-parity.mjs` gained an `ES_COGNATES` allow-list for genuine Spanish cognates and brand/format tokens. Brings the supported-language count to 9 (en / de / es / fr / it / ja / pt-BR / zh-CN / zh-TW). Parity green, frontend build clean.
 - **Currency: Belize Dollars (BZD) added to the Settings → Cost currency dropdown (#1454, requested by @PLGuerraDesigns)** — Reporter accurately tracks 3D-printing filament costs in his local currency and BZD wasn't selectable, forcing a manual 2:1 mental conversion from USD. Added `BZD: 'BZ$'` to `frontend/src/utils/currency.ts` next to MXN (Americas dollar-prefix grouping); `getCurrencySymbol('BZD')` returns `'BZ$'` and the SUPPORTED_CURRENCIES list now has 30 entries. Unit test added in `frontend/src/__tests__/utils/currency.test.ts` covering the symbol lookup and presence in SUPPORTED_CURRENCIES; entry-count assertion bumped to 30 so any future additions/removals are caught immediately. 14 currency tests green; frontend build clean.
 - **Currency: Belize Dollars (BZD) added to the Settings → Cost currency dropdown (#1454, requested by @PLGuerraDesigns)** — Reporter accurately tracks 3D-printing filament costs in his local currency and BZD wasn't selectable, forcing a manual 2:1 mental conversion from USD. Added `BZD: 'BZ$'` to `frontend/src/utils/currency.ts` next to MXN (Americas dollar-prefix grouping); `getCurrencySymbol('BZD')` returns `'BZ$'` and the SUPPORTED_CURRENCIES list now has 30 entries. Unit test added in `frontend/src/__tests__/utils/currency.test.ts` covering the symbol lookup and presence in SUPPORTED_CURRENCIES; entry-count assertion bumped to 30 so any future additions/removals are caught immediately. 14 currency tests green; frontend build clean.
 - **Connection Diagnostic — self-service triage for "printer won't connect / won't print"** — A triage review of recently-closed issues found roughly a third were user-side setup errors (printer not in LAN developer mode, blocked ports, Docker bridge networking, wrong access code, printer on a different subnet), each costing a multi-round-trip "enable debug logging → build a support bundle → upload it" exchange. A new diagnostic (`backend/app/services/printer_diagnostic.py`) runs those checks automatically: TCP reachability of MQTT 8883 / FTPS 990 / RTSPS 322, LAN developer mode, Docker network mode, printer/host subnet match, and MQTT credential class — each returning a pass / fail / warn / skip status with a localized plain-language fix. Exposed via `GET /printers/{id}/diagnostic` (saved printer) and `POST /printers/diagnostic` (pre-save Add-Printer flow), and surfaced as a one-click "Run diagnostic" from the printer card actions menu (plus a quick button on the card when a printer is offline), the Add-Printer dialog, and a new Connection Diagnostic section on the System page. The in-app bug reporter scans configured printers when the report form opens and always shows the result — a healthy confirmation when nothing's wrong, or the detected problem and its fix inline — so setup mistakes get self-resolved instead of becoming GitHub issues. The GitHub `config.yml` troubleshooting link was repointed from the wiki source repo to the rendered troubleshooting page. Backend service unit tests (15) and frontend modal tests (3) added; all diagnostic strings translated across the 8 locales. Backend ruff clean, frontend build clean, i18n parity green.
 - **Connection Diagnostic — self-service triage for "printer won't connect / won't print"** — A triage review of recently-closed issues found roughly a third were user-side setup errors (printer not in LAN developer mode, blocked ports, Docker bridge networking, wrong access code, printer on a different subnet), each costing a multi-round-trip "enable debug logging → build a support bundle → upload it" exchange. A new diagnostic (`backend/app/services/printer_diagnostic.py`) runs those checks automatically: TCP reachability of MQTT 8883 / FTPS 990 / RTSPS 322, LAN developer mode, Docker network mode, printer/host subnet match, and MQTT credential class — each returning a pass / fail / warn / skip status with a localized plain-language fix. Exposed via `GET /printers/{id}/diagnostic` (saved printer) and `POST /printers/diagnostic` (pre-save Add-Printer flow), and surfaced as a one-click "Run diagnostic" from the printer card actions menu (plus a quick button on the card when a printer is offline), the Add-Printer dialog, and a new Connection Diagnostic section on the System page. The in-app bug reporter scans configured printers when the report form opens and always shows the result — a healthy confirmation when nothing's wrong, or the detected problem and its fix inline — so setup mistakes get self-resolved instead of becoming GitHub issues. The GitHub `config.yml` troubleshooting link was repointed from the wiki source repo to the rendered troubleshooting page. Backend service unit tests (15) and frontend modal tests (3) added; all diagnostic strings translated across the 8 locales. Backend ruff clean, frontend build clean, i18n parity green.
 
 

+ 17 - 0
frontend/scripts/check-i18n-parity.mjs

@@ -285,11 +285,28 @@ const ZH_TW_COGNATES = [
   'EC984C,#6CD4BC,A66EB9,D87694',
   'EC984C,#6CD4BC,A66EB9,D87694',
 ];
 ];
 
 
+// Spanish cognates — words/phrases that are genuinely identical in Spanish.
+const ES_COGNATES = [
+  'Error', 'Firmware', 'General', 'Control', 'Total', 'total', 'Material',
+  'Material:', 'Color', 'Hex', 'Local', 'Global', 'China', 'Editable',
+  'Normal', 'Metal', 'Multicolor', 'Proxy', 'Host', 'Factor', 'Original',
+  'Sport (124%)', 'Ludicrous (166%)', 'MakerWorld: {{designer}}',
+  '{{printer}}: {{error}}', 'Base: {{name}}',
+  '{{name}} — {{stage}} ({{percent}}%) — {{elapsed}}', 'total: {{minutes}} min',
+  '({{count}}/8)', 'Hex: #{{hex}}', '(25%, 50%, 75%)',
+  'EC984C,#6CD4BC,A66EB9,D87694', 'Est.',
+  'ntfy, Pushover, Discord, etc.',
+  'Box label (62 × 29 mm)',
+  'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
+  'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',
+];
+
 const IDENTICAL_TO_EN_ALLOWED = {
 const IDENTICAL_TO_EN_ALLOWED = {
   de: new Set(DE_COGNATES),
   de: new Set(DE_COGNATES),
   fr: new Set(FR_COGNATES),
   fr: new Set(FR_COGNATES),
   it: new Set(IT_COGNATES),
   it: new Set(IT_COGNATES),
   ja: new Set(JA_COGNATES),
   ja: new Set(JA_COGNATES),
+  es: new Set(ES_COGNATES),
   'pt-BR': new Set(PT_BR_COGNATES),
   'pt-BR': new Set(PT_BR_COGNATES),
   'zh-CN': new Set(ZH_CN_COGNATES),
   'zh-CN': new Set(ZH_CN_COGNATES),
   'zh-TW': new Set(ZH_TW_COGNATES),
   'zh-TW': new Set(ZH_TW_COGNATES),

+ 4 - 1
frontend/src/i18n/index.ts

@@ -5,6 +5,7 @@ import LanguageDetector from 'i18next-browser-languagedetector';
 // Import translations directly for bundling
 // Import translations directly for bundling
 import en from './locales/en';
 import en from './locales/en';
 import de from './locales/de';
 import de from './locales/de';
+import es from './locales/es';
 import fr from './locales/fr';
 import fr from './locales/fr';
 import ja from './locales/ja';
 import ja from './locales/ja';
 import it from './locales/it';
 import it from './locales/it';
@@ -15,6 +16,7 @@ import zhTW from './locales/zh-TW';
 const resources = {
 const resources = {
   en: { translation: en },
   en: { translation: en },
   de: { translation: de },
   de: { translation: de },
+  es: { translation: es },
   fr: { translation: fr },
   fr: { translation: fr },
   ja: { translation: ja },
   ja: { translation: ja },
   it: { translation: it },
   it: { translation: it },
@@ -29,7 +31,7 @@ i18n
   .init({
   .init({
     resources,
     resources,
     fallbackLng: 'en',
     fallbackLng: 'en',
-    supportedLngs: ['en', 'de', 'fr', 'ja', 'it', 'pt-BR', 'zh-CN', 'zh-TW'],
+    supportedLngs: ['en', 'de', 'es', 'fr', 'ja', 'it', 'pt-BR', 'zh-CN', 'zh-TW'],
 
 
     detection: {
     detection: {
       // Order of detection methods
       // Order of detection methods
@@ -55,6 +57,7 @@ export default i18n;
 export const availableLanguages = [
 export const availableLanguages = [
   { code: 'en', name: 'English', nativeName: 'English' },
   { code: 'en', name: 'English', nativeName: 'English' },
   { code: 'de', name: 'German', nativeName: 'Deutsch' },
   { code: 'de', name: 'German', nativeName: 'Deutsch' },
+  { code: 'es', name: 'Spanish', nativeName: 'Español' },
   { code: 'fr', name: 'French', nativeName: 'Français' },
   { code: 'fr', name: 'French', nativeName: 'Français' },
   { code: 'ja', name: 'Japanese', nativeName: '日本語' },
   { code: 'ja', name: 'Japanese', nativeName: '日本語' },
   { code: 'it', name: 'Italian', nativeName: 'Italiano' },
   { code: 'it', name: 'Italian', nativeName: 'Italiano' },

+ 5950 - 0
frontend/src/i18n/locales/es.ts

@@ -0,0 +1,5950 @@
+export default {
+  // Navigation
+  nav: {
+    printers: 'Impresoras',
+    archives: 'Archivos',
+    queue: 'Cola de impresión',
+    stats: 'Estadísticas',
+    profiles: 'Perfiles',
+    maintenance: 'Mantenimiento',
+    projects: 'Proyectos',
+    inventory: 'Filamento',
+    files: 'Gestor de archivos',
+    makerworld: 'MakerWorld',
+    notifications: 'Notificaciones',
+    settings: 'Ajustes',
+    system: 'Sistema',
+    collapseSidebar: 'Contraer barra lateral',
+    expandSidebar: 'Expandir barra lateral',
+    update: 'Actualizar',
+    updateAvailable: 'Actualización disponible: v{{version}}',
+    updateAvailableBanner: '¡La versión {{version}} está disponible!',
+    viewUpdate: 'Ver actualización',
+    viewOnGithub: 'Ver en GitHub',
+    keyboardShortcuts: 'Atajos de teclado (?)',
+    switchToLight: 'Cambiar a modo claro',
+    switchToDark: 'Cambiar a modo oscuro',
+    smartSwitches: 'Interruptores inteligentes',
+    logout: 'Cerrar sesión',
+  },
+
+  // Common
+  common: {
+    save: 'Guardar',
+    saving: 'Guardando...',
+    cancel: 'Cancelar',
+    delete: 'Eliminar',
+    edit: 'Editar',
+    add: 'Añadir',
+    close: 'Cerrar',
+    confirm: 'Confirmar',
+    loading: 'Cargando...',
+    error: 'Error',
+    errorLoading: 'Error al cargar los datos',
+    retry: 'Reintentar',
+    success: 'Éxito',
+    warning: 'Advertencia',
+    enabled: 'Activado',
+    disabled: 'Desactivado',
+    yes: 'Sí',
+    no: 'No',
+    on: 'Encendido',
+    off: 'Apagado',
+    all: 'Todos',
+    none: 'Ninguno',
+    search: 'Buscar',
+    filter: 'Filtrar',
+    sort: 'Ordenar',
+    refresh: 'Actualizar',
+    download: 'Descargar',
+    upload: 'Subir',
+    uploading: 'Subiendo...',
+    uploadFailed: 'Error al subir',
+    actions: 'Acciones',
+    status: 'Estado',
+    name: 'Nombre',
+    description: 'Descripción',
+    date: 'Fecha',
+    time: 'Hora',
+    hours: 'horas',
+    minutes: 'minutos',
+    seconds: 'segundos',
+    days: 'días',
+    enable: 'Activar',
+    disable: 'Desactivar',
+    permissions: 'Permisos',
+    noPrinters: 'No hay impresoras configuradas',
+    noData: 'No hay datos disponibles',
+    linkNotFound: 'Enlace no encontrado',
+    required: 'Obligatorio',
+    optional: 'Opcional',
+    dismiss: 'Descartar',
+    apply: 'Aplicar',
+    reset: 'Restablecer',
+    export: 'Exportar',
+    import: 'Importar',
+    clear: 'Borrar',
+    selectAll: 'Seleccionar todo',
+    deselectAll: 'Deseleccionar todo',
+    noChange: '— Sin cambios —',
+    unchanged: 'Sin cambios',
+    unassigned: 'Sin asignar',
+    unknown: 'Desconocido',
+    unknownError: 'Error desconocido',
+    today: 'Hoy',
+    tomorrow: 'Mañana',
+    asap: 'Lo antes posible',
+    overdue: 'Atrasado',
+    now: 'Ahora',
+    collapse: 'Contraer',
+    expand: 'Expandir',
+    viewArchive: 'Ver archivo',
+    viewInFileManager: 'Ver en el gestor de archivos',
+    addedBy: 'Añadido por {{username}}',
+    prints: 'impresiones',
+    more: '+{{count}} más',
+    ascending: 'Ascendente',
+    descending: 'Descendente',
+    back: 'Atrás',
+    copy: 'Copiar',
+    copied: '¡Copiado!',
+    printer: 'Impresora',
+    remove: 'Quitar',
+    type: 'Tipo',
+    print: 'Imprimir',
+    rename: 'Renombrar',
+    move: 'Mover',
+    create: 'Crear',
+    duplicate: 'Duplicar',
+    left: 'Izquierda',
+    right: 'Derecha',
+  },
+
+  // Printers page
+  printers: {
+    title: 'Impresoras',
+    addPrinter: 'Añadir impresora',
+    editPrinter: 'Editar impresora',
+    deletePrinter: 'Eliminar impresora',
+    printerName: 'Nombre de la impresora',
+    serialNumber: 'Número de serie',
+    ipAddress: 'Dirección IP / Nombre de host',
+    accessCode: 'Código de acceso',
+    model: 'Modelo',
+    nozzleCount: 'Número de boquillas',
+    autoArchive: 'Archivado automático',
+    status: {
+      available: 'Disponible',
+      idle: 'Inactiva',
+      printing: 'Imprimiendo',
+      paused: 'En pausa',
+      offline: 'Desconectada',
+      problem: 'Problema',
+      error: 'Error',
+      finished: 'Finalizada',
+      unknown: 'Desconocido',
+    },
+    temperatures: {
+      nozzle: 'Boquilla',
+      bed: 'Cama',
+      chamber: 'Cámara',
+    },
+    progress: '{{percent}}% completado',
+    timeRemaining: '{{time}} restante',
+    deleteConfirm: '¿Está seguro de que desea eliminar "{{name}}"?',
+    maintenanceOk: 'Mantenimiento correcto',
+    maintenanceWarning: '{{count}} advertencia',
+    maintenanceWarning_plural: '{{count}} advertencias',
+    maintenanceDue: '{{count}} pendiente',
+    maintenanceDue_plural: '{{count}} pendientes',
+    // Sort options
+    sort: {
+      name: 'Nombre',
+      status: 'Estado',
+      model: 'Modelo',
+      location: 'Ubicación',
+      ascending: 'Orden ascendente',
+      descending: 'Orden descendente',
+    },
+    // Card size
+    cardSize: {
+      small: 'Tarjetas pequeñas',
+      medium: 'Tarjetas medianas',
+      large: 'Tarjetas grandes',
+      extraLarge: 'Tarjetas extragrandes',
+    },
+    // Controls
+    hideOffline: 'Ocultar desconectadas',
+    nextAvailable: 'Próxima disponible',
+    powerOn: 'Encender',
+    offlinePrintersWithPlugs: 'Impresoras desconectadas con enchufes inteligentes',
+    noPrintersConfigured: 'Aún no hay impresoras configuradas',
+    search: 'Buscar impresoras...',
+    noSearchResults: 'Ninguna impresora coincide con su búsqueda o filtros',
+    filter: {
+      allStatuses: 'Todos los estados',
+      allLocations: 'Todas las ubicaciones',
+    },
+    toolbar: {
+      filters: 'Filtros',
+      view: 'Vista',
+      actions: 'Acciones',
+    },
+    // Printer card
+    readyToPrint: 'Lista para imprimir',
+    external: 'Externo',
+    extL: 'Ext-L',
+    extR: 'Ext-R',
+    deleteArchives: 'Eliminar archivos de impresión',
+    noLabel: 'Sin etiqueta',
+    printPreview: 'Vista previa de impresión',
+    width: 'Anchura',
+    height: 'Altura',
+    noObjectsFound: 'No se encontraron objetos',
+    objectsLoadedOnPrintStart: 'Los objetos se cargan al iniciar una impresión',
+    willBeSkipped: 'Se omitirá',
+    name: 'Nombre',
+    serialCannotBeChanged: 'El número de serie no se puede cambiar',
+    locationHelp: 'Se usa para agrupar impresoras y filtrar trabajos en cola',
+    // WiFi signal strength
+    wifiSignal: {
+      veryWeak: 'Muy débil',
+      weak: 'Débil',
+      fair: 'Aceptable',
+      good: 'Buena',
+      excellent: 'Excelente',
+    },
+    // Maintenance
+    maintenanceUpToDate: 'Todo el mantenimiento al día - Haga clic para ver',
+    // Chamber light
+    chamberLightOn: 'Encender luz de la cámara',
+    chamberLightOff: 'Apagar luz de la cámara',
+    // Files
+    files: 'Archivos',
+    browseFiles: 'Explorar archivos de la impresora',
+    // Smart plug
+    autoOffAfterPrint: 'Apagado automático tras la impresión',
+    autoOffExecuted: 'Se ejecutó el apagado automático - encienda la impresora para restablecer',
+    // HMS errors
+    hmsErrors: 'Errores HMS',
+    viewHmsErrors: 'Ver {{count}} error(es) HMS',
+    // Actions
+    resume: 'Reanudar',
+    pause: 'Pausar',
+    stop: 'Detener',
+    camera: 'Cámara',
+    skipObject: 'Omitir objeto',
+    reconnect: 'Reconectar',
+    forceRefresh: 'Forzar actualización',
+    forceRefreshSuccess: 'Actualización solicitada',
+    mqttDebug: 'Depuración MQTT',
+    printerInformation: 'Información de la impresora',
+    copyToClipboard: 'Copiar',
+    copied: '¡Copiado!',
+    state: 'Estado',
+    wifiSignalLabel: 'Señal Wi-Fi',
+    developerMode: 'Modo desarrollador',
+    enabled: 'Activado',
+    disabled: 'Desactivado',
+    addedOn: 'Añadida',
+    sdCard: 'Tarjeta SD',
+    inserted: 'Insertada',
+    notInserted: 'No insertada',
+    totalPrintHours: 'Horas de impresión',
+    activeNozzle: 'Activa: boquilla {{nozzle}}',
+    nozzleRack: 'Soporte de boquillas',
+    nozzleDocked: 'Acoplada',
+    nozzleMounted: 'Montada',
+    nozzleActive: 'Activa',
+    nozzleIdle: 'Inactiva',
+    nozzleDiameter: 'Diámetro',
+    nozzleType: 'Tipo',
+    nozzleStatus: 'Estado',
+    nozzleFilament: 'Filamento',
+    nozzleWear: 'Desgaste',
+    nozzleMaxTemp: 'Temp. máx.',
+    nozzleSerial: 'Serie',
+    nozzleHardenedSteel: 'Acero endurecido',
+    nozzleStainlessSteel: 'Acero inoxidable',
+    nozzleTungstenCarbide: 'Carburo de tungsteno',
+    nozzleFlow: 'Flujo',
+    nozzleHighFlow: 'Flujo alto',
+    nozzleStandardFlow: 'Estándar',
+    // Firmware
+    firmwareUpdate: 'Actualización de firmware',
+    firmwareInstructions: 'En la pantalla táctil de la impresora, vaya a',
+    firmwareNav: 'Navegue hasta',
+    settings: 'Ajustes',
+    firmware: 'Firmware',
+    // Discovery
+    discoverPrinters: 'Detectar impresoras',
+    searching: 'Buscando...',
+    manualEntry: 'Entrada manual',
+    addFromCloud: 'Añadir desde la nube',
+    // Toast messages
+    toast: {
+      printerDeleted: 'Impresora eliminada',
+      missingSpoolAssignment: 'Impresión iniciada en {{printer}}. Falta la asignación de bobina para: {{slots}}',
+      printerAdded: 'Impresora añadida',
+      printerUpdated: 'Impresora actualizada',
+      failedToDelete: 'Error al eliminar la impresora',
+      failedToAdd: 'Error al añadir la impresora',
+      connectionFailedNotAdded: 'No se pudo conectar con la impresora. Verifique la IP, el número de serie y el código de acceso, y confirme que el modo solo LAN está activado. La impresora no se añadió.',
+      failedToUpdate: 'Error al actualizar la impresora',
+      commandSent: 'Comando enviado',
+      failedToSendCommand: 'Error al enviar el comando',
+      turnedOn: '{{name}} encendida',
+      failedToPowerOn: 'Error al encender {{name}}',
+      scriptTriggered: 'Script ejecutado',
+      printStopped: 'Impresión detenida',
+      printPaused: 'Impresión en pausa',
+      printResumed: 'Impresión reanudada',
+      referenceDeleted: 'Referencia eliminada',
+      detectionAreaSaved: 'Área de detección guardada',
+      failedToRunScript: 'Error al ejecutar el script',
+      failedToStopPrint: 'Error al detener la impresión',
+      failedToPausePrint: 'Error al pausar la impresión',
+      failedToResumePrint: 'Error al reanudar la impresión',
+      failedToControlChamberLight: 'Error al controlar la luz de la cámara',
+      failedToSetSpeed: 'Error al establecer la velocidad de impresión',
+      failedToUpdateSetting: 'Error al actualizar el ajuste',
+      failedToSkipObjects: 'Error al omitir objetos',
+      failedToRereadRfid: 'Error al releer el RFID',
+      failedToCheckPlate: 'Error al comprobar la cama',
+      failedToUpdateLabel: 'Error al actualizar la etiqueta',
+      failedToDeleteReference: 'Error al eliminar la referencia',
+      failedToSaveDetectionArea: 'Error al guardar el área de detección',
+      plateCheckEnabled: 'Comprobación de cama activada',
+      plateCheckDisabled: 'Comprobación de cama desactivada',
+      calibrationSaved: '¡Calibración guardada!',
+      calibrationFailed: 'Error en la calibración',
+      rfidRereadInitiated: 'Relectura de RFID iniciada',
+      loadInitiated: 'Cargando filamento…',
+      unloadInitiated: 'Descargando filamento…',
+      failedToLoad: 'Error al cargar el filamento',
+      failedToUnload: 'Error al descargar el filamento',
+    },
+    // Connection status
+    connection: {
+      connected: 'Conectada',
+      offline: 'Desconectada',
+    },
+    plateStatus: {
+      markCleared: 'Marcar cama como despejada',
+      cleared: 'Cama despejada',
+      notCleared: 'Cama no despejada',
+      inUse: 'Cama en uso',
+    },
+    // Queue info
+    queue: {
+      inQueue: '{{count}} impresión en cola',
+      inQueue_plural: '{{count}} impresiones en cola',
+    },
+    // Controls section
+    controls: 'Controles',
+    // RFID
+    rfid: {
+      reread: 'Releer RFID',
+    },
+    // AMS load/unload (#891)
+    ams: {
+      load: 'Cargar',
+      unload: 'Descargar',
+    },
+    bedJog: {
+      title: 'Mover la cama de impresión',
+      bed: 'Cama',
+      step: 'Paso (mm)',
+      up: 'Subir la cama',
+      down: 'Bajar la cama',
+      disabledWhilePrinting: 'Desactivado durante la impresión',
+      notHomedTitle: 'La impresora no está en posición de origen',
+      notHomedMessage: 'La impresora no se ha llevado a su posición de origen desde la última impresión. Ejecute el autoorigen primero para un posicionamiento seguro (estaciona el cabezal y luego lleva X, Y y Z al origen), o mueva de todos modos — los finales de carrera por software se omitirán.',
+      homeZ: 'Autoorigen',
+      moveAnyway: 'Mover de todos modos',
+      homingStarted: 'Llevando la impresora al origen…',
+    },
+    // Permissions
+    permission: {
+      noAdd: 'No tiene permiso para añadir impresoras',
+      noEdit: 'No tiene permiso para editar impresoras',
+      noDelete: 'No tiene permiso para eliminar impresoras',
+      noControl: 'No tiene permiso para controlar impresoras',
+      noFiles: 'No tiene permiso para acceder a los archivos de la impresora',
+      noAmsRfid: 'No tiene permiso para releer el RFID del AMS',
+      noSmartPlugControl: 'No tiene permiso para controlar enchufes inteligentes',
+      noCamera: 'No tiene permiso para ver las cámaras',
+    },
+    // Add/Edit modal
+    modal: {
+      addTitle: 'Añadir impresora',
+      editTitle: 'Editar impresora',
+      myPrinter: 'Mi impresora',
+      selectModel: 'Seleccionar modelo...',
+      locationGroup: 'Ubicación / Grupo (opcional)',
+      locationPlaceholder: 'p. ej., Taller, Oficina, Sótano',
+      autoArchiveLabel: 'Archivar automáticamente las impresiones completadas',
+      fromPrinterSettings: 'Desde los ajustes de la impresora',
+      modelOptional: 'Modelo (opcional)',
+      saveChanges: 'Guardar cambios',
+    },
+    // Skip objects
+    skipObjects: {
+      tooltip: 'Omitir objetos',
+      onlyWhilePrinting: 'Omitir objetos (solo durante la impresión)',
+      requiresMultiple: 'Omitir objetos (requiere 2 o más objetos)',
+      title: 'Omitir objetos',
+      matchIdsInfo: 'Haga coincidir los ID con la pantalla de su impresora',
+      printerShowsIds: 'La pantalla de la impresora muestra los ID de los objetos en la cama de impresión',
+      skipSelected: 'Omitir seleccionados',
+      skipping: 'Omitiendo...',
+      noObjectsSelected: 'No hay objetos seleccionados',
+      selectObjectsToSkip: 'Seleccione los objetos que desea omitir de la impresión actual',
+      skipped: 'omitidos',
+      objectsSkipped: 'Objetos omitidos',
+      activeCount: '{{count}} activos',
+      waitForLayer: 'Espere a la capa 2 o superior para omitir objetos (capa actual {{layer}})',
+      skip: 'Omitir',
+      confirmTitle: '¿Omitir objeto?',
+      confirmMessage: '¿Está seguro de que desea omitir "{{name}}"? Esto no se puede deshacer.',
+    },
+    // Confirm modals
+    confirm: {
+      deleteTitle: 'Eliminar impresora',
+      deleteMessage: '¿Está seguro de que desea eliminar "{{name}}"? Esto eliminará todos los ajustes de conexión.',
+      deleteArchivesNote: 'Todo el historial de impresión de esta impresora se eliminará permanentemente.',
+      keepArchivesNote: 'El historial de impresión se conservará pero ya no estará asociado a esta impresora.',
+      stopTitle: 'Detener impresión',
+      stopMessage: '¿Está seguro de que desea detener la impresión actual en "{{name}}"? Esto cancelará el trabajo de impresión.',
+      stopButton: 'Detener impresión',
+      pauseTitle: 'Pausar impresión',
+      pauseMessage: '¿Está seguro de que desea pausar la impresión actual en "{{name}}"?',
+      pauseButton: 'Pausar impresión',
+      resumeTitle: 'Reanudar impresión',
+      resumeMessage: '¿Está seguro de que desea reanudar la impresión en "{{name}}"?',
+      resumeButton: 'Reanudar impresión',
+      powerOnTitle: 'Encender impresora',
+      powerOnMessage: '¿Está seguro de que desea ENCENDER "{{name}}"?',
+      powerOnButton: 'Encender',
+      powerOffTitle: 'Apagar impresora',
+      powerOffMessage: '¿Está seguro de que desea APAGAR "{{name}}"?',
+      powerOffWarning: 'ADVERTENCIA: ¡"{{name}}" está imprimiendo actualmente! ¿Está seguro de que desea APAGARLA? Esto interrumpirá la impresión y podría dañar la impresora.',
+      powerOffButton: 'Apagar',
+      haToggleTitle: 'Conmutar "{{name}}"',
+      haToggleMessage: '¿Conmutar la entidad de Home Assistant {{entity}}? Esto puede apagarla si está encendida actualmente.',
+      haToggleWarning: 'ADVERTENCIA: ¡"{{name}}" está imprimiendo actualmente! Conmutar {{entity}} puede cortar la corriente e interrumpir la impresión. ¿Continuar?',
+      haToggleButton: 'Conmutar',
+    },
+    // Bulk actions
+    bulk: {
+      select: 'Seleccionar',
+      selectAll: 'Seleccionar todo',
+      selectByLocation: 'Seleccionar por ubicación',
+      selected: '{{count}} seleccionadas',
+      actions: {
+        stop: 'Detener',
+        pause: 'Pausar',
+        resume: 'Reanudar',
+        clearPlate: 'Despejar cama',
+        clearHMS: 'Borrar notificaciones',
+      },
+      confirm: {
+        stopTitle: 'Detener {{count}} impresiones',
+        stopMessage: 'Esto cancelará las impresiones activas en {{count}} impresora(s). Esta acción no se puede deshacer.',
+        stopButton: 'Detener todo',
+        pauseTitle: 'Pausar {{count}} impresiones',
+        pauseMessage: 'Esto pausará las impresiones activas en {{count}} impresora(s).',
+        pauseButton: 'Pausar todo',
+        clearPlateTitle: 'Despejar {{count}} camas de impresión',
+        clearPlateMessage: 'Esto despejará la cama de impresión en {{count}} impresora(s) y puede activar trabajos en cola.',
+        clearPlateButton: 'Despejar todo',
+      },
+      success: '{{action}} completado en {{count}} impresora(s)',
+      partial: '{{succeeded}} con éxito, {{failed}} con error',
+      noneApplicable: 'Ninguna de las impresoras seleccionadas está en el estado adecuado para esta acción',
+      selectByState: 'Seleccionar por estado',
+    },
+    // Discovery
+    discovery: {
+      title: 'Detectar impresoras',
+      searching: 'Buscando...',
+      scanning: 'Escaneando...',
+      scanProgress: 'Escaneando... {{scanned}}/{{total}}',
+      foundPrinters: 'Se encontraron {{count}} impresora(s)',
+      noPrintersFound: 'No se encontraron impresoras',
+      noPrintersFoundSubnet: 'No se encontraron impresoras en la subred especificada.',
+      noPrintersFoundNetwork: 'No se encontraron impresoras en la red.',
+      allConfigured: 'Todas las impresoras detectadas ya están configuradas.',
+      alreadyAdded: 'Ya añadida',
+      select: 'Seleccionar',
+      manualEntry: 'Entrada manual',
+      addFromCloud: 'Añadir desde la nube',
+      subnetToScan: 'Subred a escanear',
+      dockerNote: 'Docker detectado. Introduzca la subred de su impresora en notación CIDR. Requiere network_mode: host en docker-compose.yml.',
+      scanSubnet: 'Escanear subred en busca de impresoras',
+      discoverNetwork: 'Detectar impresoras en la red',
+      scanningSubnet: 'Escaneando la subred en busca de impresoras Bambu...',
+      scanningNetwork: 'Escaneando la red...',
+      serialRequired: 'Número de serie obligatorio',
+      unknown: 'Desconocido',
+      failedToStart: 'Error al iniciar la detección',
+    },
+    // AMS Drying
+    drying: {
+      start: 'Iniciar secado',
+      stop: 'Detener secado',
+      temperature: 'Temperatura',
+      duration: 'Duración',
+      hours: 'horas',
+      timeRemaining: '{{time}} restante',
+      active: 'Secando',
+      notSupported: 'Secado no compatible',
+      powerRequired: 'Conecte el adaptador de corriente del AMS para activar el secado',
+      startingDrying: 'Iniciando el secado...',
+      stoppingDrying: 'Deteniendo el secado...',
+      rotateTray: 'Girar la bobina durante el secado',
+    },
+    // Filaments section
+    filaments: 'Filamentos',
+    // Camera
+    openCameraOverlay: 'Abrir la cámara superpuesta',
+    openCameraWindow: 'Abrir la cámara en una ventana nueva',
+    // Firmware
+    firmwareUpdateAvailable: 'Actualización de firmware disponible: {{current}} → {{latest}}',
+    firmwareUpToDate: 'Firmware {{version}} — Actualizado',
+    firmwareUpdateButton: 'Actualizar',
+    // Plate detection
+    plateDetection: {
+      noPermission: 'No tiene permiso para actualizar impresoras',
+      enabledClick: 'Comprobación de cama activada - Haga clic para desactivar',
+      disabledClick: 'Comprobación de cama desactivada - Haga clic para activar',
+      manageCalibration: 'Gestionar la calibración de detección de cama',
+      calibrationRequired: 'Calibración necesaria',
+      calibrationInstructions: 'Asegúrese de que la cama de impresión esté <strong>completamente vacía</strong> y luego haga clic en Calibrar.',
+      calibrationDescription: 'La calibración captura una imagen de referencia de la cama vacía. Las comprobaciones futuras se compararán con esta referencia para detectar objetos.',
+      calibrationTip: '<strong>Consejo:</strong> puede almacenar hasta 5 calibraciones para distintas camas. El sistema usa automáticamente la mejor coincidencia al comprobar.',
+      plateEmpty: 'La cama parece estar vacía',
+      objectsDetected: 'Objetos detectados en la cama',
+      confidence: 'Confianza',
+      difference: 'Diferencia',
+      analysisPreview: 'Vista previa del análisis:',
+      analysisLegend: 'Recuadro verde = área de detección, Superposición roja = diferencias respecto a la calibración',
+      savedReferences: 'Referencias guardadas ({{count}}/{{max}})',
+      deleteReference: 'Eliminar referencia',
+      labelPlaceholder: 'Etiqueta...',
+      clickToEdit: '{{label}} - Haga clic para editar',
+      clickToAddLabel: 'Haga clic para añadir una etiqueta',
+    },
+    // Speed
+    speed: {
+      title: 'Velocidad de impresión',
+      silent: 'Silencioso (50%)',
+      standard: 'Estándar (100%)',
+      sport: 'Sport (124%)',
+      ludicrous: 'Ludicrous (166%)',
+    },
+    airduct: {
+      title: 'Modo de conducto de aire',
+      cooling: 'Refrigeración',
+      heating: 'Calentamiento',
+    },
+    noSdCard: 'Sin SD',
+    door: {
+      open: 'Abierta',
+      closed: 'Cerrada',
+    },
+    // Fans
+    fans: {
+      partCooling: 'Ventilador de refrigeración de piezas',
+      auxiliary: 'Ventilador auxiliar',
+      chamber: 'Ventilador de la cámara',
+    },
+    // HMS errors
+    clickToViewHmsErrors: 'Haga clic para ver los errores HMS',
+    estimatedCompletion: 'Hora estimada de finalización',
+    plateNumber: 'Cama {{number}}',
+    slotOptions: 'Opciones de la ranura',
+    // AMS hover popup
+    amsPopup: {
+      friendlyName: 'Nombre del AMS',
+      friendlyNamePlaceholder: 'p. ej. Nombre descriptivo del AMS',
+      serialNumber: 'Número de serie',
+      firmwareVersion: 'Firmware',
+      save: 'Guardar',
+      clear: 'Borrar',
+      noEditPermission: 'No tiene permiso para renombrar unidades AMS',
+    },
+    // Firmware modal
+    firmwareModal: {
+      title: 'Actualización de firmware',
+      titleUpToDate: 'Información del firmware',
+      currentVersion: 'Actual:',
+      latestVersion: 'Más reciente:',
+      releaseNotes: 'Notas de la versión',
+      checkingPrereqs: 'Comprobando los requisitos previos...',
+      sdCardReady: 'Tarjeta SD lista. Haga clic abajo para subir el firmware.',
+      uploadedSuccess: '¡Firmware subido a la tarjeta SD!',
+      applyInstructions: 'Para aplicar la actualización en su impresora:',
+      step1: 'En la pantalla táctil de la impresora, vaya a <strong>Ajustes</strong>',
+      step2: 'Navegue hasta <strong>Firmware</strong>',
+      step3: 'Seleccione <strong>Actualizar desde tarjeta SD</strong>',
+      step4: 'La actualización tardará entre 10 y 20 minutos',
+      done: 'Hecho',
+      starting: 'Iniciando...',
+      uploadFirmware: 'Subir firmware',
+      uploadFailed: 'Error al iniciar la subida: {{error}}',
+      uploadedToast: '¡Firmware subido! Inicie la actualización desde la pantalla de la impresora.',
+      availableVersions: 'Versiones disponibles',
+      usable: 'Utilizable',
+      unavailable: 'No disponible',
+      installed: 'Instalada',
+      newerBadge: 'más reciente',
+      olderBadge: 'más antigua',
+      currentBadge: 'actual',
+    },
+    accessCodePlaceholder: 'Dejar vacío para conservar el actual',
+    // ROI editor
+    roi: {
+      title: 'Área de detección (ROI)',
+      xStart: 'Inicio X',
+      yStart: 'Inicio Y',
+      width: 'Anchura',
+      height: 'Altura',
+      instruction: 'Ajuste el área de detección para centrarse en la cama de impresión. El recuadro verde de la vista previa muestra el área actual.',
+    },
+    developerModeWarning: 'El modo desarrollador LAN no está activado en: {{names}}. Es posible que algunas funciones no funcionen.',
+    howToEnable: 'Cómo activarlo',
+    incompatibleFile: 'Este archivo se laminó para {{slicedFor}}, pero esta impresora es una {{printerModel}}',
+    dropNotPrintable: 'Solo se pueden imprimir archivos .gcode y .gcode.3mf',
+    dropToPrint: 'Suelte para imprimir',
+    cannotPrint: 'Impresora ocupada',
+  },
+
+  // Archives page
+  archives: {
+    title: 'Archivos de impresión',
+    searchPlaceholder: 'Buscar archivos...',
+    filterByPrinter: 'Filtrar por impresora',
+    filterByStatus: 'Filtrar por estado',
+    sortBy: 'Ordenar por',
+    sortNewest: 'Más recientes primero',
+    sortOldest: 'Más antiguos primero',
+    sortName: 'Nombre',
+    sortDuration: 'Duración',
+    sortLargest: 'Mayores primero',
+    sortSmallest: 'Menores primero',
+    sortSize: 'Tamaño',
+    noArchives: 'No se encontraron archivos',
+    noArchivesSearch: 'Ningún archivo coincide con su búsqueda',
+    originalPrintNotVisible: 'La impresión original no está visible - pruebe a borrar los filtros',
+    noArchivesYet: 'Aún no hay archivos',
+    prints: 'impresiones',
+    pagination: {
+      showing: 'Mostrando',
+      to: 'a',
+      of: 'de',
+      show: 'Mostrar',
+      page: 'Página',
+      all: 'Todos',
+    },
+    loadingArchives: 'Cargando archivos...',
+    releaseToUpload: 'Suelte para subir',
+    showAll: 'Mostrar todos',
+    showFavoritesOnly: 'Mostrar solo favoritos',
+    gridView: 'Vista de cuadrícula',
+    listView: 'Vista de lista',
+    calendarView: 'Vista de calendario',
+    logView: 'Registro de impresión',
+    manageTags: 'Gestionar etiquetas',
+    showFailedPrints: 'Mostrar impresiones fallidas',
+    hideFailedPrints: 'Ocultar impresiones fallidas',
+    hideDuplicates: 'Ocultar duplicados',
+    viewOriginalPrint: 'Haga clic para ver la impresión original (n.º {{id}})',
+    printTime: 'Tiempo de impresión',
+    filamentUsed: 'Filamento usado',
+    cost: 'Coste',
+    reprint: 'Reimprimir',
+    preview: 'Vista previa',
+    deleteArchive: 'Eliminar archivo',
+    deleteConfirm: '¿Está seguro de que desea eliminar este archivo?',
+    favorite: 'Favorito',
+    unfavorite: 'Quitar de favoritos',
+    viewDetails: 'Ver detalles',
+    status: {
+      completed: 'Completada',
+      failed: 'Fallida',
+      stopped: 'Detenida',
+    },
+    toast: {
+      source3mfAttached: '3MF de origen adjuntado: {{filename}}',
+      failedUploadSource3mf: 'Error al subir el 3MF de origen',
+      source3mfRemoved: '3MF de origen eliminado',
+      failedRemoveSource3mf: 'Error al eliminar el 3MF de origen',
+      f3dAttached: 'F3D adjuntado: {{filename}}',
+      failedUploadF3d: 'Error al subir el F3D',
+      f3dRemoved: 'F3D eliminado',
+      failedRemoveF3d: 'Error al eliminar el F3D',
+      timelapseAttached: 'Time-lapse adjuntado: {{filename}}',
+      timelapseAlreadyAttached: 'El time-lapse ya está adjuntado',
+      noMatchingTimelapse: 'No se encontró ningún time-lapse coincidente',
+      failedScanTimelapse: 'Error al buscar el time-lapse',
+      failedAttachTimelapse: 'Error al adjuntar el time-lapse',
+      timelapseRemoved: 'Time-lapse eliminado',
+      failedRemoveTimelapse: 'Error al eliminar el time-lapse',
+      timelapseUploaded: 'Time-lapse subido: {{filename}}',
+      failedUploadTimelapse: 'Error al subir el time-lapse',
+      archiveDeleted: 'Archivo eliminado',
+      failedDeleteArchive: 'Error al eliminar el archivo',
+      addedToFavorites: 'Añadido a favoritos',
+      removedFromFavorites: 'Quitado de favoritos',
+      projectUpdated: 'Proyecto actualizado',
+      failedUpdateProject: 'Error al actualizar el proyecto',
+      linkCopied: 'Enlace copiado al portapapeles',
+      failedCopyLink: 'Error al copiar el enlace',
+      photoDeleted: 'Foto eliminada',
+      failedDeletePhoto: 'Error al eliminar la foto',
+      failedDeleteArchives: 'Error al eliminar los archivos',
+      failedUpdateFavorites: 'Error al actualizar los favoritos',
+      exportDownloaded: 'Exportación descargada',
+      exportFailed: 'Error en la exportación',
+    },
+    menu: {
+      print: 'Imprimir',
+      schedule: 'Programar',
+      openInBambuStudio: 'Abrir en el laminador',
+      slice: 'Laminar',
+      externalLink: 'Enlace externo',
+      viewOnMakerWorld: 'Ver en MakerWorld',
+      preview3d: 'Vista previa 3D',
+      viewTimelapse: 'Ver time-lapse',
+      scanForTimelapse: 'Buscar time-lapse',
+      uploadTimelapse: 'Subir time-lapse',
+      removeTimelapse: 'Eliminar time-lapse',
+      downloadSource3mf: 'Descargar 3MF de origen',
+      uploadSource3mf: 'Subir 3MF de origen',
+      replaceSource3mf: 'Reemplazar 3MF de origen',
+      removeSource3mf: 'Eliminar 3MF de origen',
+      uploadF3d: 'Subir F3D',
+      replaceF3d: 'Reemplazar F3D',
+      downloadF3d: 'Descargar F3D',
+      removeF3d: 'Eliminar F3D',
+      download: 'Descargar',
+      copyDownloadLink: 'Copiar enlace de descarga',
+      qrCode: 'Código QR',
+      viewPhotos: 'Ver fotos',
+      viewPhotosCount: 'Ver fotos ({{count}})',
+      projectPage: 'Página del proyecto',
+      addToFavorites: 'Añadir a favoritos',
+      removeFromFavorites: 'Quitar de favoritos',
+      edit: 'Editar',
+      printLog: 'Registro de impresión',
+      goToProject: 'Ir al proyecto: {{name}}',
+      addToProject: 'Añadir al proyecto',
+      removeFromProject: 'Quitar del proyecto',
+      loading: 'Cargando...',
+      noProjectsAvailable: 'No hay proyectos disponibles',
+      searchProjects: 'Buscar proyectos…',
+      select: 'Seleccionar',
+      deselect: 'Deseleccionar',
+      delete: 'Eliminar',
+    },
+    permission: {
+      noReprint: 'No tiene permiso para reimprimir este archivo',
+      noAddToQueue: 'No tiene permiso para añadir a la cola',
+      noUpdateArchives: 'No tiene permiso para actualizar archivos',
+      noUploadFiles: 'No tiene permiso para subir archivos',
+      noDownload: 'No tiene permiso para descargar archivos',
+      noCopyLink: 'No tiene permiso para copiar enlaces de descarga',
+      noDelete: 'No tiene permiso para eliminar este archivo',
+      noCreate: 'No tiene permiso para crear archivos',
+    },
+    platePicker: {
+      title: 'Seleccione la cama para la vista previa',
+      hint: 'Este archivo tiene varias camas. Elija una para abrirla en el visor de G-code.',
+      plateLabel: 'Cama {{index}}',
+      objectCount: '{{count}} objeto',
+      objectCount_plural: '{{count}} objetos',
+      noGcode: 'Este archivo no tiene G-code laminado para previsualizar. Ábralo en Bambu Studio para laminarlo primero.',
+    },
+    card: {
+      previousPlate: 'Cama anterior',
+      nextPlate: 'Cama siguiente',
+      plateNumber: 'Cama {{index}}',
+      moreOptions: 'Haga clic con el botón derecho para más opciones',
+      addToFavorites: 'Añadir a favoritos',
+      removeFromFavorites: 'Quitar de favoritos',
+      cancelled: 'cancelada',
+      failed: 'fallida',
+      duplicate: 'duplicado',
+      duplicateTitle: 'Este modelo ya se ha impreso anteriormente',
+      openSource3mf: 'Abrir el 3MF de origen en Bambu Studio (clic derecho para más opciones)',
+      downloadF3d: 'Descargar el archivo de diseño de Fusion 360',
+      viewTimelapse: 'Ver time-lapse',
+      viewPhoto: 'Ver 1 foto',
+      viewPhotos: 'Ver {{count}} fotos',
+      openFolder: 'Abrir carpeta: {{name}}',
+      slicedFile: 'Archivo laminado - listo para imprimir',
+      sourceFile: 'Solo archivo de origen - no hay mapeo de AMS disponible',
+      gcode: 'GCODE',
+      source: 'ORIGEN',
+      project: 'Proyecto: {{name}}',
+      runsBadge: '{{count}} impresiones',
+      runsBadgeTitle: '{{count}} impresiones en total — {{successful}} con éxito, {{failed}} fallidas. Haga clic para ver el registro completo de impresión.',
+      estimated: 'Estimado: {{time}}',
+      actual: 'Real: {{time}}',
+      accuracy: 'Precisión: {{percent}}%',
+      filament: '{{weight}} g',
+      layer: '{{count}} capa',
+      layers: '{{count}} capas',
+      object: '{{count}} objeto',
+      objects: '{{count}} objetos',
+      slicedFor: 'Laminado para {{model}}',
+      uploadedBy: 'Subido por',
+      noPermissionReprint: 'No tiene permiso para reimprimir',
+      noFileForReprint: 'No hay archivo 3MF disponible — no se pudo descargar el archivo de la impresora cuando se registró la impresión',
+      noPermissionEdit: 'No tiene permiso para editar archivos',
+      noPermissionDelete: 'No tiene permiso para eliminar archivos',
+      reprint: 'Reimprimir',
+      schedulePrint: 'Programar impresión',
+      schedule: 'Programar',
+      openInBambuStudio: 'Abrir en el laminador',
+      openInBambuStudioToSlice: 'Abrir en el laminador para laminar',
+      slice: 'Laminar',
+      externalLink: 'Enlace externo',
+      makerWorld: 'MakerWorld: {{designer}}',
+      viewProject: 'Ver proyecto',
+      noExternalLink: 'Sin enlace externo',
+      preview3d: 'Vista previa 3D',
+      download: 'Descargar',
+      edit: 'Editar',
+      delete: 'Eliminar',
+    },
+    runLog: {
+      title: 'Registro de impresión',
+      modalTitle: 'Registro de impresión — {{name}}',
+      modalTitleFallback: 'este archivo',
+      empty: 'Aún no se han registrado eventos de impresión para este archivo.',
+      col: {
+        date: 'Fecha',
+        status: 'Estado',
+        duration: 'Duración',
+        filament: 'Filamento',
+        cost: 'Coste',
+      },
+      status: {
+        completed: 'Completada',
+        failed: 'Fallida',
+        cancelled: 'Cancelada',
+        stopped: 'Detenida',
+        skipped: 'Omitida',
+        printing: 'Imprimiendo',
+      },
+    },
+    modal: {
+      deleteArchive: 'Eliminar archivo',
+      deleteConfirm: '¿Está seguro de que desea eliminar "{{name}}"? Esta acción no se puede deshacer.',
+      deleteButton: 'Eliminar',
+      deletePurgeStats: 'Eliminar también esta impresión de las estadísticas rápidas (filamento, tiempo, coste, energía)',
+      removeSource3mf: 'Eliminar 3MF de origen',
+      removeSource3mfConfirm: '¿Está seguro de que desea eliminar el archivo 3MF de origen de "{{name}}"? Esto eliminará el archivo de proyecto original del laminador.',
+      removeButton: 'Eliminar',
+      removeF3d: 'Eliminar F3D',
+      removeF3dConfirm: '¿Está seguro de que desea eliminar el archivo de diseño de Fusion 360 de "{{name}}"?',
+      removeTimelapse: 'Eliminar time-lapse',
+      removeTimelapseConfirm: '¿Está seguro de que desea eliminar el vídeo time-lapse de "{{name}}"?',
+      timelapse: '{{name}} - Time-lapse',
+      selectTimelapse: 'Seleccionar time-lapse',
+      selectTimelapseDesc: 'No se encontró ninguna coincidencia automática. Seleccione el time-lapse para esta impresión:',
+      deleteArchives: 'Eliminar archivos',
+      deleteArchivesConfirm: '¿Está seguro de que desea eliminar {{count}} archivo(s)? Esta acción no se puede deshacer.',
+      deleteCount: 'Eliminar {{count}}',
+    },
+    page: {
+      title: 'Archivos',
+      printsCount: '{{filtered}} de {{total}} impresiones',
+      dropFilesHere: 'Suelte aquí los archivos .3mf',
+      releaseToUpload: 'Suelte para subir',
+      only3mfSupported: 'Solo se admiten archivos .3mf',
+      close: 'Cerrar',
+      selected: '{{count}} seleccionados',
+      selectAll: 'Seleccionar todo',
+      tags: 'Etiquetas',
+      project: 'Proyecto',
+      favorite: 'Favorito',
+      delete: 'Eliminar',
+      toggledFavorites: 'Se cambió el estado de favorito de {{count}} archivo(s)',
+      failedUpdateFavorites: 'Error al actualizar los favoritos',
+      archivesDeleted: '{{count}} archivo(s) eliminado(s)',
+      failedDeleteArchives: 'Error al eliminar los archivos',
+      photoDeleted: 'Foto eliminada',
+      failedDeletePhoto: 'Error al eliminar la foto',
+    },
+    list: {
+      name: 'Nombre',
+      printer: 'Impresora',
+      date: 'Fecha',
+      size: 'Tamaño',
+      actions: 'Acciones',
+      hasTimelapse: 'Tiene time-lapse',
+    },
+    log: {
+      date: 'Fecha',
+      printName: 'Nombre de la impresión',
+      printer: 'Impresora',
+      user: 'Usuario',
+      status: 'Estado',
+      duration: 'Duración',
+      filament: 'Filamento',
+      allPrinters: 'Todas las impresoras',
+      allUsers: 'Todos los usuarios',
+      allStatuses: 'Todos los estados',
+      cancelled: 'Cancelada',
+      skipped: 'Omitida',
+      dateFrom: 'Desde',
+      dateTo: 'Hasta',
+      noEntries: 'No se encontraron entradas en el registro de impresión',
+      showing: 'Mostrando {{count}} de {{total}} entradas',
+      rowsPerPage: 'Filas',
+      page: 'Página',
+      prev: 'Anterior',
+      next: 'Siguiente',
+      clearLog: 'Borrar registro',
+      clearLogTitle: 'Borrar el registro de impresión',
+      clearLogConfirm: 'Todas las entradas del registro de impresión se eliminarán permanentemente. Los archivos y los elementos de la cola no se ven afectados. Esta acción no se puede deshacer. ¿Está seguro?',
+      clearLogButton: 'Borrar todo',
+      cleared: '{{count}} entradas del registro borradas',
+      clearFailed: 'Error al borrar el registro de impresión',
+    },
+  },
+
+  // Queue page
+  queue: {
+    title: 'Cola de impresión',
+    subtitle: 'Programe y gestione sus trabajos de impresión',
+    addToQueue: 'Añadir a la cola',
+    // Print modal
+    print: 'Imprimir',
+    reprint: 'Reimprimir',
+    schedulePrint: 'Programar impresión',
+    editQueueItem: 'Editar elemento de la cola',
+    printToPrinters: 'Imprimir en {{count}} impresoras',
+    queueToPrinters: 'Encolar en {{count}} impresoras',
+    queueSelectedPlates: 'Encolar {{count}} camas',
+    selectAllPlates: 'Seleccionar las {{count}} camas',
+    deselectAll: 'Deseleccionar todo',
+    printQueued: 'Impresión encolada',
+    itemsQueued: '{{count}} elementos encolados',
+    sending: 'Enviando...',
+    sendingProgress: 'Enviando {{current}}/{{total}}...',
+    adding: 'Añadiendo...',
+    addingProgress: 'Añadiendo {{current}}/{{total}}...',
+    savingProgress: 'Guardando {{current}}/{{total}}...',
+    clearQueue: 'Vaciar la cola',
+    clearHistory: 'Borrar el historial',
+    emptyQueue: 'La cola está vacía',
+    position: 'Posición',
+    scheduledTime: 'Hora programada',
+    moveUp: 'Subir',
+    moveDown: 'Bajar',
+    startNow: 'Iniciar ahora',
+    printingInProgress: 'Impresión en curso...',
+    viewArchive: 'Ver archivo',
+    viewInFileManager: 'Ver en el gestor de archivos',
+    itemCount: '{{count}} elemento',
+    itemCount_plural: '{{count}} elementos',
+    dragToReorder: 'Arrastre para reordenar (solo elementos «lo antes posible»)',
+    reorderHint: 'La posición solo afecta a los elementos «lo antes posible». Los elementos programados se ejecutan a su hora establecida.',
+    sjf: {
+      label: 'SJF',
+      tooltip: 'Trabajo más corto primero — el planificador prioriza las impresiones más cortas',
+    },
+    addedBy: 'Añadido por {{name}}',
+    nextInQueue: 'Siguiente en la cola',
+    clearPlateSuccess: 'Cama despejada — lista para la próxima impresión',
+    plateNumber: 'Cama {{index}}',
+    // Batch / quantity
+    quantity: 'Cantidad',
+    quantityHint: 'Crea {{count}} elementos en la cola',
+    activeBatches: 'Lotes activos',
+    batchProgress: '{{completed}} de {{total}} completados',
+    cancelBatch: 'Cancelar los restantes',
+    batchCancelled: 'Elementos restantes del lote cancelados',
+    cancelBatchConfirmTitle: 'Cancelar lote',
+    cancelBatchConfirmMessage: '¿Cancelar todos los elementos pendientes restantes de este lote?',
+    batch: 'Lote',
+    // Sections
+    sections: {
+      currentlyPrinting: 'Imprimiendo actualmente',
+      queued: 'En cola',
+      history: 'Historial',
+    },
+    // Status
+    status: {
+      pending: 'Pendiente',
+      waiting: 'En espera',
+      printing: 'Imprimiendo',
+      paused: 'En pausa',
+      completed: 'Completada',
+      failed: 'Fallida',
+      skipped: 'Omitida',
+      cancelled: 'Cancelada',
+    },
+    // Summary cards
+    summary: {
+      printing: 'Imprimiendo',
+      queued: 'En cola',
+      totalTime: 'Tiempo total de la cola',
+      totalWeight: 'Peso total de la cola',
+      history: 'Historial',
+    },
+    // Filters
+    filter: {
+      allPrinters: 'Todas las impresoras',
+      unassigned: 'Sin asignar',
+      allStatus: 'Todos los estados',
+      allLocations: 'Todas las ubicaciones',
+      any: 'Cualquiera',
+    },
+    // Sort
+    sort: {
+      byPosition: 'Ordenar por posición',
+      byName: 'Ordenar por nombre',
+      byPrinter: 'Ordenar por impresora',
+      bySchedule: 'Ordenar por programación',
+      byDate: 'Ordenar por fecha',
+      ascendingOldest: 'Ascendente (más antiguos primero)',
+      descendingNewest: 'Descendente (más recientes primero)',
+    },
+    // Badges
+    badges: {
+      staged: 'Preparado',
+      requiresPrevious: 'Requiere éxito previo',
+      autoPowerOff: 'Apagado automático',
+      gcodeInjection: 'G-code',
+    },
+    // Empty state
+    empty: {
+      title: 'No hay impresiones programadas',
+      description: 'Programe una impresión desde la página de Archivos con la opción «Programar» del menú contextual, o arrastre y suelte archivos para empezar.',
+    },
+    // Time
+    time: {
+      asap: 'Lo antes posible',
+      overdue: 'Atrasada',
+      now: 'Ahora',
+      lessThanMinute: 'En menos de un minuto',
+      inMinutes: 'En {{count}} min',
+      inHours: 'En {{count}} horas',
+    },
+    // Actions
+    actions: {
+      stopPrint: 'Detener impresión',
+      startPrint: 'Iniciar impresión',
+      requeue: 'Volver a encolar',
+    },
+    // Bulk edit
+    bulkEdit: {
+      title: 'Editar {{count}} elemento',
+      title_plural: 'Editar {{count}} elementos',
+      description: 'Solo se aplicarán los ajustes modificados a los elementos seleccionados.',
+      printer: 'Impresora',
+      noChange: '— Sin cambios —',
+      queueOptions: 'Opciones de la cola',
+      staged: 'Preparado (inicio manual)',
+      autoPowerOff: 'Apagado automático tras la impresión',
+      requirePrevious: 'Requerir éxito previo',
+      printOptions: 'Opciones de impresión',
+      bedLevelling: 'Nivelación de la cama',
+      flowCalibration: 'Calibración del flujo',
+      vibrationCalibration: 'Calibración de vibración',
+      layerInspection: 'Inspección de la primera capa',
+      timelapse: 'Time-lapse',
+      useAms: 'Usar AMS',
+      applyChanges: 'Aplicar cambios',
+      selectAll: 'Seleccionar todo',
+      deselectAll: 'Deseleccionar todo',
+      selected: '{{count}} seleccionados',
+      editSelected: 'Editar seleccionados',
+      cancelSelected: 'Cancelar seleccionados',
+    },
+    // Confirmations
+    confirm: {
+      cancelTitle: 'Cancelar impresión programada',
+      cancelMessage: '¿Está seguro de que desea cancelar "{{name}}"?',
+      stopTitle: 'Detener impresión',
+      stopMessage: '¿Está seguro de que desea detener la impresión actual "{{name}}"? Esto cancelará el trabajo de impresión en la impresora.',
+      removeTitle: 'Quitar del historial',
+      removeMessage: '¿Está seguro de que desea quitar "{{name}}" del historial de la cola?',
+      clearHistoryTitle: 'Borrar el historial',
+      clearHistoryMessage: '¿Está seguro de que desea quitar los {{count}} elemento(s) del historial?',
+      cancelButton: 'Cancelar impresión',
+      stopButton: 'Detener impresión',
+      thisPrint: 'esta impresión',
+      thisItem: 'este elemento',
+    },
+    // Toast messages
+    toast: {
+      cancelled: 'Elemento de la cola cancelado',
+      cancelFailed: 'Error al cancelar el elemento',
+      removed: 'Elemento de la cola eliminado',
+      removeFailed: 'Error al eliminar el elemento',
+      stopped: 'Impresión detenida',
+      stopFailed: 'Error al detener la impresión',
+      released: 'Impresión liberada a la cola',
+      startFailed: 'Error al iniciar la impresión',
+      reorderFailed: 'Error al reordenar la cola',
+      historyCleared: 'Se borraron {{count}} elemento(s) del historial',
+      clearHistoryFailed: 'Error al borrar el historial',
+      updateFailed: 'Error al actualizar los elementos',
+      bulkCancelled: 'Se cancelaron {{count}} elemento(s)',
+      bulkCancelFailed: 'Error al cancelar los elementos',
+    },
+    // Timeline view
+    timeline: {
+      listView: 'Lista',
+      timelineView: 'Cronología',
+      unassigned: 'Sin asignar',
+      noData: 'No hay impresiones programadas para este día',
+      allDoneBy: 'Todas las impresiones estimadas para las {{time}}',
+      staged: 'Preparado',
+      filterAll: 'Mostrar todo',
+      filterPrinting: 'Imprimiendo',
+      filterQueued: 'En cola',
+      time: {
+        anyMoment: 'en cualquier momento',
+        minutesLeft: '{{minutes}} min restantes',
+        hoursLeft: '{{hours}} h restantes',
+        hoursMinutesLeft: '{{hours}} h {{minutes}} min restantes',
+      },
+      day: {
+        previous: 'Día anterior',
+        next: 'Día siguiente',
+        today: 'Hoy',
+      },
+    },
+    // Permissions
+    permissions: {
+      noStopPrint: 'No tiene permiso para detener impresiones',
+      noStartPrint: 'No tiene permiso para iniciar impresiones',
+      noEdit: 'No tiene permiso para editar este elemento de la cola',
+      noCancel: 'No tiene permiso para cancelar este elemento de la cola',
+      noRequeue: 'No tiene permiso para volver a encolar elementos',
+      noRemove: 'No tiene permiso para eliminar este elemento de la cola',
+      noClearHistory: 'No tiene permiso para borrar todo el historial',
+      noEditItems: 'No tiene permiso para editar elementos de la cola',
+      noCancelItems: 'No tiene permiso para cancelar elementos de la cola',
+    },
+  },
+
+  backgroundDispatch: {
+    unknownFile: 'Archivo desconocido',
+    unknownPrinter: 'Impresora desconocida',
+    startingPrints: 'Iniciando impresiones',
+    progressSummary: '{{complete}}/{{total}} completadas • Enviadas: {{dispatched}} • Procesando: {{processing}}',
+    expandDetails: 'Expandir los detalles del envío',
+    collapseDetails: 'Contraer los detalles del envío',
+    dismissToast: 'Descartar la notificación de envío',
+    cancelDispatchJob: 'Cancelar el trabajo de envío',
+    cancel: 'Cancelar',
+    cancelling: 'Cancelando…',
+    awaitingPrinter: 'Esperando a la impresora…',
+    status: {
+      dispatched: 'Enviada',
+      processing: 'Procesando',
+      completed: 'Completada',
+      failed: 'Fallida',
+      cancelled: 'Cancelada',
+    },
+    toast: {
+      cancellingUpload: 'Cancelando la subida...',
+      cancelled: 'Envío cancelado',
+      cancelFailed: 'Error al cancelar el envío',
+      completeWithFailures: 'Envío en segundo plano completado: {{completed}} con éxito, {{failed}} con error',
+      completeSuccess: 'Envío en segundo plano completado: {{completed}} con éxito',
+      printStartedRemaining: '{{completed}} impresión(es) iniciada(s), enviando {{remaining}} más...',
+    },
+  },
+
+  // Statistics page
+  stats: {
+    title: 'Estadísticas',
+    subtitle: 'Arrastre los widgets para reorganizarlos. Haga clic en el icono del ojo para ocultarlos.',
+    overview: 'Resumen',
+    totalPrints: 'Impresiones totales',
+    successRate: 'Tasa de éxito',
+    totalPrintTime: 'Tiempo total de impresión',
+    printTime: 'Tiempo de impresión',
+    totalFilament: 'Filamento total usado',
+    filamentUsed: 'Filamento usado',
+    filamentCost: 'Coste del filamento',
+    totalCost: 'Coste total',
+    energyUsed: 'Energía usada',
+    energyCost: 'Coste de la energía',
+    energyWarmingUpTooltip: 'El seguimiento de energía aún está recopilando instantáneas horarias. Los totales por rango de fechas serán precisos una vez que exista al menos una instantánea antes del rango seleccionado. Los valores iniciales pueden quedarse cortos.',
+    averagePrintTime: 'Tiempo medio de impresión',
+    printsPerDay: 'Impresiones por día',
+    byPrinter: 'Por impresora',
+    printsByPrinter: 'Impresiones por impresora',
+    byMaterial: 'Por material',
+    byMonth: 'Por mes',
+    last7Days: 'Últimos 7 días',
+    last30Days: 'Últimos 30 días',
+    last90Days: 'Últimos 90 días',
+    allTime: 'Todo el tiempo',
+    // Widgets
+    quickStats: 'Estadísticas rápidas',
+    printActivity: 'Actividad de impresión',
+    filamentTypes: 'Tipos de filamento',
+    filamentTrends: 'Tendencias del filamento',
+    failureAnalysis: 'Análisis de fallos',
+    timeAccuracy: 'Precisión temporal',
+    successful: 'Con éxito:',
+    failed: 'Fallidas:',
+    perfectEstimate: '100% = estimación perfecta',
+    noTimeAccuracyData: 'Aún no hay datos de precisión temporal',
+    noFilamentData: 'No hay datos de filamento disponibles',
+    noPrinterData: 'No hay datos de impresora disponibles',
+    noPrintData: 'No hay datos de impresión disponibles',
+    noPrintDataLast30Days: 'No hay datos de impresión en los últimos 30 días',
+    failureReasons: 'Motivos de fallo',
+    topFailureReasons: 'Principales motivos de fallo',
+    failedPrintsCount: '{{failed}} / {{total}} impresiones fallidas',
+    lastWeekRate: 'Semana pasada: {{rate}}%',
+    // Actions
+    resetLayout: 'Restablecer disposición',
+    recalculateCosts: 'Recalcular costes',
+    recalculateCostsHint: 'Recalcular todos los costes de archivo usando los precios actuales del filamento',
+    exportStats: 'Exportar estadísticas',
+    exportAsCsv: 'Exportar como CSV',
+    exportAsExcel: 'Exportar como Excel',
+    hiddenCount: '{{count}} ocultos',
+    // Toast
+    exportDownloaded: 'Exportación descargada',
+    exportFailed: 'Error en la exportación',
+    layoutReset: 'Disposición restablecida',
+    recalculatedCosts: 'Costes recalculados para {{count}} archivos',
+    recalculateFailed: 'Error al recalcular los costes',
+    // Loading
+    loadingStats: 'Cargando estadísticas...',
+    // Permissions
+    noPermissionResetLayout: 'No tiene permiso para restablecer la disposición',
+    noPermissionRecalculate: 'No tiene permiso para recalcular los costes',
+    noPrintDataInRange: 'No hay datos de impresión en el rango seleccionado',
+    periodFilament: 'Filamento del periodo',
+    periodCost: 'Coste del periodo',
+    avgPerPrint: 'Promedio por impresión',
+    usageOverTime: 'Uso a lo largo del tiempo',
+    filamentByWeight: 'Peso',
+    printDuration: 'Duración de la impresión',
+    printerUtilization: 'Uso de la impresora',
+    filamentSuccess: 'Éxito por material',
+    printHabits: 'Hábitos de impresión',
+    printTimeOfDay: 'Hora del día de impresión',
+    colorDistribution: 'Distribución de colores',
+    noColorData: 'No hay datos de color disponibles',
+    records: 'Récords',
+    longestPrint: 'Impresión más larga',
+    heaviestPrint: 'Impresión más pesada',
+    mostExpensivePrint: 'Más cara',
+    busiestDay: 'Día más activo',
+    successStreak: 'Racha de éxitos',
+    streakPrint: 'impresión consecutiva',
+    streakPrints: '{{count}} impresiones consecutivas',
+    printerStats: 'Estadísticas de la impresora',
+    hours: 'horas',
+    avgPrints: 'Impresiones medias',
+    noArchiveData: 'No hay datos de impresión disponibles',
+    filamentByTime: 'Tiempo',
+    avgWeight: 'Peso medio',
+    avgTime: 'Tiempo medio',
+    filamentByPrints: 'Impresiones',
+    timeframe: {
+      'today': 'Hoy',
+      'this-week': 'Esta semana',
+      'this-month': 'Este mes',
+      'last-7': 'Últimos 7 días',
+      'last-30': 'Últimos 30 días',
+      'last-90': 'Últimos 90 días',
+      'this-year': 'Este año',
+      'all-time': 'Todo el tiempo',
+      'custom': 'Rango personalizado',
+      from: 'Desde',
+      to: 'Hasta',
+    },
+    // User filter
+    allUsers: 'Todos los usuarios',
+    noUser: 'Sin usuario (sistema)',
+    filterByUser: 'Filtrar por usuario',
+  },
+
+  // Maintenance page
+  maintenance: {
+    title: 'Mantenimiento',
+    overview: 'Resumen',
+    allOk: 'Todo el mantenimiento al día',
+    dueCount: '{{count}} tarea pendiente',
+    dueCount_plural: '{{count}} tareas pendientes',
+    warningCount: '{{count}} advertencia',
+    warningCount_plural: '{{count}} advertencias',
+    totalPrintTime: 'Tiempo total de impresión',
+    nextMaintenance: 'Próximo mantenimiento',
+    nothingDue: 'Nada pendiente',
+    tasks: 'Tareas',
+    lastPerformed: 'Realizado por última vez',
+    interval: 'Intervalo',
+    hoursRemaining: '{{hours}} h restantes',
+    hoursOverdue: '{{hours}} h de retraso',
+    markDone: 'Marcar como hecho',
+    performMaintenance: 'Realizar mantenimiento',
+    history: 'Historial',
+    noHistory: 'No hay historial de mantenimiento',
+    editPrintHours: 'Editar horas de impresión',
+    currentHours: 'Horas actuales',
+    // Tabs
+    statusTab: 'Estado',
+    settingsTab: 'Ajustes',
+    // Status
+    overdueCount: '{{count}} atrasadas',
+    dueSoonCount: '{{count}} pronto pendientes',
+    dueSoon: 'Pronto pendiente',
+    allGood: 'Todo correcto',
+    overdueBy: 'Atrasado {{duration}}',
+    dueIn: 'Pendiente en {{duration}}',
+    timeLeft: '{{duration}} restante',
+    // Duration formats
+    day: '1 día',
+    days: '{{count}} días',
+    week: '1 semana',
+    weeks: '{{count}} semanas',
+    month: '1 mes',
+    months: '{{count}} meses',
+    year: '1 año',
+    // Settings
+    maintenanceTypes: 'Tipos de mantenimiento',
+    maintenanceTypesDescription: 'Tipos del sistema y sus tareas de mantenimiento personalizadas',
+    addCustomType: 'Añadir tipo personalizado',
+    restoreDefaults: 'Restaurar tareas predeterminadas',
+    intervalType: 'Tipo de intervalo',
+    intervalValue: 'Intervalo ({{type}})',
+    icon: 'Icono',
+    documentationLink: 'Enlace de documentación (opcional)',
+    assignToPrinters: 'Asignar a impresoras',
+    selectAtLeastOnePrinter: 'Seleccione al menos una impresora',
+    addType: 'Añadir tipo',
+    custom: 'Personalizado',
+    printHours: 'Horas de impresión',
+    calendarDays: 'Días naturales',
+    exampleName: 'p. ej., Reemplazar filtro HEPA',
+    viewDocumentation: 'Ver documentación',
+    timeBasedInterval: 'Intervalo basado en el tiempo',
+    // Interval overrides
+    intervalOverrides: 'Anulaciones de intervalo',
+    intervalOverridesDescription: 'Personalizar intervalos para impresoras concretas',
+    // Printer assignment
+    assignedToPrinters: 'Asignado a las impresoras:',
+    noPrintersAssigned: 'No hay impresoras asignadas',
+    addPrinterShort: 'Añadir:',
+    printersAssignedClick: '{{count}} impresora(s) asignada(s) - haga clic para gestionar',
+    removeFromPrinter: 'Quitar de esta impresora',
+    // Types
+    types: {
+      lubricateCarbonRods: 'Lubricar varillas de carbono',
+      lubricateRails: 'Lubricar guías lineales',
+      cleanNozzle: 'Limpiar boquilla/fusor',
+      checkBelts: 'Comprobar la tensión de las correas',
+      cleanBuildPlate: 'Limpiar la cama de impresión',
+      checkExtruder: 'Comprobar los engranajes del extrusor',
+      checkCooling: 'Comprobar los ventiladores de refrigeración',
+      generalInspection: 'Inspección general',
+      cleanCarbonRods: 'Limpiar varillas de carbono',
+      lubricateSteelRods: 'Lubricar varillas de acero',
+      cleanSteelRods: 'Limpiar varillas de acero',
+      cleanLinearRails: 'Limpiar guías lineales',
+      checkPtfeTube: 'Comprobar el tubo de PTFE',
+      replaceHepaFilter: 'Reemplazar filtro HEPA',
+      replaceCarbonFilter: 'Reemplazar filtro de carbono',
+      lubricateLeftNozzleRail: 'Lubricar la guía de la boquilla izquierda',
+    },
+    // Toast
+    maintenanceComplete: 'Mantenimiento marcado como completado',
+    typeUpdated: 'Tipo de mantenimiento actualizado',
+    typeDeleted: 'Tipo de mantenimiento eliminado',
+    defaultsRestored: 'Se restauraron {{count}} tarea(s) predeterminada(s)',
+    printHoursUpdated: 'Horas de impresión actualizadas',
+    printerAssigned: 'Impresora asignada',
+    printerRemoved: 'Impresora eliminada',
+    // Confirmation
+    deleteTypeConfirm: '¿Eliminar "{{name}}"?',
+    deleteSystemTypeTitle: '¿Eliminar la tarea de mantenimiento predeterminada?',
+    deleteSystemTypeMessage: '¿Está seguro de que desea eliminar la tarea de mantenimiento predeterminada "{{name}}"?',
+    // Permissions
+    noPermissionUpdate: 'No tiene permiso para actualizar elementos de mantenimiento',
+    noPermissionPerform: 'No tiene permiso para realizar mantenimiento',
+    noPermissionEditTypes: 'No tiene permiso para editar tipos de mantenimiento',
+    noPermissionDeleteTypes: 'No tiene permiso para eliminar tipos de mantenimiento',
+    noPermissionEditHours: 'No tiene permiso para editar las horas de impresión',
+    noPermissionRemovePrinter: 'No tiene permiso para quitar asignaciones de impresoras',
+    noPermissionAssignPrinter: 'No tiene permiso para asignar impresoras',
+    noPermissionEditIntervals: 'No tiene permiso para editar intervalos',
+    // Configure link
+    configureSettings: 'Configurar tipos e intervalos de mantenimiento',
+  },
+
+  // Settings page
+  settings: {
+    title: 'Ajustes',
+    general: 'General',
+    // Tab names
+    tabs: {
+      general: 'General',
+      smartPlugs: 'Enchufes inteligentes',
+      notifications: 'Notificaciones',
+      queue: 'Flujo de trabajo',
+      filament: 'Filamento',
+      network: 'Red',
+      apiKeys: 'Claves API',
+      virtualPrinter: 'Impresora virtual',
+      spoolbuddy: 'SpoolBuddy',
+      failureDetection: 'Detección de fallos',
+      users: 'Autenticación',
+      backup: 'Copia de seguridad',
+      emailAuth: 'Autenticación por correo',
+      ldap: 'LDAP',
+      twoFa: 'Autenticación de dos factores',
+      oidc: 'SSO / OIDC',
+      security: 'Seguridad',
+    },
+    spoolbuddy: {
+      infoTitle: 'Dispositivos SpoolBuddy',
+      infoBody: 'Los quioscos SpoolBuddy se registran automáticamente mediante una señal de latido. Anule el registro de un dispositivo aquí si ya no está en uso o si quedó un duplicado obsoleto tras el fallo de un demonio.',
+      duplicatesTitle: '{{count}} dispositivos registrados',
+      duplicatesBody: 'La interfaz del quiosco solo usa el primer dispositivo registrado. Si uno de estos es un duplicado obsoleto de un fallo, anule su registro — un dispositivo en línea se volverá a registrar en su próxima señal de latido.',
+      empty: 'Aún no hay dispositivos SpoolBuddy registrados.',
+      online: 'En línea',
+      offline: 'Desconectado',
+      unregister: 'Anular registro',
+      unregisterSuccess: 'Registro del dispositivo anulado',
+      unregisterError: 'Error al anular el registro del dispositivo',
+      confirmTitle: '¿Anular el registro del dispositivo SpoolBuddy?',
+      confirmBody: 'Esto eliminará "{{hostname}}" ({{deviceId}}) de la base de datos. Si el dispositivo está en línea, se volverá a registrar en su próxima señal de latido.',
+      ipAddress: 'Dirección IP',
+      firmware: 'Firmware',
+      lastSeen: 'Visto por última vez',
+      daemonUptime: 'Tiempo de actividad del demonio',
+      systemUptime: 'Tiempo de actividad del sistema',
+      never: 'nunca',
+      nfc: 'NFC',
+      scale: 'Báscula',
+      cpuTemp: 'Temp. de CPU',
+      memory: 'Memoria',
+      disk: 'Disco',
+      // Device actions
+      update: 'Actualizar',
+      updateConfirmTitle: '¿Actualizar el demonio de Spoolbuddy?',
+      updateConfirmBody: '¿Iniciar una actualización de software en "{{hostname}}"? El demonio se reiniciará una vez aplicada la actualización.',
+      restartBrowser: 'Reiniciar navegador',
+      restartBrowserConfirmTitle: '¿Reiniciar el navegador del quiosco?',
+      restartBrowserConfirmBody: '¿Reiniciar el navegador del quiosco en "{{hostname}}"? La pantalla se quedará en negro brevemente.',
+      restartDaemon: 'Reiniciar demonio',
+      restartDaemonConfirmTitle: '¿Reiniciar el demonio de Spoolbuddy?',
+      restartDaemonConfirmBody: '¿Reiniciar el demonio de Spoolbuddy en "{{hostname}}"? El dispositivo se desconectará durante unos segundos.',
+      reboot: 'Reiniciar',
+      rebootConfirmTitle: '¿Reiniciar el dispositivo?',
+      rebootConfirmBody: '¿Reiniciar "{{hostname}}"? El dispositivo estará desconectado durante alrededor de un minuto.',
+      shutdown: 'Apagar',
+      shutdownConfirmTitle: '¿Apagar el dispositivo?',
+      shutdownConfirmBody: '¿Apagar "{{hostname}}"? Necesitará acceso físico para volver a encenderlo.',
+      commandConfirm: 'Confirmar',
+      commandQueued: 'Comando encolado',
+      commandError: 'Error al enviar el comando',
+    },
+    // LDAP settings
+    ldap: {
+      title: 'Autenticación LDAP',
+      enabledDesc: 'La autenticación LDAP está activada',
+      disabledDesc: 'La autenticación LDAP está desactivada',
+      disabledHint: 'Configure y guarde los ajustes de LDAP a continuación y luego actívela.',
+      enabled: 'Autenticación LDAP activada',
+      disabled: 'Autenticación LDAP desactivada',
+      feature1: 'Los usuarios pueden iniciar sesión con credenciales LDAP',
+      feature2: 'La cuenta de administrador local permanece como alternativa',
+      feature3: 'Los grupos LDAP se asignan a los grupos de Bambuddy al iniciar sesión',
+      serverConfig: 'Configuración del servidor LDAP',
+      serverUrl: 'URL del servidor',
+      serverUrlHint: 'Use ldaps:// para SSL o ldap:// con StartTLS',
+      security: 'Seguridad',
+      securityHint: 'StartTLS actualiza una conexión sin cifrar a TLS. LDAPS usa TLS desde el principio.',
+      bindDn: 'Bind DN (cuenta de servicio)',
+      bindPassword: 'Contraseña de Bind',
+      searchBase: 'DN base de búsqueda',
+      userFilter: 'Filtro de búsqueda de usuarios',
+      userFilterHint: '{username} se sustituye por el nombre de usuario de inicio de sesión. Use (uid={username}) para OpenLDAP.',
+      advanced: 'Avanzado',
+      autoProvision: 'Aprovisionar usuarios automáticamente',
+      autoProvisionHint: 'Crear automáticamente una cuenta de Bambuddy en el primer inicio de sesión LDAP',
+      defaultGroup: 'Grupo predeterminado',
+      defaultGroupNone: '— Ninguno (sin alternativa) —',
+      defaultGroupHint: 'Grupo alternativo asignado cuando un usuario LDAP se autentica pero no figura en ningún grupo LDAP asignado. Déjelo vacío para dejar a los usuarios sin asignar sin permisos.',
+      groupMapping: 'Asignación de grupos (JSON)',
+      groupMappingHint: 'Asignar los DN de grupos LDAP a los grupos de Bambuddy. Grupos disponibles: ',
+      testConnection: 'Probar conexión',
+      settingsSaved: 'Ajustes de LDAP guardados',
+      errors: {
+        serverRequired: 'La URL del servidor LDAP es obligatoria',
+        searchBaseRequired: 'El DN base de búsqueda es obligatorio',
+        enableAuthFirst: 'Active primero la autenticación',
+        configureLdapFirst: 'Guarde primero los ajustes de LDAP',
+      },
+    },
+    // Email settings
+    email: {
+      smtpSettings: 'Configuración SMTP',
+      smtpHost: 'Servidor SMTP',
+      smtpPort: 'Puerto SMTP',
+      security: 'Seguridad',
+      authentication: 'Autenticación',
+      username: 'Nombre de usuario',
+      password: 'Contraseña',
+      fromEmail: 'Correo del remitente',
+      fromName: 'Nombre del remitente',
+      testConnection: 'Probar conexión SMTP',
+      testRecipient: 'Correo del destinatario de prueba',
+      sendTest: 'Enviar correo de prueba',
+      sending: 'Enviando...',
+      save: 'Guardar ajustes',
+      saving: 'Guardando...',
+      advancedAuth: 'Autenticación avanzada',
+      advancedAuthEnabled: 'La autenticación avanzada está activada',
+      advancedAuthEnabledDesc: 'Las funciones de gestión de usuarios basadas en correo están activas. Los nuevos usuarios recibirán contraseñas autogeneradas por correo, y los usuarios podrán restablecer sus contraseñas mediante la función de contraseña olvidada.',
+      advancedAuthDisabled: 'La autenticación avanzada está desactivada',
+      advancedAuthDisabledDesc: 'Active la autenticación avanzada para habilitar las funciones basadas en correo para la gestión de usuarios.',
+      enable: 'Activar',
+      disable: 'Desactivar',
+      feature1: 'Las contraseñas se autogeneran y se envían por correo a los nuevos usuarios',
+      feature2: 'Los usuarios pueden iniciar sesión con el nombre de usuario o el correo',
+      feature3: 'La función de contraseña olvidada está disponible',
+      feature4: 'Los administradores pueden restablecer las contraseñas de los usuarios por correo',
+      // Error messages
+      errors: {
+        requiredFields: 'Rellene todos los campos obligatorios',
+        usernameRequired: 'El nombre de usuario es obligatorio cuando la autenticación está activada',
+        enterTestEmail: 'Introduzca una dirección de correo de prueba',
+        smtpServerAndEmail: 'Rellene el servidor SMTP y el correo del remitente antes de probar',
+        usernamePasswordRequired: 'El nombre de usuario y la contraseña son obligatorios cuando la autenticación está activada',
+        configureSmtpFirst: 'Configure y pruebe primero los ajustes de SMTP',
+        enableAuthFirst: 'Active primero la autenticación para usar las funciones basadas en correo.',
+      },
+      // Success messages
+      success: {
+        settingsSaved: 'Ajustes de SMTP guardados correctamente',
+      },
+      // Security options
+      securityOptions: {
+        starttls: 'STARTTLS (puerto 587)',
+        ssl: 'SSL/TLS (puerto 465)',
+        none: 'Ninguna (puerto 25)',
+      },
+      // Authentication options
+      authOptions: {
+        enabled: 'Activada',
+        disabled: 'Desactivada',
+      },
+    },
+    appearance: 'Apariencia',
+    notifications: 'Notificaciones',
+    smartPlugs: 'Enchufes inteligentes',
+    spoolman: 'Spoolman',
+    updates: 'Actualizaciones',
+    language: 'Idioma',
+    languageDescription: 'Seleccione su idioma preferido',
+    theme: 'Tema',
+    themeLight: 'Claro',
+    themeDark: 'Oscuro',
+    themeSystem: 'Sistema',
+    defaultView: 'Vista predeterminada',
+    defaultViewDescription: 'Página que se muestra al abrir la aplicación',
+    checkForUpdates: 'Buscar actualizaciones',
+    autoUpdate: 'Actualización automática',
+    currentVersion: 'Versión actual',
+    latestVersion: 'Versión más reciente',
+    upToDate: 'Está actualizado',
+    updateAvailable: 'Actualización disponible',
+    // Notifications
+    notificationLanguage: 'Idioma de las notificaciones',
+    notificationLanguageDescription: 'Idioma de las notificaciones push',
+    bedCooledThreshold: 'Umbral de cama enfriada',
+    bedCooledThresholdDescription: 'Temperatura por debajo de la cual se considera que la cama se ha enfriado tras una impresión',
+    userNotificationsEnabled: 'Notificaciones de usuario',
+    userNotificationsEnabledDescription: 'Activa el menú de notificaciones de usuario y las notificaciones por correo de los eventos de los trabajos de impresión. Requiere autenticación avanzada.',
+    userNotificationsDisabledHint: 'Active la autenticación avanzada para usar las notificaciones de usuario.',
+    notificationProviders: 'Proveedores de notificaciones',
+    addProvider: 'Añadir proveedor',
+    editProvider: 'Editar proveedor',
+    providerType: 'Tipo de proveedor',
+    testNotification: 'Notificación de prueba',
+    testSuccess: 'Notificación de prueba enviada correctamente',
+    testFailed: 'Error al enviar la notificación de prueba',
+    quietHours: 'Horas de silencio',
+    quietHoursDescription: 'No molestar durante estas horas',
+    quietHoursStart: 'Inicio',
+    quietHoursEnd: 'Fin',
+    events: {
+      title: 'Eventos de notificación',
+      printStart: 'Impresión iniciada',
+      printComplete: 'Impresión completada',
+      printFailed: 'Impresión fallida',
+      printStopped: 'Impresión detenida',
+      printProgress: 'Hitos de progreso',
+      printProgressDescription: 'Notificar al 25%, 50% y 75%',
+      printerOffline: 'Impresora desconectada',
+      printerError: 'Error de la impresora',
+      filamentLow: 'Filamento bajo',
+      maintenanceDue: 'Mantenimiento pendiente',
+      maintenanceDueDescription: 'Notificar cuando se necesite mantenimiento',
+    },
+    // Smart Plugs
+    smartPlug: {
+      title: 'Enchufes inteligentes',
+      add: 'Añadir enchufe inteligente',
+      edit: 'Editar enchufe inteligente',
+      name: 'Nombre',
+      ipAddress: 'Dirección IP',
+      linkedPrinter: 'Impresora vinculada',
+      autoOn: 'Encendido automático',
+      autoOnDescription: 'Encender cuando comienza la impresión',
+      autoOff: 'Apagado automático',
+      autoOffDescription: 'Apagar tras completar la impresión',
+      offDelay: 'Retardo de apagado',
+      offDelayMinutes: 'Minutos tras la impresión',
+      offDelayTemp: 'Cuando la boquilla está por debajo de la temperatura',
+      currentState: 'Estado actual',
+      turnOn: 'Encender',
+      turnOff: 'Apagar',
+    },
+    // Filament Tracking Mode
+    filamentTracking: 'Seguimiento del filamento',
+    filamentTrackingDesc: 'Elija cómo realizar el seguimiento de sus bobinas de filamento. Puede usar el inventario integrado o conectar un servidor Spoolman externo.',
+    filamentChecks: 'Comprobaciones de filamento',
+    disableFilamentWarnings: 'Desactivar las advertencias de filamento',
+    disableFilamentWarningsDesc: 'No mostrar advertencias sobre filamento insuficiente al imprimir o encolar',
+    preferLowestFilament: 'Preferir el filamento con menos restante',
+    preferLowestFilamentDesc: 'Cuando varias bobinas coincidan, usar la que tenga menos filamento restante',
+    trackingModeBuiltIn: 'Inventario integrado',
+    trackingModeBuiltInDesc: 'Incluye coincidencia automática por RFID y seguimiento del uso',
+    trackingModeSpoolmanDesc: 'Servidor externo de gestión de filamento',
+    builtInFeatureRfid: 'Detecta automáticamente las bobinas RFID de Bambu Lab en el AMS',
+    builtInFeatureUsage: 'Realiza el seguimiento del consumo de filamento por impresión',
+    builtInFeatureCatalog: 'Gestione bobinas, colores y perfiles de factor K',
+    builtInFeatureThirdParty: 'Las bobinas de terceros se pueden asignar a bobinas del inventario',
+    amsSyncButton: 'Sincronizar pesos desde el AMS',
+    amsSyncTitle: 'Sincronizar los pesos de las bobinas desde el AMS',
+    amsSyncMessage: 'Esto sobrescribirá todos los pesos de las bobinas del inventario con los valores actuales de % restante del AMS de las impresoras conectadas. Use esto para recuperarse de datos de peso dañados. Las impresoras deben estar en línea.',
+    amsSyncing: 'Sincronizando...',
+    amsSyncSuccess: '{{synced}} bobina(s) sincronizada(s), {{skipped}} omitida(s)',
+    amsSyncError: 'Error al sincronizar los pesos desde el AMS',
+    spoolmanAmsSyncButton: 'Sincronizar pesos de Spoolman desde el AMS',
+    spoolmanAmsSyncTitle: 'Sincronizar los pesos de las bobinas de Spoolman desde el AMS',
+    spoolmanAmsSyncMessage: 'Esto actualizará todos los pesos de las bobinas de Spoolman según los valores actuales de % restante del AMS de las impresoras conectadas. Las impresoras deben estar en línea.',
+    spoolmanAmsSyncing: 'Sincronizando...',
+    spoolmanAmsSyncSuccess: '{{synced}} bobina(s) sincronizada(s), {{skipped}} omitida(s)',
+    spoolmanAmsSyncError: 'Error al sincronizar los pesos de Spoolman desde el AMS',
+    spoolmanAmsSyncErrorUnreachable: 'Error al sincronizar los pesos de Spoolman (Spoolman inaccesible)',
+    spoolmanAmsSyncErrorNotConfigured: 'Error al sincronizar los pesos de Spoolman (Spoolman no configurado)',
+    spoolmanNotConfigured: 'Spoolman no configurado',
+    // Spoolman filament catalog section in spool catalog settings
+    spoolmanFilamentCatalogTitle: 'Catálogo de filamentos de Spoolman',
+    spoolmanFilamentCatalogDesc: 'Nombres de filamentos y pesos de tara de su instancia de Spoolman. El nombre y el peso de la bobina se pueden editar aquí; el resto de las propiedades se gestionan directamente en Spoolman.',
+    // Spoolman settings
+    spoolmanUrl: 'URL de Spoolman',
+    spoolmanUrlHint: 'URL de su servidor Spoolman (p. ej., http://localhost:7912)',
+    spoolmanConnected: 'Conectado',
+    spoolmanDisconnected: 'Desconectado',
+    status: 'Estado',
+    connect: 'Conectar',
+    disconnect: 'Desconectar',
+    howSyncWorks: 'Cómo funciona la sincronización',
+    syncInfoRfidOnly: 'Solo se sincronizan las bobinas oficiales de Bambu Lab con RFID',
+    syncInfoAutoCreate: 'Las bobinas nuevas se crean automáticamente en Spoolman en la primera sincronización',
+    syncInfoThirdPartySkipped: 'Las bobinas que no son de Bambu Lab (de terceros, rellenadas) se omiten',
+    linkingExistingSpools: 'Vincular bobinas existentes',
+    linkingExistingSpoolsDesc: 'Para vincular bobinas existentes de Spoolman a su AMS, pase el cursor sobre una ranura del AMS y haga clic en "Vincular a Spoolman".',
+    syncMode: 'Modo de sincronización',
+    syncModeAuto: 'Automática',
+    syncModeManual: 'Solo manual',
+    syncModeAutoDesc: 'Los datos del AMS se sincronizan automáticamente cuando se detectan cambios',
+    syncModeManualDesc: 'Sincronizar solo cuando se active manualmente',
+    syncAmsData: 'Sincronizar datos del AMS',
+    syncAmsDataDesc: 'Sincronizar manualmente los datos del AMS de la impresora con Spoolman',
+    allPrinters: 'Todas las impresoras',
+    // Default printer
+    noDefaultPrinter: 'Sin predeterminada (preguntar cada vez)',
+    // Sidebar
+    sidebarOrder: 'Orden de la barra lateral',
+    // Camera
+    saveThumbnails: 'Guardar miniaturas',
+    captureFinishPhoto: 'Capturar foto de finalización',
+    noPrintersConfigured: 'No hay impresoras configuradas',
+    // Archive settings
+    archiveMode: {
+      always: 'Crear siempre una entrada de archivo',
+      never: 'No crear nunca una entrada de archivo',
+      ask: 'Preguntar cada vez',
+    },
+    // Updates
+    checkForUpdatesLabel: 'Buscar actualizaciones',
+    checkPrinterFirmware: 'Comprobar el firmware de la impresora',
+    includeBetaUpdates: 'Incluir versiones beta',
+    includeBetaUpdatesDesc: 'Notificar sobre versiones beta y preliminares al buscar actualizaciones',
+    // Queue
+    enableRetry: 'Activar reintentos',
+    // Home Assistant
+    homeAssistantDescription: 'Controlar enchufes inteligentes mediante Home Assistant',
+    environmentManagedLabel: '(Gestionado por el entorno)',
+    autoEnabledViaEnv: 'Activado automáticamente mediante variables de entorno',
+    urlFromEnvReadOnly: 'Valor establecido por la variable de entorno HA_URL (solo lectura)',
+    tokenFromEnvReadOnly: 'Valor establecido por la variable de entorno HA_TOKEN (solo lectura)',
+    // MQTT
+    mqttConnectedTo: 'Conectado a',
+    // Prometheus
+    prometheusDescription: 'Exponer los datos de la impresora en formato Prometheus',
+    // Smart plugs empty state
+    noSmartPlugsTitle: 'No hay enchufes inteligentes configurados',
+    noSmartPlugsDescription: 'Añada un enchufe inteligente basado en Tasmota para realizar el seguimiento del consumo de energía y automatizar el control de la alimentación.',
+    // Notifications empty state
+    noProvidersTitle: 'No hay proveedores configurados',
+    noProvidersDescription: 'Añada un proveedor para recibir alertas.',
+    noTemplatesAvailable: 'No hay plantillas disponibles. Reinicie el backend para cargar las plantillas predeterminadas.',
+    // API permissions
+    apiPermissionView: 'Ver el estado de la impresora y la cola',
+    apiPermissionEdit: 'Añadir y quitar elementos de la cola de impresión',
+    // API keys
+    apiKeysEmptyTitle: 'No hay claves API',
+    apiKeysEmptyDescription: 'Cree una clave API para integrarse con servicios externos.',
+    // Users
+    noUsersFound: 'No se encontraron usuarios',
+    noGroupsFound: 'No se encontraron grupos',
+    noGroupsAvailable: 'No hay grupos disponibles',
+    passwordsDoNotMatch: 'Las contraseñas no coinciden',
+    systemGroupWarning: 'Los nombres de los grupos del sistema no se pueden cambiar',
+    // Auth disabled
+    authDisabledTitle: 'La autenticación está desactivada',
+    authDisabledFeature1: 'Exigir inicio de sesión para acceder al sistema',
+    authDisabledFeature2: 'Crear varios usuarios con permisos basados en grupos',
+    authDisabledFeature3: 'Controlar el acceso con más de 50 permisos granulares',
+    // User deletion
+    userHasCreated: 'Este usuario ha creado:',
+    userItemsQuestion: '¿Qué desea hacer con estos elementos?',
+    deleteUserConfirm: '¿Está seguro de que desea eliminar este usuario?',
+    actionCannotBeUndone: 'Esta acción no se puede deshacer.',
+    // Smart plugs
+    addFirstSmartPlug: 'Añada su primer enchufe inteligente',
+    // Notifications
+    providers: 'Proveedores',
+    log: 'Registro',
+    testAll: 'Probar todo',
+    testResults: 'Resultados de la prueba',
+    testPassedCount: '{{count}} superadas',
+    testFailedCount: '{{count}} fallidas',
+    messageTemplates: 'Plantillas de mensajes',
+    messageTemplatesDescription: 'Personalice los mensajes de notificación para cada evento.',
+    // API Keys section
+    apiKeys: 'Claves API',
+    apiKeysDescription: 'Cree claves API para integraciones externas y webhooks.',
+    createKey: 'Crear clave',
+    apiKeyCreated: 'Clave API creada correctamente',
+    apiKeyCopyWarning: '¡Copie esta clave ahora; no se volverá a mostrar!',
+    useInApiBrowser: 'Usar en el explorador de API',
+    createNewApiKey: 'Crear nueva clave API',
+    keyName: 'Nombre de la clave',
+    keyNamePlaceholder: 'p. ej., Home Assistant, OctoPrint',
+    readStatus: 'Leer estado',
+    readStatusDescription: 'Ver el estado de la impresora y la cola',
+    manageQueue: 'Gestionar la cola',
+    manageQueueDescription: 'Añadir y quitar elementos de la cola de impresión',
+    controlPrinter: 'Controlar la impresora',
+    controlPrinterDescription: 'Pausar, reanudar y detener impresiones',
+    cloudAccess: 'Permitir el acceso a la nube',
+    cloudAccessDescription: 'Leer los preajustes y filamentos de Bambu Cloud en su nombre. Requiere que haya iniciado sesión en Bambu Cloud.',
+    cloudBadge: 'Nube',
+    updateEnergyCost: 'Actualizar el precio de la electricidad',
+    updateEnergyCostDescription: 'Permite que esta clave envíe por POST un nuevo precio de electricidad por kWh a /settings/electricity-price. Útil para las automatizaciones de tarifa dinámica de Home Assistant (Tibber, Octopus, etc.). Este es el único campo de ajustes que se puede escribir mediante una clave API.',
+    energyCostBadge: 'Energía',
+    legacyKey: 'Heredada',
+    legacyKeyTooltip: 'Creada antes de la propiedad por usuario; vuelva a crearla para usar el acceso a la nube',
+    unnamedKey: 'Clave sin nombre',
+    lastUsed: 'Usada por última vez',
+    read: 'Lectura',
+    control: 'Control',
+    createFirstKey: 'Cree su primera clave',
+    webhookEndpoints: 'Puntos de conexión de webhook',
+    webhookApiKeyHint: 'Use su clave API en la cabecera X-API-Key.',
+    webhook: {
+      getAllStatus: 'Obtener el estado de todas las impresoras',
+      getSpecificStatus: 'Obtener el estado de una impresora concreta',
+      addToQueue: 'Añadir a la cola de impresión',
+      pausePrint: 'Pausar impresión',
+      resumePrint: 'Reanudar impresión',
+      stopPrint: 'Detener impresión',
+    },
+    apiBrowser: 'Explorador de API',
+    apiBrowserDescription: 'Explore y pruebe todos los puntos de conexión de API disponibles.',
+    apiKeyForTesting: 'Clave API para pruebas',
+    apiKeyPlaceholder: 'Pegue aquí su clave API para probar los puntos de conexión autenticados...',
+    apiKeyHint: 'Esta clave se enviará como cabecera X-API-Key en las solicitudes.',
+    deleteApiKeyTitle: 'Eliminar clave API',
+    deleteApiKeyMessage: '¿Está seguro de que desea eliminar esta clave API? Cualquier integración que use esta clave dejará de funcionar.',
+    deleteKey: 'Eliminar clave',
+    // Filament tab
+    amsDisplayThresholds: 'Umbrales de visualización del AMS',
+    amsThresholdsDescription: 'Configure los umbrales de color para los indicadores de humedad y temperatura del AMS.',
+    humidity: 'Humedad',
+    goodGreen: 'Buena (verde)',
+    fairOrange: 'Aceptable (naranja)',
+    aboveFairBad: 'Por encima del umbral aceptable se muestra en rojo (mala)',
+    fairAlsoDryingThreshold: 'Este umbral también se usa para activar el secado automático cuando está habilitado',
+    temperature: 'Temperatura',
+    goodBlue: 'Buena (azul)',
+    aboveFairHot: 'Por encima del umbral aceptable se muestra en rojo (caliente)',
+    historyRetention: 'Retención del historial',
+    keepSensorHistory: 'Conservar el historial del sensor durante',
+    historyRetentionDescription: 'Los datos de humedad y temperatura más antiguos se eliminarán automáticamente',
+    defaultPrintOptions: 'Opciones de impresión predeterminadas',
+    defaultPrintOptionsDescription: 'Establezca valores predeterminados para las opciones de impresión al iniciar nuevas impresiones. Se pueden anular por impresión en el diálogo de impresión.',
+    defaultBedLevelling: 'Nivelación de la cama',
+    defaultBedLevellingDesc: 'Nivelar automáticamente la cama antes de imprimir',
+    defaultFlowCali: 'Calibración del flujo',
+    defaultFlowCaliDesc: 'Calibrar el flujo de extrusión',
+    defaultVibrationCali: 'Calibración de vibración',
+    defaultVibrationCaliDesc: 'Reducir los artefactos de resonancia',
+    defaultLayerInspect: 'Inspección de la primera capa',
+    defaultLayerInspectDesc: 'Inspección de la primera capa por IA',
+    defaultTimelapse: 'Time-lapse',
+    defaultTimelapseDesc: 'Grabar vídeo time-lapse',
+    staggeredStart: 'Inicio escalonado',
+    staggeredStartDescription: 'Tamaño de grupo e intervalo predeterminados al escalonar los inicios de lotes en varias impresoras. Se pueden anular por lote en la ventana de impresión.',
+    plateClear: 'Confirmación de cama despejada',
+    requirePlateClear: 'Requerir confirmación de cama despejada',
+    requirePlateClearDescription: 'Cuando está activado, el planificador espera la confirmación de cama despejada por impresora antes de iniciar impresiones en cola en impresoras con trabajos finalizados. Desactivar esto también oculta la insignia de estado de la cama y el botón "Marcar cama como despejada" en las tarjetas de impresora.',
+    gcodeInjection: 'Inyección de G-code',
+    gcodeInjectionDescription: 'Configure G-code personalizado para inyectar al inicio o al final de las impresiones para sistemas de impresión automática como Farmloop, SwapMod, AutoClear y Printflow 3D. Los fragmentos se configuran por modelo de impresora y se aplican cuando se activa "Inyectar G-code" en un elemento de la cola.',
+    gcodeInjectionNoPrinters: 'No se encontraron impresoras. Añada impresoras para configurar fragmentos de G-code.',
+    gcodeStartLabel: 'G-code de inicio',
+    gcodeEndLabel: 'G-code de fin',
+    gcodeStartPlaceholder: 'G-code añadido antes de que comience la impresión...',
+    gcodeEndPlaceholder: 'G-code añadido después de que termine la impresión...',
+    staggerGroupSize: 'Tamaño de grupo',
+    staggerGroupSizeHelp: 'Impresoras que se inician simultáneamente por grupo',
+    staggerInterval: 'Intervalo (minutos)',
+    staggerIntervalHelp: 'Retardo entre el inicio de cada grupo',
+    queueDrying: 'Secado automático de la cola',
+    queueDryingDescription: 'Secar automáticamente el filamento del AMS cuando la impresora está inactiva entre impresiones en cola. Usa el umbral de humedad de arriba para activar el secado.',
+    queueDryingEnabled: 'Activar el secado automático',
+    queueDryingEnabledDescription: 'Iniciar el secado del AMS automáticamente cuando la impresora está inactiva y la humedad supera el umbral',
+    queueDryingBlock: 'Esperar a que termine el secado',
+    queueDryingBlockDescription: 'Bloquear la cola de impresión hasta que termine el secado. Cuando está desactivado, las impresiones tienen prioridad sobre el secado.',
+    ambientDryingEnabled: 'Secado ambiental',
+    ambientDryingEnabledDescription: 'Secar automáticamente el filamento en impresoras inactivas cuando la humedad supera el umbral, incluso sin impresiones en cola.',
+    dryingPresets: 'Preajustes de secado',
+    dryingPresetsDescription: 'Temperatura y duración por tipo de filamento. El AMS 2 Pro usa temperaturas más bajas; el AMS-HT admite temperaturas más altas.',
+    dryingFilament: 'Filamento',
+    printModal: 'Ventana de impresión',
+    expandCustomMapping: 'Expandir el mapeo personalizado de forma predeterminada',
+    expandCustomMappingDescription: 'Al imprimir en varias impresoras, mostrar el mapeo de AMS por impresora expandido',
+    // User management
+    authentication: 'Autenticación',
+    authEnabledDescription: 'Su instancia está protegida con autenticación de usuario',
+    authDisabledDescription: 'Active para exigir el inicio de sesión y gestionar el acceso de los usuarios',
+    authDisabledMessage: 'Active la autenticación para crear cuentas de usuario, gestionar permisos y proteger su instancia de Bambuddy.',
+    enableAuthentication: 'Activar la autenticación',
+    currentUser: 'Usuario actual',
+    changePassword: 'Cambiar contraseña',
+    admin: 'Administrador',
+    users: 'Usuarios',
+    addUser: 'Añadir usuario',
+    groups: 'Grupos',
+    addGroup: 'Añadir grupo',
+    system: 'Sistema',
+    noDescription: 'Sin descripción',
+    userCount: '{{count}} usuarios',
+    permissionCount: '{{count}} permisos',
+    createUser: 'Crear usuario',
+    username: 'Nombre de usuario',
+    enterUsername: 'Introduzca el nombre de usuario',
+    password: 'Contraseña',
+    enterPassword: 'Introduzca la contraseña',
+    passwordRequirements: 'Al menos 8 caracteres, con una mayúscula, una minúscula, un dígito y un carácter especial.',
+    confirmPassword: 'Confirmar contraseña',
+    confirmPasswordPlaceholder: 'Confirme la contraseña',
+    // Title tooltips
+    viewReleaseOnGitHub: 'Ver la versión en GitHub',
+    turnAllPlugsOn: 'Encender todos los enchufes',
+    turnAllPlugsOff: 'Apagar todos los enchufes',
+    // Modal: Clear logs
+    clearNotificationLogs: 'Borrar los registros de notificaciones',
+    clearLogsMessage: 'Esto eliminará permanentemente todos los registros de notificaciones de más de 30 días. Esta acción no se puede deshacer.',
+    clearLogs: 'Borrar registros',
+    // Modal: Reset UI
+    resetUiPreferences: 'Restablecer las preferencias de la interfaz',
+    resetUiPreferencesMessage: 'Esto restablecerá todas las preferencias de la interfaz a sus valores predeterminados: orden de la barra lateral, tema, disposición del panel, modos de vista y preferencias de ordenación. Sus impresoras, archivos y ajustes del servidor NO se verán afectados. La página se recargará tras el restablecimiento.',
+    resetPreferences: 'Restablecer preferencias',
+    // Modal: Delete group
+    deleteGroupTitle: 'Eliminar grupo',
+    deleteGroupMessage: '¿Está seguro de que desea eliminar este grupo? Los usuarios de este grupo perderán estos permisos.',
+    deleteGroup: 'Eliminar grupo',
+    // Modal: Disable auth
+    disableAuthenticationTitle: 'Desactivar la autenticación',
+    disableAuthenticationMessage: '¿Está seguro de que desea desactivar la autenticación? Esto hará que su instancia de Bambuddy sea accesible sin iniciar sesión. Todos los usuarios permanecerán en la base de datos, pero la autenticación se desactivará.',
+    disableAuthentication: 'Desactivar la autenticación',
+    // Additional settings
+    configureBambuddy: 'Configurar Bambuddy',
+    systemDefault: 'Predeterminado del sistema',
+    archiveSettings: 'Ajustes de archivado',
+    newWindow: 'Ventana nueva',
+    embeddedOverlay: 'Superposición integrada',
+    preferredSlicer: 'Laminador preferido',
+    preferredSlicerDescription: 'Elija con qué aplicación de laminado abrir los archivos',
+    orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev tienen errores conocidos de la CLI que impiden laminar muchos 3MF creados con Bambu — consulte las incidencias del proyecto original #12426 (cierre inesperado en archivos multiextrusor pintados) y #13386 (rechazo por validación estricta del rango de parámetros). Se recomienda Bambu Studio hasta que lleguen las correcciones.',
+    useSlicerApi: 'Usar la API del laminador',
+    useSlicerApiDescription: 'Cuando está activado, las acciones de «Laminar» abren la ventana del laminador integrada en la aplicación y llaman al contenedor auxiliar de la API del laminador. Cuando está desactivado (predeterminado), delegan en el laminador de escritorio mediante un esquema de URI.',
+    slicerCard: 'Laminador',
+    orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
+    bambuStudioApiUrl: 'Bambu Studio sidecar URL',
+    slicerApiUrlDescription: 'URL del contenedor auxiliar de la API del laminador. Déjelo en blanco para usar los valores predeterminados de las variables de entorno SLICER_API_URL / BAMBU_STUDIO_API_URL.',
+    slicerBundles: {
+      title: 'Paquetes del laminador',
+      description: 'Importe un paquete de preajustes de impresora (.bbscfg) exportado desde BambuStudio (Archivo → Exportar → Exportar paquete de preajustes → «Paquete de preajustes de impresora»). Una vez importado, las solicitudes de laminado pueden elegir preajustes del paquete por nombre sin volver a subir el trío de perfiles JSON.',
+      uploadButton: 'Subir paquete',
+      uploading: 'Subiendo…',
+      loading: 'Cargando paquetes…',
+      empty: 'Aún no se ha importado ningún paquete.',
+      summary: '{{processCount}} preajustes de proceso · {{filamentCount}} de filamento',
+      delete: 'Eliminar',
+      uploadSuccess: '{{name}} importado',
+      uploadError: 'Error al subir el paquete: {{message}}',
+      deleteSuccess: 'Paquete eliminado',
+      deleteError: 'Error al eliminar el paquete: {{message}}',
+      confirmDeleteTitle: '¿Eliminar este paquete?',
+      confirmDeleteMessage: 'Las solicitudes de laminado que hagan referencia a "{{name}}" fallarán hasta que se vuelva a importar el paquete.',
+    },
+    externalCameras: 'Cámaras externas',
+    costTracking: 'Seguimiento de costes',
+    printsOnly: 'Solo impresiones',
+    totalConsumption: 'Consumo total',
+    dataManagement: 'Gestión de datos',
+    storageUsage: 'Uso del almacenamiento',
+    storageUsageDescription: 'Desglose del uso de datos por categoría',
+    storageUsageTotal: 'Total',
+    storageUsageErrors: 'Errores',
+    storageUsageOtherBreakdown: 'Otros (incluye recursos estáticos, scripts y archivos de configuración)',
+    storageUsageSystem: 'Sistema',
+    storageUsageData: 'Datos',
+    storageUsageUnavailable: 'Información de uso del almacenamiento no disponible',
+    clearNotificationLogsDescription: 'Eliminar los registros de notificaciones de más de 30 días',
+    resetUiPreferencesDescription: 'Restablecer el orden de la barra lateral, el tema, los modos de vista y las preferencias de disposición. Las impresoras, los archivos y los ajustes no se ven afectados.',
+    enableHomeAssistant: 'Activar Home Assistant',
+    enableMqtt: 'Activar MQTT',
+    useTls: 'Usar TLS',
+    enableMetricsEndpoint: 'Activar el punto de conexión de métricas',
+    availableMetrics: 'Métricas disponibles',
+    editUser: 'Editar usuario',
+    deleteUserTitle: 'Eliminar usuario',
+    groupName: 'Nombre del grupo',
+    // Placeholders
+    leaveEmptyForAnonymous: 'Dejar vacío para anónimo',
+    leaveEmptyForNoAuth: 'Dejar vacío para sin autenticación',
+    enterNewPassword: 'Introduzca la nueva contraseña',
+    confirmNewPassword: 'Confirme la nueva contraseña',
+    enterGroupName: 'Introduzca el nombre del grupo',
+    enterDescriptionOptional: 'Introduzca una descripción (opcional)',
+    enterCurrentPassword: 'Introduzca la contraseña actual',
+    enterNewPasswordMin6: 'Introduzca la nueva contraseña (mín. 6 caracteres)',
+    toast: {
+      keyCopied: 'Clave copiada al portapapeles',
+      copyFailed: 'Error al copiar la clave',
+      keyAddedToBrowser: 'Clave añadida al explorador de API',
+      clearLogsFailed: 'Error al borrar los registros',
+      uiPreferencesReset: 'Preferencias de la interfaz restablecidas. Actualizando...',
+      authDisabled: 'Autenticación desactivada correctamente',
+      authDisableFailed: 'Error al desactivar la autenticación',
+      apiKeyCreated: 'Clave API creada',
+      apiKeyDeleted: 'Clave API eliminada',
+      userCreated: 'Usuario creado correctamente',
+      userUpdated: 'Usuario actualizado correctamente',
+      userDeleted: 'Usuario eliminado correctamente',
+      groupCreated: 'Grupo creado correctamente',
+      groupUpdated: 'Grupo actualizado correctamente',
+      groupDeleted: 'Grupo eliminado correctamente',
+      fillRequiredFields: 'Rellene todos los campos obligatorios',
+      passwordsDoNotMatch: 'Las contraseñas no coinciden',
+      passwordTooShort: 'La contraseña debe tener al menos 8 caracteres',
+      passwordNeedsUppercase: 'La contraseña debe contener al menos una letra mayúscula',
+      passwordNeedsLowercase: 'La contraseña debe contener al menos una letra minúscula',
+      passwordNeedsDigit: 'La contraseña debe contener al menos un dígito',
+      passwordNeedsSpecial: 'La contraseña debe contener al menos un carácter especial',
+      enterGroupName: 'Introduzca un nombre de grupo',
+      settingsSaved: 'Ajustes guardados',
+      noPermissionUpdate: 'No tiene permiso para cambiar los ajustes',
+      cameraSettingsSaved: 'Ajustes de la cámara guardados',
+      enterCameraUrl: 'Introduzca una URL de cámara',
+      passwordChanged: 'Contraseña cambiada correctamente',
+      connectionFailed: 'Error de conexión',
+      testFailed: 'La prueba falló',
+      cameraConnected: 'Cámara conectada{{resolution}}',
+    },
+    testConnection: 'Probar conexión',
+    catalog: {
+      spoolCatalog: 'Catálogo de bobinas',
+      spoolCatalogDescription: 'Pesos de bobinas vacías por marca/tipo. Se usa para la búsqueda automática de peso al añadir bobinas.',
+      searchCatalog: 'Buscar en el catálogo...',
+      addNewEntry: 'Añadir nueva entrada',
+      namePlaceholder: 'Nombre (p. ej., Bambu Lab - Plástico)',
+      weight: 'Peso',
+      type: 'Tipo',
+      default: 'Predeterminado',
+      custom: 'Personalizado',
+      noMatch: 'Ninguna entrada coincide con su búsqueda',
+      empty: 'No hay entradas en el catálogo',
+      deleteEntry: 'Eliminar entrada',
+      deleteConfirm: '¿Está seguro de que desea eliminar "{{name}}"?',
+      resetCatalog: 'Restablecer catálogo',
+      resetConfirm: '¿Restablecer el catálogo a los valores predeterminados? Esto eliminará todas las entradas personalizadas.',
+      loadFailed: 'Error al cargar el catálogo de bobinas',
+      nameWeightRequired: 'El nombre y el peso son obligatorios',
+      entryAdded: 'Entrada añadida',
+      addFailed: 'Error al añadir la entrada',
+      entryUpdated: 'Entrada actualizada',
+      updateFailed: 'Error al actualizar la entrada',
+      entryDeleted: 'Entrada eliminada',
+      deleteFailed: 'Error al eliminar la entrada',
+      resetSuccess: 'Catálogo restablecido a los valores predeterminados',
+      resetFailed: 'Error al restablecer el catálogo',
+      exported: 'Se exportaron {{count}} entradas',
+      imported: 'Se importaron {{added}} entradas ({{skipped}} omitidas)',
+      importFailed: 'Error al importar: formato JSON no válido',
+      exportTooltip: 'Exportar el catálogo a JSON',
+      importTooltip: 'Importar el catálogo desde JSON',
+      resetTooltip: 'Restablecer a los valores predeterminados',
+      selectedCount: '{{count}} seleccionadas',
+      deleteSelected: 'Eliminar seleccionadas',
+      bulkDeleteConfirm: '¿Está seguro de que desea eliminar {{count}} entradas?',
+      bulkDeleted: 'Se eliminaron {{count}} entradas',
+      bulkDeleteFailed: 'Error al eliminar las entradas',
+      material: 'Material',
+      spoolWeight: 'Peso de la bobina',
+      color: 'Color',
+      updateSpoolWeight: 'Actualizar el peso de la bobina',
+      filamentUpdated: 'Filamento actualizado',
+      filamentUpdateFailed: 'Error al actualizar el filamento',
+      filamentUpdateInvalid: 'Datos de filamento no válidos',
+      keepExistingSpoolWeight: 'Conservar el peso anterior para las bobinas existentes',
+      keepExistingSpoolWeightDesc: 'Las bobinas ya creadas con este tipo de filamento conservan el peso de tara anterior. Las bobinas nuevas usan el valor actualizado.',
+      applyToAllSpools: 'Aplicar a todas las bobinas',
+      applyToAllSpoolsDesc: 'Todos los cálculos de peso para este tipo de filamento usan inmediatamente el nuevo peso de tara.',
+    },
+    colorCatalog: {
+      title: 'Catálogo de colores',
+      description: 'Colores de filamento por fabricante/material. Se usa para la búsqueda automática de color al añadir bobinas.',
+      searchColors: 'Buscar colores...',
+      allManufacturers: 'Todos los fabricantes',
+      addNewColor: 'Añadir nuevo color',
+      manufacturer: 'Fabricante',
+      colorName: 'Nombre del color',
+      hex: 'Hex',
+      materialOptional: 'Material (opcional)',
+      showing: 'Mostrando {{filtered}} de {{total}} colores',
+      noMatch: 'Ningún color coincide con su búsqueda',
+      empty: 'No hay colores en el catálogo',
+      deleteColor: 'Eliminar color',
+      deleteConfirm: '¿Está seguro de que desea eliminar "{{name}}"?',
+      resetCatalog: 'Restablecer el catálogo de colores',
+      resetConfirm: '¿Restablecer el catálogo a los valores predeterminados? Esto eliminará todos los colores personalizados.',
+      sync: 'Sincronizar',
+      starting: 'Iniciando...',
+      syncTooltip: 'Sincronizar desde FilamentColors.xyz (más de 2000 colores, puede tardar un minuto)',
+      loadFailed: 'Error al cargar el catálogo de colores',
+      fieldsRequired: 'El fabricante, el nombre del color y el color hexadecimal son obligatorios',
+      colorAdded: 'Color añadido',
+      addFailed: 'Error al añadir el color',
+      colorUpdated: 'Color actualizado',
+      updateFailed: 'Error al actualizar el color',
+      colorDeleted: 'Color eliminado',
+      deleteFailed: 'Error al eliminar el color',
+      resetSuccess: 'Catálogo de colores restablecido a los valores predeterminados',
+      resetFailed: 'Error al restablecer el catálogo',
+      syncUpToDate: 'Ya está actualizado ({{count}} colores comprobados)',
+      syncComplete: 'Se añadieron {{added}} colores nuevos ({{skipped}} ya existían)',
+      syncError: 'Error de sincronización',
+      syncFailed: 'Error al sincronizar desde FilamentColors.xyz',
+      exported: 'Se exportaron {{count}} colores',
+      imported: 'Se importaron {{added}} colores ({{skipped}} omitidos)',
+      importFailed: 'Error al importar: formato JSON no válido',
+      selectedCount: '{{count}} seleccionados',
+      deleteSelected: 'Eliminar seleccionados',
+      bulkDeleteConfirm: '¿Está seguro de que desea eliminar {{count}} colores?',
+      bulkDeleted: 'Se eliminaron {{count}} colores',
+      bulkDeleteFailed: 'Error al eliminar los colores',
+    },
+    // General tab
+    dateFormat: 'Formato de fecha',
+    dateFormatUs: 'EE. UU. (MM/DD/AAAA)',
+    dateFormatEu: 'UE (DD/MM/AAAA)',
+    dateFormatIso: 'ISO (AAAA-MM-DD)',
+    timeFormat: 'Formato de hora',
+    timeFormat12: '12 horas (3:30 PM)',
+    timeFormat24: '24 horas (15:30)',
+    defaultPrinter: 'Impresora predeterminada',
+    defaultPrinterDescription: 'Preseleccione esta impresora para subidas, reimpresiones y otras operaciones.',
+    slicerBambuStudio: 'Bambu Studio',
+    slicerOrcaSlicer: 'OrcaSlicer',
+    sidebarOrderDescription: 'Arrastre los elementos de la barra lateral para reordenarlos. Restablezca aquí el orden predeterminado.',
+    setDefault: 'Establecer como predeterminado',
+    sidebarOrderSetDefaultHint: 'Establecer como predeterminado aplica el orden de menú actual a los usuarios que no han personalizado el suyo.',
+    sidebarDefaultSet: 'Se ha establecido el orden de menú predeterminado.',
+    sidebarDefaultCleared: 'Orden de menú predeterminado borrado.',
+    sidebarDefaultFailed: 'Error al establecer el orden de menú predeterminado.',
+    reset: 'Restablecer',
+    // Appearance
+    darkMode: 'Modo oscuro',
+    lightMode: 'Modo claro',
+    active: '(activo)',
+    background: 'Fondo',
+    accent: 'Acento',
+    style: 'Estilo',
+    bgNeutral: 'Neutro',
+    bgWarm: 'Cálido',
+    bgCool: 'Frío',
+    bgOled: 'Negro OLED',
+    bgSlate: 'Azul pizarra',
+    bgForest: 'Verde bosque',
+    accentGreen: 'Verde',
+    accentTeal: 'Verde azulado',
+    accentBlue: 'Azul',
+    accentOrange: 'Naranja',
+    accentPurple: 'Morado',
+    accentRed: 'Rojo',
+    styleClassic: 'Clásico',
+    styleGlow: 'Resplandor',
+    styleVibrant: 'Vibrante',
+    themeToggleHint: 'Alterne entre el modo oscuro y claro con el icono del sol/luna en la barra lateral.',
+    // Archive
+    autoArchivePrints: 'Archivar impresiones automáticamente',
+    autoArchiveDescription: 'Guardar automáticamente los archivos 3MF cuando se completan las impresiones',
+    saveThumbnailsDescription: 'Extraer y guardar imágenes de vista previa de los archivos 3MF',
+    captureFinishPhotoDescription: 'Tomar una foto desde la cámara de la impresora cuando se completa la impresión',
+    ffmpegNotInstalled: 'ffmpeg no instalado',
+    ffmpegRequired: 'La captura de cámara requiere ffmpeg. Instálelo mediante <brew>brew install ffmpeg</brew> (macOS) o <apt>apt install ffmpeg</apt> (Linux).',
+    // Camera
+    camera: 'Cámara',
+    cameraViewMode: 'Modo de vista de la cámara',
+    cameraOverlayDescription: 'La cámara se abre en una superposición redimensionable en la pantalla principal',
+    cameraWindowDescription: 'La cámara se abre en una ventana de navegador independiente',
+    externalCamerasDescription: 'Configure cámaras externas para sustituir la cámara integrada de la impresora. Admite transmisiones MJPEG, RTSP, capturas HTTP y cámaras USB (V4L2). Cuando está activada, la cámara externa se usa para la vista en directo y las fotos de finalización.',
+    cameraPlaceholderUsb: 'Ruta del dispositivo (/dev/video0)',
+    cameraPlaceholderUrl: 'URL de la cámara (rtsp://... o http://...)',
+    cameraTypeMjpeg: 'Transmisión MJPEG',
+    cameraTypeRtsp: 'Transmisión RTSP',
+    cameraTypeSnapshot: 'Captura HTTP',
+    cameraTypeUsb: 'Cámara USB (V4L2)',
+    cameraSnapshotUrl: 'URL de captura (opcional)',
+    cameraSnapshotUrlPlaceholder: 'http://192.168.1.61:1984/api/frame.jpeg?src=printer',
+    cameraSnapshotUrlHelp: 'URL de fotograma único usada para las miniaturas de notificaciones, las fotos de finalización, los fotogramas de time-lapse por capa y la detección de cama. El time-lapse y la detección de cama requieren cada uno su propio interruptor por impresora; esta URL es solo la fuente de imagen de la que se nutren cuando están activos. Déjelo en blanco para capturar desde la transmisión en directo de arriba. Útil para go2rtc (/api/frame.jpeg) y cámaras IP con un punto de conexión de captura dedicado.',
+    cameraRotation: 'Rotación',
+    test: 'Probar',
+    connected: 'Conectada',
+    disconnected: 'Desconectada',
+    // Cost tracking
+    currency: 'Moneda',
+    defaultFilamentCost: 'Coste predeterminado del filamento (por kg)',
+    electricityCost: 'Coste de la electricidad por kWh',
+    energyDisplayMode: 'Modo de visualización de la energía',
+    energyModePrintDescription: 'El panel muestra la suma de la energía usada durante las impresiones',
+    energyModeTotalDescription: 'El panel muestra la energía total de los enchufes inteligentes',
+    // File Manager
+    fileManager: 'Gestor de archivos',
+    createArchiveEntry: 'Crear una entrada de archivo al imprimir',
+    createArchiveEntryDescription: 'Al imprimir desde el gestor de archivos, crear opcionalmente una entrada de archivo',
+    lowDiskSpaceWarning: 'Advertencia de poco espacio en disco',
+    lowDiskSpaceDescription: 'Mostrar una advertencia cuando el espacio libre en disco caiga por debajo de este umbral',
+    // Updates
+    printerFirmware: 'Firmware de la impresora',
+    checkFirmwareDescription: 'Buscar actualizaciones de firmware de la impresora de Bambu Lab',
+    bambuddySoftware: 'Software de Bambuddy',
+    autoCheckDescription: 'Buscar automáticamente nuevas versiones al iniciar',
+    checkNow: 'Buscar ahora',
+    updateAvailableVersion: 'Actualización disponible: v{{version}}',
+    releaseNotes: 'Notas de la versión',
+    updateViaDocker: 'Actualizar mediante Docker Compose:',
+    updateViaHomeAssistant: 'Las actualizaciones las gestiona el Supervisor de Home Assistant. Abra Ajustes → Complementos → Bambuddy en Home Assistant para instalar la nueva versión.',
+    installUpdate: 'Instalar actualización',
+    latestVersionRunning: 'Está ejecutando la versión más reciente',
+    failedToCheckUpdates: 'Error al buscar actualizaciones: {{error}}',
+    // Data Management
+    backupRestore: 'Copia de seguridad y restauración',
+    backupRestoreDescription: 'Exportar/importar ajustes y configurar la copia de seguridad en GitHub',
+    goToBackup: 'Ir a la copia de seguridad',
+    // Network tab
+    externalUrl: 'URL externa',
+    externalUrlDescription: 'La URL externa donde Bambuddy es accesible. Se usa para las imágenes de notificación y las integraciones externas.',
+    bambuddyUrl: 'URL de Bambuddy',
+    externalUrlHint: 'Incluya el protocolo y el puerto (p. ej., http://192.168.1.100:8000)',
+    ftpRetry: 'Reintento de FTP',
+    ftpRetryDescription: 'Reintentar las operaciones FTP cuando el Wi-Fi de la impresora no es fiable. Se aplica a las descargas de 3MF, las subidas de impresión, las descargas de time-lapse y las actualizaciones de firmware.',
+    autoRetryDescription: 'Reintentar automáticamente las operaciones FTP fallidas',
+    retryAttempts: 'Intentos de reintento',
+    retryDelay: 'Retardo de reintento',
+    connectionTimeout: 'Tiempo de espera de la conexión',
+    time_one: '{{count}} vez',
+    time_other: '{{count}} veces',
+    second_one: '{{count}} segundo',
+    second_other: '{{count}} segundos',
+    nSeconds: '{{count}} segundos',
+    increaseForWeakWifi: 'Aumente para impresoras con Wi-Fi débil',
+    // Home Assistant
+    homeAssistant: 'Home Assistant',
+    homeAssistantFullDescription: 'Conéctese a Home Assistant para controlar enchufes inteligentes mediante la API REST de HA. Admite las entidades switch, light, input_boolean y script.',
+    homeAssistantUrl: 'URL de Home Assistant',
+    longLivedAccessToken: 'Token de acceso de larga duración',
+    haTokenHint: 'Cree un token en HA: Perfil → Tokens de acceso de larga duración → Crear token',
+    connectionSuccessful: 'Conexión correcta',
+    connectionFailed: 'Error de conexión',
+    haConnectionSuccess: 'Conexión con Home Assistant realizada correctamente.',
+    haConnectionFailed: 'Error al conectar con Home Assistant.',
+    // MQTT
+    mqttPublishing: 'Publicación MQTT',
+    mqttDescription: 'Publique los eventos de Bambuddy en un broker MQTT externo para la integración con Node-RED, Home Assistant y otros sistemas de automatización.',
+    mqttEnableDescription: 'Publicar eventos en un broker MQTT externo',
+    brokerHostname: 'Nombre de host del broker',
+    port: 'Puerto',
+    usernameOptional: 'Nombre de usuario (opcional)',
+    passwordOptional: 'Contraseña (opcional)',
+    topicPrefix: 'Prefijo del tema',
+    topicPrefixHint: 'Los temas serán: {{prefix}}/printers/<serial>/status, etc.',
+    // Prometheus
+    prometheusMetrics: 'Métricas de Prometheus',
+    prometheusEndpointDescription: 'Exponer las métricas de la impresora en <code>/api/v1/metrics</code> para la supervisión con Prometheus/Grafana.',
+    bearerTokenOptional: 'Bearer Token (opcional)',
+    bearerTokenHint: 'Si se establece, las solicitudes deben incluir <code>Authorization: Bearer <token></code>',
+    metricsConnectionStatus: 'Estado de la conexión',
+    metricsPrinterState: 'Estado de la impresora (inactiva/imprimiendo/etc.)',
+    metricsPrintProgress: 'Progreso de la impresión 0-100%',
+    metricsBedTemp: 'Temperatura de la cama',
+    metricsNozzleTemp: 'Temperatura de la boquilla',
+    metricsPrintsTotal: 'Total de impresiones por resultado',
+    metricsMore: '...y más (capas, ventiladores, cola, uso de filamento)',
+    // Smart Plugs
+    smartPlugsDescription: 'Conecte enchufes inteligentes (Tasmota o Home Assistant) para automatizar el control de la alimentación y realizar el seguimiento del consumo de energía de sus impresoras.',
+    allOn: 'Encender todo',
+    allOff: 'Apagar todo',
+    addSmartPlug: 'Añadir enchufe inteligente',
+    energySummary: 'Resumen de energía',
+    currentPower: 'Potencia actual',
+    plugsOnline: '{{reachable}}/{{total}} enchufes en línea',
+    today: 'Hoy',
+    yesterday: 'Ayer',
+    total: 'Total',
+    enablePlugsForSummary: 'Active enchufes para ver el resumen de energía',
+    addNotificationProvider: 'Añadir',
+    // Users
+    systemBadge: '(Sistema)',
+    creating: 'Creando...',
+    changing: 'Cambiando...',
+    deleteUserAndItems: 'Eliminar el usuario Y sus elementos',
+    deleteUserKeepItems: 'Eliminar el usuario, conservar los elementos (quedan sin propietario)',
+    ok: 'Aceptar',
+
+    // 2FA settings
+    twoFa: {
+      totpTitle: 'Aplicación de autenticación (TOTP)',
+      totpDesc: 'Use una aplicación de autenticación como Google Authenticator, Aegis o Authy.',
+      emailOtpTitle: 'OTP por correo',
+      emailOtpDesc: 'Enviar un código de un solo uso a {{email}} cuando inicie sesión.',
+      emailOtpNoEmail: 'Añada una dirección de correo a su cuenta para habilitar este método.',
+      addEmailFirst: 'Su cuenta no tiene dirección de correo. Pida a un administrador que añada una antes de activar el OTP por correo.',
+      setupTotp: 'Configurar la aplicación de autenticación',
+      setupAuthApp: 'Configurar la aplicación de autenticación',
+      setupInstructions: 'Escanee el código QR de abajo con su aplicación de autenticación y luego confirme con un código.',
+      manualEntry: '¿No puede escanear? Introduzca este secreto manualmente:',
+      scannedContinue: 'He escaneado el código — continuar',
+      enterCodeToConfirm: 'Introduzca el código de 6 dígitos de su aplicación de autenticación para confirmar la configuración.',
+      activate: 'Activar',
+      disableTotp: 'Desactivar el autenticador',
+      disableConfirmHint: 'Introduzca un código TOTP válido o un código de respaldo para desactivar el autenticador.',
+      totpDisabled: 'Aplicación de autenticación desactivada.',
+      emailOtpEnabled: 'OTP por correo activado.',
+      emailOtpDisabled: 'OTP por correo desactivado.',
+      smtpRequired: 'Configure y pruebe primero los ajustes de SMTP.',
+      invalidCode: 'Código no válido. Inténtelo de nuevo.',
+      enableEmailOtp: 'Activar el OTP por correo',
+      disableEmailOtp: 'Desactivar el OTP por correo',
+      emailSetupEnterCode: 'Se ha enviado un código de verificación a su dirección de correo. Introdúzcalo a continuación para confirmar que es el propietario de este buzón.',
+      verifyAndEnable: 'Verificar y activar',
+      emailDisablePasswordHint: 'Introduzca la contraseña de su cuenta para confirmar la desactivación del OTP por correo.',
+      passwordPlaceholder: 'Introduzca su contraseña',
+      backupCodesTitle: 'Guarde sus códigos de respaldo',
+      backupCodesWarning: 'Guarde estos códigos en un lugar seguro. Cada código solo se puede usar una vez y no se volverán a mostrar.',
+      backupCodesRemaining: 'Quedan {{count}} códigos de respaldo',
+      savedCodes: 'He guardado mis códigos',
+      regenBackup: 'Regenerar los códigos de respaldo',
+      regenBackupHint: 'Introduzca su código TOTP actual para generar 10 códigos de respaldo nuevos. Todos los códigos de respaldo existentes quedarán invalidados.',
+      newBackupCodes: 'Nuevos códigos de respaldo',
+      linkedAccounts: 'Cuentas de SSO vinculadas',
+      linkedAccountsDesc: 'Estos proveedores de identidad externos están vinculados a su cuenta.',
+      oidcUnlinked: 'Cuenta desvinculada.',
+    },
+
+    // OIDC provider settings
+    oidc: {
+      title: 'Proveedores SSO / OIDC',
+      desc: 'Configure proveedores de OpenID Connect para permitir el inicio de sesión único mediante proveedores de identidad externos.',
+      addProvider: 'Añadir proveedor',
+      newProvider: 'Nuevo proveedor',
+      empty: 'Aún no hay proveedores OIDC configurados.',
+      created: 'Proveedor creado.',
+      updated: 'Proveedor actualizado.',
+      deleted: 'Proveedor eliminado.',
+      refreshIcon: 'Actualizar icono',
+      removeIcon: 'Quitar icono',
+      iconRefreshed: 'Icono actualizado.',
+      iconRemoved: 'Icono eliminado.',
+      iconFetchFailed: 'No se pudo obtener el icono de la URL del proveedor.',
+      deleteTitle: 'Eliminar proveedor',
+      deleteMessage: '¿Eliminar "{{name}}"? Todas las cuentas de usuario vinculadas se desconectarán.',
+      form: {
+        name: 'Nombre visible',
+        issuerUrl: 'URL del emisor',
+        clientId: 'ID de cliente',
+        clientSecret: 'Secreto de cliente',
+        scopes: 'Ámbitos',
+        iconUrl: 'URL del icono (opcional)',
+        enabled: 'Activado',
+        autoCreate: 'Crear usuarios automáticamente',
+        autoCreateDesc: 'Crear automáticamente una cuenta local en el primer inicio de sesión.',
+        autoLink: 'Vincular cuentas existentes automáticamente',
+        autoLinkDesc: 'Vincular cuentas locales existentes haciendo coincidir el correo en el primer inicio de sesión.',
+        secretHint: 'dejar en blanco para conservar el actual',
+        secretPlaceholder: 'nuevo secreto',
+        emailClaim: 'Claim de correo',
+        emailClaimDesc: 'Claim de JWT usado como identidad de correo. Use «preferred_username» o «upn» para Azure Entra ID (que no envía email_verified). Use solo nombres de claim de confianza.',
+        emailClaimPlaceholder: 'correo',
+        emailClaimCustomClaimAutoLinkWarning: 'Los claims personalizados solo son seguros para la vinculación automática cuando el valor lo administra el inquilino (p. ej. upn / preferred_username de Azure Entra ID). No active la vinculación automática si su IdP permite a los usuarios autoasignarse este claim.',
+        requireEmailVerified: 'Requerir correo verificado',
+        requireEmailVerifiedDesc: 'Aceptar el claim de correo solo cuando el proveedor lo marca como verificado.',
+        requireEmailVerifiedWarning: 'Advertencia: el correo se aceptará incluso sin verificación. Úselo solo con proveedores de confianza.',
+        requireEmailVerifiedAutoLink: 'Desactive primero la vinculación automática para cambiar este ajuste.',
+        defaultGroup: 'Grupo predeterminado',
+        defaultGroupDesc: 'Grupo asignado a los usuarios creados automáticamente. Si no se establece, se usa Visores como alternativa.',
+        defaultGroupViewersFallback: 'Visores (predeterminado)',
+      },
+    },
+
+    encryption: {
+      title: 'Estado del cifrado de MFA',
+      enabledFromEnv: 'Cifrado en reposo activado (clave de la variable de entorno MFA_ENCRYPTION_KEY)',
+      enabledFromFile: 'Cifrado en reposo activado (clave cargada del directorio de datos)',
+      enabledGenerated: 'Cifrado en reposo activado con una clave autogenerada',
+      notConfigured: 'Cifrado en reposo no configurado',
+      notConfiguredDesc: 'Los secretos TOTP y los client_secrets de OIDC se almacenan en texto plano. Establezca MFA_ENCRYPTION_KEY o reinicie Bambuddy con un directorio de datos con permisos de escritura para generar una automáticamente.',
+      allEncrypted: 'Todos los secretos de MFA están cifrados en reposo.',
+      legacyRowsLabel: 'Filas heredadas en texto plano',
+      encryptedRowsLabel: 'Filas cifradas',
+      legacyRowsWarning: 'Se detectaron {{count}} fila(s) heredada(s) en texto plano. Vuelva a guardar el proveedor OIDC o vuelva a registrar la aplicación de autenticación del usuario para migrar al almacenamiento cifrado.',
+      backupHint: 'La clave autogenerada se almacena en DATA_DIR/.mfa_encryption_key y se incluye en los ZIP de copia de seguridad local. Mantenga sus copias de seguridad seguras o establezca MFA_ENCRYPTION_KEY de forma explícita.',
+      decryptionBrokenTitle: 'Falta la clave de cifrado',
+      decryptionBrokenError: '{{count}} registro(s) cifrado(s) no se pueden descifrar porque la clave de cifrado ya no está disponible. Restaure la MFA_ENCRYPTION_KEY anterior o DATA_DIR/.mfa_encryption_key para recuperarlos.',
+      migrationErrorWarning: '{{count}} fila(s) heredada(s) no se pudieron volver a cifrar al iniciar. Revise los registros del servidor y reinicie Bambuddy para reintentarlo.',
+    },
+
+  },
+
+  // Notifications (for push notifications)
+  notification: {
+    printStarted: {
+      title: 'Impresión iniciada',
+      body: '{{printer}}: {{filename}} ha empezado a imprimirse',
+    },
+    printCompleted: {
+      title: 'Impresión completada',
+      body: '{{printer}}: {{filename}} se completó correctamente',
+    },
+    printFailed: {
+      title: 'Impresión fallida',
+      body: '{{printer}}: {{filename}} ha fallado',
+    },
+    printStopped: {
+      title: 'Impresión detenida',
+      body: '{{printer}}: {{filename}} se detuvo',
+    },
+    printProgress: {
+      title: 'Progreso de la impresión',
+      body: '{{printer}}: {{filename}} está al {{percent}}%',
+    },
+    printerOffline: {
+      title: 'Impresora desconectada',
+      body: '{{printer}} está desconectada',
+    },
+    printerError: {
+      title: 'Error de la impresora',
+      body: '{{printer}}: {{error}}',
+    },
+    filamentLow: {
+      title: 'Filamento bajo',
+      body: '{{printer}}: el filamento se está agotando',
+    },
+    maintenanceDue: {
+      title: 'Mantenimiento pendiente',
+      body: '{{printer}}: {{items}} necesitan atención',
+    },
+  },
+
+  // Errors
+  errors: {
+    generic: 'Algo salió mal',
+    networkError: 'Error de red. Compruebe su conexión.',
+    notFound: 'No encontrado',
+    unauthorized: 'No autorizado',
+    serverError: 'Error del servidor',
+    validationError: 'Compruebe los datos introducidos',
+    printerConnectionFailed: 'Error al conectar con la impresora',
+    saveFailed: 'Error al guardar los cambios',
+    deleteFailed: 'Error al eliminar',
+    loadFailed: 'Error al cargar los datos',
+  },
+
+  // HMS Errors modal
+  hmsErrors: {
+    title: 'Errores - {{name}}',
+    noErrors: 'No hay errores',
+    viewOnWiki: 'Ver en la wiki de Bambu Lab',
+    clearInstructions: 'Borre los errores en la impresora para descartarlos aquí.',
+    clearErrors: 'Borrar errores',
+    clearSuccess: 'Errores HMS borrados',
+    clearFailed: 'Error al borrar los errores HMS',
+  },
+
+  // MQTT Debug modal
+  mqttDebug: {
+    title: 'Registro de depuración MQTT',
+    searchPlaceholder: 'Buscar tema o carga útil...',
+    noMessages: 'Aún no se han registrado mensajes',
+    startLoggingHint: 'Haga clic en «Iniciar registro» para empezar a capturar mensajes MQTT',
+    noMessagesMatch: 'Ningún mensaje coincide con su filtro',
+    adjustFilterHint: 'Pruebe a ajustar su búsqueda o los criterios del filtro',
+    incoming: 'Entrante',
+    outgoing: 'Saliente',
+    loggingStopped: 'Registro detenido',
+    loggingActive: 'Registro activo - los mensajes se actualizarán automáticamente',
+    startLogging: 'Iniciar registro',
+    stopLogging: 'Detener registro',
+    clearLog: 'Borrar registro',
+    topic: 'Tema',
+    timestamp: 'Marca de tiempo',
+    direction: 'Dirección',
+    all: 'Todos',
+  },
+
+  // Printer File Manager modal (printer internal storage)
+  printerFiles: {
+    title: 'Gestor de archivos',
+    storageUsed: 'Usado:',
+    storageFree: 'Libre:',
+    filterPlaceholder: 'Filtrar archivos...',
+    deleteButton: 'Eliminar',
+    deleteFiles: 'Eliminar {{count}} archivos',
+    deleteFileConfirm: '¿Eliminar "{{name}}"? Esto no se puede deshacer.',
+    deleteFilesConfirm: '¿Eliminar {{count}} archivos seleccionados? Esto no se puede deshacer.',
+    noFiles: 'No hay archivos en la impresora',
+    loadingFiles: 'Cargando archivos...',
+    failedToLoad: 'Error al cargar los archivos',
+    toast: {
+      filesDeleted: 'Se eliminaron {{count}} archivo(s)',
+      deleteFailed: 'Error al eliminar: {{error}}',
+    },
+  },
+
+  // Confirmations
+  confirm: {
+    delete: '¿Está seguro de que desea eliminar esto?',
+    unsavedChanges: 'Tiene cambios sin guardar. ¿Está seguro de que desea salir?',
+    clearQueue: '¿Está seguro de que desea vaciar la cola?',
+  },
+
+  // Login page
+  login: {
+    title: 'Inicio de sesión en Bambuddy',
+    subtitle: 'Inicie sesión en su cuenta',
+    username: 'Nombre de usuario',
+    usernamePlaceholder: 'Introduzca su nombre de usuario',
+    usernameOrEmail: 'Nombre de usuario o correo',
+    usernameOrEmailPlaceholder: 'Nombre de usuario o @ correo',
+    password: 'Contraseña',
+    passwordPlaceholder: 'Introduzca su contraseña',
+    signIn: 'Iniciar sesión',
+    signingIn: 'Iniciando sesión...',
+    rememberMe: 'Recordarme',
+    forgotPassword: '¿Olvidó su contraseña?',
+    loginSuccess: 'Sesión iniciada correctamente',
+    loginFailed: 'Error al iniciar sesión',
+    enterCredentials: 'Introduzca el nombre de usuario y la contraseña',
+    enterEmail: 'Introduzca su dirección de correo',
+    oidcLoginFailed: 'Error en el inicio de sesión con OIDC',
+    oidcErrors: {
+      providerError: 'El proveedor de identidad devolvió un error',
+      missingParameters: 'A la respuesta de OIDC le faltan parámetros obligatorios',
+      invalidState: 'El estado de OIDC no es válido o ya se ha usado',
+      stateExpired: 'La sesión de inicio de sesión con OIDC ha caducado — inténtelo de nuevo',
+      providerNotFound: 'Proveedor OIDC no encontrado',
+      discoveryFailed: 'Error al obtener el documento de descubrimiento de OIDC',
+      invalidDiscovery: 'El documento de descubrimiento de OIDC no es válido',
+      networkError: 'Error de red durante el intercambio de tokens de OIDC',
+      badResponse: 'Respuesta inesperada durante el intercambio de tokens de OIDC',
+      noIdToken: 'El proveedor OIDC no devolvió un token de ID',
+      validationFailed: 'Error en la validación del token de OIDC',
+      nonceMismatch: 'Discrepancia de nonce de OIDC — posible ataque de repetición',
+      missingSubClaim: 'Al token de OIDC le falta el claim sub',
+      noLinkedAccount: 'Ninguna cuenta local está vinculada a esta identidad de OIDC',
+      accountInactive: 'Su cuenta está inactiva',
+      userResolutionFailed: 'Error al resolver su cuenta',
+      internalError: 'Se produjo un error interno durante el inicio de sesión con OIDC',
+      tokenExchangeFailed: 'Error en el intercambio de tokens de OIDC',
+    },
+    forgotPasswordTitle: 'Contraseña olvidada',
+    forgotPasswordMessage: 'Si ha olvidado su contraseña, póngase en contacto con el administrador de su sistema para restablecerla.',
+    forgotPasswordEmailMessage: 'Introduzca su dirección de correo y le enviaremos una contraseña nueva.',
+    emailAddress: 'Dirección de correo',
+    emailPlaceholder: 'su.correo@ejemplo.com',
+    cancel: 'Cancelar',
+    sending: 'Enviando...',
+    sendResetEmail: 'Enviar correo de restablecimiento',
+    howToReset: 'Cómo restablecer su contraseña:',
+    resetStep1: 'Póngase en contacto con su administrador de Bambuddy',
+    resetStep2: 'Pídale que restablezca su contraseña en la gestión de usuarios',
+    resetStep3: 'Puede establecerle una nueva contraseña temporal',
+    resetStep4: 'Inicie sesión con la nueva contraseña y cámbiela en Ajustes',
+    gotIt: 'Entendido',
+    resetPassword: {
+      title: 'Establecer nueva contrase\u00f1a',
+      subtitle: 'Introduzca y confirme su nueva contrase\u00f1a a continuaci\u00f3n.',
+      newPassword: 'Nueva contrase\u00f1a',
+      newPasswordPlaceholder: 'Al menos 8 caracteres',
+      confirmPassword: 'Confirmar contrase\u00f1a',
+      confirmPasswordPlaceholder: 'Repita la nueva contrase\u00f1a',
+      saving: 'Guardando\u2026',
+      submit: 'Establecer nueva contrase\u00f1a',
+      backToLogin: 'Volver al inicio de sesi\u00f3n',
+      passwordsDoNotMatch: 'Las contrase\u00f1as no coinciden',
+      passwordTooShort: 'La contrase\u00f1a debe tener al menos 8 caracteres',
+      resetFailed: 'Error al restablecer la contrase\u00f1a. Es posible que el enlace haya caducado.',
+    },
+    twoFA: {
+      title: 'Autenticación de dos factores',
+      subtitle: 'Su cuenta está protegida con 2FA. Introduzca el código de verificación a continuación.',
+      methodAuthenticator: 'Aplicación de autenticación',
+      methodEmail: 'Código por correo',
+      methodBackup: 'Código de respaldo',
+      instructionsTotp: 'Abra su aplicación de autenticación e introduzca el código de 6 dígitos para Bambuddy.',
+      instructionsEmail: 'Se ha enviado un código de 6 dígitos a su dirección de correo. Caduca en 10 minutos.',
+      instructionsEmailNotSent: 'Haga clic en el botón de abajo para recibir un código de verificación por correo.',
+      instructionsBackup: 'Introduzca uno de sus códigos de recuperación de respaldo de 8 caracteres. Cada código solo se puede usar una vez.',
+      sendCodeButton: 'Enviar código por correo',
+      sendingCode: 'Enviando...',
+      resendCode: 'Reenviar código',
+      codeLabel: 'Código de verificación',
+      backupCodeLabel: 'Código de respaldo',
+      codePlaceholder: '000000',
+      backupCodePlaceholder: 'XXXXXXXX',
+      verifyButton: 'Verificar',
+      verifyingButton: 'Verificando...',
+      backToLogin: '← Volver al inicio de sesión',
+      orContinueWith: 'o continuar con',
+      signInWith: 'Iniciar sesión con {{provider}}',
+      enterCode: 'Introduzca el código de verificación',
+      sendCodeFailed: 'Error al enviar el código de verificación',
+      invalidCode: 'Código no válido. Inténtelo de nuevo.',
+    },
+
+  },
+
+  // Setup page
+  setup: {
+    title: 'Configuración de Bambuddy',
+    subtitle: 'Configure la autenticación para su instancia de Bambuddy',
+    enableAuth: 'Activar la autenticación',
+    adminAccount: 'Cuenta de administrador',
+    adminAccountDesc: 'Si ya existen usuarios administradores, la autenticación se activará usando las cuentas de administrador existentes. Deje los campos de abajo vacíos para usar los administradores existentes, o introduzca nuevas credenciales para crear un nuevo usuario administrador.',
+    adminUsername: 'Nombre de usuario del administrador',
+    adminPassword: 'Contraseña del administrador',
+    optionalIfAdminExists: '(opcional si existen usuarios administradores)',
+    adminUsernamePlaceholder: 'Introduzca el nombre de usuario del administrador (opcional)',
+    adminPasswordPlaceholder: 'Introduzca la contraseña del administrador (opcional)',
+    confirmPassword: 'Confirmar contraseña',
+    confirmPasswordPlaceholder: 'Confirme la contraseña del administrador',
+    settingUp: 'Configurando...',
+    completeSetup: 'Completar la configuración',
+    toast: {
+      authEnabledAdminCreated: 'Autenticación activada y usuario administrador creado',
+      authEnabledExistingAdmins: 'Autenticación activada usando los usuarios administradores existentes',
+      setupCompleted: 'Configuración completada',
+      enterBothCredentials: 'Introduzca el nombre de usuario y la contraseña del administrador, o deje ambos vacíos para usar los usuarios administradores existentes',
+      passwordsDoNotMatch: 'Las contraseñas no coinciden',
+      passwordTooShort: 'La contraseña debe tener al menos 6 caracteres',
+    },
+  },
+
+  // Password change
+  changePassword: {
+    title: 'Cambiar contraseña',
+    currentPassword: 'Contraseña actual',
+    currentPasswordPlaceholder: 'Introduzca la contraseña actual',
+    newPassword: 'Nueva contraseña',
+    newPasswordPlaceholder: 'Introduzca la nueva contraseña (mín. 6 caracteres)',
+    confirmPassword: 'Confirmar nueva contraseña',
+    confirmPasswordPlaceholder: 'Confirme la nueva contraseña',
+    passwordsDoNotMatch: 'Las contraseñas no coinciden',
+    passwordTooShort: 'La contraseña debe tener al menos 6 caracteres',
+    changing: 'Cambiando...',
+    success: 'Contraseña cambiada correctamente',
+    failed: 'Error al cambiar la contraseña',
+  },
+
+  // Plate detection alert
+  plateAlert: {
+    title: '¡Impresión en pausa!',
+    message: 'Se han detectado objetos en la cama de impresión. La impresión se ha pausado automáticamente. Despeje la cama y reanude la impresión.',
+    understand: 'Entendido',
+  },
+
+  // Camera page
+  camera: {
+    title: 'Vista de la cámara',
+    invalidPrinterId: 'ID de impresora no válido',
+    live: 'En directo',
+    snapshot: 'Captura',
+    restartStream: 'Reiniciar la transmisión',
+    refreshSnapshot: 'Actualizar la captura',
+    fullscreen: 'Pantalla completa',
+    exitFullscreen: 'Salir de la pantalla completa',
+    connectingToCamera: 'Conectando con la cámara...',
+    capturingSnapshot: 'Capturando imagen...',
+    connectionLost: 'Conexión perdida',
+    connectionFailed: 'Error de conexión con la cámara',
+    reconnecting: 'Reconectando en {{countdown}} s... (intento {{attempt}}/{{max}})',
+    reconnectNow: 'Reconectar ahora',
+    cameraUnavailable: 'Cámara no disponible',
+    cameraUnavailableDesc: 'Asegúrese de que la impresora está encendida y conectada.',
+    noCamera: 'No hay cámara disponible',
+    retry: 'Reintentar',
+    cameraStream: 'Transmisión de la cámara',
+    zoomOut: 'Alejar',
+    zoomIn: 'Acercar',
+    resetZoom: 'Restablecer el zoom',
+    recording: 'Grabando',
+    startRecording: 'Iniciar grabación',
+    stopRecording: 'Detener grabación',
+    chamberLight: 'Conmutar la luz de la cámara',
+    unavailable: 'Cámara no disponible',
+    diagnose: {
+      button: 'Diagnosticar',
+      modalTitle: 'Diagnóstico de la cámara',
+      running: 'Ejecutando el diagnóstico...',
+      runFailed: 'No se pudo ejecutar el diagnóstico: {{error}}',
+      retry: 'Ejecutar de nuevo',
+      stage: {
+        tcp_reachable: 'Accesibilidad de la red',
+        first_frame: 'Captura de fotogramas',
+        live_stream_active: 'Transmisión en directo activa',
+      },
+      summary: {
+        all_ok: 'La cámara funciona. El diagnóstico completó todas las etapas correctamente.',
+        live_stream_active_healthy: 'La cámara está transmitiendo actualmente con fotogramas recientes — no se necesita ninguna prueba.',
+        printer_unreachable: 'La impresora no es accesible. Compruebe la dirección IP, la conexión de red y que la impresora está encendida.',
+        camera_port_closed: 'La impresora es accesible pero el puerto de la cámara está cerrado. Asegúrese de que el modo solo LAN y el modo desarrollador están activados en los ajustes de la impresora.',
+        no_frame: 'Se conectó a la cámara pero no se recibieron fotogramas. Inténtelo de nuevo o compruebe que la cámara está activada en los ajustes de la impresora.',
+        unknown_failure: 'El diagnóstico de la cámara falló por un motivo desconocido. Consulte el registro de soporte para más detalles.',
+      },
+      meta: {
+        protocol: 'Protocolo',
+        port: 'Puerto',
+        profile: 'Perfil',
+      },
+    },
+  },
+
+  // Groups management
+  groups: {
+    title: 'Gestión de grupos',
+    subtitle: 'Gestione los grupos de permisos para el control de acceso',
+    backToSettings: 'Volver a Ajustes',
+    createGroup: 'Crear grupo',
+    noPermission: 'No tiene permiso para acceder a esta página.',
+    system: 'Sistema',
+    noDescription: 'Sin descripción',
+    usersCount: '{{count}} usuarios',
+    permissionsCount: '{{count}} permisos',
+    edit: 'Editar',
+    delete: 'Eliminar',
+    toast: {
+      created: 'Grupo creado correctamente',
+      updated: 'Grupo actualizado correctamente',
+      deleted: 'Grupo eliminado correctamente',
+      enterGroupName: 'Introduzca un nombre de grupo',
+    },
+    modal: {
+      editGroup: 'Editar grupo',
+      createGroup: 'Crear grupo',
+      cancel: 'Cancelar',
+      saving: 'Guardando...',
+      creating: 'Creando...',
+      saveChanges: 'Guardar cambios',
+    },
+    form: {
+      groupName: 'Nombre del grupo',
+      groupNamePlaceholder: 'Introduzca el nombre del grupo',
+      systemGroupWarning: 'Los nombres de los grupos del sistema no se pueden cambiar',
+      description: 'Descripción',
+      descriptionPlaceholder: 'Introduzca una descripción (opcional)',
+      permissions: 'Permisos ({{count}} seleccionados)',
+    },
+    deleteModal: {
+      title: 'Eliminar grupo',
+      message: '¿Está seguro de que desea eliminar este grupo? Los usuarios de este grupo perderán estos permisos.',
+      confirm: 'Eliminar grupo',
+    },
+    editor: {
+      title: 'Editar grupo',
+      createTitle: 'Crear grupo',
+      search: 'Buscar permisos...',
+      selectAll: 'Seleccionar todo',
+      clearAll: 'Borrar todo',
+      permissionsSelected: '{{count}} seleccionados',
+      noResults: 'Ningún permiso coincide con su búsqueda',
+    },
+  },
+
+  // Users management
+  users: {
+    title: 'Gestión de usuarios',
+    subtitle: 'Gestione los usuarios y su acceso a su instancia de Bambuddy',
+    backToSettings: 'Volver a Ajustes',
+    createUser: 'Crear usuario',
+    noPermission: 'No tiene permiso para acceder a esta página.',
+    admin: 'Administrador',
+    noGroups: 'Sin grupos',
+    active: 'Activo',
+    inactive: 'Inactivo',
+    edit: 'Editar',
+    delete: 'Eliminar',
+    system: 'Sistema',
+    noGroupsAvailable: 'No hay grupos disponibles',
+    table: {
+      username: 'Nombre de usuario',
+      groups: 'Grupos',
+      status: 'Estado',
+      actions: 'Acciones',
+    },
+    toast: {
+      created: 'Usuario creado correctamente',
+      updated: 'Usuario actualizado correctamente',
+      deleted: 'Usuario eliminado correctamente',
+      fillRequired: 'Rellene todos los campos obligatorios',
+      passwordsDoNotMatch: 'Las contraseñas no coinciden',
+      passwordTooShort: 'La contraseña debe tener al menos 6 caracteres',
+      ldapProvisioned: 'Usuario LDAP "{{username}}" aprovisionado',
+    },
+    modal: {
+      createUser: 'Crear usuario',
+      editUser: 'Editar usuario',
+      cancel: 'Cancelar',
+      creating: 'Creando...',
+      saving: 'Guardando...',
+      saveChanges: 'Guardar cambios',
+      advancedAuthSubtitle: 'con autenticación avanzada',
+      // LDAP manual provisioning (#1298)
+      tabsAriaLabel: 'Origen del usuario',
+      localTab: 'Local',
+      ldapTab: 'LDAP',
+      ldapSearchLabel: 'Buscar en el directorio',
+      ldapSearchPlaceholder: 'Escriba un nombre de usuario, nombre o correo...',
+      ldapMinChars: 'Escriba al menos 2 caracteres para buscar',
+      ldapTypeToSearch: 'Empiece a escribir para buscar en el directorio LDAP',
+      ldapSearching: 'Buscando en el directorio...',
+      ldapNoResults: 'No hay usuarios coincidentes en el directorio',
+      ldapSearchError: 'Error en la búsqueda en el directorio. Compruebe el estado del servidor LDAP.',
+      ldapAlreadyProvisioned: 'Ya aprovisionado',
+      ldapSelectedLabel: 'Seleccionado',
+      ldapProvision: 'Aprovisionar usuario',
+      ldapProvisioning: 'Aprovisionando...',
+      ldapErrorProvision: 'Error en el aprovisionamiento. Compruebe el estado del servidor LDAP e inténtelo de nuevo.',
+    },
+    form: {
+      username: 'Nombre de usuario',
+      usernamePlaceholder: 'Introduzca el nombre de usuario',
+      email: 'Correo',
+      emailPlaceholder: 'usuario@ejemplo.com',
+      password: 'Contraseña',
+      passwordPlaceholder: 'Introduzca la contraseña',
+      confirmPassword: 'Confirmar contraseña',
+      confirmPasswordPlaceholder: 'Confirme la contraseña',
+      newPasswordPlaceholder: 'Introduzca la nueva contraseña',
+      confirmNewPasswordPlaceholder: 'Confirme la nueva contraseña',
+      leaveBlankToKeep: 'dejar en blanco para conservar la actual',
+      groups: 'Grupos',
+      optional: 'opcional',
+      autoGeneratedPassword: 'Se generará automáticamente una contraseña segura y se enviará al usuario por correo.',
+      passwordManagedByAdvancedAuth: 'La contraseña la gestiona la autenticación avanzada. Use «Restablecer contraseña» para enviar una nueva contraseña al usuario por correo.',
+      resetPassword: 'Restablecer contraseña',
+      resettingPassword: 'Restableciendo la contraseña...',
+    },
+    deleteModal: {
+      title: 'Eliminar usuario',
+      message: '¿Está seguro de que desea eliminar este usuario? Esta acción no se puede deshacer.',
+      confirm: 'Eliminar usuario',
+    },
+  },
+
+  // Stream overlay
+  streamOverlay: {
+    title: 'Superposición de transmisión',
+    invalidPrinterId: 'ID de impresora no válido',
+    cameraStream: 'Transmisión de la cámara',
+    progress: 'Progreso',
+    eta: 'Tiempo estimado',
+    printerIdle: 'La impresora está inactiva',
+    printerOffline: 'Impresora desconectada',
+    status: {
+      printing: 'Imprimiendo',
+      paused: 'En pausa',
+      finished: 'Finalizada',
+      failed: 'Fallida',
+      idle: 'Inactiva',
+      unknown: 'Desconocido',
+    },
+  },
+
+  // Profiles
+  profiles: {
+    title: 'Perfiles',
+    subtitle: 'Gestione sus preajustes del laminador y las calibraciones de avance de presión',
+    tabs: {
+      cloud: 'Perfiles en la nube',
+      local: 'Perfiles locales',
+      kprofiles: 'Perfiles K',
+    },
+    localProfiles: {
+      title: 'Perfiles locales',
+      subtitle: 'Importe y gestione preajustes del laminador desde OrcaSlicer',
+      import: 'Importar perfiles',
+      importDesc: 'Suelte aquí archivos .bbscfg, .bbsflmt, .orca_filament, .zip o .json',
+      importing: 'Importando...',
+      search: 'Buscar preajustes locales...',
+      noPresets: 'Aún no hay preajustes locales',
+      noSearchResults: 'Ningún preajuste coincide con su búsqueda',
+      badge: 'Local',
+      edit: 'Editar',
+      delete: 'Eliminar',
+      cancel: 'Cancelar',
+      deleteConfirmTitle: 'Eliminar preajuste',
+      deleteConfirm: '¿Está seguro de que desea eliminar este preajuste? Esto no se puede deshacer.',
+      source: 'Origen',
+      inheritsFrom: 'Hereda de',
+      filamentType: 'Tipo',
+      vendor: 'Proveedor',
+      compatiblePrinters: 'Impresoras',
+      nozzleTemp: 'Temp. de la boquilla',
+      cost: 'Coste',
+      density: 'Densidad',
+      pressureAdvance: 'Avance de presión',
+      filament: 'Filamento',
+      process: 'Proceso',
+      printer: 'Impresora',
+      toast: {
+        importSuccess: '{{count}} preajuste(s) importado(s)',
+        importSkipped: '{{count}} preajuste(s) omitido(s) (duplicados)',
+        importError: '{{count}} error(es) durante la importación',
+        deleted: 'Preajuste eliminado',
+        updated: 'Preajuste actualizado',
+      },
+    },
+    connectedAs: 'Conectado como',
+    logout: 'Cerrar sesión',
+    noLogoutPermission: 'No tiene permiso para cerrar sesión',
+    failedToLoad: 'Error al cargar los perfiles',
+    retry: 'Reintentar',
+    time: {
+      justNow: 'Ahora mismo',
+      minsAgo: 'hace {{count}} min',
+      hoursAgo: 'hace {{count}} h',
+      daysAgo: 'hace {{count}} d',
+    },
+    toast: {
+      loggedOut: 'Sesión cerrada',
+    },
+    login: {
+      title: 'Conectarse a Bambu Cloud',
+      subtitle: 'Sincronice sus preajustes del laminador entre dispositivos',
+      email: 'Correo',
+      password: 'Contraseña',
+      region: 'Región',
+      regionGlobal: 'Global',
+      regionChina: 'China',
+      verificationCode: 'Código de verificación',
+      totpCode: 'Código del autenticador',
+      checkEmail: 'Compruebe su correo ({{email}}) en busca de un código de 6 dígitos',
+      enterTotpHint: 'Introduzca el código de 6 dígitos de su aplicación de autenticación',
+      accessToken: 'Token de acceso',
+      accessTokenHint: 'Pegue su token de acceso de Bambu Cloud. Las cuentas de la región de China deben usar esta vía (vinculada al teléfono — inicio de sesión por correo no disponible). Consulte la wiki para saber cómo obtener el token de las cookies de MakerWorld.',
+      back: 'Atrás',
+      loginButton: 'Iniciar sesión',
+      verifyButton: 'Verificar',
+      setTokenButton: 'Establecer token',
+      useToken: 'Usar token de acceso en su lugar',
+      useEmail: 'Iniciar sesión con correo en su lugar',
+      toast: {
+        loggedIn: 'Sesión iniciada correctamente',
+        codeSent: 'Código de verificación enviado a su correo',
+        enterTotp: 'Introduzca el código de su aplicación de autenticación',
+        tokenSet: 'Token establecido correctamente',
+      },
+    },
+    presets: {
+      myPreset: 'Mi preajuste (editable)',
+      duplicate: 'Duplicar',
+      editable: 'Editable',
+      failedToLoadDetails: 'Error al cargar los detalles del preajuste',
+      deleteConfirm: '¿Eliminar este preajuste?',
+      deleteWarning: 'Esto eliminará permanentemente "{{name}}" de Bambu Cloud. Esto no se puede deshacer.',
+      noDuplicatePermission: 'No tiene permiso para duplicar preajustes',
+      noEditPermission: 'No tiene permiso para editar preajustes',
+      noDeletePermission: 'No tiene permiso para eliminar preajustes',
+      types: {
+        filament: 'Preajuste de filamento',
+        printer: 'Preajuste de impresora',
+        process: 'Preajuste de proceso',
+      },
+      toast: {
+        deleted: 'Preajuste eliminado',
+        created: 'Preajuste creado',
+        updated: 'Preajuste actualizado',
+        duplicated: 'Preajuste duplicado',
+        fieldAdded: 'Campo "{{key}}" añadido',
+        exported: 'Preajuste exportado',
+      },
+      baseLabel: 'Base: {{name}}',
+      currentLabel: 'Actual: {{name}}',
+      newPreset: 'Nuevo preajuste',
+      editPreset: 'Editar preajuste',
+      duplicatePreset: 'Duplicar preajuste',
+      createNewPreset: 'Crear nuevo preajuste',
+      customizeSettings: 'Personalice los ajustes de su nuevo preajuste',
+      compareWithBase: 'Comparar con el preajuste base',
+      compare: 'Comparar',
+      // CreatePresetModal - Basic Info
+      basePreset: 'Preajuste base',
+      selectBasePreset: 'Seleccionar preajuste base...',
+      presetName: 'Nombre del preajuste',
+      myCustomPreset: 'Mi preajuste personalizado',
+      inheritsFrom: 'Hereda de',
+      dropJsonToImport: 'Suelte un JSON para importar',
+      // CreatePresetModal - Tabs
+      tabs: {
+        common: 'Común',
+        allFields: 'Todos los campos',
+      },
+      // CreatePresetModal - All Fields Tab
+      availableFields: 'Campos disponibles',
+      searchFieldsPlaceholder: 'Buscar campos...',
+      noMatchingFields: 'No hay campos coincidentes',
+      allFieldsAdded: 'Todos los campos añadidos',
+      addCustomField: 'Añadir campo personalizado',
+      yourOverrides: 'Sus anulaciones',
+      noOverridesYet: 'Aún no hay anulaciones',
+      clickFieldsToAdd: 'Haga clic en los campos de la izquierda para añadirlos',
+      saveAsTemplate: 'Guardar como plantilla',
+      jsonTip: 'Consejo: arrastre y suelte un archivo .json en cualquier parte de esta ventana para importar ajustes',
+    },
+    cloudView: {
+      searchPlaceholder: 'Buscar preajustes...',
+      templates: 'Plantillas',
+      refresh: 'Actualizar',
+      newPreset: 'Nuevo preajuste',
+      clearFilters: 'Borrar filtros',
+      // Compare mode
+      compareMode: 'Modo de comparación',
+      selectAnotherPreset: 'Seleccione otro preajuste de {{type}}',
+      clickTwoPresets: 'Haga clic en dos preajustes del mismo tipo para compararlos',
+      selectFirst: '1. Seleccione el primero',
+      selectSecond: '2. Seleccione el segundo',
+      compareNow: 'Comparar ahora',
+      // Status row
+      lastSynced: 'Última sincronización:',
+      showingCount: 'Mostrando {{showing}} de {{total}} preajustes',
+      noPresetsFound: 'No se encontraron preajustes',
+      // Column headers
+      columns: {
+        filament: 'Filamento',
+        process: 'Proceso',
+        printer: 'Impresora',
+      },
+      noFilamentPresets: 'No hay preajustes de filamento',
+      noProcessPresets: 'No hay preajustes de proceso',
+      noPrinterPresets: 'No hay preajustes de impresora',
+      // Filters
+      filters: {
+        type: 'Tipo',
+        owner: 'Propietario',
+        printer: 'Impresora',
+        nozzle: 'Boquilla',
+        filament: 'Filamento',
+        layer: 'Capa',
+        all: 'Todos',
+        myPresets: 'Mis preajustes',
+        builtIn: 'Integrados',
+        process: 'Proceso',
+      },
+      // Permissions
+      noTemplatesPermission: 'No tiene permiso para gestionar plantillas',
+      noRefreshPermission: 'No tiene permiso para actualizar perfiles',
+      noCreatePermission: 'No tiene permiso para crear preajustes',
+    },
+    templates: {
+      title: 'Plantillas rápidas',
+      noTemplates: 'Aún no hay plantillas',
+      createFirst: 'Cree plantillas desde el editor de preajustes',
+      typeFilter: 'Tipo:',
+      deleteTitle: 'Eliminar plantilla',
+      deleteWarning: 'Esta acción no se puede deshacer',
+      deleteConfirm: '¿Está seguro de que desea eliminar "{{name}}"?',
+      namePlaceholder: 'Nombre de la plantilla',
+      descriptionPlaceholder: 'Descripción',
+      settingsJson: 'Ajustes (JSON)',
+      fieldsCount: '{{count}} campos',
+      shownInModals: 'Mostrada en las ventanas',
+      hiddenInModals: 'Oculta en las ventanas',
+      apply: 'Aplicar',
+      toast: {
+        deleted: 'Plantilla eliminada',
+        updated: 'Plantilla actualizada',
+        created: 'Plantilla creada',
+        applied: 'Plantilla aplicada',
+      },
+    },
+  },
+
+  // Support/Debug
+  support: {
+    debugLoggingActive: 'El registro de depuración está activo',
+    manageLogs: 'Gestionar',
+    collectItem7: 'Conectividad de la impresora y versiones de firmware',
+    collectItem8: 'Estado de las integraciones (Spoolman, MQTT, HA)',
+    collectItem9: 'Interfaces de red (solo subredes)',
+    collectItem10: 'Versiones de los paquetes de Python',
+    collectItem11: 'Comprobaciones de estado de la base de datos',
+    collectItem12: 'Detalles del entorno de Docker',
+  },
+
+  // File manager
+  fileManager: {
+    title: 'Gestor de archivos',
+    subtitle: 'Organice y gestione sus archivos de impresión',
+    uploadFiles: 'Subir archivos',
+    newFolder: 'Nueva carpeta',
+    folderName: 'Nombre de la carpeta',
+    folderNamePlaceholder: 'p. ej., Piezas funcionales',
+    renameFile: 'Renombrar archivo',
+    renameFolder: 'Renombrar carpeta',
+    moveFiles: 'Mover {{count}} archivo(s)',
+    rootNoFolder: 'Raíz (sin carpeta)',
+    current: 'actual',
+    linkFolder: 'Vincular carpeta',
+    linkFolderDescription: 'Vincule "{{name}}" a un proyecto o archivo para un acceso rápido.',
+    project: 'Proyecto',
+    archive: 'Archivo',
+    noProjectsFound: 'No se encontraron proyectos',
+    noArchivesFound: 'No se encontraron archivos',
+    unlink: 'Desvincular',
+    link: 'Vincular',
+    dragDropFiles: 'Arrastre y suelte archivos aquí',
+    dropFilesHere: 'Suelte archivos aquí',
+    orClickToBrowse: 'o haga clic para examinar',
+    allFileTypesSupported: 'Se admiten todos los tipos de archivo. Los archivos ZIP se extraerán.',
+    zipFilesDetected: 'Archivos ZIP detectados',
+    zipExtractOptions: 'Los archivos ZIP se extraerán. Elija cómo gestionar la estructura de carpetas:',
+    preserveZipStructure: 'Conservar la estructura de carpetas del ZIP',
+    createFolderFromZip: 'Crear una carpeta a partir del nombre del archivo ZIP',
+    stlThumbnailGeneration: 'Generación de miniaturas STL',
+    zipMayContainStl: 'Los archivos ZIP pueden contener archivos STL. Se pueden generar miniaturas durante la extracción.',
+    thumbnailsCanBeGenerated: 'Se pueden generar miniaturas para los archivos STL. Los modelos grandes pueden tardar más en procesarse.',
+    generateThumbnailsForStl: 'Generar miniaturas para los archivos STL',
+    threemfDetected: 'Archivos 3MF detectados',
+    threemfExtractionInfo: 'El modelo de impresora, el material, el color y los ajustes de impresión se extraerán automáticamente de los archivos 3MF.',
+    willBeExtracted: 'Se extraerá',
+    filesExtracted: '{{count}} archivos extraídos',
+    uploadComplete: 'Subida completada: {{succeeded}} con éxito',
+    uploadFailed: 'Error al subir',
+    zipFilesFailed: '{{count}} archivos fallidos',
+    uploading: 'Subiendo...',
+    changeLink: 'Cambiar enlace...',
+    linkTo: 'Vincular a...',
+    linkToProjectOrArchive: 'Vincular a un proyecto o archivo',
+    addToQueue: 'Añadir a la cola',
+    schedulePrint: 'Programar',
+    generateThumbnail: 'Generar miniatura',
+    generateThumbnails: 'Generar miniaturas',
+    generateThumbnailsForMissing: 'Generar miniaturas para los archivos STL que no las tienen',
+    gridView: 'Vista de cuadrícula',
+    listView: 'Vista de lista',
+    lowDiskSpaceWarning: 'Advertencia de poco espacio en disco',
+    lowDiskSpaceDetails: 'Solo {{free}} libres de {{total}} en total. El umbral está establecido en {{threshold}} GB en los ajustes.',
+    files: 'Archivos',
+    folders: 'Carpetas',
+    size: 'Tamaño',
+    free: 'Libre',
+    allFiles: 'Todos los archivos',
+    wrap: 'Ajustar',
+    enableTextWrapping: 'Activar el ajuste de texto',
+    disableTextWrapping: 'Desactivar el ajuste de texto',
+    collapse: 'Contraer',
+    collapseFoldersByDefault: 'Contraer las carpetas de forma predeterminada',
+    expandFoldersByDefault: 'Expandir las carpetas de forma predeterminada',
+    dragToResizeTooltip: 'Arrastre para redimensionar, doble clic para restablecer',
+    searchFiles: 'Buscar archivos...',
+    allTypes: 'Todos los tipos',
+    prints: 'Impresiones',
+    ascending: 'Ascendente',
+    descending: 'Descendente',
+    resultsCount: '{{showing}} de {{total}} archivos',
+    selectAll: 'Seleccionar todo',
+    deselectAll: 'Deseleccionar todo',
+    selected: '{{count}} seleccionados',
+    adding: 'Añadiendo...',
+    loadingFiles: 'Cargando archivos...',
+    folderIsEmpty: 'La carpeta está vacía',
+    noFilesYet: 'Aún no hay archivos',
+    folderEmptyDescription: 'Suba archivos o mueva archivos a esta carpeta para empezar.',
+    noFilesDescription: 'Suba archivos para empezar a organizar sus archivos relacionados con la impresión.',
+    noMatchingFiles: 'No hay archivos coincidentes',
+    noMatchingFilesDescription: 'Ningún archivo coincide con su búsqueda o los criterios de filtro actuales.',
+    clearFilters: 'Borrar filtros',
+    printedCount: 'Impreso {{count}} veces',
+    uploadedBy: 'Subido por',
+    deleteFolder: 'Eliminar carpeta',
+    deleteFile: 'Eliminar archivo',
+    deleteFilesCount: 'Eliminar {{count}} archivos',
+    deleteFolderConfirm: '¿Está seguro de que desea eliminar esta carpeta? Todos los archivos que contiene también se eliminarán.',
+    deleteFileConfirm: '¿Está seguro de que desea eliminar este archivo?',
+    deleteFilesConfirm: '¿Está seguro de que desea eliminar {{count}} archivos seleccionados? Esta acción no se puede deshacer.',
+    deleting: 'Eliminando...',
+    noPermissionRenameFolder: 'No tiene permiso para renombrar carpetas',
+    noPermissionLinkFolder: 'No tiene permiso para vincular carpetas',
+    noPermissionDeleteFolder: 'No tiene permiso para eliminar carpetas',
+    noPermissionPrint: 'No tiene permiso para imprimir',
+    noPermissionAddToQueue: 'No tiene permiso para añadir a la cola',
+    noPermissionSlice: 'No tiene permiso para laminar archivos',
+    noPermissionDownload: 'No tiene permiso para descargar archivos',
+    noPermissionRenameFile: 'No tiene permiso para renombrar este archivo',
+    noPermissionGenerateThumbnail: 'No tiene permiso para generar miniaturas',
+    noPermissionDeleteFile: 'No tiene permiso para eliminar este archivo',
+    noPermissionCreateFolder: 'No tiene permiso para crear carpetas',
+    noPermissionUpload: 'No tiene permiso para subir archivos',
+    noPermissionMoveFiles: 'No tiene permiso para mover archivos',
+    noPermissionDeleteFiles: 'No tiene permiso para eliminar archivos',
+    // External folder
+    linkExternal: 'Vincular externa',
+    linkExternalFolder: 'Vincular carpeta externa',
+    linkExternalFolderDescription: 'Monte un directorio del host (NAS, USB, recurso compartido de red) en el gestor de archivos. Los archivos no se copian — se accede a ellos directamente desde la ruta original.',
+    externalFolderNamePlaceholder: 'p. ej., Impresiones del NAS',
+    externalPath: 'Ruta del host',
+    externalPathHelp: 'Ruta absoluta al directorio en el host de Docker. Debe estar montado por enlace en el contenedor.',
+    readOnly: 'Solo lectura',
+    readOnlyHelp: 'impide las subidas y eliminaciones',
+    showHiddenFiles: 'Mostrar archivos ocultos (con punto)',
+    externalFolder: 'Carpeta externa',
+    scanFolder: 'Escanear',
+    toast: {
+      folderCreated: 'Carpeta creada',
+      folderDeleted: 'Carpeta eliminada',
+      fileDeleted: 'Archivo eliminado',
+      filesDeleted: 'Se eliminaron {{count}} archivos',
+      filesMoved: 'Archivos movidos',
+      folderLinked: 'Carpeta vinculada',
+      folderUnlinked: 'Carpeta desvinculada',
+      externalFolderLinked: 'Carpeta externa vinculada y escaneada',
+      folderScanned: 'Escaneo completado: {{added}} añadidos, {{removed}} eliminados',
+      addedToQueue: 'Se añadieron {{count}} archivo(s) a la cola',
+      addedToQueuePartial: 'Se añadieron {{added}} archivo(s), {{failed}} fallidos',
+      failedToAddToQueue: 'Error al añadir los archivos: {{error}}',
+      fileRenamed: 'Archivo renombrado',
+      folderRenamed: 'Carpeta renombrada',
+      thumbnailsGenerated: 'Se generaron {{count}} miniatura(s)',
+      thumbnailsGeneratedPartial: 'Se generaron {{succeeded}} miniatura(s), {{failed}} fallidas',
+      noStlMissingThumbnails: 'No hay archivos STL sin miniaturas',
+      failedToGenerateThumbnails: 'Error al generar las miniaturas: {{error}}',
+      thumbnailGenerated: 'Miniatura generada',
+      failedToGenerateThumbnail: 'Error al generar la miniatura: {{error}}',
+    },
+  },
+
+  // Projects
+  projects: {
+    title: 'Proyectos',
+    subtitle: 'Organice y haga el seguimiento de sus proyectos de impresión 3D',
+    newProject: 'Nuevo proyecto',
+    editProject: 'Editar proyecto',
+    deleteProject: 'Eliminar proyecto',
+    projectName: 'Nombre del proyecto',
+    description: 'Descripción',
+    noProjects: 'Aún no hay proyectos',
+    noProjectsFiltered: 'No hay proyectos {{status}}',
+    noProjectsFilteredHelp: 'No tiene ningún proyecto {{status}}. Los proyectos aparecerán aquí cuando cambie su estado.',
+    createFirst: 'Cree su primer proyecto para empezar a organizar impresiones relacionadas, hacer el seguimiento del progreso y gestionar sus construcciones.',
+    createFirstButton: 'Cree su primer proyecto',
+    create: 'Crear',
+    files: 'Archivos',
+    prints: 'Impresiones',
+    plates: 'camas',
+    parts: 'piezas',
+    lastModified: 'Última modificación',
+    deleteConfirm: '¿Está seguro de que desea eliminar este proyecto? Los archivos y los elementos de la cola se desvincularán pero no se eliminarán.',
+    addFiles: 'Añadir archivos',
+    removeFile: 'Quitar archivo',
+    viewDetails: 'Ver detalles',
+    // Modal fields
+    namePlaceholder: 'p. ej., Construcción de Voron 2.4',
+    descriptionPlaceholder: 'Descripción opcional...',
+    urlLabel: 'URL',
+    urlPlaceholder: 'https://makerworld.com/...',
+    urlInvalid: 'La URL debe empezar por http:// o https://',
+    openExternalUrl: 'Abrir la URL del proyecto',
+    coverImageLabel: 'Foto de portada',
+    coverImageAlt: 'Foto de portada del proyecto',
+    coverImageUpload: 'Subir',
+    coverImageReplace: 'Reemplazar',
+    coverImageRemove: 'Quitar',
+    color: 'Color',
+    targetPlates: 'Camas objetivo',
+    targetPlatesPlaceholder: 'p. ej., 25',
+    targetPlatesHelp: 'Número de trabajos de impresión',
+    targetParts: 'Piezas objetivo',
+    targetPartsPlaceholder: 'p. ej., 150',
+    targetPartsHelp: 'Total de objetos necesarios',
+    tagsLabel: 'Etiquetas (separadas por comas)',
+    tagsPlaceholder: 'p. ej., voron, funcional, regalo',
+    dueDate: 'Fecha límite',
+    priority: 'Prioridad',
+    priorityLow: 'Baja',
+    priorityNormal: 'Normal',
+    priorityHigh: 'Alta',
+    priorityUrgent: 'Urgente',
+    // Status
+    statusActive: 'Activo',
+    statusCompleted: 'Completado',
+    statusArchived: 'Archivado',
+    done: 'Hecho',
+    completed: 'completadas',
+    failed: 'fallidas',
+    inQueue: 'en cola',
+    noPrintsYet: 'Aún no hay impresiones',
+    // Footer stats
+    printJobs: 'Trabajos de impresión (camas)',
+    partsPrinted: 'Piezas impresas',
+    failedParts: 'Piezas fallidas',
+    // Actions
+    import: 'Importar',
+    export: 'Exportar',
+    importProject: 'Importar proyecto',
+    exportAll: 'Exportar todos los proyectos',
+    loading: 'Cargando proyectos...',
+    // Permissions
+    noEditPermission: 'No tiene permiso para editar proyectos',
+    noDeletePermission: 'No tiene permiso para eliminar proyectos',
+    noCreatePermission: 'No tiene permiso para crear proyectos',
+    noImportPermission: 'No tiene permiso para importar proyectos',
+    noExportPermission: 'No tiene permiso para exportar proyectos',
+    // Toast
+    toast: {
+      created: 'Proyecto creado',
+      updated: 'Proyecto actualizado',
+      deleted: 'Proyecto eliminado',
+      imported: 'Proyecto importado',
+      multipleImported: '{{count}} proyectos importados',
+      importFailed: 'Error en la importación',
+      exported: 'Proyectos exportados (solo metadatos)',
+    },
+  },
+
+  // Project detail page
+  projectDetail: {
+    notFound: 'Proyecto no encontrado',
+    backToProjects: 'Volver a Proyectos',
+    export: 'Exportar',
+    exportProject: 'Exportar proyecto',
+    noExportPermission: 'No tiene permiso para exportar proyectos',
+    noEditPermission: 'No tiene permiso para editar proyectos',
+    partOf: 'Parte de:',
+    priorityLabel: 'Prioridad:',
+    noPrints: 'Aún no hay impresiones en este proyecto',
+    status: {
+      active: 'Activo',
+      completed: 'Completado',
+      archived: 'Archivado',
+    },
+    priority: {
+      low: 'Baja',
+      normal: 'Normal',
+      high: 'Alta',
+      urgent: 'Urgente',
+    },
+    dueDate: {
+      overdue: 'Atrasado',
+      today: 'Vence hoy',
+      daysLeft: 'Quedan {{count}} días',
+    },
+    progress: {
+      platesProgress: 'Progreso de camas',
+      partsProgress: 'Progreso de piezas',
+      printJobs: 'trabajos de impresión',
+      parts: 'piezas',
+      percentComplete: '{{percent}}% completado',
+      remaining: '{{count}} restantes',
+    },
+    stats: {
+      printJobs: 'Trabajos de impresión',
+      total: 'total',
+      failed: '{{count}} fallidos',
+      partsPrinted: '{{count}} piezas impresas',
+      printTime: 'Tiempo de impresión',
+      filamentUsed: 'Filamento usado',
+    },
+    cost: {
+      title: 'Seguimiento de costes',
+      filamentCost: 'Coste del filamento',
+      energy: 'Energía',
+      totalCost: 'Coste total',
+      total: 'Total',
+      includesBom: 'incl. lista de materiales',
+      budget: 'Presupuesto',
+      remaining: 'Restante',
+    },
+    subProjects: {
+      title: 'Subproyectos ({{count}})',
+    },
+    notes: {
+      title: 'Notas',
+      noEditPermission: 'No tiene permiso para editar notas',
+      placeholder: 'Añada notas sobre este proyecto...',
+      empty: 'Aún no hay notas. Haga clic en Editar para añadir notas.',
+    },
+    files: {
+      title: 'Archivos',
+      linkFolders: 'Vincule carpetas del gestor de archivos',
+      forQuickAccess: 'a este proyecto para un acceso rápido.',
+      fileCount: '{{count}} archivo(s)',
+      empty: 'No hay carpetas vinculadas. Vaya al gestor de archivos y vincule una carpeta a este proyecto.',
+      noFiles: 'No hay archivos en esta carpeta.',
+      print: 'Imprimir ahora',
+      addToQueue: 'Añadir a la cola',
+    },
+    bom: {
+      title: 'Lista de materiales',
+      acquired: '{{completed}}/{{total}} adquiridos',
+      showAll: 'Mostrar todo',
+      hideDone: 'Ocultar los hechos',
+      addPart: 'Añadir pieza',
+      noAddPermission: 'No tiene permiso para añadir piezas',
+      partNamePlaceholder: 'Nombre de la pieza (p. ej., tornillos M3x8)',
+      partName: 'Nombre de la pieza',
+      qty: 'Cant.',
+      price: 'Precio ({{currency}})',
+      sourcingUrlPlaceholder: 'URL de aprovisionamiento (opcional)',
+      remarksPlaceholder: 'Observaciones (opcional)',
+      deletePart: 'Eliminar pieza',
+      deleteConfirm: '¿Está seguro de que desea eliminar "{{name}}"?',
+      noUpdatePermission: 'No tiene permiso para actualizar piezas',
+      noEditPermission: 'No tiene permiso para editar piezas',
+      noDeletePermission: 'No tiene permiso para eliminar piezas',
+      totalCost: 'Coste total:',
+      empty: 'No hay piezas en la lista de materiales. Añada hardware, componentes electrónicos u otros componentes para hacer el seguimiento de lo que hay que aprovisionar.',
+    },
+    timeline: {
+      title: 'Cronología de actividad',
+      empty: 'Aún no hay actividad.',
+    },
+    template: {
+      saveAsTemplate: 'Guardar como plantilla',
+      noCreatePermission: 'No tiene permiso para crear plantillas',
+    },
+    queue: {
+      title: 'Cola',
+      viewAll: 'Ver todo',
+      printing: '{{count}} imprimiendo',
+      queued: '{{count}} en cola',
+    },
+    prints: {
+      title: 'Impresiones ({{count}})',
+    },
+    toast: {
+      projectUpdated: 'Proyecto actualizado',
+      partAdded: 'Pieza añadida',
+      partRemoved: 'Pieza eliminada',
+      exportFailed: 'Error en la exportación',
+      projectExported: 'Proyecto exportado',
+      templateCreated: 'Plantilla creada',
+    },
+  },
+
+  // System info
+  system: {
+    title: 'Información del sistema',
+    version: 'Versión',
+    uptime: 'Tiempo de actividad',
+    cpuUsage: 'Uso de la CPU',
+    memoryUsage: 'Uso de la memoria',
+    diskUsage: 'Uso del disco',
+    networkInfo: 'Información de la red',
+    logs: 'Registros',
+    debugMode: 'Modo de depuración',
+    enableDebug: 'Activar el registro de depuración',
+    disableDebug: 'Desactivar el registro de depuración',
+    downloadLogs: 'Descargar registros',
+    clearLogs: 'Borrar registros',
+    dockerInfo: 'Información de Docker',
+    containerName: 'Nombre del contenedor',
+    imageName: 'Nombre de la imagen',
+    platform: 'Plataforma',
+    architecture: 'Arquitectura',
+  },
+
+  // Library (K Profiles)
+  library: {
+    title: 'Biblioteca de filamentos',
+    addFilament: 'Añadir filamento',
+    editFilament: 'Editar filamento',
+    deleteFilament: 'Eliminar filamento',
+    vendor: 'Proveedor',
+    material: 'Material',
+    color: 'Color',
+    kFactor: 'Factor K',
+    temperature: 'Temperatura',
+    noFilaments: 'No hay filamentos en la biblioteca',
+    deleteConfirm: '¿Está seguro de que desea eliminar este filamento?',
+    importFromPrinter: 'Importar desde la impresora',
+    exportToFile: 'Exportar a un archivo',
+  },
+
+  // Slice (slicer-API integration via SliceModal)
+  slice: {
+    title: 'Laminar modelo',
+    action: 'Laminar',
+    slicing: 'Laminando…',
+    printer: 'Perfil de impresora',
+    process: 'Perfil de proceso',
+    filament: 'Perfil de filamento',
+    filamentSlot: 'Filamento {{index}} ({{type}})',
+    selectPreset: '— Seleccione un preajuste —',
+    loadingPresets: 'Cargando preajustes…',
+    analyzingPlateFilaments: 'Analizando los filamentos de la cama…',
+    analyzingPlateFilamentsHint: 'Ejecutando un laminado de vista previa para descubrir qué ranuras del AMS usa esta cama. Después se almacena en caché — volver a abrir es instantáneo.',
+    previewToast: 'Analizando {{name}} — {{elapsed}}',
+    previewWithProgress: 'Analizando {{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
+    notUsedByPlate: '— no usado por esta cama',
+    noPresetsForSlot: 'No hay preajustes disponibles',
+    presetsLoadFailed: 'Error al cargar los preajustes. Abra Ajustes → Perfiles para importarlos primero.',
+    allPresetsRequired: 'Deben seleccionarse todos los preajustes',
+    bundle: 'Paquete del laminador',
+    bundleNone: '— Ninguno (elegir preajustes individualmente) —',
+    bundleAllRequired: 'Deben elegirse el proceso del paquete y todas las ranuras de filamento',
+    enqueuing: 'Enviando el trabajo de laminado…',
+    queued: 'En cola…',
+    failed: 'Error al laminar. Consulte los registros del contenedor auxiliar del laminador.',
+    startedToast: 'Laminando {{name}} en segundo plano…',
+    queuedToast: 'En cola: {{name}} — {{elapsed}}',
+    runningToast: 'Laminando {{name}} — {{elapsed}}',
+    runningWithProgress: '{{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
+    completedToast: '{{name}} laminado',
+    failedToast: 'Error al laminar {{name}}: {{detail}}',
+    tier: {
+      local: 'Importado',
+      cloud: 'Nube',
+      standard: 'Estándar',
+    },
+    cloud: {
+      notAuthenticated: 'Inicie sesión en Bambu Cloud (Ajustes → Perfiles → Nube) para ver sus preajustes de la nube.',
+      expired: 'La sesión de Bambu Cloud ha caducado — inicie sesión de nuevo para actualizar sus preajustes de la nube.',
+      unreachable: 'Bambu Cloud no es accesible en este momento. Los preajustes locales y estándar siguen funcionando.',
+    },
+    bedType: {
+      label: 'Cama de impresión',
+      auto: 'Automático (usar el preajuste de proceso)',
+      coolPlate: 'Cool Plate',
+      coolPlateSuperTack: 'Cool Plate SuperTack',
+      engineering: 'Engineering Plate',
+      highTemp: 'High Temp Plate',
+      texturedPEI: 'Textured PEI Plate',
+      smoothPEI: 'Smooth PEI Plate',
+    },
+  },
+
+  // Spoolman
+  spoolman: {
+    title: 'Integración con Spoolman',
+    enabled: 'Spoolman activado',
+    url: 'URL de Spoolman',
+    connected: 'Conectado',
+    disconnected: 'No conectado',
+    testConnection: 'Probar conexión',
+    sync: 'Sincronizar',
+    syncing: 'Sincronizando...',
+    lastSync: 'Última sincronización',
+    linkToSpoolman: 'Vincular a Spoolman',
+    openInSpoolman: 'Abrir en Spoolman',
+    unlinkSpool: 'Desvincular bobina',
+    unlinkConfirmTitle: '¿Desasignar la bobina?',
+    unlinkConfirmMessage: 'Esto quitará la bobina de esta ranura. Los datos de la bobina en sí permanecerán sin cambios.',
+    selectSpool: 'Seleccionar bobina',
+    noUnlinkedSpools: 'No hay bobinas sin asignar disponibles',
+    linkSuccess: 'Bobina asignada correctamente',
+    linkFailed: 'Error al asignar la bobina',
+    unlinkSuccess: 'Bobina desasignada correctamente',
+    unlinkFailed: 'Error al desasignar la bobina',
+    linkedSpool: 'Bobina asignada',
+    spoolId: 'ID de la bobina',
+    fillSourceLabel: '(Spoolman)',
+    weight: 'Peso',
+    remaining: 'Restante',
+    disableWeightSync: 'Desactivar la sincronización del peso estimado del AMS',
+    disableWeightSyncDesc: 'No actualizar la capacidad restante a partir de las estimaciones del AMS. Use esto si prefiere el seguimiento del uso de Spoolman a las estimaciones del AMS basadas en porcentajes. Las bobinas nuevas seguirán usando la estimación del AMS como su peso inicial.',
+    reportPartialUsage: 'Informar del uso parcial en las impresiones fallidas',
+    reportPartialUsageDesc: 'Cuando una impresión falla o se cancela, informar del filamento estimado usado hasta ese punto según el progreso de las capas.',
+  },
+
+  // Inventory
+  inventory: {
+    title: 'Inventario de bobinas',
+    subtitle: 'Gestione sus bobinas',
+    spoolmanMixedContentTitle: 'Spoolman no se puede cargar por HTTPS — contenido mixto bloqueado por su navegador',
+    spoolmanMixedContentBody: 'Bambuddy se sirve por HTTPS (mediante su proxy inverso), pero su URL de Spoolman sigue siendo HTTP sin cifrar. Los navegadores bloquean el contenido mixto por seguridad, por lo que la interfaz integrada de Spoolman no se puede mostrar. Spoolman debe ser accesible por HTTPS para que esto funcione.',
+    spoolmanMixedContentFixReverseProxy: 'Ponga Spoolman tras el mismo proxy inverso que Bambuddy (Traefik / Nginx / Caddy) con HTTPS y luego actualice la URL de Spoolman en Ajustes a la nueva dirección HTTPS.',
+    spoolmanMixedContentFixOpenNewTab: 'Como solución alternativa, abra Spoolman en una pestaña nueva del navegador por HTTP — las reglas de contenido mixto solo se aplican a los marcos integrados, por lo que una pestaña independiente sí funciona.',
+    spoolmanOpenInNewTab: 'Abrir Spoolman en una pestaña nueva',
+    labels: {
+      title: 'Imprimir etiquetas de bobinas',
+      selectedCount: '{{count}} seleccionadas',
+      pickSpools: 'Elija para qué bobinas imprimir etiquetas:',
+      searchPlaceholder: 'Buscar nombre, marca o n.º de ID',
+      filterByMaterial: 'Material:',
+      allMaterials: 'Todos',
+      selectVisible: 'Seleccionar todas las visibles ({{count}})',
+      deselectVisible: 'Deseleccionar las visibles',
+      clearAll: 'Borrar todo',
+      noSpoolsToShow: 'No hay bobinas que mostrar. Ajuste su filtro e inténtelo de nuevo.',
+      noMatches: 'Ninguna bobina coincide con la búsqueda o el filtro actuales.',
+      printOne: 'Imprimir etiqueta para esta bobina',
+      printLabels: 'Imprimir etiquetas…',
+      bulkTitle: 'Elija las bobinas para las que imprimir etiquetas de las {{count}} mostradas actualmente',
+      noSpoolsTitle: 'No hay bobinas que etiquetar',
+      error: 'No se pudieron generar las etiquetas: {{msg}}',
+      sortBy: {
+        label: 'Ordenar:',
+        id: 'Por ID',
+        color: 'Por color',
+      },
+      templates: {
+        amsHolderSmall: {
+          label: 'Soporte de AMS — pequeño (74 × 33 mm)',
+          hint: 'Una etiqueta por página; coincide con la etiqueta imprimible del modelo 752566 de MakerWorld (AMS Filament Label Holder).',
+        },
+        amsHolderLarge: {
+          label: 'Soporte de AMS — grande (75 × 55 mm)',
+          hint: 'Una etiqueta por página; encaja en la variante con inserto de cartulina del AMS Filament Label Holder. Lo bastante amplia para muestra de color, marca, material, ID y código QR.',
+        },
+        box40x30: {
+          label: 'Etiqueta de caja (40 × 30 mm)',
+          hint: 'Una etiqueta por página; tamaño de rollo DK/Brother habitual, adecuado para etiquetas de bolsas de filamento y cajas de almacenamiento.',
+        },
+        box: {
+          label: 'Box label (62 × 29 mm)',
+          hint: 'Una etiqueta por página; dimensionada para las etiquetas pequeñas de Brother PT/QL y Dymo.',
+        },
+        averyL7160: {
+          label: 'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
+          hint: 'Hojas de tamaño UE; 21 etiquetas por página A4.',
+        },
+        avery5160: {
+          label: 'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',
+          hint: 'Hojas de tamaño EE. UU.; 30 etiquetas por página Letter.',
+        },
+      },
+    },
+    addSpool: 'Añadir bobina',
+    editSpool: 'Editar bobina',
+    copySpool: 'Copiar bobina',
+    material: 'Material',
+    selectMaterial: 'Seleccionar material...',
+    subtype: 'Subtipo',
+    brand: 'Marca',
+    searchBrand: 'Buscar marca...',
+    useCustomBrand: 'Usar "{{brand}}"',
+    useCustomMaterial: 'Usar material personalizado: {{material}}',
+    colorName: 'Nombre del color',
+    colorNamePlaceholder: 'Blanco jade, Rojo fuego...',
+    color: 'Color',
+    hexColor: 'Color hexadecimal',
+    pickColor: 'Elegir color personalizado',
+    labelWeight: 'Peso de la etiqueta',
+    coreWeight: 'Peso de la bobina vacía',
+    searchSpoolWeight: 'Buscar peso de la bobina...',
+    weightUsed: 'Usado',
+    currentWeight: 'Peso restante',
+    measuredWeight: 'Peso medido',
+    spoolName: 'Bobina',
+    costPerKg: 'Coste por kg',
+    storageLocation: 'Ubicación de almacenamiento',
+    storageLocationPlaceholder: 'p. ej. Estante A, Cajón 1',
+    openInInventory: 'Abrir en el inventario',
+    measuredWeightError: 'El peso medido debe estar entre {{min}} g y {{max}} g.',
+    slicerFilament: 'Filamento del laminador',
+    slicerFilamentName: 'Nombre del preajuste del laminador',
+    slicerPreset: 'Preajuste del laminador',
+    searchPresets: 'Buscar preajustes de filamento...',
+    selectedPreset: 'Seleccionado',
+    noPresetsFound: 'No se encontraron preajustes',
+    tempOverrides: 'Anulaciones de temperatura',
+    note: 'Nota',
+    notePlaceholder: 'Cualquier nota adicional sobre esta bobina...',
+    // Per-spool category + low-stock threshold override (#729)
+    category: 'Categoría',
+    categoryPlaceholder: 'p. ej. Producción, Prototipo, Cliente A',
+    categoryNone: 'Sin categoría',
+    // #1400: storage-location filter chip
+    storageLocationNone: 'Sin ubicación establecida',
+    lowStockThresholdOverride: 'Umbral de existencias bajas (esta bobina)',
+    lowStockThresholdOverrideHelp: 'Déjelo en blanco para usar el umbral global ({{global}}%).',
+    // RFID button rename
+    clearRfid: 'Borrar etiqueta RFID',
+    rfidCleared: 'Etiqueta RFID borrada',
+    archive: 'Archivar',
+    restore: 'Restaurar',
+    noSpools: 'Aún no hay bobinas. Añada su primera bobina para empezar.',
+    noAvailableSpools: 'No hay bobinas disponibles. Añada una bobina a su inventario o desasigne una de otra ranura primero.',
+    kProfiles: 'Perfiles K',
+    addKProfile: 'Añadir perfil K',
+    assignSpool: 'Asignar bobina',
+    unassignSpool: 'Desasignar',
+    assignSuccess: 'Bobina asignada y ranura del AMS configurada',
+    assignFailed: 'Error al asignar la bobina',
+    selectSpool: 'Seleccione una bobina para asignar a esta ranura',
+    assigned: 'Asignada',
+    assigning: 'Asignando...',
+    searchSpools: 'Buscar bobinas...',
+    showAllSpools: 'Mostrar todas las bobinas',
+    spoolmanSpools: 'Bobinas de Spoolman',
+    allMaterials: 'Todos los materiales',
+    filterByBrand: 'Filtrar por marca...',
+    showArchived: 'Mostrar archivadas',
+    quickAdd: 'Añadir rápido (existencias)',
+    quantity: 'Cantidad',
+    stock: 'Existencias',
+    configured: 'Configurada',
+    spoolsCreated: '{{count}} bobinas creadas',
+    spoolsPartiallyCreated: '{{created}} de {{total}} bobinas creadas (algunas fallaron)',
+    spoolCreated: 'Bobina creada',
+    spoolUpdated: 'Bobina actualizada',
+    spoolDeleted: 'Bobina eliminada',
+    deepLinkSpoolNotFound: 'Bobina no encontrada',
+    deepLinkFetchFailed: 'No se pudo cargar la bobina — inténtelo de nuevo',
+    spoolArchived: 'Bobina archivada',
+    spoolRestored: 'Bobina restaurada',
+    kProfileSaveFailed: 'No se pudieron guardar los ajustes del perfil K',
+    syncWeightSpoolNotFound: 'Bobina no encontrada — es posible que se haya eliminado',
+    syncWeightSpoolmanUnreachable: 'Spoolman no es accesible — inténtelo más tarde',
+    syncWeightFailed: 'Error al sincronizar el peso',
+    spoolmanUnreachable: 'Spoolman no es accesible — inténtelo de nuevo más tarde',
+    deleteSpoolNotFound: 'Bobina no encontrada — es posible que ya se haya eliminado',
+    deleteFailed: 'Error al eliminar la bobina',
+    archiveSpoolNotFound: 'Bobina no encontrada — es posible que ya se haya eliminado',
+    archiveFailed: 'Error al archivar la bobina',
+    restoreSpoolNotFound: 'Bobina no encontrada — es posible que ya se haya eliminado',
+    restoreFailed: 'Error al restaurar la bobina',
+    saveFailed: 'Error al guardar los cambios',
+    tagClearFailed: 'Error al borrar la etiqueta',
+    deleteConfirm: '¿Está seguro de que desea eliminar esta bobina? Esto no se puede deshacer.',
+    archiveConfirm: '¿Está seguro de que desea archivar esta bobina?',
+    advancedSettings: 'Ajustes avanzados',
+    // Tabs
+    filamentInfoTab: 'Información del filamento',
+    paProfileTab: 'Perfil PA',
+    filamentInfo: 'Filamento',
+    additional: 'Adicional',
+    // Cloud
+    loadingPresets: 'Cargando preajustes de la nube...',
+    cloudConnected: 'Nube conectada',
+    cloudNotConnected: 'Nube no conectada (usando los valores predeterminados)',
+    // Colors
+    recentColors: 'Recientes',
+    searchColors: 'Buscar colores...',
+    searchResults: 'Resultados de búsqueda',
+    allColors: 'Todos los colores',
+    commonColors: 'Colores comunes',
+    showLess: 'Mostrar menos',
+    showAll: 'Mostrar todos',
+    noColorsFound: 'Ningún color coincide con su búsqueda',
+    noResults: 'No se encontraron coincidencias',
+    // Multi-color gradient + visual effect (#1154)
+    extraColorsLabel: 'Colores adicionales',
+    extraColorsPlaceholder: 'EC984C,#6CD4BC,A66EB9,D87694',
+    extraColorsHint: 'Pegue de 2 a 8 paradas hexadecimales, separadas por comas. Se muestra como un degradado.',
+    extraColorsInvalid: 'Se ignoró el hexadecimal no válido: {{tokens}}',
+    colorEffectLabel: 'Efecto',
+    colorEffect: {
+      none: 'Ninguno',
+      // Surface effects
+      sparkle: 'Destellos',
+      wood: 'Madera',
+      marble: 'Mármol',
+      glow: 'Brillante en la oscuridad',
+      matte: 'Mate',
+      // Sheen / finish variants
+      silk: 'Seda',
+      galaxy: 'Galaxia',
+      rainbow: 'Arcoíris',
+      metal: 'Metal',
+      translucent: 'Translúcido',
+      // Multi-color structural variants
+      gradient: 'Degradado',
+      dualColor: 'Bicolor',
+      triColor: 'Tricolor',
+      multicolor: 'Multicolor',
+    },
+    // PA Profiles
+    selectMaterialFirst: 'Seleccione primero un material en la pestaña de información del filamento.',
+    noPrintersConfigured: 'No hay impresoras configuradas. Añada impresoras para usar perfiles PA.',
+    matchingFilter: 'Coincidentes',
+    anyBrand: 'Cualquier marca',
+    anyVariant: 'Cualquier variante',
+    autoSelect: 'Selección automática',
+    matches: 'coincidencias',
+    match: 'coincidencia',
+    noMatches: 'Sin coincidencias',
+    connected: 'Conectada',
+    offline: 'Desconectada',
+    printerOffline: 'La impresora está desconectada. Conéctela para ver los perfiles de calibración.',
+    noKProfilesMatch: 'Ningún perfil K coincide con el filamento seleccionado.',
+    leftNozzle: 'Boquilla izquierda',
+    rightNozzle: 'Boquilla derecha',
+    profilesSelected: 'perfil(es) de calibración seleccionado(s)',
+    // Stats & enhanced table
+    totalInventory: 'Inventario total',
+    totalConsumed: 'Total consumido',
+    byMaterial: 'Por material',
+    inPrinter: 'En la impresora',
+    lowStock: 'Existencias bajas',
+    sinceTracking: 'Desde que comenzó el seguimiento',
+    resetUsage: 'Restablecer el uso a 0',
+    resetUsageTooltip: 'Poner a cero el contador de gramos consumidos de esta bobina',
+    resetUsageConfirm: '¿Restablecer a 0 el contador de gramos consumidos de esta bobina? Las impresiones futuras volverán a contar desde cero. La bobina en sí, su cálculo de peso restante y sus ajustes no cambian.',
+    resetAllUsage: 'Restablecer el uso de todas las bobinas',
+    resetAllUsageTooltip: 'Poner a cero el contador de gramos consumidos de todas las bobinas',
+    resetAllUsageConfirm: '¿Restablecer a 0 el contador de gramos consumidos de las {{count}} bobinas (incluidas las archivadas)? Esto borra la estadística «Total consumido» para que las impresiones futuras cuenten desde cero. Las bobinas y los pesos restantes no cambian.',
+    usageReset: 'Uso de la bobina restablecido a 0',
+    allUsageReset: 'Se restablecieron {{count}} bobina(s)',
+    resetUsageFailed: 'Error al restablecer el uso de la bobina',
+    loadedInAms: 'Cargada en AMS/ext.',
+    remaining: 'Restante',
+    weightCheck: 'Comprobación de peso',
+    lastWeighed: 'Pesada por última vez',
+    neverWeighed: 'Nunca pesada',
+    search: 'Buscar bobinas...',
+    showing: 'Mostrando',
+    to: 'a',
+    of: 'de',
+    show: 'Mostrar',
+    spools: 'bobinas',
+    spool: 'bobina',
+    page: 'Página',
+    noSpoolsMatch: 'No se encontraron resultados',
+    noSpoolsMatchDesc: 'Pruebe a ajustar su búsqueda o sus filtros para encontrar lo que busca.',
+    active: 'Activas',
+    archived: 'Archivadas',
+    all: 'Todas',
+    used: 'Usada',
+    new: 'Nueva',
+    clearFilters: 'Borrar filtros',
+    table: 'Tabla',
+    cards: 'Tarjetas',
+    net: 'Neto',
+    // Grouping
+    groupSimilar: 'Agrupar',
+    groupedSpools: '{{count}} bobinas idénticas',
+    groupedRows: 'filas',
+    // Column config
+    columns: 'Columnas',
+    configureColumns: 'Configurar columnas',
+    configureColumnsDesc: 'Arrastre para reordenar las columnas o use las flechas. Conmute la visibilidad con el icono del ojo.',
+    visible: 'visibles',
+    reset: 'Restablecer',
+    cancel: 'Cancelar',
+    applyChanges: 'Aplicar cambios',
+    moveUp: 'Subir',
+    moveDown: 'Bajar',
+    hideColumn: 'Ocultar columna',
+    showColumn: 'Mostrar columna',
+    // Tag linking
+    linkToSpool: 'Vincular a bobina',
+    tagLinked: 'Etiqueta vinculada a la bobina',
+    tagLinkFailed: 'Error al vincular la etiqueta',
+    tagAlreadyLinked: 'La etiqueta ya está vinculada a otra bobina',
+    unknownTag: 'Se detectó una etiqueta RFID desconocida',
+    // Usage history
+    usageHistory: 'Historial de uso',
+    noUsageHistory: 'Aún no se ha registrado ningún uso',
+    printName: 'Nombre de la impresión',
+    weightConsumed: 'Peso consumido',
+    clearHistory: 'Borrar',
+    historyCleared: 'Historial de uso borrado',
+    fillSourceLabel: '(Inv.)',
+    lowStockThresholdError: 'El umbral debe estar entre 0,1 y 99,9',
+    assignMismatchTitle: 'Discrepancia de material',
+    assignMismatchMessage: 'El material de la bobina seleccionada "{{spoolMaterial}}" no coincide con el material de la bandeja "{{trayMaterial}}" de {{location}}. ¿Asignar de todos modos?',
+    assignMismatchConfirm: 'Asignar de todos modos',
+    assignPartialMismatchMessage: 'El material de la bobina "{{spoolMaterial}}" es similar pero no coincide exactamente con "{{trayMaterial}}" en {{location}}. ¿Desea continuar?',
+    assignProfileMismatchMessage: 'El perfil de la bobina "{{spoolProfile}}" no coincide con el perfil de la bandeja "{{trayProfile}}" en {{location}}. ¿Desea continuar?',
+    // Spoolman filament catalog picker
+    spoolmanFilamentCatalog: 'Catálogo de filamentos de Spoolman',
+    pickFromSpoolmanCatalog: 'Elegir del catálogo de Spoolman…',
+    spoolmanFilamentSelected: 'Filamento seleccionado del catálogo de Spoolman',
+    spoolmanFilamentUnlinked: 'Vínculo del catálogo de filamentos borrado',
+    noSpoolmanFilaments: 'No se encontraron filamentos en el catálogo de Spoolman',
+    spoolmanFilamentColorSwatch: 'Color del filamento',
+    spoolWeightManagedBySpoolman: 'El peso de la bobina vacía se gestiona por tipo de filamento en Spoolman',
+    spoolmanCatalogLoadFailed: 'Error al cargar el catálogo de filamentos de Spoolman',
+  },
+
+  // Timelapse
+  timelapse: {
+    title: 'Time-lapse',
+    create: 'Crear time-lapse',
+    download: 'Descargar',
+    delete: 'Eliminar',
+    preview: 'Vista previa',
+    frameRate: 'Velocidad de fotogramas',
+    quality: 'Calidad',
+    processing: 'Procesando...',
+    noTimelapses: 'No hay time-lapses disponibles',
+  },
+
+  // AMS
+  ams: {
+    title: 'AMS',
+    slot: 'Ranura',
+    empty: 'Vacía',
+    emptySlot: 'Ranura vacía',
+    slotEmpty: 'Vacía',
+    emptySlotReset: 'No hay filamento asignado',
+    unknown: 'Desconocido',
+    humidity: 'Humedad',
+    temperature: 'Temperatura',
+    filamentType: 'Tipo de filamento',
+    filamentColor: 'Color',
+    remaining: 'Restante',
+    history: 'Historial del AMS',
+    noHistory: 'No hay historial disponible',
+    configureSlot: 'Configurar ranura',
+    externalSpool: 'Bobina externa',
+    profile: 'Perfil',
+    kFactor: 'Factor K',
+    fill: 'Rellenar',
+    configure: 'Configurar',
+    used: 'usado',
+    remainingUnit: 'restante',
+  },
+
+  // Print modal
+  printModal: {
+    title: 'Iniciar impresión',
+    selectPrinter: 'Seleccionar impresora',
+    selectPlate: 'Seleccionar cama',
+    filamentMapping: 'Mapeo de filamentos',
+    totalCost: 'Coste total:',
+    slotRemainingShort: ' - quedan {{grams}} g',
+    printSettings: 'Ajustes de impresión',
+    bedLeveling: 'Nivelación de la cama',
+    flowCalibration: 'Calibración del flujo',
+    vibrationCalibration: 'Calibración de vibración',
+    layerInspection: 'Inspección de la primera capa',
+    timelapse: 'Time-lapse',
+    startPrint: 'Iniciar impresión',
+    addToQueue: 'Añadir a la cola',
+    cancel: 'Cancelar',
+    noPrintersAvailable: 'No hay impresoras disponibles',
+    printerBusy: 'La impresora está ocupada',
+    printerOffline: 'La impresora está desconectada',
+    sameTypeDifferentColor: 'Mismo tipo, color distinto',
+    filamentTypeNotLoaded: 'Tipo de filamento no cargado',
+    openCalendar: 'Abrir calendario',
+    leftNozzle: 'I',
+    rightNozzle: 'D',
+    leftNozzleTooltip: 'Boquilla izquierda',
+    rightNozzleTooltip: 'Boquilla derecha',
+    filamentOverride: 'Anulación de filamento',
+    filamentOverrideHint: 'Anule opcionalmente los filamentos para la asignación basada en el modelo. El planificador comparará con los filamentos que ha seleccionado en lugar de los valores originales del 3MF.',
+    originalFilament: 'Original',
+    overrideWith: 'Anular con',
+    resetToOriginal: 'Restablecer al original',
+    insufficientFilamentTitle: 'No hay suficiente filamento',
+    insufficientFilamentMessage: 'Algunas bobinas asignadas tienen menos filamento restante del que necesita esta impresión:',
+    insufficientFilamentLine: '{{printer}} - {{slot}}: necesita {{required}} g, restante {{remaining}} g',
+    printAnyway: 'Imprimir de todos modos',
+    forceColorMatch: 'Forzar la coincidencia de color',
+    staggerPrinterStarts: 'Escalonar los inicios de las impresoras',
+    staggerGroupSize: 'Tamaño de grupo',
+    staggerInterval: 'Intervalo (min)',
+    staggerPreview: '{{printers}} impresoras → {{groups}} grupos de {{size}}, iniciando cada {{interval}} min',
+    staggerLastGroup: 'último grupo: {{count}}',
+    staggerTotal: 'total: {{minutes}} min',
+    staggerToPrinters: 'Escalonar en {{count}} impresoras',
+    gcodeInjection: 'Inyectar G-code de impresión automática',
+  },
+
+  // Backup
+  backup: {
+    includesEncryptionKey: 'Las copias de seguridad locales incluyen el archivo de clave de cifrado de MFA (DATA_DIR/.mfa_encryption_key) para que un ZIP de copia de seguridad sea autónomo. Trate el ZIP como información sensible — cualquiera que tenga el archivo puede descifrar los secretos de cliente de OIDC y los secretos TOTP que contiene.',
+    title: 'Copia de seguridad y restauración',
+    createBackup: 'Crear copia de seguridad',
+    restoreBackup: 'Restaurar copia de seguridad',
+    restoreDescription: 'Reemplazar todos los datos desde un archivo de copia de seguridad',
+    downloadBackup: 'Descargar copia de seguridad',
+    uploadBackup: 'Subir copia de seguridad',
+    lastBackup: 'Última copia de seguridad',
+    autoBackup: 'Copia de seguridad automática',
+    backupNow: 'Hacer copia de seguridad ahora',
+    restoreWarning: 'Advertencia: restaurar una copia de seguridad sobrescribirá todos los datos actuales.',
+    includeArchives: 'Incluir archivos',
+    includeSettings: 'Incluir ajustes',
+    includeProfiles: 'Incluir perfiles',
+    backupSuccess: 'Copia de seguridad creada correctamente',
+    restoreSuccess: 'Copia de seguridad restaurada correctamente',
+    backupFailed: 'Error en la copia de seguridad',
+    restoreFailed: 'Error en la restauración',
+    restoreNote: 'La impresora virtual se detendrá durante la restauración',
+
+    // GitHub Backup
+    githubBackup: 'Copia de seguridad en Git',
+    enabled: 'Activada',
+    cloudLoginRequired: 'Se requiere iniciar sesión en Bambu Cloud. Inicie sesión en Perfiles → Perfiles en la nube para activar la copia de seguridad en GitHub.',
+    cloudLoginRequiredShort: 'Se requiere iniciar sesión en la nube',
+    githubDescription: 'Sincronice automáticamente sus perfiles con un repositorio privado de GitHub para la copia de seguridad y el historial de versiones.',
+    repoIsPrivate: 'El repositorio es privado — es seguro hacer copias de seguridad en él.',
+    repoIsPublicWarning: 'El repositorio es PÚBLICO. Las copias de seguridad de Bambuddy incluyen credenciales de MQTT, tokens de Home Assistant, tokens de Prometheus, su correo de Bambu Cloud y los códigos de acceso de las impresoras mediante los perfiles K. El guardado está bloqueado hasta que haga el repositorio privado en los ajustes de su proveedor.',
+    repoVisibilityUnknown: 'No se pudo determinar la visibilidad del repositorio. Bambuddy se niega a hacer copias de seguridad en cualquier cosa que no se confirme como privada; el guardado se bloqueará.',
+    repositoryUrl: 'URL del repositorio',
+    repoUrlPlaceholderGitHub: 'https://github.com/username/repo-name',
+	repoUrlPlaceholderGitea: 'https://gitea.example.com/username/repo-name',
+    repoUrlPlaceholderForgejo: 'https://forgejo.example.com/username/repo-name',
+    repoUrlPlaceholderGitLab: 'https://gitlab.com/username/repo-name',
+    allowInsecureHttp: 'Permitir HTTP no seguro',
+    allowInsecureHttpHint: 'Actívelo para instancias autoalojadas en redes privadas sin TLS',
+    personalAccessToken: 'Token de acceso personal',
+    tokenSaved: '(guardado)',
+    enterNewToken: 'Introduzca un nuevo token para actualizar',
+    tokenHint: 'Token de granularidad fina con permiso de lectura/escritura de Contents',
+    branch: 'Rama',
+    provider: 'Proveedor de Git',
+    providerGitHub: 'GitHub',
+    providerGitLab: 'GitLab',
+	providerGitea: 'Gitea',
+    providerForgejo: 'Forgejo',
+    manualOnly: 'Solo manual',
+    hourly: 'Cada hora',
+    daily: 'Diaria',
+    weekly: 'Semanal',
+    includeInBackup: 'Incluir en la copia de seguridad',
+    kProfiles: 'Perfiles K',
+    kProfilesDescription: 'Calibración de avance de presión de las impresoras conectadas',
+    noPrintersConnected: 'No hay impresoras conectadas',
+    printersConnected: '{{connected}}/{{total}} conectadas',
+    cloudProfiles: 'Perfiles en la nube',
+    cloudProfilesDescription: 'Preajustes de filamento, impresora y proceso de Bambu Cloud',
+    appSettings: 'Ajustes de la aplicación',
+    appSettingsDescription: 'Configuración de Bambuddy (base de datos completa)',
+    spoolInventory: 'Inventario de bobinas',
+    spoolInventoryDescription: 'Bobinas de filamento, historial de uso y seguimiento de costes',
+    printArchives: 'Archivos de impresión',
+    printArchivesDescription: 'Metadatos del historial de impresión (sin archivos gcode/3MF)',
+    lastBackupAt: 'Última copia de seguridad:',
+    noBackupsYet: 'Aún no hay copias de seguridad',
+    next: 'Siguiente:',
+    startingBackup: 'Iniciando la copia de seguridad...',
+    test: 'Probar',
+    enableBackup: 'Activar la copia de seguridad',
+    testConnection: 'Probar conexión',
+    enterRepoUrl: 'Introduzca la URL del repositorio',
+    enterRepoAndToken: 'Introduzca la URL del repositorio y el token de acceso',
+    repoRequired: 'La URL del repositorio es obligatoria',
+    tokenRequired: 'El token de acceso es obligatorio',
+    githubBackupEnabled: 'Copia de seguridad en GitHub activada',
+    tokenUpdated: 'Token actualizado',
+    settingsSaved: 'Ajustes guardados',
+    failedToSave: 'Error al guardar: {{message}}',
+    backupCompleteFiles: 'Copia de seguridad completada - {{count}} archivos actualizados',
+    backupSkippedNoChanges: 'Copia de seguridad omitida - sin cambios',
+    backupFailed2: 'Error en la copia de seguridad: {{message}}',
+    clearedLogs: 'Se borraron {{count}} registros',
+    failedToClearLogs: 'Error al borrar los registros: {{message}}',
+
+    // History
+    history: 'Historial',
+    clear: 'Borrar',
+    date: 'Fecha',
+    status: 'Estado',
+    commit: 'Confirmación',
+
+    // Local Backup
+    localBackup: 'Copia de seguridad local',
+    localBackupDescription: 'Cree una copia de seguridad completa de sus datos de Bambuddy, incluida la base de datos, los archivos, las subidas y todos los archivos.',
+    downloadBackupLabel: 'Descargar copia de seguridad',
+    completeBackupZip: 'Copia de seguridad completa: base de datos + todos los archivos (ZIP)',
+    download: 'Descargar',
+    preparingBackup: 'Preparando la copia de seguridad...',
+    creatingArchive: 'Creando el archivo de copia de seguridad... Esto puede tardar un rato para archivos grandes.',
+    downloadingFile: 'Descargando el archivo de copia de seguridad...',
+    backupDownloaded: 'Copia de seguridad descargada correctamente',
+    failedToCreateBackup: 'Error al crear la copia de seguridad: {{message}}',
+    restore: 'Restaurar',
+    restoreReplacesAll: 'La restauración reemplaza todos los datos.',
+    restoreReplacesAllDetail: 'Su base de datos y archivos actuales se reemplazarán por completo. Se requiere un reinicio tras la restauración.',
+    restoreConfirmTitle: 'Restaurar copia de seguridad',
+    restoreConfirmMessage: '¿Está seguro de que desea restaurar desde "{{filename}}"? Esto reemplazará por completo su base de datos actual y todos los archivos. La aplicación deberá reiniciarse tras la restauración.',
+    restoreConfirmButton: 'Restaurar copia de seguridad',
+    uploadingFile: 'Subiendo el archivo de copia de seguridad...',
+    backupRestoredRestart: 'Copia de seguridad restaurada. Reinicie Bambuddy.',
+    failedToRestore: 'Error al restaurar la copia de seguridad. Compruebe el formato del archivo.',
+    reloadNow: 'Recargar ahora',
+    creatingBackup: 'Creando copia de seguridad',
+    restoringBackup: 'Restaurando copia de seguridad',
+    preparing: 'Preparando...',
+    processing: 'Procesando...',
+    doNotClosePage: 'No cierre esta página ni navegue a otra. Esta operación puede tardar varios minutos para copias de seguridad grandes.',
+
+    // RestoreModal
+    restoring: 'Restaurando...',
+    restoreComplete: 'Restauración completada',
+    restoreFailed2: 'Error en la restauración',
+    importSettings: 'Importar ajustes desde un archivo de copia de seguridad',
+    pleaseWaitRestoring: 'Espere mientras se restauran sus datos',
+    selectBackupFile: 'Haga clic para seleccionar el archivo de copia de seguridad (.json o .zip)',
+    duplicateHandling: 'Cómo funciona la gestión de duplicados:',
+    matchPrinters: 'Impresoras',
+    matchPrintersBy: 'coincidencia por número de serie',
+    matchSmartPlugs: 'Enchufes inteligentes',
+    matchSmartPlugsBy: 'coincidencia por dirección IP',
+    matchNotificationProviders: 'Proveedores de notificaciones',
+    matchNotificationProvidersBy: 'coincidencia por nombre',
+    matchFilaments: 'Filamentos',
+    matchFilamentsBy: 'coincidencia por nombre + tipo + marca',
+    matchArchives: 'Archivos',
+    matchArchivesBy: 'coincidencia por hash del contenido (siempre se omiten)',
+    matchPendingUploads: 'Subidas pendientes',
+    matchPendingUploadsBy: 'coincidencia por nombre de archivo',
+    matchSettingsTemplates: 'Ajustes y plantillas',
+    matchSettingsTemplatesBy: 'siempre se sobrescriben',
+    replaceExisting: 'Reemplazar los datos existentes',
+    keepExisting: 'Conservar los datos existentes',
+    overwriteDescription: 'Sobrescribir los elementos que ya existen con los datos de la copia de seguridad',
+    keepDescription: 'Restaurar solo los elementos que aún no existen',
+    overwriteCaution: 'Precaución:',
+    overwriteWarning: 'Sobrescribir reemplazará sus configuraciones actuales con los datos de la copia de seguridad. Los códigos de acceso de las impresoras nunca se sobrescriben por seguridad.',
+    cancel: 'Cancelar',
+    processingBackup: 'Procesando el archivo de copia de seguridad...',
+    itemsRestored: 'Elementos restaurados',
+    itemsSkipped: 'Elementos omitidos',
+    restored: 'Restaurados',
+    skippedAlreadyExist: 'Omitidos (ya existen)',
+    filesCategory: 'Archivos (3MF, miniaturas, etc.)',
+    andMore: '...y {{count}} más',
+    newApiKeysGenerated: 'Nuevas claves API generadas',
+    keysShownOnce: 'Estas claves solo se muestran una vez. ¡Cópielas ahora!',
+    copy: 'Copiar',
+    noDataFound: 'No se encontraron datos para restaurar en el archivo de copia de seguridad.',
+    close: 'Cerrar',
+
+    // Scheduled local backups (#884)
+    scheduledBackup: 'Copias de seguridad programadas',
+    scheduledBackupDescription: 'Cree automáticamente instantáneas de copia de seguridad según una programación. El directorio de salida se puede montar en un NAS o en almacenamiento externo.',
+    frequency: 'Frecuencia',
+    backupTime: 'Hora',
+    retention: 'Retención',
+    retentionDescription: 'Número de copias de seguridad que conservar',
+    outputPath: 'Ruta de salida',
+    outputPathPlaceholder: 'Predeterminada: {{path}}',
+    outputPathDescription: 'Déjelo vacío para la ubicación predeterminada',
+    runNow: 'Ejecutar ahora',
+    backupFiles: 'Archivos de copia de seguridad',
+    noScheduledBackups: 'Aún no hay copias de seguridad',
+    deleteBackup: 'Eliminar',
+    deleteBackupConfirm: '¿Eliminar este archivo de copia de seguridad?',
+    backupRunning: 'Copia de seguridad en curso...',
+    scheduledBackupComplete: 'Copia de seguridad completada correctamente',
+    scheduledBackupFailed: 'Error en la copia de seguridad',
+    nextBackup: 'Próxima copia de seguridad',
+    backupSize: 'Tamaño',
+    utc: 'UTC',
+    defaultPathLabel: 'Predeterminada:',
+
+    // Category labels
+    categories: {
+      settings: 'Ajustes',
+      notification_providers: 'Proveedores de notificaciones',
+      notification_templates: 'Plantillas de notificaciones',
+      smart_plugs: 'Enchufes inteligentes',
+      printers: 'Impresoras',
+      filaments: 'Filamentos',
+      maintenance_types: 'Tipos de mantenimiento',
+      archives: 'Archivos',
+      projects: 'Proyectos',
+      pending_uploads: 'Subidas pendientes',
+      external_links: 'Enlaces externos',
+      api_keys: 'Claves API',
+    },
+  },
+
+  // Tags
+  tags: {
+    title: 'Etiquetas',
+    addTag: 'Añadir etiqueta',
+    editTag: 'Editar etiqueta',
+    deleteTag: 'Eliminar etiqueta',
+    tagName: 'Nombre de la etiqueta',
+    tagColor: 'Color de la etiqueta',
+    noTags: 'No hay etiquetas',
+    deleteConfirm: '¿Está seguro de que desea eliminar esta etiqueta?',
+    manageTags: 'Gestionar etiquetas',
+  },
+
+  // Upload modal (archives)
+  uploadModal: {
+    title: 'Subir archivos 3MF',
+    dragDrop: 'Arrastre y suelte archivos .3mf aquí',
+    or: 'o',
+    browseFiles: 'Examinar archivos',
+    extractionInfo: 'El modelo de impresora se extraerá automáticamente de los metadatos del archivo 3MF.',
+    uploaded: 'subidos',
+    failed: 'fallidos',
+    uploading: 'Subiendo...',
+    upload: 'Subir',
+    uploadFailed: 'Error al subir',
+  },
+
+  // Edit archive modal
+  // Edit Archive Modal
+  editArchive: {
+    title: 'Editar archivo',
+    name: 'Nombre',
+    namePlaceholder: 'Nombre de la impresión',
+    printer: 'Impresora',
+    noPrinter: 'Sin impresora',
+    project: 'Proyecto',
+    noProject: 'Sin proyecto',
+    itemsPrinted: 'Elementos impresos',
+    itemsPrintedHelp: 'Número de elementos producidos en este trabajo de impresión',
+    notes: 'Notas',
+    notesPlaceholder: 'Añada notas sobre esta impresión...',
+    externalLink: 'Enlace externo',
+    externalLinkPlaceholder: 'https://printables.com/model/...',
+    externalLinkHelp: 'Enlace a Printables, Thingiverse u otra fuente',
+    tags: 'Etiquetas',
+    tagsPlaceholder: 'Añada etiquetas...',
+    addMoreTags: 'Añada más etiquetas...',
+    matchingTags: 'Coincidentes con "{{query}}"',
+    existingTags: 'Etiquetas existentes',
+    clickToAdd: '(haga clic para añadir)',
+    status: 'Estado',
+    failureReason: 'Motivo del fallo',
+    selectReason: 'Seleccionar motivo...',
+    photos: 'Fotos del resultado impreso',
+    photosHelp: 'Haga clic en + para añadir fotos de su resultado impreso',
+    printResult: 'Resultado de la impresión',
+    saving: 'Guardando...',
+    // Failure reasons
+    failureReasons: {
+      adhesionFailure: 'Fallo de adhesión',
+      spaghettiDetached: 'Espagueti / Desprendido',
+      layerShift: 'Desplazamiento de capa',
+      cloggedNozzle: 'Boquilla obstruida',
+      filamentRunout: 'Agotamiento del filamento',
+      warping: 'Alabeo',
+      stringing: 'Hilos',
+      underExtrusion: 'Subextrusión',
+      powerFailure: 'Corte de corriente',
+      userCancelled: 'Cancelada por el usuario',
+      other: 'Otro',
+    },
+    // Archive statuses
+    statuses: {
+      completed: 'Completada',
+      failed: 'Fallida',
+      aborted: 'Cancelada',
+      printing: 'Imprimiendo',
+    },
+  },
+
+  // K-Profiles
+  kProfiles: {
+    title: 'Perfiles K',
+    noPrintersConfigured: 'No hay impresoras configuradas',
+    addPrinterInSettings: 'Añada una impresora en Ajustes para gestionar perfiles K',
+    noActivePrinters: 'No hay impresoras activas',
+    enablePrinterConnection: 'Active la conexión de una impresora para ver sus perfiles K',
+    loadingProfiles: 'Cargando perfiles K...',
+    printerOffline: 'Impresora desconectada',
+    printerOfflineDesc: 'La impresora seleccionada no está conectada. Enciéndala para ver los perfiles K.',
+    noMatchingProfiles: 'No hay perfiles coincidentes',
+    noMatchingProfilesDesc: 'Ningún perfil coincide con sus criterios de búsqueda',
+    noKProfiles: 'No hay perfiles K',
+    noKProfilesDesc: 'No se encontraron perfiles de avance de presión para la boquilla de {{diameter}} mm',
+    createFirstProfile: 'Crear el primer perfil',
+    // Controls
+    printer: 'Impresora',
+    nozzle: 'Boquilla',
+    refresh: 'Actualizar',
+    addProfile: 'Añadir perfil',
+    export: 'Exportar',
+    import: 'Importar',
+    select: 'Seleccionar',
+    selectAll: 'Seleccionar todo',
+    delete: 'Eliminar',
+    // Filters
+    searchPlaceholder: 'Buscar por nombre o filamento...',
+    allExtruders: 'Todos los extrusores',
+    leftOnly: 'Solo izquierdo',
+    rightOnly: 'Solo derecho',
+    allFlow: 'Todos los flujos',
+    hfOnly: 'Solo flujo alto',
+    sOnly: 'Solo estándar',
+    sortName: 'Ordenar: nombre',
+    sortKValue: 'Ordenar: valor K',
+    sortFilament: 'Ordenar: filamento',
+    // Dual extruder labels
+    leftExtruder: 'Extrusor izquierdo',
+    rightExtruder: 'Extrusor derecho',
+    // Modal
+    modal: {
+      addTitle: 'Añadir perfil K',
+      editTitle: 'Editar perfil K',
+      profileName: 'Nombre del perfil',
+      profileNamePlaceholder: 'Mi perfil de PLA',
+      kValue: 'Valor K',
+      kValuePlaceholder: '0,020',
+      kValueHelp: 'Rango típico: 0,01 - 0,06 para PLA, 0,02 - 0,10 para PETG',
+      filament: 'Filamento',
+      selectFilament: 'Seleccionar filamento...',
+      noFilamentsHelp: 'No se encontraron filamentos. Cree primero un perfil K en Bambu Studio.',
+      flowType: 'Tipo de flujo',
+      highFlow: 'Flujo alto',
+      standard: 'Estándar',
+      nozzleSize: 'Tamaño de la boquilla',
+      extruder: 'Extrusor',
+      extruders: 'Extrusores',
+      left: 'Izquierdo',
+      right: 'Derecho',
+      notes: 'Notas (almacenadas localmente)',
+      notesPlaceholder: 'Añada notas sobre este perfil...',
+      notesHelp: 'Las notas se guardan en Bambuddy, no en la impresora',
+      syncing: 'Sincronizando con la impresora...',
+      savingExtruder: 'Guardando en el extrusor {{current}}/{{total}}...',
+      pleaseWait: 'Espere',
+    },
+    // Delete confirmation
+    deleteConfirm: {
+      title: 'Eliminar perfil',
+      cannotUndo: 'Esto no se puede deshacer',
+      message: '¿Está seguro de que desea eliminar "{{name}}" de la impresora?',
+    },
+    // Bulk delete
+    bulkDelete: {
+      title: 'Eliminar perfiles',
+      cannotUndo: 'Esto no se puede deshacer',
+      message: '¿Está seguro de que desea eliminar {{count}} perfiles seleccionados de la impresora?',
+    },
+    // Toast
+    toast: {
+      profileSaved: 'Perfil K guardado',
+      profilesSaved: 'Perfil K guardado en {{count}} extrusores',
+      selectAtLeastOneExtruder: 'Seleccione al menos un extrusor',
+      profileDeleted: 'Perfil K eliminado',
+      profilesDeleted: 'Se eliminaron {{count}} perfiles',
+      exportedProfiles: 'Se exportaron {{count}} perfiles',
+      importedProfiles: 'Se importaron {{count}} de {{total}} perfiles',
+      noProfilesToExport: 'No hay perfiles que exportar',
+      invalidFileFormat: 'Formato de archivo no válido',
+      failedToParseImport: 'Error al analizar el archivo de importación',
+      failedToSaveBatch: 'Error al guardar los perfiles K',
+      noteSaved: 'Nota guardada',
+      failedToSaveNote: 'Error al guardar la nota',
+    },
+    // Permissions
+    permission: {
+      noRead: 'No tiene permiso para actualizar perfiles',
+      noCreate: 'No tiene permiso para añadir perfiles',
+      noUpdate: 'No tiene permiso para actualizar perfiles K',
+      noDelete: 'No tiene permiso para eliminar perfiles K',
+      noExport: 'No tiene permiso para exportar perfiles',
+      noImport: 'No tiene permiso para importar perfiles',
+    },
+  },
+
+  // Virtual Printer
+  virtualPrinter: {
+    title: 'Impresora virtual',
+    running: 'En ejecución',
+    stopped: 'Detenida',
+    description: {
+      default: 'Active una impresora virtual que aparece en Bambu Studio y OrcaSlicer. Los archivos enviados a esta impresora se archivarán directamente sin imprimir.',
+      proxy: 'Active un proxy que retransmite el tráfico del laminador a una impresora real, permitiendo la impresión remota a través de cualquier red.',
+    },
+    enable: {
+      title: 'Activar la impresora virtual',
+      visibleInSlicer: 'Visible como "Bambuddy" en la detección del laminador',
+      proxyingTo: 'Haciendo de proxy hacia {{name}}',
+      notActive: 'No activa',
+    },
+    model: {
+      title: 'Modelo de impresora',
+      description: 'Seleccione qué modelo de impresora emular.',
+      restartWarning: 'Cambiar el modelo reiniciará la impresora virtual',
+    },
+    accessCode: {
+      title: 'Código de acceso',
+      isSet: 'El código de acceso está establecido',
+      notSet: 'No hay código de acceso establecido - necesario para activarla',
+      placeholder: 'Introduzca un código de 8 caracteres',
+      placeholderChange: 'Introduzca un nuevo código para cambiarlo',
+      hint: 'Debe tener exactamente 8 caracteres. Los laminadores lo usan para autenticarse.',
+      charCount: '({{count}}/8)',
+    },
+    targetPrinter: {
+      title: 'Impresora de destino',
+      configured: 'Destino del proxy configurado',
+      notConfigured: 'No hay impresora de destino seleccionada - necesaria para el modo proxy',
+      placeholder: 'Seleccionar una impresora...',
+      hint: 'Seleccione la impresora a la que retransmitir el tráfico del laminador. La impresora debe estar en modo LAN.',
+      noPrinters: 'No hay impresoras configuradas. Añada primero una impresora para usar el modo proxy.',
+    },
+    remoteInterface: {
+      title: 'Anulación de la interfaz de red',
+      configured: 'Anulación de la interfaz activa',
+      optional: 'Opcional - úsela si la IP detectada automáticamente es incorrecta (p. ej. varias tarjetas de red, Docker, VPN)',
+      placeholder: 'Detección automática (predeterminada)...',
+      hint: 'Anula la dirección IP anunciada mediante SSDP y usada en el certificado TLS. Útil cuando Bambuddy tiene varias interfaces de red.',
+    },
+    mode: {
+      title: 'Modo',
+      archive: 'Archivar',
+      archiveDesc: 'Archivar los archivos inmediatamente',
+      review: 'Revisar',
+      reviewDesc: 'Revisar antes de archivar',
+      queue: 'Encolar',
+      queueDesc: 'Archivar y añadir a la cola',
+      proxy: 'Proxy',
+      proxyDesc: 'Retransmitir a una impresora real',
+    },
+    autoDispatch: {
+      title: 'Envío automático',
+      description: 'Iniciar automáticamente las impresiones al añadirlas a la cola. Cuando está desactivado, las impresiones esperan al envío manual.',
+    },
+    queueForceColorMatch: {
+      title: 'Forzar la coincidencia de color',
+      description: 'Negarse a enviar a una impresora que no tiene cargados el tipo y el color exactos de filamento. Desactivado de forma predeterminada — sin esto, la cola usa la coincidencia solo por modelo y puede elegir una impresora con el color equivocado cargado.',
+    },
+    tailscaleDisabled: {
+      title: 'Integración con Tailscale',
+      description: 'Actívelo para marcar esta IV como expuesta a través de Tailscale. Muestra la dirección de Tailscale del host para que sepa qué IP pegar en el laminador. El paso de importación de la CA no cambia — este interruptor no tiene ningún efecto sobre los certificados.',
+    },
+    setupRequired: {
+      title: 'Configuración necesaria',
+      description: 'La función de impresora virtual requiere configuración adicional del sistema antes de funcionar. Esto incluye el reenvío de puertos, las reglas del cortafuegos y los ajustes específicos de la plataforma.',
+      readGuide: 'Lea la guía de configuración antes de activarla',
+    },
+    archiveNameSource: {
+      title: 'Origen del nombre del archivo',
+      description: 'Elija cómo se nombran los archivos nuevos cuando llegan a través de la impresora virtual. «Metadatos» usa el título incrustado por el laminador del 3MF (predeterminado). «Nombre de archivo» usa el nombre de archivo que Bambu Studio envió por FTP — útil si renombró el trabajo en el diálogo de «enviar a la impresora».',
+      metadata: 'Metadatos',
+      filename: 'Nombre de archivo',
+    },
+    howItWorks: {
+      title: 'Cómo funciona',
+      step1: 'En la misma LAN, las impresoras virtuales aparecen automáticamente en su laminador (Bambu Studio / OrcaSlicer) mediante detección. Desde otras redes, añádalas manualmente por dirección IP y código de acceso.',
+      step2: 'En los modos Archivar, Revisar y Encolar, use el botón «Enviar» de su laminador para subir archivos 3MF a Bambuddy. El laminador mostrará «Impresión correcta» — el archivo se almacena, no se imprime.',
+      step3: 'En el modo Proxy, la impresora virtual retransmite todo el tráfico a una impresora real — las impresiones comienzan inmediatamente como si estuviera conectada directamente.',
+    },
+    status: {
+      title: 'Detalles del estado',
+      printerName: 'Nombre de la impresora',
+      model: 'Modelo',
+      serialNumber: 'Número de serie',
+      mode: 'Modo',
+      pendingFiles: 'Archivos pendientes',
+      targetPrinter: 'Impresora de destino',
+      ftpPort: 'Puerto FTP',
+      mqttPort: 'Puerto MQTT',
+      ftpConnections: 'Conexiones FTP',
+      mqttConnections: 'Conexiones MQTT',
+    },
+    toast: {
+      updated: 'Ajustes de la impresora virtual actualizados',
+      failedToUpdate: 'Error al actualizar los ajustes',
+      copyFailed: 'Error al copiar — pruebe a seleccionar el texto manualmente',
+      accessCodeRequired: 'Establezca primero un código de acceso',
+      targetPrinterRequired: 'Seleccione primero una impresora de destino',
+      bindIpRequired: 'Establezca primero una IP de enlace',
+      accessCodeEmpty: 'El código de acceso no puede estar vacío',
+      accessCodeLength: 'El código de acceso debe tener exactamente 8 caracteres',
+      created: 'Impresora virtual creada',
+      failedToCreate: 'Error al crear la impresora virtual',
+      deleted: 'Impresora virtual eliminada',
+      failedToDelete: 'Error al eliminar la impresora virtual',
+    },
+    list: {
+      title: 'Impresoras virtuales',
+      add: 'Añadir',
+      addFirst: 'Añadir impresora virtual',
+      empty: 'No hay impresoras virtuales configuradas. Añada una para empezar.',
+    },
+    bindIp: {
+      title: 'Interfaz de enlace',
+      placeholder: 'Seleccionar interfaz...',
+      hint: 'Interfaz de red a la que se enlaza esta impresora virtual. Debe ser única por impresora.',
+    },
+    proxy: {
+      accessCodeHint: 'En el modo proxy, use el código de acceso de su impresora de destino en el laminador. La conexión se reenvía de forma transparente a la impresora real.',
+    },
+    addDialog: {
+      title: 'Añadir impresora virtual',
+      name: 'Nombre',
+      hint: 'Puede configurar el código de acceso, la impresora de destino y otros ajustes después de crearla.',
+      create: 'Crear',
+    },
+    deleteConfirm: {
+      title: 'Eliminar impresora virtual',
+      message: '¿Está seguro de que desea eliminar "{{name}}"? Esto detendrá todos los servicios de esta impresora.',
+    },
+  },
+
+  // Model Viewer
+  modelViewer: {
+    openInSlicer: 'Abrir en el laminador',
+    tabs: {
+      model: 'Modelo 3D',
+      gcode: 'Vista previa de G-code',
+    },
+    notAvailable: 'no disponible',
+    notSliced: 'no laminado',
+    plates: 'Camas',
+    allPlates: 'Todas las camas',
+    plateNumber: 'Cama {{number}}',
+    plateCount: '{{count}} cama',
+    plateCount_other: '{{count}} camas',
+    objectCount: '{{count}} objeto',
+    objectCount_other: '{{count}} objetos',
+    filamentCount: '{{count}} filamento',
+    filamentCount_other: '{{count}} filamentos',
+    eta: 'Tiempo estimado {{minutes}} min',
+    noPreview: 'No hay vista previa disponible para este archivo',
+    pagination: {
+      pageOf: 'Página {{current}} de {{total}}',
+      prev: 'Anterior',
+      next: 'Siguiente',
+    },
+    errors: {
+      failedToLoad: 'Error al cargar el archivo',
+      noMeshes: 'No se encontraron mallas en el archivo 3MF',
+      unsupportedFormat: 'Formato de archivo no compatible',
+    },
+  },
+
+  // Maintenance type descriptions (built-in)
+  maintenanceDescriptions: {
+    lubricateCarbonRods: 'Aplique lubricante a las varillas de carbono para un movimiento suave',
+    lubricateRails: 'Aplique lubricante a las guías lineales para un movimiento suave',
+    cleanNozzle: 'Limpie el fusor y la boquilla para evitar obstrucciones',
+    checkBelts: 'Verifique la tensión de las correas para impresiones precisas',
+    cleanBuildPlate: 'Limpie la cama de impresión para una mejor adhesión',
+    checkExtruder: 'Inspeccione los engranajes del extrusor en busca de desgaste',
+    checkCooling: 'Asegúrese de que los ventiladores de refrigeración funcionan correctamente',
+    generalInspection: 'Inspección general de la impresora',
+    cleanCarbonRods: 'Limpie las varillas de carbono para reducir la fricción',
+    lubricateSteelRods: 'Aplique lubricante a las varillas de acero para un movimiento suave',
+    cleanSteelRods: 'Limpie las varillas de acero para reducir la fricción',
+    cleanLinearRails: 'Limpie las guías lineales para eliminar el polvo y los residuos',
+    checkPtfeTube: 'Inspeccione el tubo de PTFE en busca de desgaste o daños',
+    replaceHepaFilter: 'Reemplace el filtro HEPA para la calidad del aire',
+    replaceCarbonFilter: 'Reemplace el filtro de carbón activado',
+    lubricateLeftNozzleRail: 'Lubrique la guía de la boquilla izquierda (serie H2)',
+  },
+
+  // Smart Plugs
+  smartPlugs: {
+    offline: 'Desconectado',
+    admin: 'Administración',
+    openPlugAdminPage: 'Abrir la página de administración del enchufe',
+    deleteSmartPlug: 'Eliminar enchufe inteligente',
+    turnOnSmartPlug: 'Encender el enchufe inteligente',
+    turnOffSmartPlug: 'Apagar el enchufe inteligente',
+    turnOn: 'Encender',
+    turnOff: 'Apagar',
+    addSmartPlug: {
+      scanningNetwork: 'Escaneando la red...',
+      chooseEntity: 'Elija una entidad...',
+      connectionFailed: 'Error de conexión',
+      searchEntities: 'Buscar entidades...',
+      searchPowerSensors: 'Buscar sensores de potencia...',
+      searchEnergySensors: 'Buscar sensores de energía...',
+      placeholders: {
+        plugName: 'Enchufe del salón',
+        mqttStateOnValue: 'ON, true, 1',
+        mqttSameAsPower: 'Igual que el tema de potencia, o distinto',
+      },
+    },
+    // SmartPlugCard
+    linkedTo: 'Vinculado a:',
+    monitorOnly: 'Solo supervisión',
+    alerts: 'Alertas',
+    scheduleOn: 'Encendido a las {{time}}',
+    scheduleOff: 'Apagado a las {{time}}',
+    on: 'Encendido',
+    off: 'Apagado',
+    power: 'Potencia',
+    kwhToday: 'kWh hoy',
+    settings: 'Ajustes',
+    automationSettings: 'Ajustes de automatización',
+    showInSwitchbar: 'Mostrar en la barra de interruptores',
+    quickAccessSidebar: 'Acceso rápido desde la barra lateral',
+    enabled: 'Activada',
+    enableAutomation: 'Activar la automatización para este enchufe',
+    autoOn: 'Encendido automático',
+    autoOnDescription: 'Encender cuando comienza la impresión',
+    autoOff: 'Apagado automático',
+    autoOffDescription: 'Apagar cuando se completa la impresión (una sola vez)',
+    autoOffPersistent: 'Mantener activado',
+    autoOffPersistentDescription: 'Permanecer activado entre impresiones en lugar de una sola vez',
+    autoOffAfterDrying: 'Apagado automático tras el secado',
+    autoOffAfterDryingDescription: 'Apagar cuando se completa el secado del AMS',
+    delayAfterDryingMinutes: 'Retardo de secado (minutos)',
+    turnOffDelayMode: 'Modo de retardo de apagado',
+    time: 'Tiempo',
+    temp: 'Temp.',
+    delayMinutes: 'Retardo (minutos)',
+    tempThreshold: 'Umbral de temperatura (°C)',
+    tempThresholdDescription: 'Se apaga cuando la boquilla se enfría por debajo de esta temperatura',
+    edit: 'Editar',
+    deleteConfirm: '¿Está seguro de que desea eliminar "{{name}}"? Esto no se puede deshacer.',
+    turnOnConfirm: '¿Está seguro de que desea encender "{{name}}"?',
+    turnOffConfirm: '¿Está seguro de que desea apagar "{{name}}"? Esto cortará la corriente al dispositivo conectado.',
+    failedToTurn: 'Error al {{action}} "{{name}}"',
+    unknown: 'Desconocido',
+    // AddSmartPlugModal
+    addTitle: 'Añadir enchufe inteligente',
+    editTitle: 'Editar enchufe inteligente',
+    stopScanning: 'Detener escaneo',
+    discoverTasmota: 'Detectar dispositivos Tasmota',
+    foundDevices: 'Se encontraron {{count}} dispositivo(s) - haga clic para seleccionar:',
+    noDevicesFound: 'No se encontraron dispositivos Tasmota en su red',
+    haNotConfigured: 'Home Assistant no está configurado. Configúrelo en',
+    haSettingsPath: 'Ajustes → Red → Home Assistant',
+    selectEntity: 'Seleccionar entidad *',
+    ipAddress: 'Dirección IP *',
+    nameLabel: 'Nombre *',
+    username: 'Nombre de usuario',
+    password: 'Contraseña',
+    authHint: 'Déjelo vacío si su dispositivo Tasmota no requiere autenticación',
+    linkToPrinter: 'Vincular a una impresora',
+    noPrinter: 'Sin impresora (solo control manual)',
+    linkingDescription: 'La vinculación activa el encendido/apagado automático cuando las impresiones comienzan/terminan',
+    powerAlerts: 'Alertas de potencia',
+    alertAbove: 'Alertar si supera (W)',
+    alertBelow: 'Alertar si está por debajo (W)',
+    alertDescription: 'Reciba una notificación cuando el consumo de energía cruce estos umbrales. Déjelo vacío para desactivar esa dirección.',
+    dailySchedule: 'Programación diaria',
+    turnOnAt: 'Encender a las',
+    turnOffAt: 'Apagar a las',
+    scheduleDescription: 'Encender/apagar automáticamente el enchufe a estas horas cada día. Déjelo vacío para omitir esa acción.',
+    showOnPrinterCard: 'Mostrar en la tarjeta de la impresora',
+    displayOnPrinterCard: 'Mostrar el botón en la tarjeta de la impresora',
+    connectedResult: '¡Conectado!',
+    deviceLabel: 'Dispositivo: {{name}} - ',
+    stateLabel: 'Estado: {{state}}',
+    test: 'Probar',
+    delete: 'Eliminar',
+    save: 'Guardar',
+    add: 'Añadir',
+    cancel: 'Cancelar',
+    failedToStartScan: 'Error al iniciar el escaneo',
+    nameRequired: 'El nombre es obligatorio',
+    entityRequired: 'La entidad es obligatoria para los enchufes de Home Assistant',
+    mqttTopicRequired: 'Debe configurarse al menos un tema MQTT para la supervisión de potencia, energía o estado',
+    loadingEntities: 'Cargando entidades...',
+    loading: 'Cargando...',
+    failedToLoadEntities: 'Error al cargar las entidades: {{error}}',
+    noEntitiesMatching: 'No se encontraron entidades que coincidan con "{{search}}"',
+    noEntitiesAvailable: 'No hay entidades disponibles',
+    searchingEntities: 'Buscando en todas las entidades ({{count}} encontradas)',
+    showingEntities: 'Mostrando switch, light, input_boolean ({{count}} disponibles)',
+    energyMonitoringOptional: 'Supervisión de energía (opcional)',
+    energyMonitoringHint: 'Busque y seleccione sensores que proporcionen datos de potencia/energía.',
+    powerSensorW: 'Sensor de potencia (W)',
+    energyTodayKwh: 'Energía de hoy (kWh)',
+    totalEnergyKwh: 'Energía total (kWh)',
+    noMatchingSensors: 'No hay sensores coincidentes',
+    none: 'Ninguno',
+    mqttNotConfigured: 'El broker MQTT no está configurado. Establezca la dirección del broker en',
+    mqttSettingsPath: 'Ajustes → Red → Publicación MQTT',
+    mqttNotConfiguredSuffix: '(no necesita activar la publicación, solo rellenar los datos del broker).',
+    mqttMonitorOnlyDescription: 'Los enchufes MQTT reciben los datos de potencia/energía mediante una suscripción MQTT. El control de encendido/apagado no está disponible - use su broker MQTT o sistema de domótica.',
+    powerMonitoring: 'Supervisión de potencia',
+    energyMonitoring: 'Supervisión de energía',
+    stateMonitoring: 'Supervisión de estado',
+    optional: 'opcional',
+    topic: 'Tema',
+    jsonPath: 'Ruta JSON',
+    multiplier: 'Multiplicador',
+    onValue: 'Valor de ON',
+    mqttPowerHint: 'La ruta JSON extrae el valor de la carga útil JSON (p. ej., "power_l1"). Déjelo vacío si el tema publica valores numéricos sin procesar.\nUse el multiplicador 0,001 para mW→W, 1000 para kW→W.',
+    mqttEnergyHint: 'La ruta JSON extrae el valor de la carga útil JSON. Déjelo vacío para valores sin procesar.\nUse el multiplicador 0,001 para Wh→kWh, 1000 para MWh→kWh.',
+    mqttStateHint: 'La ruta JSON extrae el valor de la carga útil JSON. Déjelo vacío para valores sin procesar.\nValor de ON: la cadena exacta que significa «ON». Déjelo vacío para la detección automática (ON, true, 1).',
+    // REST smart plug
+    restControl: 'Control',
+    restOnUrl: 'URL de encendido',
+    restOffUrl: 'URL de apagado',
+    restOnBody: 'Cuerpo de la solicitud de encendido',
+    restOffBody: 'Cuerpo de la solicitud de apagado',
+    restMethod: 'Método HTTP',
+    restHeaders: 'Cabeceras personalizadas (JSON)',
+    restStatusUrl: 'URL de estado',
+    restStatusPath: 'Ruta JSON del estado',
+    restStatusOnValue: 'Valor de ON',
+    restPowerUrl: 'URL de potencia',
+    restPowerPath: 'Ruta JSON de la potencia',
+    restPowerMultiplier: 'Multiplicador de potencia',
+    restEnergyUrl: 'URL de energía',
+    restEnergyPath: 'Ruta JSON de la energía',
+    restEnergyMultiplier: 'Multiplicador de energía',
+    restUrlRequired: 'Se requiere al menos una URL (de encendido o apagado) para los enchufes REST',
+    restHeadersHint: 'p. ej. {"Authorization": "Bearer your-token"}',
+    restBodyHint: 'p. ej. ON, {"state": "on"}',
+    restStatusHint: 'URL para sondear el estado actual',
+    restPathHint: 'p. ej. state o data.power.status',
+    restPowerUrlHint: 'URL independiente para los datos de potencia (usa la URL de estado si está vacía)',
+    restEnergyUrlHint: 'URL independiente para los datos de energía (usa la URL de estado si está vacía)',
+    restEnergyHint: 'Cada valor puede usar su propia URL o recurrir a la URL de estado. Use multiplicadores para la conversión de unidades (p. ej. 0,001 para convertir Wh en kWh).',
+    testConnection: 'Probar conexión',
+    connectionSuccess: 'Conexión correcta',
+    noSwitchesInSwitchbar: 'No hay interruptores en la barra de interruptores',
+    enableSwitchbarHint: 'Active «Mostrar en la barra de interruptores» en Ajustes > Enchufes inteligentes',
+  },
+
+  // Notifications
+  notifications: {
+    // Provider types
+    providerTypes: {
+      callmebot: 'CallMeBot/WhatsApp',
+      ntfy: 'ntfy',
+      pushover: 'Pushover',
+      telegram: 'Telegram',
+      email: 'Correo',
+      discord: 'Discord',
+      webhook: 'Webhook',
+      homeassistant: 'Home Assistant',
+    },
+    // Provider descriptions
+    providerDescriptions: {
+      email: 'Notificaciones por correo SMTP',
+      telegram: 'Notificaciones mediante un bot de Telegram',
+      discord: 'Enviar a un canal de Discord mediante un webhook',
+      ntfy: 'Notificaciones push gratuitas y autoalojables',
+      pushover: 'Notificaciones push sencillas y fiables',
+      callmebot: 'Notificaciones de WhatsApp gratuitas mediante CallMeBot',
+      webhook: 'POST HTTP genérico a cualquier URL',
+      homeassistant: 'Notificaciones persistentes en el panel de Home Assistant',
+    },
+    // NotificationProviderCard
+    lastSuccess: 'Última: {{date}}',
+    error: 'Error',
+    printer: 'Impresora:',
+    allPrinters: 'Todas las impresoras',
+    sendTestNotification: 'Enviar notificación de prueba',
+    eventSettings: 'Ajustes de eventos',
+    enabled: 'Activado',
+    sendFromProvider: 'Enviar notificaciones desde este proveedor',
+    // Event categories
+    printEvents: 'Eventos de impresión',
+    printerStatus: 'Estado de la impresora',
+    amsAlarms: 'Alarmas del AMS',
+    amsHtAlarms: 'Alarmas del AMS-HT',
+    printQueue: 'Cola de impresión',
+    // Event tags (badges)
+    start: 'Inicio',
+    plateCheck: 'Comprobación de cama',
+    complete: 'Completada',
+    failed: 'Fallida',
+    stopped: 'Detenida',
+    progress: 'Progreso',
+    offline: 'Desconectada',
+    lowFilament: 'Filamento bajo',
+    maintenance: 'Mantenimiento',
+    amsHumidity: 'Humedad del AMS',
+    amsTemp: 'Temp. del AMS',
+    amsHtHumidity: 'Humedad del AMS-HT',
+    amsHtTemp: 'Temp. del AMS-HT',
+    bedCooled: 'Cama enfriada',
+    firstLayer: 'Primera capa',
+    quiet: 'Silencio',
+    digest: 'Resumen {{time}}',
+    // Event labels (expanded settings)
+    printStarted: 'Impresión iniciada',
+    plateNotEmpty: 'Cama no vacía',
+    plateNotEmptyDescription: 'Objetos detectados antes de la impresión',
+    printCompleted: 'Impresión completada',
+    bedCooledLabel: 'Cama enfriada',
+    bedCooledDescription: 'La cama se enfrió por debajo del umbral tras la impresión',
+    firstLayerCompleteLabel: 'Primera capa completada',
+    firstLayerCompleteDescription: 'Notificar con una captura cuando termina la primera capa',
+    missingSpoolAssignmentLabel: 'Falta la asignación de bobina',
+    missingSpoolAssignmentDescription: 'Notificar cuando la impresión comienza y las bandejas necesarias no tienen ninguna bobina asignada',
+    printFailed: 'Impresión fallida',
+    printStopped: 'Impresión detenida',
+    progressMilestones: 'Hitos de progreso',
+    progressMilestonesDescription: 'Notificar al 25%, 50% y 75%',
+    printerOffline: 'Impresora desconectada',
+    printerError: 'Error de la impresora',
+    lowFilamentLabel: 'Filamento bajo',
+    maintenanceDue: 'Mantenimiento pendiente',
+    maintenanceDueDescription: 'Notificar cuando se necesite mantenimiento',
+    amsHumidityHigh: 'Humedad alta del AMS',
+    amsHumidityHighDescription: 'La humedad del AMS normal supera el umbral',
+    amsTemperatureHigh: 'Temperatura alta del AMS',
+    amsTemperatureHighDescription: 'La temperatura del AMS normal supera el umbral',
+    amsHtHumidityHigh: 'Humedad alta del AMS-HT',
+    amsHtHumidityHighDescription: 'La humedad del AMS-HT supera el umbral',
+    amsHtTemperatureHigh: 'Temperatura alta del AMS-HT',
+    amsHtTemperatureHighDescription: 'La temperatura del AMS-HT supera el umbral',
+    // Inventory stock alert events
+    inventoryAlerts: 'Alertas de inventario',
+    stockReorderAlert: 'Alerta de reabastecimiento',
+    stockReorderAlertDescription: 'El SKU ha alcanzado su punto de reabastecimiento',
+    stockBreakAlert: 'Alerta de rotura de existencias',
+    stockBreakAlertDescription: 'Las existencias se agotarán antes de que llegue el reabastecimiento',
+    // Queue events
+    jobAdded: 'Trabajo añadido',
+    jobAddedDescription: 'Trabajo añadido a la cola',
+    jobAssigned: 'Trabajo asignado',
+    jobAssignedDescription: 'Trabajo basado en el modelo asignado a una impresora',
+    jobStarted: 'Trabajo iniciado',
+    jobStartedDescription: 'El trabajo de la cola empezó a imprimirse',
+    jobWaiting: 'Trabajo en espera',
+    jobWaitingDescription: 'Trabajo a la espera de filamento o impresora',
+    jobSkipped: 'Trabajo omitido',
+    jobSkippedDescription: 'Trabajo omitido (el anterior falló)',
+    jobFailed: 'Trabajo fallido',
+    jobFailedDescription: 'El trabajo no pudo iniciarse',
+    queueComplete: 'Cola completada',
+    queueCompleteDescription: 'Todos los trabajos de la cola han terminado',
+    // Quiet hours
+    quietHours: 'Horas de silencio',
+    noNotificationsDuring: 'No hay notificaciones durante estas horas',
+    editProviderToChangeQuietHours: 'Edite el proveedor para cambiar las horas de silencio',
+    // Daily digest
+    dailyDigest: 'Resumen diario',
+    batchNotifications: 'Agrupar las notificaciones en un único resumen diario',
+    sendAt: 'Enviar a las {{time}}',
+    editProviderToChangeDigestTime: 'Edite el proveedor para cambiar la hora del resumen',
+    // Actions
+    edit: 'Editar',
+    deleteProvider: 'Eliminar proveedor de notificaciones',
+    deleteConfirm: '¿Está seguro de que desea eliminar "{{name}}"? Esto no se puede deshacer.',
+    delete: 'Eliminar',
+    // AddNotificationModal
+    addTitle: 'Añadir proveedor de notificaciones',
+    editTitle: 'Editar proveedor de notificaciones',
+    nameLabel: 'Nombre *',
+    namePlaceholder: 'Mis notificaciones',
+    providerTypeLabel: 'Tipo de proveedor *',
+    configuration: 'Configuración',
+    testConfiguration: 'Probar configuración',
+    printerFilter: 'Filtro de impresora',
+    onlyFromPrinter: 'Enviar notificaciones solo para los eventos de esta impresora',
+    quietHoursDnd: 'Horas de silencio (no molestar)',
+    quietStart: 'Inicio',
+    quietEnd: 'Fin',
+    dailyDigestLabel: 'Resumen diario',
+    sendDigestAt: 'Enviar el resumen a las',
+    digestCollected: 'Los eventos se recopilarán y se enviarán como un único resumen a esta hora',
+    notificationEvents: 'Eventos de notificación',
+    progressPercent: '(25%, 50%, 75%)',
+    bedCooledAfterPrint: '(después de completar la impresión)',
+    // Per-event ntfy priority (#990)
+    eventPriority: {
+      sectionTitle: 'Prioridad de ntfy',
+      helpNtfy: 'Elija una prioridad para cada evento activado. ntfy las usa para escalar las alertas (sonido, visibilidad, comportamiento de las notificaciones push). Los niveles no establecidos aquí usan el valor predeterminado del servidor ntfy.',
+      min: 'Mínima',
+      low: 'Baja',
+      default: 'Predeterminada',
+      high: 'Alta',
+      urgent: 'Urgente',
+    },
+    cancel: 'Cancelar',
+    save: 'Guardar',
+    add: 'Añadir',
+    nameRequired: 'El nombre es obligatorio',
+    fieldRequired: '{{field}} es obligatorio',
+    // Config field labels
+    phoneNumber: 'Número de teléfono',
+    apiKey: 'Clave API',
+    serverUrl: 'URL del servidor',
+    topic: 'Tema',
+    authToken: 'Token de autenticación',
+    userKey: 'Clave de usuario',
+    appToken: 'Token de la aplicación',
+    priority: 'Prioridad',
+    botToken: 'Token del bot',
+    chatId: 'ID del chat',
+    smtpServer: 'Servidor SMTP',
+    smtpPort: 'Puerto SMTP',
+    security: 'Seguridad',
+    authentication: 'Autenticación',
+    username: 'Nombre de usuario',
+    password: 'Contraseña',
+    fromEmail: 'Correo del remitente',
+    toEmail: 'Correo del destinatario',
+    webhookUrl: 'URL del webhook',
+    payloadFormat: 'Formato de la carga útil',
+    authorization: 'Autorización',
+    titleFieldName: 'Nombre del campo del título',
+    messageFieldName: 'Nombre del campo del mensaje',
+    // NotificationTemplateEditor
+    editTemplate: 'Editar plantilla: {{name}}',
+    titleLabel: 'Título',
+    bodyLabel: 'Cuerpo',
+    titlePlaceholder: 'Título de la notificación...',
+    bodyPlaceholder: 'Cuerpo de la notificación...',
+    availableVariables: 'Variables disponibles',
+    clickToInsert: 'Haga clic para insertar en la posición del cursor en el cuerpo',
+    livePreview: 'Vista previa en directo',
+    hide: 'Ocultar',
+    show: 'Mostrar',
+    loadingPreview: 'Cargando la vista previa...',
+    enterTemplateContent: 'Introduzca el contenido de la plantilla para ver la vista previa',
+    titlePreview: 'Título:',
+    bodyPreview: 'Cuerpo:',
+    resetToDefault: 'Restablecer al valor predeterminado',
+    titleRequired: 'El título es obligatorio',
+    bodyRequired: 'El cuerpo es obligatorio',
+    // NotificationLogViewer
+    notificationLog: 'Registro de notificaciones',
+    showFailedOnly: 'Solo fallidas',
+    last24Hours: 'Últimas 24 horas',
+    last7Days: 'Últimos 7 días',
+    last30Days: 'Últimos 30 días',
+    last90Days: 'Últimos 90 días',
+    justNow: 'Ahora mismo',
+    noFailedNotifications: 'No hay notificaciones fallidas',
+    noNotificationsLogged: 'No hay notificaciones registradas',
+    unknownProvider: 'Proveedor desconocido',
+    logTitle: 'Título',
+    logMessage: 'Mensaje',
+    logError: 'Error',
+    logProvider: 'Proveedor: {{type}}',
+    logTime: 'Hora: {{time}}',
+    refresh: 'Actualizar',
+    clearOld: 'Borrar antiguas',
+    statsSummary: 'Últimos {{days}} días:',
+    statsNotifications: 'notificaciones',
+    statsSent: '{{count}} enviadas',
+    statsFailed: '{{count}} fallidas',
+    // Event type labels (for log viewer)
+    eventTypes: {
+      print_start: 'Impresión iniciada',
+      print_complete: 'Impresión completada',
+      print_failed: 'Impresión fallida',
+      print_stopped: 'Impresión detenida',
+      print_progress: 'Progreso',
+      printer_offline: 'Impresora desconectada',
+      printer_error: 'Error de la impresora',
+      filament_low: 'Filamento bajo',
+      maintenance_due: 'Mantenimiento pendiente',
+      test: 'Prueba',
+    },
+    // User email notification preferences
+    userEmail: {
+      title: 'Notificaciones',
+      emailNotifications: 'Notificaciones por correo',
+      emailNotificationsDesc: 'Reciba notificaciones por correo de sus propios trabajos de impresión. Los correos se envían usando los ajustes de SMTP del sistema configurados en la autenticación avanzada.',
+      sendingTo: 'Las notificaciones se enviarán a',
+      noEmailWarning: 'Su cuenta no tiene una dirección de correo. Póngase en contacto con un administrador para añadir una.',
+      printJobNotifications: 'Notificaciones de trabajos de impresión',
+      printJobNotificationsDesc: 'Elija qué eventos activan las notificaciones por correo para los trabajos de impresión que envíe.',
+      printJobStarts: 'El trabajo de impresión comienza',
+      printJobStartsDesc: 'Reciba una notificación cuando su trabajo de impresión comience.',
+      printJobFinishes: 'El trabajo de impresión termina',
+      printJobFinishesDesc: 'Reciba una notificación cuando su trabajo de impresión se complete correctamente.',
+      printErrors: 'Errores de impresión',
+      printErrorsDesc: 'Reciba una notificación cuando su trabajo de impresión falle o encuentre un error.',
+      printJobStops: 'El trabajo de impresión se detiene',
+      printJobStopsDesc: 'Reciba una notificación cuando su trabajo de impresión se cancele o se detenga.',
+      saveSuccess: 'Preferencias de notificación guardadas.',
+      saveError: 'Error al guardar las preferencias de notificación.',
+    },
+  },
+
+  // Rich Text Editor
+  richTextEditor: {
+    bold: 'Negrita',
+    italic: 'Cursiva',
+    underline: 'Subrayado',
+    bulletList: 'Lista con viñetas',
+    numberedList: 'Lista numerada',
+    alignLeft: 'Alinear a la izquierda',
+    alignCenter: 'Centrar',
+    alignRight: 'Alinear a la derecha',
+    addLink: 'Añadir enlace',
+    removeLink: 'Quitar enlace',
+  },
+
+  // External Links
+  externalLinks: {
+    noLinksConfigured: 'No hay enlaces externos configurados',
+    deleteLink: 'Eliminar enlace',
+    removeCustomIcon: 'Quitar el icono personalizado',
+    openInNewTab: 'Abrir en una pestaña nueva',
+    placeholders: {
+      linkName: 'Mi enlace',
+    },
+  },
+
+  // Keyboard Shortcuts Modal
+  keyboardShortcuts: {
+    title: 'Atajos de teclado',
+    navigation: 'Navegación',
+    archivesSection: 'Archivos',
+    kProfilesSection: 'Perfiles K',
+    generalSection: 'General',
+    shortcuts: {
+      goToPrinters: 'Ir a Impresoras',
+      goToArchives: 'Ir a Archivos',
+      goToQueue: 'Ir a la cola',
+      goToStats: 'Ir a Estadísticas',
+      goToProfiles: 'Ir a Perfiles en la nube',
+      goToSettings: 'Ir a Ajustes',
+      focusSearch: 'Enfocar la búsqueda',
+      openUploadModal: 'Abrir la ventana de subida',
+      clearSelection: 'Borrar la selección / quitar el foco del campo',
+      contextMenu: 'Menú contextual en las tarjetas',
+      refreshProfiles: 'Actualizar perfiles',
+      newProfile: 'Nuevo perfil',
+      exitSelectionMode: 'Salir del modo de selección',
+      showHelp: 'Mostrar esta ayuda',
+    },
+    footer: 'Pulse Esc o haga clic fuera para cerrar',
+  },
+
+  // Notification Log
+  notificationLog: {
+    title: 'Registro de notificaciones',
+    events: {
+      printStarted: 'Impresión iniciada',
+      printComplete: 'Impresión completada',
+      printFailed: 'Impresión fallida',
+      printStopped: 'Impresión detenida',
+      progress: 'Progreso',
+      printerOffline: 'Impresora desconectada',
+      printerError: 'Error de la impresora',
+      lowFilament: 'Filamento bajo',
+      maintenanceDue: 'Mantenimiento pendiente',
+      test: 'Prueba',
+    },
+    timeAgo: {
+      justNow: 'Ahora mismo',
+      minutesAgo: 'hace {{minutes}} min',
+      hoursAgo: 'hace {{hours}} h',
+    },
+  },
+
+  // Restore/Backup Modal
+  restoreBackup: {
+    title: 'Restaurar copia de seguridad',
+    restoring: 'Restaurando...',
+    restoreComplete: 'Restauración completada',
+    restoreFailed: 'Error en la restauración',
+    importSettings: 'Importar ajustes desde un archivo de copia de seguridad',
+    pleaseWait: 'Espere mientras se restauran sus datos',
+    clickToSelect: 'Haga clic para seleccionar el archivo de copia de seguridad (.json o .zip)',
+    howDuplicateHandling: 'Cómo funciona la gestión de duplicados:',
+    categories: {
+      printers: 'Impresoras',
+      smartPlugs: 'Enchufes inteligentes',
+      notificationProviders: 'Proveedores de notificaciones',
+      filaments: 'Filamentos',
+      archives: 'Archivos',
+      pendingUploads: 'Subidas pendientes',
+      settingsTemplates: 'Ajustes y plantillas',
+    },
+    matchingInfo: {
+      printers: 'coincidencia por número de serie',
+      smartPlugs: 'coincidencia por dirección IP',
+      notificationProviders: 'coincidencia por nombre',
+      filaments: 'coincidencia por nombre + tipo + marca',
+      archives: 'coincidencia por hash del contenido',
+      pendingUploads: 'coincidencia por nombre de archivo',
+      settingsTemplates: 'siempre se sobrescriben',
+    },
+    replaceExisting: 'Reemplazar los datos existentes',
+    keepExisting: 'Conservar los datos existentes',
+    replaceDescription: 'Sobrescribir los elementos que ya existen con los datos de la copia de seguridad',
+    keepDescription: 'Restaurar solo los elementos que aún no existen',
+    caution: 'Precaución:',
+    cautionText: 'Sobrescribir reemplazará sus configuraciones actuales con los datos de la copia de seguridad. Los códigos de acceso de las impresoras nunca se sobrescriben por seguridad.',
+    itemsRestored: 'Elementos restaurados',
+    itemsSkipped: 'Elementos omitidos',
+    restored: 'Restaurados',
+    skipped: 'Omitidos (ya existen)',
+    filesLabel: 'Archivos (3MF, miniaturas, etc.)',
+    newApiKeysGenerated: 'Nuevas claves API generadas',
+    newApiKeysWarning: 'Estas claves solo se muestran una vez. ¡Cópielas ahora!',
+    processingBackup: 'Procesando el archivo de copia de seguridad...',
+    noDataFound: 'No se encontraron datos para restaurar en el archivo de copia de seguridad.',
+    failedToRestore: 'Error al restaurar la copia de seguridad. Compruebe el formato del archivo.',
+  },
+
+  // Backup Export Modal
+  backupExport: {
+    title: 'Exportar copia de seguridad',
+    selectData: 'Seleccione los datos que incluir',
+    selectAll: 'Seleccionar todo',
+    selectNone: 'No seleccionar nada',
+    categoryDescriptions: {
+      settings: 'Idioma, tema, preferencias de actualización',
+      notifications: 'ntfy, Pushover, Discord, etc.',
+      templates: 'Plantillas de mensajes personalizadas',
+      smartPlugs: 'Configuraciones de enchufes Tasmota',
+      externalLinks: 'Enlaces de la barra lateral a servicios externos',
+      printers: 'Información de las impresoras (códigos de acceso excluidos)',
+      plateDetection: 'Imágenes de referencia de la cama vacía',
+      filaments: 'Tipos y costes de filamento',
+      maintenance: 'Programaciones de mantenimiento personalizadas',
+      archives: 'Todos los datos de impresión + archivos (3MF, miniaturas, fotos)',
+      projects: 'Proyectos, elementos de la lista de materiales y adjuntos',
+      pendingUploads: 'Subidas de la impresora virtual pendientes de revisión',
+      apiKeys: 'Claves API de webhook (se generan claves nuevas al importar)',
+    },
+    requiresPrinters: 'Requiere que se seleccionen las impresoras',
+    zipFileWarning: 'Se creará un archivo ZIP.',
+    zipFileDescription: 'Incluye todos los archivos 3MF, las miniaturas, los time-lapses y las fotos. Esto puede tardar un rato y dar como resultado un archivo grande.',
+    includeAccessCodes: 'Incluir los códigos de acceso',
+    includeAccessCodesDescription: 'Para transferir a otra máquina',
+    includeAccessCodesWarning: 'Los códigos de acceso se incluirán en texto plano. ¡Mantenga este archivo de copia de seguridad seguro!',
+    categoriesSelected: '{{selectedCount}} categorías seleccionadas',
+  },
+
+  // Pending Uploads Panel
+  pendingUploads: {
+    placeholders: {
+      notes: 'Añada notas sobre esta impresión...',
+    },
+    discardUpload: 'Descartar subida',
+    archiveAllUploads: 'Archivar todas las subidas',
+    discardAllUploads: 'Descartar todas las subidas',
+    archive: 'Archivar',
+    timeAgo: {
+      justNow: 'Ahora mismo',
+      minutesAgo: 'hace {{minutes}} min',
+      hoursAgo: 'hace {{hours}} h',
+      daysAgo: 'hace {{days}} d',
+    },
+  },
+
+  // API Browser
+  apiBrowser: {
+    placeholders: {
+      requestBody: 'Cuerpo de la solicitud JSON...',
+      searchEndpoints: 'Buscar puntos de conexión...',
+    },
+  },
+
+  // Configure AMS Slot Modal
+  configureAmsSlot: {
+    title: 'Configurar la ranura del AMS',
+    slotConfigured: '¡Ranura configurada!',
+    configuringSlot: 'Configurando la ranura:',
+    slotLabel: '{{ams}} Ranura {{slot}}',
+    searchPresets: 'Buscar preajustes...',
+    colorPlaceholder: 'Nombre del color o hexadecimal (p. ej., marrón, FF8800)',
+    clearCustomColor: 'Borrar el color personalizado',
+    noCloudPresets: 'No hay preajustes de la nube. Inicie sesión en Bambu Cloud para sincronizar.',
+    noPresetsAvailable: 'No hay preajustes disponibles. Inicie sesión en Bambu Cloud o importe perfiles locales.',
+    noMatchingPresets: 'No se encontraron preajustes coincidentes.',
+    custom: 'Personalizado',
+    builtin: 'Integrado',
+    settingsSentToPrinter: 'Ajustes enviados a la impresora',
+    filamentProfile: 'Perfil de filamento',
+    kProfileLabel: 'Perfil K (avance de presión)',
+    filteringFor: 'Filtrando por: {{material}}',
+    noKProfile: 'Sin perfil K (usar el predeterminado 0,020)',
+    noMatchingKProfiles: 'No se encontraron perfiles K coincidentes. Se usará el K=0,020 predeterminado.',
+    selectFilamentFirst: 'Seleccione primero un perfil de filamento',
+    kFromCalibration: 'K={{value}} de la calibración de la impresora',
+    customColorLabel: 'Color personalizado (opcional)',
+    presetColors: 'Colores de {{name}}:',
+    showLessColors: 'Mostrar menos colores',
+    showMoreColors: 'Mostrar más colores',
+    clear: 'Borrar',
+    hexLabel: 'Hex: #{{hex}}',
+    resetting: 'Restableciendo...',
+    resetSlot: 'Restablecer ranura',
+    cancel: 'Cancelar',
+    configuring: 'Configurando...',
+    configureSlot: 'Configurar ranura',
+  },
+
+  // Git Backup Settings
+  githubBackup: {
+    title: 'Copia de seguridad en Git',
+    history: 'Historial',
+    downloadBackup: 'Descargar copia de seguridad',
+    restoreBackup: 'Restaurar copia de seguridad',
+    noBackupsYet: 'Aún no hay copias de seguridad',
+  },
+
+  // Email Settings
+  emailSettings: {
+    placeholders: {
+      fromName: 'BamBuddy',
+    },
+  },
+
+  // Tag Management Modal
+  tagManagement: {
+    searchTags: 'Buscar etiquetas...',
+    renameTag: 'Renombrar etiqueta',
+    deleteTag: 'Eliminar etiqueta',
+  },
+
+  // Notification Template Editor
+  notificationTemplates: {
+    placeholders: {
+      title: 'Título de la notificación...',
+      body: 'Cuerpo de la notificación...',
+    },
+  },
+
+  // Batch Tag Modal
+  batchTag: {
+    placeholders: {
+      newTag: 'Introduzca una nueva etiqueta...',
+    },
+  },
+
+  // Photo Gallery Modal
+  photoGallery: {
+    deletePhoto: 'Eliminar foto',
+  },
+
+  // Filament Hover Card
+  filamentHoverCard: {
+    copySpoolUuid: 'Copiar el UUID de la bobina',
+  },
+
+  // K Profiles View
+  kProfilesView: {
+    hasNote: 'Tiene nota',
+    copyProfile: 'Copiar perfil',
+  },
+
+  // Layout/Navigation
+  layout: {
+    openMenu: 'Abrir menú',
+    noPermissionSystemInfo: 'No tiene permiso para ver la información del sistema',
+  },
+
+  // Dashboard
+  dashboard: {
+    dragToReorder: 'Arrastre para reordenar',
+    hideWidget: 'Ocultar widget',
+  },
+
+  // Notification Provider Card
+  notificationProviderCard: {
+    deleteNotificationProvider: 'Eliminar proveedor de notificaciones',
+  },
+
+  // File Manager Modal
+  fileManagerModal: {
+    closeFileManager: 'Cerrar el gestor de archivos',
+    sortFiles: 'Ordenar archivos',
+    goToParentFolder: 'Ir a la carpeta superior',
+    threeView: 'Vista 3D',
+  },
+
+  // Embedded Camera Viewer
+  embeddedCameraViewer: {
+    refreshStream: 'Actualizar la transmisión',
+    close: 'Cerrar',
+    zoomOut: 'Alejar',
+    resetZoom: 'Restablecer el zoom',
+    zoomIn: 'Acercar',
+    dragToResize: 'Arrastre para redimensionar',
+  },
+
+  // Timelapse Viewer
+  timelapseViewer: {
+    skipBack5s: 'Retroceder 5 s',
+    skipForward5s: 'Avanzar 5 s',
+  },
+
+  // Notification Providers
+  notificationProviders: {
+    descriptions: {
+      email: 'Notificaciones por correo SMTP',
+      telegram: 'Notificaciones mediante un bot de Telegram',
+      discord: 'Enviar a un canal de Discord mediante un webhook',
+      ntfy: 'Notificaciones push gratuitas y autoalojables',
+      pushover: 'Notificaciones push sencillas y fiables',
+      callmebot: 'Notificaciones de WhatsApp gratuitas mediante CallMeBot',
+      webhook: 'POST HTTP genérico a cualquier URL',
+    },
+  },
+
+  // Log Viewer
+  logViewer: {
+    searchPlaceholder: 'Buscar mensaje o nombre del registrador...',
+    noLogEntries: 'No se encontraron entradas de registro',
+  },
+
+  // Switchbar Popover
+  switchbarPopover: {
+    noSwitchesInSwitchbar: 'No hay interruptores en la barra de interruptores',
+  },
+
+  // Project Page Modal
+  projectPageModal: {
+    placeholders: {
+      title: 'Título',
+      designer: 'Diseñador',
+      license: 'Licencia',
+      description: 'Introduzca una descripción...',
+      profileTitle: 'Título del perfil',
+      profileDescription: 'Descripción del perfil...',
+    },
+  },
+
+  // Spoolman Settings
+  spoolmanSettings: {},
+
+  // Time
+  time: {
+    unknown: '-',
+    waiting: 'En espera',
+    justNow: 'Ahora mismo',
+    now: 'Ahora',
+    minsAgo: 'hace {{count}} min',
+    inMins: 'en {{count}} min',
+    hoursAgo: 'hace {{count}} h',
+    inHours: 'en {{count}} h',
+    daysAgo: 'hace {{count}} d',
+    inDays: 'en {{count}} d',
+  },
+
+  // SpoolBuddy Kiosk
+  spoolbuddy: {
+    nav: {
+      dashboard: 'Panel',
+      ams: 'AMS',
+      inventory: 'Inventario',
+      writeTag: 'Escribir',
+      settings: 'Ajustes',
+    },
+    status: {
+      nfcReady: 'NFC listo',
+      nfcOff: 'NFC apagado',
+      offline: 'Desconectado',
+      online: 'En línea',
+      noPrinters: 'No hay impresoras',
+      deviceOffline: 'Dispositivo desconectado',
+      waitingConnection: 'Esperando la conexión del dispositivo...',
+      systemReady: 'Sistema listo',
+      status: 'Estado',
+    },
+    dashboard: {
+      readyToScan: 'Listo para escanear',
+      idleMessage: 'Coloque una bobina en la báscula para identificarla',
+      nfcHint: 'La etiqueta NFC se leerá automáticamente',
+      device: 'Dispositivo',
+      syncWeight: 'Sincronizar peso',
+      weightSynced: '¡Sincronizado!',
+      unknownTag: 'Etiqueta desconocida',
+      newTag: 'Nueva etiqueta detectada',
+      onScale: 'en la báscula',
+      linkSpool: 'Vincular a bobina',
+      linkTagTitle: 'Vincular la etiqueta a una bobina',
+      linkTag: 'Vincular etiqueta',
+      selectSpool: 'Seleccione una bobina a la que vincular esta etiqueta:',
+      noUntagged: 'No se encontraron bobinas sin etiquetas',
+      tagDetected: 'Etiqueta detectada',
+      noTag: 'Sin etiqueta',
+      tagId: 'Etiqueta',
+      grossWeight: 'Peso bruto',
+      spoolSize: 'Tamaño de la bobina',
+      close: 'Cerrar',
+      currentSpool: 'Bobina actual',
+      plateReady: 'Cama lista: {{name}}',
+      plateReadyLabel: 'Camas listas para despejar',
+      plateClearAction: 'Despejar',
+      plateClearedToast: 'Cama marcada como despejada',
+      plateClearFailed: 'No se pudo marcar la cama como despejada',
+    },
+    modal: {
+      spoolDetected: 'Bobina detectada',
+      assignToAms: 'Asignar al AMS',
+      syncWeight: 'Sincronizar peso',
+      weightSynced: '¡Sincronizado!',
+      syncing: 'Sincronizando...',
+      newTagDetected: 'Nueva etiqueta detectada',
+      addToInventory: 'Añadir al inventario',
+      assignToAmsTitle: 'Asignar al AMS',
+      selectSlot: 'Seleccione una ranura',
+      assign: 'Asignar',
+      assigning: 'Asignando...',
+      assignSuccess: '¡Asignada!',
+      assignPendingInsert: 'Asignada. La ranura se configurará cuando inserte la bobina.',
+      assignError: 'Error al asignar la bobina. Inténtelo de nuevo.',
+      noPrinterSelected: 'Seleccionar una impresora...',
+      noAmsDetected: 'No se detectó ningún AMS en esta impresora',
+      slot: 'Ranura',
+    },
+    weight: {
+      noReading: 'Sin lectura',
+      stable: 'Estable',
+      measuring: 'Midiendo...',
+      tare: 'Tara',
+      calibrate: 'Calibrar',
+    },
+    spool: {
+      remaining: 'Restante',
+      material: 'Material',
+      brand: 'Marca',
+      color: 'Color',
+      coreWeight: 'Núcleo',
+      labelWeight: 'Etiqueta',
+      scaleWeight: 'Báscula',
+      netWeight: 'Neto',
+      lastUsed: 'Usada por última vez',
+    },
+    ams: {
+      noData: 'No se detectó ningún AMS',
+      connectAms: 'Conecte un AMS para ver las ranuras de filamento',
+      noPrinter: 'No hay impresora seleccionada',
+      selectPrinter: 'Seleccione una impresora en la barra superior',
+      printerDisconnected: 'Impresora desconectada',
+      humidity: 'Humedad',
+      level: 'Nivel',
+      active: 'Activa',
+      slot: 'Ranura',
+      empty: 'Vacía',
+    },
+    inventory: {
+      search: 'Buscar bobinas...',
+      empty: 'No hay bobinas en el inventario',
+      noResults: 'No hay bobinas coincidentes',
+      spools: 'bobinas',
+      addSpool: 'Añadir bobina',
+    },
+    settings: {
+      // Tabs
+      tabDevice: 'Dispositivo',
+      tabDisplay: 'Pantalla',
+      tabScale: 'Báscula',
+      tabUpdates: 'Actualizaciones',
+      // Device tab
+      nfcReader: 'Lector NFC',
+      type: 'Tipo',
+      connection: 'Conexión',
+      notConnected: 'N/D',
+      deviceInfo: 'Información del dispositivo',
+      hostname: 'Host',
+      uptime: 'Tiempo de actividad',
+      systemConfig: 'Backend y autenticación',
+      backendUrl: 'URL del backend de Bambuddy',
+      apiToken: 'Token de API',
+      apiTokenPlaceholder: 'Introduzca el token de API',
+      saveConfig: 'Guardar configuración',
+      systemQueued: 'Configuración encolada.',
+      nfcDiagnostic: 'Diagnóstico de NFC',
+      scaleDiagnostic: 'Diagnóstico de la báscula',
+      readTagDiagnostic: 'Diagnóstico de lectura de etiqueta',
+      testNfc: 'Probar el lector',
+      testScale: 'Probar la precisión',
+      testReadTag: 'Leer etiqueta',
+      systemFieldsRequired: 'La URL del backend es obligatoria.',
+      // Display tab
+      brightness: 'Brillo',
+      saved: 'Guardado',
+      noBacklight: 'No se detectó retroiluminación DSI. El control del brillo requiere una pantalla DSI.',
+      screenBlank: 'Tiempo de espera para apagar la pantalla',
+      screenBlankDesc: 'La pantalla se apaga tras la inactividad. Toque para activarla.',
+      displayNote: 'El brillo se aplica como un filtro de software.',
+      // Scale tab
+      scaleCalibration: 'Calibración de la báscula',
+      currentWeight: 'Peso actual',
+      tareOffset: 'Tara',
+      calFactor: 'Factor',
+      knownWeight: 'Peso conocido',
+      calStep1: 'Retire todos los elementos de la báscula y pulse Poner a cero.',
+      calStep2: 'Coloque el peso conocido en la báscula.',
+      setZero: 'Poner a cero',
+      calibrateNow: 'Calibrar',
+      calibrated: 'Calibrada',
+      tareSet: 'Comando de tara enviado. Esperando al dispositivo...',
+      tareFailed: 'Error al enviar el comando de tara',
+      zeroSet: 'Punto cero establecido. Coloque el peso conocido en la báscula.',
+      calibrationDone: '¡Calibración completada!',
+      calibrationFailed: 'Error en la calibración',
+      lastCalibrated: 'Calibrada por última vez',
+      stable: 'Estable',
+      settling: 'Estabilizando...',
+      firmware: 'Firmware',
+      scale: 'Báscula',
+      noDevice: 'No se encontró ningún dispositivo SpoolBuddy',
+      // Updates tab
+      daemonVersion: 'Versión del demonio',
+      currentVersion: 'Actual',
+      versionPending: 'Esperando al demonio...',
+      checking: 'Comprobando...',
+      checkUpdates: 'Buscar actualizaciones',
+      updateAvailable: 'Actualización disponible',
+      updateInstructions: 'Actualice mediante SSH: ejecute el script de instalación de SpoolBuddy para actualizar.',
+      upToDate: 'Actualizado',
+      includeBeta: 'Incluir versiones beta',
+    },
+    writeTag: {
+      tabExisting: 'Bobina existente',
+      tabNew: 'Bobina nueva',
+      tabReplace: 'Reemplazar etiqueta',
+      searchPlaceholder: 'Buscar por material, color, marca...',
+      noUntaggedSpools: 'No hay bobinas sin etiquetas',
+      noTaggedSpools: 'No hay bobinas con etiquetas',
+      selectSpool: 'Seleccione una bobina y luego coloque una etiqueta NTAG en blanco en el lector',
+      placeTag: 'Coloque una etiqueta NTAG en el lector',
+      tagReady: 'Etiqueta detectada — lista para escribir',
+      writeTag: 'Escribir etiqueta',
+      replaceTag: 'Reemplazar etiqueta',
+      writing: 'Escribiendo la etiqueta...',
+      waiting: 'Esperando a SpoolBuddy...',
+      writeSuccess: '¡Etiqueta escrita correctamente!',
+      writeFailed: 'Error al escribir',
+      queueFailed: 'Error al encolar el comando de escritura',
+      tryAgain: 'Inténtelo de nuevo',
+      cancel: 'Cancelar',
+      replaceWarning: 'La etiqueta antigua se desvinculará. La nueva etiqueta la reemplazará.',
+      deviceOffline: 'SpoolBuddy está desconectado',
+      material: 'Material',
+      colorName: 'Nombre del color',
+      color: 'Color',
+      brand: 'Marca',
+      weight: 'Peso (g)',
+      createSpool: 'Crear bobina',
+      creating: 'Creando...',
+      spoolCreated: '¡Bobina creada! Lista para escribir.',
+      createFailed: 'Error al crear la bobina',
+      incompleteDataWarning: 'Etiqueta escrita con datos de Spoolman incompletos',
+    },
+    quickMenu: {
+      printerPower: 'Alimentación de la impresora',
+      systemControls: 'Sistema',
+      restartDaemon: 'Reiniciar demonio',
+      restartBrowser: 'Reiniciar navegador',
+      reboot: 'Reiniciar',
+      shutdown: 'Apagar',
+      swipeToClose: 'Deslice hacia abajo para cerrar',
+      confirmTitle: 'Confirmar',
+      confirmShutdown: '¿Está seguro de que desea apagar el SpoolBuddy? Necesitará acceso físico para volver a encenderlo.',
+      confirmReboot: '¿Está seguro de que desea reiniciar el SpoolBuddy?',
+      confirmRestartDaemon: '¿Reiniciar el demonio de SpoolBuddy? El NFC y la báscula no estarán disponibles temporalmente.',
+      confirmRestartBrowser: '¿Reiniciar el navegador del quiosco? La pantalla se quedará en negro brevemente.',
+      confirm: 'Confirmar',
+      confirmPlugOn: '¿Encender {{name}}?',
+      confirmPlugOff: '¿Apagar {{name}}?',
+      turnOn: 'Encender',
+      turnOff: 'Apagar',
+    },
+  },
+
+  diagnostic: {
+    modalTitle: 'Diagnóstico de conexión — {{name}}',
+    running: 'Ejecutando el diagnóstico...',
+    runFailed: 'No se pudo ejecutar el diagnóstico: {{error}}',
+    retry: 'Ejecutar de nuevo',
+    runButton: 'Ejecutar diagnóstico',
+    sectionTitle: 'Diagnóstico de conexión',
+    sectionDescription: 'Compruebe por qué una impresora no se conecta o no imprime — accesibilidad de los puertos, modo desarrollador LAN, modo de red de Docker y credenciales.',
+    noPrinters: 'No hay impresoras configuradas.',
+    overall: {
+      ok: 'No se encontraron problemas — la conexión de la impresora parece correcta.',
+      warnings: 'La impresora debería funcionar, pero algunas cosas necesitan atención.',
+      problems: 'Se encontraron problemas que explican por qué la impresora no se conecta o no imprime.',
+    },
+    check: {
+      port_mqtt: {
+        title: 'Puerto de control (MQTT 8883)',
+        pass: 'Accesible — la impresora acepta conexiones de control.',
+        fail: 'El puerto 8883 no es accesible. La impresora está apagada, en una dirección IP distinta, o un cortafuegos lo está bloqueando. Verifique la IP de la impresora y que nada bloquee el puerto 8883.',
+      },
+      port_ftps: {
+        title: 'Puerto de transferencia de archivos (FTPS 990)',
+        pass: 'Accesible — el envío de archivos de impresión funcionará.',
+        warn: 'El puerto 990 no es accesible. La supervisión puede seguir funcionando, pero el envío de impresiones a la impresora fallará. Asegúrese de que el puerto 990 no esté bloqueado.',
+      },
+      port_rtsps: {
+        title: 'Puerto de la cámara (RTSPS 322)',
+        pass: 'Accesible — la transmisión de la cámara funcionará.',
+        warn: 'El puerto 322 no es accesible. La vista de la cámara en directo no funcionará. Esto no afecta a la impresión.',
+      },
+      network_mode: {
+        title: 'Modo de red de Docker',
+        pass: 'Ejecutándose en modo de red de host.',
+        warn: 'Bambuddy se está ejecutando en red de tipo bridge de Docker. La detección de impresoras y la impresora virtual necesitan el modo de red de host — vuelva a crear el contenedor con "network_mode: host".',
+        skip: 'No se está ejecutando en Docker — no aplicable.',
+      },
+      subnet: {
+        title: 'Subred de la red',
+        pass: 'La impresora y Bambuddy están en la misma subred.',
+        warn: 'La impresora ({{printer_ip}}) y Bambuddy ({{host_ip}}) están en subredes distintas. Es posible que no se alcancen entre sí a menos que se configure el enrutamiento entre las subredes.',
+        skip: 'No se pudo determinar la subred — omitido.',
+      },
+      mqtt_auth: {
+        title: 'Credenciales de la impresora',
+        pass: 'La impresora aceptó la conexión.',
+        fail: 'La impresora es accesible pero rechazó la conexión. Lo más probable es que el código de acceso o el número de serie sean incorrectos. El código de acceso cambia cada vez que se conmuta el modo desarrollador — vuelva a copiarlo de la pantalla de la impresora.',
+        skip: 'No comprobado — no se pudo alcanzar la impresora.',
+      },
+      developer_mode: {
+        title: 'Modo desarrollador LAN',
+        pass: 'El modo desarrollador está activado.',
+        fail: 'El modo desarrollador está DESACTIVADO en la impresora. Actívelo en los ajustes de LAN de la impresora — y confirme con Aceptar. Sin él, las impresiones no comenzarán.',
+        skip: 'No se pudo comprobar — requiere una conexión activa con la impresora.',
+      },
+    },
+  },
+
+  bugReport: {
+    title: 'Informar de un error',
+    description: 'Descripción',
+    descriptionPlaceholder: '¿Qué salió mal? Describa el problema...',
+    email: 'Correo (opcional)',
+    emailPlaceholder: 'su@correo.com',
+    emailPrivacy: 'Si lo proporciona, su correo se incluirá en una sección plegada de la incidencia de GitHub para que el mantenedor pueda hacer un seguimiento.',
+    screenshot: 'Captura de pantalla',
+    uploadOrPaste: 'Suba, pegue o arrastre una imagen',
+    dataCollectedSummary: '¿Qué datos se incluyen en el informe?',
+    dataIncluded: 'Incluidos:',
+    dataIncludedList: 'Versión de la aplicación, sistema operativo, arquitectura, versión de Python, estadísticas de la base de datos (solo recuentos), modelos de impresora, número de boquillas, versiones de firmware, estado de la conectividad, estado de las integraciones (Spoolman, MQTT, HA), ajustes no sensibles, número de interfaces de red, detalles de Docker, versiones de las dependencias.',
+    dataNeverIncluded: 'Nunca incluidos:',
+    dataNeverIncludedList: 'Nombres de impresora, números de serie, códigos de acceso, contraseñas, direcciones IP, direcciones de correo, claves API, tokens, URL de webhook, nombres de host o nombres de usuario.',
+    submit: 'Enviar',
+    startLogging: 'Iniciar el registro de depuración',
+    stepEnableLogging: 'Registro de depuración activado',
+    stepReproduce: 'Reproduzca el problema ahora',
+    stepStopLogging: 'Detener y enviar el informe',
+    stopAndSubmit: 'Detener y enviar',
+    maxDuration: 'Se detiene automáticamente tras {{minutes}} min',
+    stoppingLogs: 'Recopilando registros y enviando...',
+    submitting: 'Enviando el informe de error...',
+    submitSuccess: '¡Informe de error enviado correctamente!',
+    submitFailed: 'Error al enviar el informe de error',
+    diagnosticChecking: 'Comprobando las conexiones de las impresoras...',
+    diagnosticHealthy: 'La comprobación de conexión se superó — no se encontraron problemas en sus impresoras.',
+    diagnosticHeading: 'Posible problema de configuración detectado',
+    diagnosticIntro: 'Una impresora tiene un problema de conexión que puede estar causando su problema. Compruebe la solución de abajo — resolverla podría solucionar el problema sin un informe de error. Aún puede enviar un informe a continuación.',
+    thankYou: '¡Gracias!',
+    submitted: 'Su informe de error se ha enviado.',
+    viewIssue: 'Ver incidencia',
+    unexpectedError: 'Se produjo un error inesperado',
+  },
+  failureDetection: {
+    title: 'Detección de fallos por IA',
+    description: 'Supervise las impresiones con una API de ML de Obico autoalojada y actúe automáticamente ante los fallos detectados.',
+    mlUrl: 'URL de la API de ML de Obico',
+    mlUrlHint: 'URL base de su contenedor ml_api de Obico autoalojado (p. ej. http://192.168.1.10:3333).',
+    test: 'Probar',
+    testSuccess: 'API de ML accesible y correcta.',
+    testFailed: 'No se pudo alcanzar la API de ML.',
+    sensitivity: 'Sensibilidad',
+    sensitivityLow: 'Baja (menos falsos positivos)',
+    sensitivityMedium: 'Media (equilibrada)',
+    sensitivityHigh: 'Alta (detección temprana, más falsos positivos)',
+    sensitivityHint: 'Ajusta los umbrales de confianza que activan las advertencias y los fallos.',
+    action: 'Acción ante un fallo detectado',
+    actionNotify: 'Solo notificar',
+    actionPause: 'Pausar la impresión',
+    actionPauseOff: 'Pausar y cortar la corriente',
+    pollInterval: 'Intervalo de sondeo (segundos)',
+    pollIntervalHint: 'Con qué frecuencia comprobar cada impresora mientras imprime. Mínimo 5 s, máximo 120 s.',
+    externalUrlMissing: 'La URL externa no está establecida.',
+    externalUrlHint: 'La API de ML obtiene la captura de la cámara por URL. Establezca la URL externa en los ajustes generales para que el contenedor de la API de ML pueda alcanzar Bambuddy.',
+    perPrinterTitle: 'Impresoras supervisadas',
+    perPrinterHint: 'Elija qué impresoras vigila el servicio de detección.',
+    monitorAll: 'Supervisar todas las impresoras conectadas',
+    statusTitle: 'Estado',
+    serviceRunning: 'Servicio en ejecución',
+    thresholds: 'Umbrales bajo / alto',
+    activePrinters: 'Impresiones activas',
+    noActivePrints: 'No hay impresiones en curso actualmente.',
+    historyTitle: 'Detecciones recientes',
+    noHistory: 'Aún no hay detecciones.',
+  },
+
+  makerworld: {
+    title: 'MakerWorld',
+    description: 'Pegue la URL de un modelo de MakerWorld para importarlo e imprimirlo directamente desde Bambuddy — sin tener que salir a la aplicación Bambu Handy.',
+    pasteUrlHeader: 'Importar desde MakerWorld',
+    pasteUrlPlaceholder: 'https://makerworld.com/en/models/… o pegue cualquier enlace de MakerWorld',
+    resolveButton: 'Resolver',
+    signInRequiredTitle: 'Se requiere iniciar sesión en Bambu Cloud para descargar',
+    signInRequiredBody: 'Puede explorar los detalles del modelo de forma anónima, pero MakerWorld requiere una cuenta de Bambu Cloud para descargar archivos 3MF.',
+    openCloudSettings: 'Abrir los ajustes de la nube',
+    untitledModel: 'Modelo sin título',
+    byCreator: 'de {{name}}',
+    downloadsCount: '{{count}} descargas',
+    licensePrefix: 'Licencia',
+    alreadyImported: 'Ya está en la biblioteca',
+    openOnMakerworld: 'Abrir en MakerWorld',
+    alreadyInLibrary: 'Este modelo ya está en su biblioteca — encuéntrelo en Gestor de archivos → MakerWorld',
+    importSuccess: '{{filename}} importado — guardado en Gestor de archivos → MakerWorld',
+    platesHeader: 'Camas ({{count}})',
+    plateDefaultName: 'Cama {{n}}',
+    materialCount: '{{count}} filamentos',
+    amsRequired: 'AMS necesario',
+    slicedFor: 'Laminado para {{printer}}',
+    alsoCompatible: 'También marcado como compatible: {{printers}}',
+    importToLibrary: 'Guardar',
+    sliceIn: 'Guardar y laminar en {{slicer}}',
+    disclaimer: 'La integración con MakerWorld usa puntos de conexión de API documentados por la comunidad. Bambuddy no está afiliado a MakerWorld ni a Bambu Lab ni cuenta con su respaldo.',
+    lastImportSuccess: 'Importado a su biblioteca',
+    lastImportAlreadyInLibrary: 'Ya está en su biblioteca',
+    viewInLibrary: 'Ver en el gestor de archivos',
+    openInBambuStudio: 'Abrir en Bambu Studio',
+    openInOrcaSlicer: 'Abrir en OrcaSlicer',
+    importTo: 'Importar al gestor de archivos',
+    recentImportsHeader: 'Importaciones recientes',
+    phaseResolving: 'Resolviendo',
+    phaseDownloading: 'Descargando',
+    folderAuto: 'MakerWorld (predeterminada)',
+    importAll: 'Importar todo',
+    importAllProgress: 'Importando {{current}}/{{total}}',
+    openGallery: 'Abrir la galería de imágenes',
+    galleryPrev: 'Imagen anterior',
+    galleryNext: 'Imagen siguiente',
+    deleteImport: 'Quitar de la biblioteca',
+    importDeleting: 'Quitando…',
+    importDeleted: 'Quitado de la biblioteca',
+    confirmDelete: '¿Quitar {{filename}} de la biblioteca? Esto elimina el archivo local, pero la cama se puede volver a importar desde MakerWorld.',
+    errors: {
+      resolveFailed: 'No se pudo resolver esa URL de MakerWorld.',
+      downloadFailed: 'Error en la descarga. Inténtelo de nuevo.',
+      deleteFailed: 'No se pudo quitar el archivo de la biblioteca.',
+    },
+  },
+  gcodeViewer: {
+    back: 'Atrás',
+    backToArchives: 'Volver a los archivos de impresión',
+    backToFiles: 'Volver al gestor de archivos',
+  },
+  libraryTrash: {
+    title: 'Papelera',
+    headerButton: 'Papelera',
+    headerTooltip: 'Ver los archivos movidos a la papelera',
+    backToFiles: 'Volver al gestor de archivos',
+    subtitleAdmin: 'Los archivos eliminados permanecen aquí durante {{days}} días y luego se eliminan automáticamente. Esta vista muestra los archivos en la papelera de todos los usuarios.',
+    subtitleUser: 'Los archivos eliminados permanecen aquí durante {{days}} días y luego se eliminan automáticamente.',
+    loading: 'Cargando la papelera…',
+    loadError: 'No se pudo cargar la papelera.',
+    empty: 'La papelera está vacía.',
+    summary: '{{count}} archivos · {{size}}',
+    emptyTrash: 'Vaciar la papelera',
+    restore: 'Restaurar',
+    purgeNow: 'Eliminar ahora',
+    autoPurgeIn: 'Se elimina automáticamente en {{when}}',
+    days: 'días',
+    retentionLabel: 'Eliminar automáticamente tras',
+    selectAll: 'Seleccionar todo',
+    selectOne: 'Seleccionar {{filename}}',
+    selectionCount: '{{count}} seleccionados',
+    bulkRestore: 'Restaurar los seleccionados',
+    bulkPurge: 'Eliminar los seleccionados',
+    col: {
+      filename: 'Archivo',
+      folder: 'Carpeta',
+      size: 'Tamaño',
+      deleted: 'Movido a la papelera',
+      autoPurge: 'Se elimina automáticamente',
+      owner: 'Propietario',
+      actions: 'Acciones',
+    },
+    confirm: {
+      purgeTitle: '¿Eliminar permanentemente?',
+      purgeBody: '{{filename}} se eliminará del disco y no se podrá restaurar.',
+      emptyTitle: '¿Vaciar la papelera?',
+      emptyBody: 'Los {{count}} archivos se eliminarán del disco. Esto no se puede deshacer.',
+      bulkPurgeTitle: '¿Eliminar permanentemente los archivos seleccionados?',
+      bulkPurgeBody: 'Los {{count}} archivos seleccionados se eliminarán del disco y no se podrán restaurar.',
+      cta: 'Eliminar permanentemente',
+    },
+    toast: {
+      restored: 'Archivo restaurado.',
+      restoreFailed: 'No se pudo restaurar el archivo.',
+      purged: 'Archivo eliminado permanentemente.',
+      purgeFailed: 'No se pudo eliminar el archivo.',
+      emptied: 'Se eliminaron {{count}} archivo(s) de la papelera.',
+      emptyFailed: 'No se pudo vaciar la papelera.',
+      retentionSaved: 'Eliminación automática establecida en {{days}} días.',
+      retentionFailed: 'No se pudo guardar el ajuste de retención.',
+      bulkRestored: 'Se restauraron {{count}} archivo(s).',
+      bulkPurged: 'Se eliminaron {{count}} archivo(s).',
+    },
+  },
+  libraryPurge: {
+    title: 'Purgar archivos antiguos',
+    headerButton: 'Purgar antiguos',
+    headerTooltip: 'Mover en bloque los archivos antiguos a la papelera',
+    description: 'Limpie de un solo paso los archivos antiguos de su biblioteca. Los archivos con un historial de impresión envejecen según su fecha de última impresión; los archivos que nunca se imprimieron envejecen según su fecha de subida.',
+    ageLabel: 'Mover los archivos de más de',
+    days: 'días',
+    includeNeverPrinted: 'Incluir los archivos que nunca se han impreso',
+    effectsTitle: 'Qué ocurre cuando hace clic en Purgar',
+    effect1: 'Los archivos coincidentes se mueven a la papelera — aún no se eliminan del disco.',
+    effect2: 'Puede restaurarlos de la papelera en cualquier momento hasta que expire el periodo de retención.',
+    effect3: 'Tras la retención, el barrido de la papelera los elimina permanentemente del disco.',
+    effect4: 'Los archivos de carpetas externas (vinculadas) se omiten — Bambuddy nunca elimina bytes que no le pertenecen.',
+    previewLoading: 'Comprobando cuántos archivos coinciden…',
+    previewFailed: 'No se pudo previsualizar la purga.',
+    previewSummary: '{{count}} archivos · {{size}} se moverían a la papelera',
+    andMore: '…y {{count}} más',
+    warning: 'Los archivos en la papelera siguen contando para el almacenamiento hasta que expire el periodo de retención. Vacíe la papelera después para liberar el disco de inmediato.',
+    confirmCta: 'Mover {{count}} a la papelera',
+    purging: 'Moviendo a la papelera…',
+    toast: {
+      success: 'Se movieron {{count}} archivo(s) a la papelera.',
+      failed: 'No se pudieron purgar los archivos.',
+    },
+  },
+  libraryAutoPurge: {
+    enableLabel: 'Purgar automáticamente los archivos antiguos',
+    enableDescription: 'Ejecuta la purga de administración una vez al día. Los archivos van primero a la papelera — no se eliminan de inmediato.',
+    ageLabel: 'Purgar automáticamente los archivos de más de',
+    ageDescription: 'Mínimo 7 días, máximo 10 años. Usa la misma regla de antigüedad que el botón de purga manual.',
+    days: 'días',
+    includeNeverPrinted: 'Incluir los archivos que nunca se han impreso',
+    saveFailed: 'No se pudieron guardar los ajustes de purga automática.',
+  },
+  archivePurge: {
+    headerButton: 'Purgar antiguos',
+    headerTooltip: 'Eliminar en bloque los archivos antiguos',
+    title: 'Purgar archivos antiguos',
+    description: 'Limpie el historial de impresión antiguo. Cada archivo envejece según la finalización de su impresión más reciente — reimprimir un archivo actualiza su antigüedad, por lo que el trabajo activo nunca se purga.',
+    ageLabel: 'Eliminar los archivos no impresos en los últimos',
+    days: 'días',
+    effectsTitle: 'Qué ocurre cuando hace clic en Purgar',
+    effect1: 'Cada archivo coincidente se oculta de los listados y sus archivos se eliminan del disco (3MF, miniatura, time-lapse, 3MF de origen, archivo de diseño F3D, fotos).',
+    effect2: 'La fila del archivo permanece en la base de datos para que las estadísticas rápidas conserven la contribución de filamento, tiempo, coste y energía — igual que el comportamiento predeterminado de la eliminación de un solo archivo.',
+    effect3: 'Marque «Eliminar también de las estadísticas» abajo para descartar también la contribución a las estadísticas rápidas (coincide con la opción de eliminación de un solo archivo). Esa vía es irreversible.',
+    effect4: 'Reimprimir un archivo actualiza su reloj de antigüedad, por lo que los archivos que aún usa están a salvo.',
+    purgeStatsLabel: 'Eliminar también de las estadísticas',
+    purgeStatsHint: 'Descarta los archivos coincidentes de las estadísticas rápidas (filamento, tiempo, coste, energía). Sin esto, las estadísticas rápidas conservan todas las contribuciones y solo los archivos abandonan el disco.',
+    previewLoading: 'Comprobando cuántos archivos coinciden…',
+    previewFailed: 'No se pudo previsualizar la purga.',
+    previewSummary: '{{count}} archivos · {{size}} se eliminarían',
+    andMore: '…y {{count}} más',
+    warning: 'Los archivos se eliminan del disco y no se pueden restaurar. Descargue o marque como favorito todo lo que desee conservar antes de continuar.',
+    confirmCta: 'Eliminar {{count}} archivo(s)',
+    purging: 'Eliminando…',
+    toast: {
+      success: 'Se eliminaron {{count}} archivo(s).',
+      failed: 'No se pudieron purgar los archivos.',
+    },
+  },
+  archiveAutoPurge: {
+    enableLabel: 'Purgar automáticamente los archivos antiguos',
+    enableDescription: 'Una vez al día, oculta los archivos de los listados y elimina sus archivos del disco cuando no se han impreso dentro del umbral. Reimprimir un archivo reinicia el reloj.',
+    ageLabel: 'Eliminar automáticamente los archivos no impresos en los últimos',
+    ageDescription: 'Mínimo 7 días, máximo 10 años. Se basa en la finalización de la impresión más reciente — reimprimir un archivo actualiza su antigüedad. Elimina el 3MF, la miniatura, el time-lapse, el 3MF de origen, el F3D y las fotos.',
+    days: 'días',
+    purgeStatsLabel: 'Eliminar también de las estadísticas',
+    purgeStatsDescription: 'Cuando está activado, el barrido diario también descarta cada archivo purgado de las estadísticas rápidas (filamento, tiempo, coste, energía). Desactivado de forma predeterminada — las estadísticas rápidas conservan la contribución, solo los archivos abandonan el disco.',
+    runNow: 'Purgar los archivos ahora',
+    saveFailed: 'No se pudieron guardar los ajustes de purga automática.',
+  },
+  cameraTokens: {
+    title: 'Tokens de API de la cámara',
+    navTitle: 'Tokens de API de la cámara',
+    description:
+      'Tokens de larga duración para incrustar la transmisión de la cámara en Home Assistant, Frigate, quioscos o cualquier otra herramienta que necesite una URL estable. Cada token es exclusivo de la transmisión de la cámara y se puede revocar en cualquier momento.',
+    loading: 'Cargando…',
+    confirmRevoke: {
+      title: '¿Revocar este token?',
+      body: 'Cualquier dispositivo que use "{{name}}" perderá el acceso de inmediato. Esto no se puede deshacer.',
+      cancel: 'Cancelar',
+      confirm: 'Revocar',
+    },
+    create: {
+      title: 'Crear nuevo token',
+      nameLabel: 'Nombre del token',
+      namePlaceholder: 'p. ej. Home Assistant',
+      daysLabel: 'Días hasta la caducidad',
+      submit: 'Crear',
+      hint:
+        'La vida útil máxima es de 365 días. El valor del token se muestra solo una vez al crearlo — cópielo ahora.',
+    },
+    created: {
+      title: 'Token creado — cópielo ahora',
+      warning:
+        'Esta es la única vez que este token estará visible. Después de cerrar este diálogo no podrá volver a verlo nunca.',
+      copy: 'Copiar',
+      dismiss: 'Lo he guardado',
+    },
+    list: {
+      myTitle: 'Mis tokens',
+      allTitle: 'Todos los usuarios (vista de administración)',
+      empty: 'Aún no hay tokens.',
+      name: 'Nombre',
+      owner: 'Propietario',
+      prefix: 'Prefijo',
+      created: 'Creado',
+      expires: 'Caduca',
+      lastUsed: 'Usado por última vez',
+      revoke: 'Revocar',
+      expired: 'Caducado',
+    },
+    toast: {
+      created: 'Token creado',
+      createFailed: 'Error al crear el token',
+      revoked: 'Token revocado',
+      revokeFailed: 'Error al revocar el token',
+      loadFailed: 'Error al cargar los tokens',
+      copied: 'Copiado al portapapeles',
+      copyFailed: 'Error al copiar — selecciónelo y cópielo manualmente',
+    },
+  },
+  // Forecast & Inventory Intelligence
+  forecast: {
+    title: 'Previsión',
+    noSpools: 'No se encontraron bobinas activas. Añada bobinas a su inventario para ver los datos de previsión.',
+    noUsageData: 'No hay datos de uso disponibles — no se puede proyectar la cronología de existencias.',
+    // Table headers
+    sku: 'SKU',
+    material: 'Material',
+    stock: 'Existencias',
+    dailyRate: 'Tasa',
+    daysLeft: 'Días restantes',
+    emptyBy: 'Se agota el',
+    reorderBy: 'Reabastecer antes del',
+    actions: 'Acciones',
+    // Rate tier badges
+    trend: 'Tendencia',
+    estimated: 'Est.',
+    noData: 'Sin datos',
+    // Timeframe
+    timeframe: 'Periodo',
+    // Chart
+    chartTitle: 'Existencias previstas — Los 5 materiales principales',
+    dashedLinesROP: 'Líneas discontinuas = puntos de reabastecimiento',
+    stockLevel: 'Nivel de existencias',
+    reorderPoint: 'Punto de reabastecimiento',
+    safetyMargin: 'Margen de seguridad',
+    // Legend labels
+    trendLegend: 'Tendencia (basada en el historial, nivel de servicio del 95%)',
+    estimatedLegend: 'Estimada (diferencia de peso)',
+    noDataLegend: 'Sin datos',
+    ropLabel: 'PR',
+    ssLabel: 'ES',
+    safetyStockLegend: 'Existencias de seguridad',
+    stockArrivalLegend: 'Llegada de existencias',
+    stockoutLegend: 'Rotura de existencias',
+    // Alerts toolbar
+    alertCount_one: '{{count}} alerta',
+    alertCount_other: '{{count}} alertas',
+    order: 'Pedir',
+    // Settings
+    save: 'Guardar',
+    cancel: 'Cancelar',
+    settingsSaved: 'Ajustes guardados',
+    failedSaveSettings: 'Error al guardar los ajustes',
+    globalLeadTimeSaved: 'Plazo de entrega global guardado',
+    globalLeadTime: 'Plazo de entrega global',
+    globalLeadTimeHint: 'Mínimo del plazo de entrega global — se usa en el cálculo del punto de reabastecimiento para todos los SKU',
+    skuLeadTimeOverride: 'Anulación del plazo de entrega del SKU',
+    skuLeadTimeHint: '0 = usar el plazo de entrega global. Establezca >0 para anularlo para este SKU.',
+    safetyMarginLabel: 'Margen de seguridad',
+    effectiveLeadTime: 'Plazo de entrega efectivo',
+    effectiveLeadTimeHint: 'máx(global {{global}} d, SKU {{sku}} d)',
+    reorderPointHint: 'd̄ × PE + margen de seguridad — pida cuando las existencias alcancen este nivel',
+    safetyMarginHint: 'Existencias de seguridad estadísticas (z=1,65 × σ × √PE) + margen definido por el usuario',
+    safetyMarginHintDays: 'Margen añadido sobre las existencias de seguridad estadísticas.{{approx}}',
+    safetyMarginHintDaysApprox: ' ≈ {{g}} g a la tasa actual.',
+    safetyMarginHintG: 'Margen de peso fijo añadido sobre las existencias de seguridad estadísticas.{{approx}}',
+    safetyMarginHintGApprox: ' ≈ {{days}} d a la tasa actual.',
+    individualSpools: 'Bobinas individuales',
+    labelWeight: 'Etiqueta',
+    spoolCount_one: '{{count}} bobina',
+    spoolCount_other: '{{count}} bobinas',
+    // Alerts
+    stockBreakRisk: 'Riesgo de rotura de existencias',
+    stockBreakDetail: 'Quedan {{days}} d, plazo de entrega {{lt}} d.',
+    stockBreakBefore: 'Rotura de existencias antes del reabastecimiento',
+    reorderNow: 'Reabastecer ahora',
+    reorderTriggerPassed: 'La fecha de activación {{date}} ha pasado.',
+    // Shopping list
+    shoppingList: 'Lista de la compra',
+    shoppingListItems_one: '({{count}} artículo)',
+    shoppingListItems_other: '({{count}} artículos)',
+    shoppingListEmpty: 'La lista de la compra está vacía. Haga clic en el icono del carrito de cualquier fila para añadir artículos.',
+    addToCart: 'Añadir a la lista de la compra',
+    alertsSnoozed: 'Silenciar las alertas de este SKU',
+    alertsEnabled: 'Reactivar las alertas de este SKU',
+    addedToCart: 'Añadido a la lista de la compra',
+    failedAddItem: 'Error al añadir el artículo',
+    listView: 'Lista',
+    logisticsView: 'Logística',
+    qty: 'Cant.',
+    weight: 'Peso',
+    leadTime: 'Plazo de entrega',
+    expectedRestock: 'Reabastecimiento previsto',
+    status: 'Estado',
+    note: 'Nota',
+    pending: 'Pendiente',
+    purchased: 'Comprado',
+    received: 'Recibido',
+    markPurchased: 'Marcar como comprado',
+    markReceived: 'Marcar como recibido — añade bobinas al inventario de existencias',
+    resetToPending: 'Restablecer a pendiente',
+    remove: 'Quitar',
+    clearAll: 'Borrar todo',
+    downloadCsv: 'CSV',
+    // Add to cart modal
+    addToCartTitle: 'Añadir a la lista de la compra',
+    byQuantity: 'Por cantidad',
+    byDuration: 'Por duración',
+    numberOfSpools: 'Número de bobinas',
+    lastHowManyDays: '¿Cuántos días debería durar?',
+    noUsageQty: 'No hay datos de uso — cantidad establecida en 1.',
+    noteOptional: 'Nota (opcional)',
+    notePlaceholder: 'p. ej. para el proyecto X, urgente…',
+    addNSpools_one: 'Añadir {{count}} bobina',
+    addNSpools_other: 'Añadir {{count}} bobinas',
+    // Cart logistics
+    onArrival: 'A la llegada',
+    stockBreakIn: 'Rotura de existencias en {{days}} d.',
+    stockRunsOutBefore: 'Las existencias se agotan antes de que transcurra el plazo de entrega de {{lt}} d.',
+    atRate: 'A {{rate}} g/día necesita',
+    moreSpools_one: '{{count}} bobina más',
+    moreSpools_other: '{{count}} bobinas más',
+    bridgeGap: 'para cubrir el déficit.',
+    // Permissions
+    noReadAccess: 'No tiene permiso para ver las previsiones de inventario.',
+    noWriteAccess: 'No tiene permiso para modificar los ajustes de previsión.',
+  },
+};

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-Cv5ljbAV.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-lFgKj9FJ.js"></script>
+    <script type="module" crossorigin src="/assets/index-Cv5ljbAV.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-QwBJJHPy.css">
     <link rel="stylesheet" crossorigin href="/assets/index-QwBJJHPy.css">
   </head>
   </head>
   <body>
   <body>

Some files were not shown because too many files changed in this diff