diff --git a/frontend/src/components/common/LanguageToggle.tsx b/frontend/src/components/common/LanguageToggle.tsx
index 492d523..67c01ec 100644
--- a/frontend/src/components/common/LanguageToggle.tsx
+++ b/frontend/src/components/common/LanguageToggle.tsx
@@ -82,6 +82,12 @@ export function LanguageToggle({ iconOnly = false }: LanguageToggleProps) {
>
{t('common.bengali')}
+ setLanguage('es-ES')}
+ className={currentLang === 'es-ES' || currentLang.startsWith('es') ? 'bg-accent' : ''}
+ >
+ {t('common.spanish')}
+
)
diff --git a/frontend/src/lib/locales/bn-IN/index.ts b/frontend/src/lib/locales/bn-IN/index.ts
index 9cb4a50..506cf75 100644
--- a/frontend/src/lib/locales/bn-IN/index.ts
+++ b/frontend/src/lib/locales/bn-IN/index.ts
@@ -26,6 +26,7 @@ export const bnIN = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "উৎস",
notebook: "নোটবুক",
podcast: "পডকাস্ট",
diff --git a/frontend/src/lib/locales/en-US/index.ts b/frontend/src/lib/locales/en-US/index.ts
index bf9df0e..dc698aa 100644
--- a/frontend/src/lib/locales/en-US/index.ts
+++ b/frontend/src/lib/locales/en-US/index.ts
@@ -26,6 +26,7 @@ export const enUS = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "Source",
notebook: "Notebook",
podcast: "Podcast",
diff --git a/frontend/src/lib/locales/es-ES/index.ts b/frontend/src/lib/locales/es-ES/index.ts
new file mode 100644
index 0000000..5796a29
--- /dev/null
+++ b/frontend/src/lib/locales/es-ES/index.ts
@@ -0,0 +1,931 @@
+export const esES = {
+ common: {
+ search: "Buscar...",
+ create: "Nuevo",
+ new: "Nuevo",
+ cancel: "Cancelar",
+ delete: "Eliminar",
+ edit: "Editar",
+ theme: "Tema",
+ signOut: "Cerrar sesión",
+ noMatches: "No se encontraron coincidencias",
+ tryDifferentSearch: "Intenta usar un término de búsqueda diferente.",
+ light: "Claro",
+ dark: "Oscuro",
+ system: "Sistema",
+ loading: "Cargando...",
+ note: "Nota",
+ insight: "Análisis",
+ newSource: "Nueva fuente",
+ newNotebook: "Nuevo cuaderno",
+ newPodcast: "Nuevo podcast",
+ language: "Idioma",
+ english: "English",
+ chinese: "简体中文",
+ japanese: "日本語",
+ french: "Français",
+ russian: "Русский",
+ bengali: "বাংলা",
+ spanish: "Español",
+ source: "Fuente",
+ notebook: "Cuaderno",
+ podcast: "Podcast",
+ quickActions: "Acciones rápidas",
+ quickActionsDesc: "Navegación, búsqueda, preguntar, tema",
+ appName: "Open Notebook",
+ add: "Agregar",
+ remove: "Quitar",
+ confirm: "Confirmar",
+ warning: "Advertencia",
+ error: "Error",
+ success: "Éxito",
+ model: "Modelo",
+ back: "Atrás",
+ next: "Siguiente",
+ done: "Listo",
+ processing: "Procesando...",
+ creating: "Creando...",
+ linked: "Vinculado",
+ adding: "Agregando...",
+ addSelected: "Agregar seleccionados",
+ customModel: "Modelo personalizado",
+ failed: "fallido",
+ current: "Actual",
+ save: "Guardar",
+ writeNote: "Escribir nota",
+ batchMode: "Modo por lotes",
+ optional: "Opcional",
+ type: "Tipo",
+ title: "Título",
+ created: "Creado {time}",
+ updated: "Actualizado {time}",
+ actions: "Acciones",
+ noResults: "Sin resultados",
+ references: "Referencias",
+ refreshPage: "Por favor, intenta recargar la página",
+ refresh: "Recargar",
+ aiGenerated: "Generado por IA",
+ human: "Humano",
+ unknown: "Desconocido",
+ notes: "Notas",
+ chat: "Chat",
+ deleteForever: "Eliminar permanentemente",
+ connectionError: "Error de conexión",
+ unableToConnect: "No se puede conectar al servidor API",
+ retryConnection: "Reintentar conexión",
+ diagnosticInfo: "Información de diagnóstico",
+ version: "Versión",
+ built: "Compilación",
+ apiUrl: "URL de la API",
+ frontendUrl: "URL del frontend",
+ checkConsoleLogs: "Revisa la consola del navegador para logs detallados (busca mensajes 🔧 [Config])",
+ yes: "Sí",
+ no: "No",
+ saving: "Guardando...",
+ description: "Descripción",
+ saveToNote: "Guardar en nota",
+ copyToClipboard: "Copiar al portapapeles",
+ close: "Cerrar",
+ insights: "Análisis",
+ progress: "Progreso",
+ deleting: "Eliminando...",
+ created_label: "Creado",
+ updated_label: "Actualizado",
+ download: "Descargar",
+ saveChanges: "Guardar cambios",
+ name: "Nombre",
+ default: "Predeterminado",
+ nameRequired: "El nombre es obligatorio",
+ modelConfiguration: "Configuración del modelo",
+ resetToDefault: "Restablecer valores predeterminados",
+ reasoning: "Razonamiento",
+ searchTerms: "Términos de búsqueda",
+ strategy: "Estrategia",
+ individualAnswers: "Respuestas individuales ({count})",
+ finalAnswer: "Respuesta final",
+ notebookLabel: "Cuaderno: {name}",
+ itemNotFound: "No se pudo encontrar este {type}",
+ accessibility: {
+ transformationViews: "Vistas de transformación",
+ searchKB: "Pregunta o busca en tu base de conocimiento",
+ enterQuestion: "Escribe tu pregunta para consultar la base de conocimiento",
+ enterSearch: "Escribe tu búsqueda",
+ searchKBBtn: "Buscar en la base de conocimiento",
+ podcastViews: "Vistas de podcast",
+ ytVideo: "Video de YouTube",
+ askResponse: "Respuesta de consulta",
+ searchNotebooks: "Buscar cuadernos",
+ },
+ url: "URL",
+ errorDetails: "Detalles del error",
+ editTransformation: "Editar transformación",
+ retry: "Reintentar",
+ traditionalChinese: "繁體中文",
+ portuguese: "Português",
+ completed: "completado",
+ saveSuccess: "Guardado exitosamente",
+ contextModes: {
+ off: "No incluido en el chat",
+ insights: "Solo análisis",
+ full: "Contenido completo",
+ clickToCycle: "Clic para cambiar",
+ },
+ clickToEdit: "Clic para editar",
+ },
+ apiErrors: {
+ notebookNotFound: "Cuaderno no encontrado",
+ sourceNotFound: "Fuente no encontrada",
+ transformationNotFound: "Transformación no encontrada",
+ fileUploadFailed: "Error al subir el archivo",
+ urlRequired: "La URL es obligatoria para el tipo enlace",
+ contentRequired: "El contenido es obligatorio para el tipo texto",
+ invalidSourceType: "Tipo de fuente inválido",
+ processingFailed: "El procesamiento falló",
+ failedToQueue: "Error al poner en cola el procesamiento",
+ invalidSortBy: "El campo de ordenamiento debe ser 'created' o 'updated'",
+ invalidSortOrder: "El orden debe ser 'asc' o 'desc'",
+ accessDenied: "Acceso al archivo denegado",
+ fileNotFoundOnServer: "Archivo no encontrado en el servidor",
+ searchFailed: "La búsqueda falló",
+ askFailed: "La consulta falló",
+ pleaseEnterQuestion: "Por favor, escribe una pregunta",
+ pleaseConfigureModels: "Por favor, configura todos los modelos requeridos",
+ failedToCreateSession: "Error al crear la sesión",
+ failedToUpdateSession: "Error al actualizar la sesión",
+ failedToDeleteSession: "Error al eliminar la sesión",
+ failedToSendMessage: "Error al enviar el mensaje",
+ unauthorized: "Acceso no autorizado, por favor verifica tu contraseña",
+ invalidPassword: "Contraseña inválida",
+ embeddingModelRequired: "Esta función requiere un modelo de embedding. Por favor, configura uno en la sección de Modelos.",
+ strategyModelNotFound: "Modelo de estrategia no encontrado",
+ answerModelNotFound: "Modelo de respuesta no encontrado",
+ finalAnswerModelNotFound: "Modelo de respuesta final no encontrado",
+ noAnswerGenerated: "No se pudo generar una respuesta",
+ genericError: "Ocurrió un error inesperado",
+ },
+ connectionErrors: {
+ apiTitle: "No se puede conectar al servidor API",
+ apiDesc: "No se pudo alcanzar el servidor API de Open Notebook",
+ dbTitle: "Error de conexión a la base de datos",
+ dbDesc: "El servidor API está funcionando, pero la base de datos no es accesible",
+ troubleshooting: "Esto generalmente significa:",
+ apiUnreachable1: "El servidor API no está ejecutándose",
+ apiUnreachable2: "El servidor API está en una dirección diferente",
+ apiUnreachable3: "Problemas de conectividad de red",
+ dbFailed1: "SurrealDB no está ejecutándose",
+ dbFailed2: "La configuración de conexión a la base de datos es incorrecta",
+ dbFailed3: "Problemas de red entre la API y la base de datos",
+ quickFixes: "Soluciones rápidas:",
+ setApiUrl: "Establece la variable de entorno API_URL:",
+ checkSurreal: "Verifica si SurrealDB está funcionando:",
+ seeDocumentation: "Para instrucciones detalladas de configuración, consulta:",
+ docLink: "Documentación de Open Notebook",
+ showTechnical: "Mostrar detalles técnicos",
+ attemptedUrl: "URL intentada",
+ message: "Mensaje",
+ technicalDetails: "Detalles técnicos",
+ stackTrace: "Traza de error",
+ retryLabel: "Reintentar conexión",
+ retryHint: "Presiona R o haz clic en el botón para reintentar",
+ dockerLabel: "Para Docker",
+ localDevLabel: "Para desarrollo local",
+ },
+ auth: {
+ loginTitle: "Open Notebook",
+ loginDesc: "Ingresa tu contraseña para acceder a la aplicación",
+ passwordPlaceholder: "Contraseña",
+ signingIn: "Iniciando sesión...",
+ signIn: "Iniciar sesión",
+ connectErrorHint: "No se puede conectar al servidor. Por favor, verifica si la API está funcionando.",
+ },
+ navigation: {
+ collect: "Recopilar",
+ process: "Procesar",
+ create: "Crear",
+ manage: "Gestionar",
+ sources: "Fuentes",
+ notebooks: "Cuadernos",
+ askAndSearch: "Preguntar y buscar",
+ podcasts: "Podcasts",
+ models: "Modelos",
+ transformations: "Transformaciones",
+ transformation: "Transformación",
+ settings: "Configuración",
+ advanced: "Avanzado",
+ nav: "Navegación",
+ language: "Cambiar idioma",
+ theme: "Tema",
+ ask: "Preguntar",
+ },
+ notebooks: {
+ title: "Cuadernos",
+ newNotebook: "Nuevo cuaderno",
+ searchPlaceholder: "Buscar cuadernos...",
+ archived: "Archivado",
+ archive: "Archivar",
+ unarchive: "Desarchivar",
+ deleteNotebook: "Eliminar cuaderno",
+ deleteNotebookDesc: "¿Estás seguro de que quieres eliminar \"{name}\"? Esta acción no se puede deshacer.",
+ deleteNotebookLoading: "Cargando vista previa de eliminación...",
+ deleteNotebookNotes: "{count} nota(s) serán eliminadas permanentemente.",
+ deleteNotebookNoNotes: "No hay notas que eliminar.",
+ deleteNotebookExclusiveSources: "{count} fuente(s) existen solo en este cuaderno.",
+ deleteNotebookSharedSources: "{count} fuente(s) son compartidas con otros cuadernos y serán desvinculadas.",
+ deleteNotebookNoSources: "No hay fuentes en este cuaderno.",
+ deleteExclusiveSourcesLabel: "Eliminar fuentes exclusivas",
+ keepExclusiveSourcesLabel: "Desvincular y conservarlas",
+ activeNotebooks: "Cuadernos activos",
+ archivedNotebooks: "Cuadernos archivados",
+ notFound: "Cuaderno no encontrado",
+ notFoundDesc: "El cuaderno solicitado no existe.",
+ updated: "Actualizado",
+ namePlaceholder: "Nombre del cuaderno",
+ addDescription: "Agregar descripción...",
+ noNotesYet: "Aún no hay notas",
+ deleteNote: "Eliminar nota",
+ deleteNoteConfirm: "¿Estás seguro de que quieres eliminar esta nota? Esta acción no se puede deshacer.",
+ noteCreatedSuccess: "Nota creada exitosamente",
+ failedToCreateNote: "Error al crear la nota",
+ noteUpdatedSuccess: "Nota actualizada exitosamente",
+ failedToUpdateNote: "Error al actualizar la nota",
+ noteDeletedSuccess: "Nota eliminada exitosamente",
+ failedToDeleteNote: "Error al eliminar la nota",
+ createNew: "Crear nuevo cuaderno",
+ createNewDesc: "Ingresa un nombre y una descripción opcional para comenzar.",
+ descPlaceholder: "Agrega más información sobre este cuaderno aquí...",
+ createSuccess: "Cuaderno creado exitosamente",
+ updateSuccess: "Cuaderno actualizado exitosamente",
+ deleteSuccess: "Cuaderno eliminado exitosamente",
+ },
+ sources: {
+ title: "Fuentes",
+ add: "Agregar fuente",
+ addNew: "Agregar nueva fuente",
+ addExisting: "Agregar fuente existente",
+ delete: "Eliminar fuente",
+ statusPreparing: "Preparando",
+ statusQueued: "En cola",
+ statusProcessing: "Procesando",
+ statusCompleted: "Completado",
+ statusFailed: "Fallido",
+ statusPreparingDesc: "Preparando para procesar",
+ statusQueuedDesc: "Esperando ser procesado",
+ statusProcessingDesc: "Siendo procesado",
+ statusCompletedDesc: "Procesado exitosamente",
+ statusFailedDesc: "El procesamiento falló",
+ failedToLoad: "Error al cargar las fuentes",
+ allSourcesDesc: "Ve todas tus fuentes aquí. Puedes agregar nuevas fuentes o gestionar las existentes.",
+ allSources: "Todas las fuentes",
+ insights: "Análisis",
+ yes: "Sí",
+ no: "No",
+ loadingMore: "Cargando más...",
+ noSourcesYet: "Aún no hay fuentes",
+ allSourcesDescShort: "Ve todas tus fuentes aquí.",
+ cannotSaveNoteNoNotebook: "No se puede guardar la nota: ID de cuaderno no disponible",
+ createFirstSource: "Agrega tu primera fuente para comenzar a construir tu base de conocimiento.",
+ deleteSourceConfirm: "¿Estás seguro de que quieres eliminar esta fuente?",
+ deleteConfirm: "¿Estás seguro de que quieres eliminar esto?",
+ deleteConfirmWithTitle: "¿Estás seguro de que quieres eliminar \"{title}\"?",
+ deleteSuccess: "Fuente eliminada exitosamente. Nota: Para eliminar el archivo del almacenamiento, debes habilitar la opción \"eliminar archivo\" en la página de configuración.",
+ failedToDelete: "Error al eliminar la fuente",
+ sourceQueued: "Fuente en cola",
+ sourceQueuedDesc: "Fuente enviada para procesamiento en segundo plano. Puedes monitorear el progreso en la lista de fuentes.",
+ sourceAddedSuccess: "Fuente agregada exitosamente",
+ failedToAddSource: "Error al agregar la fuente",
+ sourceUpdatedSuccess: "Fuente actualizada exitosamente",
+ failedToUpdateSource: "Error al actualizar la fuente",
+ sourceDeletedSuccess: "Fuente eliminada exitosamente",
+ failedToDeleteSource: "Error al eliminar la fuente",
+ fileUploadedSuccess: "Archivo subido exitosamente",
+ failedToUploadFile: "Error al subir el archivo",
+ sourceRequeued: "Reintento de fuente en cola",
+ sourceRequeuedDesc: "La fuente ha sido puesta de nuevo en cola para procesamiento.",
+ failedToRetry: "Error en el reintento",
+ sourcesAddedToNotebook: "{count} fuente(s) agregadas al cuaderno",
+ failedToAddSourcesToNotebook: "Error al agregar fuentes al cuaderno",
+ partialAddSuccess: "{success} fuente(s) agregadas, {failed} fallaron",
+ sourceRemovedFromNotebook: "Fuente eliminada del cuaderno exitosamente",
+ failedToRemoveSourceFromNotebook: "Error al eliminar la fuente del cuaderno",
+ removeConfirm: "¿Estás seguro de que quieres quitar esto del cuaderno?",
+ checking: "Verificando...",
+ untitledSource: "Fuente sin título",
+ maxItems: "máx. {count}",
+ insightsCount: "{count} análisis",
+ details: "Detalles",
+ detailsTitle: "Detalles de la fuente",
+ content: "Contenido",
+ metadata: "Metadatos",
+ type: {
+ link: "Enlace",
+ file: "Archivo",
+ text: "Texto",
+ },
+ id: "ID de fuente",
+ topics: "Temas",
+ embedded: "Embebido",
+ notEmbedded: "No embebido",
+ embedContent: "Embeber contenido",
+ embedding: "Embebiendo...",
+ alreadyEmbedded: "Ya embebido",
+ downloadFile: "Descargar archivo",
+ fileUnavailable: "Archivo no disponible",
+ preparing: "Preparando...",
+ generateNewInsight: "Generar nuevo análisis",
+ selectTransformation: "Selecciona una transformación...",
+ noInsightsYet: "Aún no hay análisis",
+ createFirstInsight: "Crea tu primer análisis usando una transformación de arriba",
+ viewInsight: "Ver análisis",
+ deleteInsight: "Eliminar análisis",
+ deleteInsightConfirm: "¿Estás seguro de que quieres eliminar este análisis? Esta acción no se puede deshacer.",
+ insightGenerationStarted: "Generación de análisis iniciada. Aparecerá en breve.",
+ editNote: "Editar nota",
+ createNote: "Crear nota",
+ addTitle: "Agregar un título...",
+ untitledNote: "Nota sin título",
+ writeNotePlaceholder: "Escribe el contenido de tu nota aquí...",
+ saveNote: "Guardar nota",
+ createNoteBtn: "Crear nota",
+ createFirstNote: "Crea tu primera nota para capturar ideas y observaciones.",
+ urlLabel: "URL(s) *",
+ fileLabel: "Archivo(s) *",
+ textContentLabel: "Contenido de texto *",
+ enterUrlsPlaceholder: "Ingresa URLs, una por línea\nhttps://ejemplo.com/articulo1\nhttps://ejemplo.com/articulo2",
+ batchUrlHint: "Pega múltiples URLs (una por línea) para importar por lotes",
+ invalidUrlsDetected: "URLs inválidas detectadas:",
+ lineLabel: "Línea {line}",
+ fixInvalidUrls: "Por favor, corrige o elimina las URLs inválidas para continuar",
+ selectMultipleFilesHint: "Selecciona múltiples archivos para importar por lotes. Soportados: Documentos (PDF, DOC, DOCX, PPT, XLS, EPUB, TXT, MD), Multimedia (MP4, MP3, WAV, M4A), Imágenes (JPG, PNG), Archivos comprimidos (ZIP)",
+ selectedFiles: "Archivos seleccionados:",
+ textPlaceholder: "Pega o escribe tu contenido aquí...",
+ htmlDetected: "Contenido HTML detectado. Se convertirá a Markdown después del procesamiento.",
+ titlePlaceholder: "Dale a tu fuente un título descriptivo",
+ batchTitlesAuto: "Los títulos se generarán automáticamente para cada fuente.",
+ batchCommonSettings: "Los mismos cuadernos y transformaciones se aplicarán a todos los elementos.",
+ urlsCount: "{count} URL(s)",
+ filesCount: "{count} archivo(s)",
+ addSource: "Agregar fuente",
+ notEmbeddedAlert: "Contenido no embebido",
+ notEmbeddedDesc: "Este contenido no ha sido embebido para búsqueda vectorial. El embedding permite capacidades de búsqueda avanzada y mejor descubrimiento de contenido.",
+ openOnYoutube: "Abrir en YouTube",
+ urlCopied: "URL copiada al portapapeles",
+ viewSource: "Ver fuente",
+ noInsightSelected: "Ningún análisis seleccionado",
+ sourceInsight: "Análisis de fuente",
+ manageNotebooks: "Gestionar cuadernos",
+ manageNotebooksDesc: "Gestiona qué cuadernos contienen esta fuente",
+ noNotebooksAvailable: "No hay cuadernos disponibles",
+ loadFailed: "Error al cargar los detalles de la fuente",
+ removeFromNotebook: "Quitar del cuaderno",
+ retryProcessing: "Reintentar procesamiento",
+ deleteSource: "Eliminar fuente",
+ retry: "Reintentar",
+ addExistingTitle: "Agregar fuentes existentes",
+ addExistingDesc: "Selecciona fuentes existentes de todos tus cuadernos para agregar al actual.",
+ searchPlaceholder: "Buscar fuentes por nombre o URL...",
+ noNotebooksFound: "No se encontraron cuadernos.",
+ showingFirst100: "Mostrando las primeras 100 fuentes. Usa la búsqueda para encontrar una específica.",
+ selectedCount: "{count} fuentes seleccionadas",
+ added: "Agregado el {date}",
+ addUrl: "Agregar URL",
+ uploadFile: "Subir archivo",
+ enterText: "Ingresar texto",
+ processDescription: "El contenido será procesado y analizado por IA.",
+ processingFiles: "Procesando tus archivos...",
+ titleRequired: "Se requiere un título para el contenido de texto",
+ titleGenerated: "Si se deja vacío, se generará un título a partir del contenido",
+ batchCount: "{count} {type} serán procesados",
+ enableEmbedding: "Habilitar embedding para búsqueda",
+ embeddingDesc: "Permite que esta fuente sea encontrada en búsquedas vectoriales y consultas de IA",
+ embeddingAlways: "Embedding habilitado automáticamente",
+ embeddingAlwaysDesc: "Tu configuración está establecida para siempre embeber contenido para búsqueda vectorial.",
+ embeddingNever: "Embedding deshabilitado",
+ embeddingNeverDesc: "Tu configuración está establecida para omitir el embedding. La búsqueda vectorial no estará disponible para esta fuente.",
+ changeInSettings: "Puedes cambiar esto en Configuración",
+ notFound: "Fuente no encontrada",
+ noContent: "No hay contenido disponible",
+ insightsDesc: "Análisis generados a partir del análisis del modelo",
+ uploadedFile: "Archivo subido",
+ fileUnavailableDesc: "Este archivo no está disponible actualmente por razones del sistema de almacenamiento.",
+ batchSuccess: "{count} fuente(s) creadas exitosamente",
+ batchFailed: "Error al crear las {count} fuentes",
+ batchPartial: "{success} exitosas, {failed} fallidas",
+ submittingSource: "Enviando fuente para procesamiento...",
+ processingBatchSources: "Procesando {count} fuentes. Esto puede tomar unos momentos.",
+ processingSource: "Tu fuente está siendo procesada. Esto puede tomar unos momentos.",
+ maxFilesAllowed: "Máximo {count} archivos permitidos por lote",
+ },
+ chat: {
+ sessions: "Sesiones",
+ sessionTitlePlaceholder: "Escribe un título aquí...",
+ noSessions: "Aún no hay sesiones de chat",
+ deleteSession: "Eliminar sesión",
+ deleteSessionDesc: "¿Estás seguro de que quieres eliminar esta sesión de chat? Esta acción no se puede deshacer.",
+ sendPlaceholder: "Pregunta cualquier cosa sobre tus fuentes...",
+ sessionsTitle: "Sesiones de chat",
+ chatWith: "Chat con {name}",
+ startConversation: "Inicia una conversación sobre este {type}",
+ askQuestions: "Haz preguntas para entender mejor el contenido",
+ pressToSend: "Presiona {key} para enviar",
+ model: "Modelo",
+ createToStart: "Crea una sesión para comenzar.",
+ chatWithNotebook: "Chat con cuaderno",
+ unableToLoadChat: "No se pudo cargar el chat",
+ noDescription: "Sin descripción",
+ startByCreating: "Comienza creando tu primer cuaderno para organizar tu investigación.",
+ messagesCount: "{count} mensajes",
+ sessionCreated: "Sesión de chat creada",
+ sessionUpdated: "Sesión actualizada",
+ sessionDeleted: "Sesión eliminada",
+ },
+ searchPage: {
+ askAndSearch: "Preguntar y buscar",
+ chooseAMode: "Elige un modo",
+ askBeta: "Preguntar (beta)",
+ search: "Buscar",
+ askYourKb: "Pregunta a tu base de conocimiento (beta)",
+ askYourKbDesc: "El LLM responderá tu consulta basándose en los documentos de tu base de conocimiento.",
+ question: "Pregunta",
+ enterQuestionPlaceholder: "Escribe tu pregunta...",
+ pressToSubmit: "Presiona Cmd/Ctrl+Enter para enviar",
+ noEmbeddingModel: "No puedes usar esta función porque no tienes un modelo de embedding seleccionado. Por favor, configura uno en la página de Modelos.",
+ usingCustomModels: "Usando modelos personalizados",
+ usingDefaultModels: "Usando modelos predeterminados",
+ advanced: "Avanzado",
+ strategy: "Estrategia",
+ answer: "Respuesta",
+ final: "Final",
+ ask: "Preguntar",
+ processing: "Procesando...",
+ saveToNotebooks: "Guardar en cuadernos",
+ searchDesc: "Busca en tu base de conocimiento palabras clave o conceptos específicos",
+ enterSearchPlaceholder: "Escribe tu búsqueda...",
+ pressToSearch: "Presiona Enter para buscar",
+ searchType: "Tipo de búsqueda",
+ vectorSearchWarning: "La búsqueda vectorial requiere un modelo de embedding. Solo la búsqueda de texto está disponible.",
+ textSearch: "Búsqueda de texto",
+ vectorSearch: "Búsqueda vectorial",
+ searchIn: "Buscar en",
+ searchSources: "Buscar fuentes",
+ searchNotes: "Buscar notas",
+ resultsFound: "{count} resultados encontrados",
+ matches: "Coincidencias ({count})",
+ noResultsFor: "No se encontraron resultados para \"{query}\"",
+ notSet: "No configurado",
+ saveToNotebook: "Guardar en cuaderno",
+ saveSuccess: "Guardado exitosamente en el cuaderno",
+ saveError: "Error al guardar en el cuaderno",
+ selectNotebook: "Seleccionar cuaderno",
+ searchAndAsk: "Buscar y preguntar",
+ searchResultsFor: "Resultados de búsqueda para \"{query}\"",
+ askAbout: "Preguntar sobre \"{query}\"",
+ orSearchKb: "O busca en tu base de conocimiento",
+ saving: "Guardando...",
+ advancedModelTitle: "Selección avanzada de modelos",
+ advancedModelDesc: "Elige modelos específicos para cada etapa del proceso de consulta",
+ strategyModel: "Modelo de estrategia",
+ answerModel: "Modelo de respuesta",
+ finalAnswerModel: "Modelo de respuesta final",
+ selectStrategyPlaceholder: "Seleccionar modelo de estrategia",
+ selectAnswerPlaceholder: "Seleccionar modelo de respuesta",
+ selectFinalPlaceholder: "Seleccionar modelo de respuesta final",
+ saveChanges: "Guardar cambios",
+ processingQuestion: "Procesando tu pregunta...",
+ },
+ podcasts: {
+ generateEpisode: "Generar episodio de podcast",
+ generateEpisodeDesc: "Selecciona el contenido a incluir y configura los detalles del episodio antes de generar un nuevo episodio de podcast.",
+ content: "Contenido",
+ contentDesc: "Elige cuadernos, fuentes y notas para incluir en este episodio.",
+ itemsSelected: "{count} elementos seleccionados",
+ tokens: "{count} tokens",
+ chars: "{count} caracteres",
+ loadingNotebooks: "Cargando cuadernos...",
+ noNotebooksFoundInPodcasts: "No se encontraron cuadernos. Crea un cuaderno y agrega contenido antes de generar un podcast.",
+ noContentSelected: "No se seleccionó contenido",
+ summary: "Resumen",
+ fullContent: "Contenido completo",
+ untitledSource: "Fuente sin título",
+ untitledNote: "Nota sin título",
+ episodeSettings: "Configuración del episodio",
+ episodeProfile: "Perfil del episodio",
+ episodeProfilePlaceholder: "Selecciona un perfil de episodio",
+ episodeName: "Nombre del episodio",
+ episodeNamePlaceholder: "ej., IA y el futuro del trabajo",
+ additionalInstructions: "Instrucciones adicionales",
+ instructionsPlaceholder: "Cualquier consejo adicional para agregar al briefing del episodio...",
+ generating: "Generando...",
+ generate: "Generar",
+ hostPlaceholder: "Presentador {number}",
+ profileRequired: "Perfil de episodio requerido",
+ profileRequiredDesc: "Selecciona un perfil de episodio antes de generar un podcast.",
+ nameRequired: "Nombre del episodio requerido",
+ nameRequiredDesc: "Proporciona un nombre para el episodio.",
+ addContext: "Agregar contexto",
+ addContextDesc: "Selecciona al menos una fuente o nota para incluir en el episodio.",
+ generationFailed: "La generación del podcast falló",
+ speakerProfile: "Perfil de locutor",
+ usesSpeakerProfile: "Usa perfil de locutor",
+ sources: "Fuentes",
+ notes: "Notas",
+ noSources: "No hay fuentes disponibles en este cuaderno.",
+ noNotes: "No hay notas disponibles en este cuaderno.",
+ selectMode: "Seleccionar modo",
+ buildContextFailed: "Error al construir el contexto. Por favor, revisa tus selecciones.",
+ podcastTaskStarted: "Tarea de podcast iniciada",
+ loadingProfiles: "Cargando perfiles de episodio...",
+ noProfilesFound: "No se encontraron perfiles de episodio. Crea un perfil de episodio antes de generar un podcast.",
+ listTitle: "Podcasts",
+ listDesc: "Lleva un registro de los episodios generados y gestiona perfiles reutilizables.",
+ chooseAView: "Elige una vista",
+ episodesTab: "Episodios",
+ templatesTab: "Perfiles",
+ overviewTitle: "Vista general de episodios",
+ overviewDesc: "Monitorea los trabajos de generación de podcasts y revisa los artefactos finales.",
+ generateBtn: "Generar podcast",
+ total: "Total",
+ processingLabel: "Procesando",
+ completedLabel: "Completados",
+ failedLabel: "Fallidos",
+ pendingLabel: "Pendientes",
+ loadErrorTitle: "Error al cargar episodios",
+ loadErrorDesc: "No pudimos obtener los episodios más recientes. Intenta de nuevo en un momento.",
+ loadingEpisodes: "Cargando episodios…",
+ noEpisodesYet: "Aún no hay episodios de podcast. Genera tu primero desde las interfaces de chat de cuadernos o fuentes.",
+ statusRunningTitle: "Procesando actualmente",
+ statusRunningDesc: "Episodios que están generando activos activamente.",
+ statusPendingTitle: "En cola / Pendientes",
+ statusPendingDesc: "Episodios enviados esperando a comenzar el procesamiento.",
+ statusCompletedTitle: "Episodios completados",
+ statusCompletedDesc: "Listos para revisar, descargar o publicar.",
+ statusFailedTitle: "Episodios fallidos",
+ statusFailedDesc: "Episodios que encontraron problemas durante la generación.",
+ templatesWorkspaceTitle: "Espacio de trabajo de perfiles",
+ templatesWorkspaceDesc: "Construye configuraciones reutilizables de episodios y locutores para producción rápida de podcasts.",
+ howTemplatesPowerTitle: "Cómo los perfiles impulsan la generación de podcasts",
+ howTemplatesPowerDesc: "Los perfiles dividen el flujo de trabajo del podcast en dos bloques reutilizables. Mézclalos y combínalos cuando generes un nuevo episodio.",
+ episodeProfilesSetFormat: "Los perfiles de episodio establecen el formato",
+ episodeProfilesList1: "Definen el número de segmentos y cómo fluye la historia",
+ episodeProfilesList2: "Eligen los modelos de lenguaje usados para briefing, esquema y escritura del guion",
+ episodeProfilesList3: "Almacenan briefings predeterminados para que cada episodio comience con un tono consistente",
+ speakerProfilesBringVoices: "Los perfiles de locutor dan vida a las voces",
+ speakerProfilesList1: "Eligen el proveedor y modelo de texto a voz",
+ speakerProfilesList2: "Capturan personalidad, historia y notas de pronunciación por locutor",
+ speakerProfilesList3: "Reutiliza las mismas voces de presentador o invitado en diferentes formatos de episodio",
+ recommendedWorkflow: "Flujo de trabajo recomendado",
+ workflowStep1: "Crea perfiles de locutor para cada voz que necesites",
+ workflowStep2: "Construye perfiles de episodio que referencien a esos locutores por nombre",
+ workflowStep3: "Genera podcasts seleccionando el perfil de episodio que se ajuste a la historia",
+ workflowHint: "Los perfiles de episodio referencian perfiles de locutor por nombre, así que empezar con los locutores evita asignaciones de voz faltantes después.",
+ failedToLoadTemplates: "Error al cargar los datos de perfiles",
+ failedToLoadTemplatesDesc: "Asegúrate de que la API esté funcionando e intenta de nuevo. Algunas secciones pueden estar incompletas.",
+ loadingTemplates: "Cargando perfiles…",
+ speakerProfilesTitle: "Perfiles de locutor",
+ speakerProfilesDesc: "Configura voces y personalidades para los episodios generados.",
+ createSpeaker: "Crear locutor",
+ noSpeakerProfiles: "Aún no hay perfiles de locutor. Crea uno para hacer disponibles los perfiles de episodio.",
+ noDescription: "No se proporcionó descripción.",
+ usedByCount_one: "Usado por 1 episodio",
+ usedByCount_other: "Usado por {count} episodios",
+ usedByCount: "Usado por {count} episodios",
+ unused: "Sin usar",
+ voiceId: "ID de voz",
+ backstory: "Historia",
+ personality: "Personalidad",
+ edit: "Editar",
+ duplicate: "Duplicar",
+ deleteSpeakerProfileTitle: "¿Eliminar perfil de locutor?",
+ deleteSpeakerProfileDesc: "Eliminar \"{name}\" no se puede deshacer.",
+ deleteSpeakerDisabledHint: "Quita este locutor de los perfiles de episodio antes de eliminarlo.",
+ deleting: "Eliminando…",
+ episodeProfilesTitle: "Perfiles de episodio",
+ episodeProfilesDesc: "Define configuraciones reutilizables de generación para tus programas.",
+ createProfile: "Crear perfil",
+ createSpeakerFirst: "Crea un perfil de locutor antes de agregar un perfil de episodio.",
+ noEpisodeProfiles: "Aún no hay perfiles de episodio. Crea uno para iniciar la generación de podcasts.",
+ speakerCreated: "Locutor creado",
+ speakerCreatedDesc: "El locutor \"{name}\" ha sido agregado exitosamente.",
+ failedToCreateSpeaker: "Error al crear el perfil de locutor",
+ speakerUpdated: "Locutor actualizado",
+ speakerUpdatedDesc: "El locutor \"{name}\" ha sido actualizado exitosamente.",
+ failedToUpdateSpeaker: "Error al actualizar el perfil de locutor",
+ speakerDeleted: "Locutor eliminado",
+ speakerDeletedDesc: "El locutor \"{name}\" ha sido eliminado exitosamente.",
+ failedToDeleteSpeaker: "Error al eliminar el perfil de locutor",
+ speakerDuplicated: "Locutor duplicado",
+ speakerDuplicatedDesc: "El locutor \"{name}\" ha sido duplicado exitosamente.",
+ failedToDuplicateSpeaker: "Error al duplicar el perfil de locutor",
+ generationStarted: "Generación iniciada",
+ generationStartedDesc: "La generación del podcast ha sido puesta en cola.",
+ failedToStartGeneration: "Error al iniciar la generación",
+ tryAgainMoment: "Por favor, intenta de nuevo en un momento.",
+ deleteProfileTitle: "¿Eliminar perfil?",
+ deleteProfileDesc: "Esto eliminará \"{name}\". Los episodios existentes conservan sus datos, pero los nuevos ya no usarán esta configuración.",
+ profileCreated: "Perfil creado",
+ profileCreatedDesc: "El perfil de episodio \"{name}\" ha sido creado exitosamente.",
+ failedToCreateProfile: "Error al crear el perfil",
+ profileUpdated: "Perfil actualizado",
+ profileUpdatedDesc: "El perfil de episodio \"{name}\" ha sido actualizado exitosamente.",
+ failedToUpdateProfile: "Error al actualizar el perfil",
+ profileDeleted: "Perfil eliminado",
+ profileDeletedDesc: "El perfil de episodio \"{name}\" ha sido eliminado exitosamente.",
+ failedToDeleteProfile: "Error al eliminar el perfil",
+ failedToDeleteProfileDesc: "Error al eliminar el perfil de episodio.",
+ profileDuplicated: "Perfil duplicado",
+ profileDuplicatedDesc: "El perfil de episodio \"{name}\" ha sido duplicado exitosamente.",
+ failedToDuplicateProfile: "Error al duplicar el perfil",
+ episodeDeleted: "Episodio eliminado",
+ episodeDeletedDesc: "El episodio ha sido eliminado exitosamente.",
+ failedToDeleteEpisode: "Error al eliminar el episodio",
+ failedToDeleteSpeakerDesc: "Error al eliminar el perfil de locutor.",
+ outlineModel: "Modelo de esquema",
+ transcriptModel: "Modelo de transcripción",
+ segments: "Segmentos",
+ defaultBriefingTitle: "Briefing predeterminado",
+ created: "Creado el {time}",
+ details: "Detalles",
+ summaryTab: "Resumen",
+ outlineTab: "Esquema",
+ transcriptTab: "Transcripción",
+ briefing: "Briefing",
+ noOutline: "No hay esquema disponible.",
+ noTranscript: "No hay transcripción disponible.",
+ deleteEpisodeTitle: "¿Eliminar episodio?",
+ deleteEpisodeDesc: "Esto eliminará \"{name}\" y su archivo de audio permanentemente.",
+ audioUnavailable: "Audio no disponible",
+ segment: "Segmento",
+ speaker: "Locutor",
+ profile: "Perfil",
+ link: "Enlace",
+ file: "Archivo",
+ embedded: "Embebido",
+ notEmbedded: "No embebido",
+ noSpeakerProfilesAvailable: "No hay perfiles de locutor disponibles",
+ editEpisodeProfile: "Editar perfil de episodio",
+ createEpisodeProfile: "Crear perfil de episodio",
+ episodeProfileFormDesc: "Define cómo se deben generar los episodios y qué configuración de locutor usan por defecto.",
+ noSpeakerProfilesDesc: "Crea un perfil de locutor antes de configurar un perfil de episodio.",
+ profileName: "Nombre del perfil",
+ profileNamePlaceholder: "ej., Discusión tecnológica",
+ descriptionPlaceholder: "Breve resumen de cuándo usar este perfil",
+ speakerConfig: "Configuración de locutor",
+ selectSpeakerProfile: "Seleccionar un perfil de locutor",
+ outlineGeneration: "Generación de esquema",
+ transcriptGeneration: "Generación de transcripción",
+ defaultBriefingPlaceholder: "Describe la estructura, tono y objetivos para este formato de episodio",
+ editSpeakerProfile: "Editar perfil de locutor",
+ createSpeakerProfile: "Crear perfil de locutor",
+ speakerProfileFormDesc: "Configura los ajustes de texto a voz y define hasta cuatro locutores.",
+ speakers: "Locutores",
+ speakersDesc: "Configura entre una y cuatro voces para este perfil.",
+ addSpeaker: "Agregar locutor",
+ speakerNumber: "Locutor {number}",
+ backstoryPlaceholder: "Breve biografía o contexto del locutor",
+ personalityPlaceholder: "Describe el estilo y tono",
+ outlineModelRequired: "El modelo de esquema es obligatorio",
+ transcriptModelRequired: "El modelo de transcripción es obligatorio",
+ defaultBriefingRequired: "El briefing predeterminado es obligatorio",
+ segmentsInteger: "Debe ser un número entero",
+ segmentsMin: "Al menos 3 segmentos",
+ segmentsMax: "Máximo 20 segmentos",
+ voiceIdRequired: "El ID de voz es obligatorio",
+ backstoryRequired: "La historia es obligatoria",
+ personalityRequired: "La personalidad es obligatoria",
+ speakerCountMin: "Se requiere al menos un locutor",
+ speakerCountMax: "Puedes configurar hasta 4 locutores",
+ delete: "Eliminar",
+ failedToDelete: "Error al eliminar el podcast",
+ retry: "Reintentar",
+ retrying: "Reintentando…",
+ retryStarted: "Reintento iniciado",
+ retryStartedDesc: "Se ha enviado un nuevo trabajo de generación de podcast.",
+ failedToRetry: "Error al reintentar el episodio",
+ errorDetails: "Detalles del error",
+ language: "Idioma",
+ languagePlaceholder: "Selecciona un idioma (opcional)",
+ podcastLanguage: "Idioma del podcast",
+ selectOutlineModel: "Seleccionar modelo de esquema",
+ selectTranscriptModel: "Seleccionar modelo de transcripción",
+ voiceModel: "Modelo de voz",
+ voiceModelRequired: "El modelo de voz es obligatorio",
+ selectVoiceModel: "Seleccionar modelo de voz",
+ perSpeakerTtsOverride: "Anulación de TTS por locutor (opcional)",
+ useProfileDefault: "Usar predeterminado del perfil",
+ setupRequired: "Configuración requerida",
+ setupRequiredDesc:
+ "Algunos perfiles aún no tienen modelos configurados. Edítalos para seleccionar modelos antes de generar podcasts.",
+ notConfigured: "No configurado",
+ },
+ settings: {
+ contentProcessing: "Procesamiento de contenido",
+ contentProcessingDesc: "Configura cómo se procesan los documentos y URLs",
+ docEngine: "Motor de procesamiento de documentos",
+ docEnginePlaceholder: "Selecciona motor de procesamiento de documentos",
+ urlEngine: "Motor de procesamiento de URLs",
+ urlEnginePlaceholder: "Selecciona motor de procesamiento de URLs",
+ autoRecommended: "Auto (Recomendado)",
+ simple: "Simple",
+ docling: "Docling",
+ helpMeChoose: "Ayúdame a elegir",
+ docHelp: "· Docling es un poco más lento pero más preciso, especialmente si los documentos contienen tablas e imágenes. · Simple extraerá cualquier contenido del documento sin formatearlo. · Auto (recomendado) intentará procesar con Docling y por defecto usará Simple.",
+ firecrawl: "Firecrawl",
+ jina: "Jina",
+ urlHelp: "· Firecrawl es un servicio de pago (con un nivel gratuito), y muy potente. · Jina es una buena opción también y tiene un nivel gratuito. · Simple usará extracción HTTP básica y perderá contenido en sitios web basados en JavaScript. · Auto (recomendado) intentará usar Firecrawl, luego Jina, y finalmente Simple.",
+ embeddingAndSearch: "Embedding y búsqueda",
+ embeddingAndSearchDesc: "Configura las opciones de búsqueda y embedding",
+ defaultEmbeddingOption: "Opción de embedding predeterminada",
+ embeddingOptionPlaceholder: "Selecciona opción de embedding",
+ ask: "Preguntar",
+ always: "Siempre",
+ never: "Nunca",
+ embeddingHelp: "Embeber el contenido facilitará encontrarlo por ti y por tus agentes de IA. Si estás usando un modelo de embedding local (Ollama, por ejemplo), no deberías preocuparte por el costo y simplemente embeber todo.",
+ fileManagement: "Gestión de archivos",
+ fileManagementDesc: "Configura las opciones de manejo y almacenamiento de archivos",
+ autoDeleteFiles: "Eliminar archivos automáticamente",
+ autoDeletePlaceholder: "Selecciona opción de eliminación automática",
+ filesHelp: "Una vez que tus archivos se suben y procesan, ya no son necesarios. La mayoría de los usuarios deberían permitir que Open Notebook elimine los archivos subidos de la carpeta de carga automáticamente.",
+ loadFailed: "Error al cargar la configuración",
+ },
+ advanced: {
+ title: "Herramientas avanzadas",
+ desc: "Herramientas avanzadas y utilidades para usuarios expertos",
+ systemInfo: "Información del sistema",
+ rebuildEmbeddings: "Reconstruir embeddings",
+ rebuildEmbeddingsDesc: "Reconstruir el índice de búsqueda vectorial para todas las fuentes",
+ currentVersion: "Versión actual",
+ latestVersion: "Última versión",
+ status: "Estado",
+ updateAvailable: "Versión {version} disponible",
+ updateAvailableDesc: "Una nueva versión de Open Notebook está disponible.",
+ upToDate: "Actualizado",
+ unknown: "Desconocido",
+ viewOnGithub: "Ver en GitHub",
+ updateCheckFailed: "No se pudo verificar actualizaciones. GitHub puede no ser accesible.",
+ rebuild: {
+ mode: "Modo de reconstrucción",
+ existing: "Existentes",
+ all: "Todos",
+ existingDesc: "Re-embeber solo elementos que ya tienen embeddings (más rápido, para cambio de modelo)",
+ allDesc: "Re-embeber elementos existentes + crear embeddings para elementos sin ninguno (más lento, completo)",
+ include: "Incluir en la reconstrucción",
+ selectOneError: "Por favor, selecciona al menos un tipo de elemento para reconstruir",
+ starting: "Iniciando reconstrucción...",
+ startBtn: "🚀 Iniciar reconstrucción",
+ queued: "En cola",
+ running: "Enviando trabajos...",
+ completed: "¡Trabajos enviados!",
+ failed: "Fallido",
+ leavePageHint: "Puedes salir de esta página ya que se ejecutará en segundo plano",
+ startNew: "Iniciar nueva reconstrucción",
+ itemsProcessed: "{processed}/{total} trabajos enviados ({percent}%)",
+ failedItems: "{count} trabajos fallaron al enviarse",
+ time: "Tiempo",
+ whenToRebuild: "¿Cuándo debo reconstruir los embeddings?",
+ whenToRebuildAns: "Debes reconstruir al cambiar modelos, actualizar versiones, corregir corrupción o después de importaciones masivas.",
+ howLong: "¿Cuánto tiempo tarda la reconstrucción?",
+ howLongAns: "El tiempo de procesamiento depende de la cantidad de elementos, velocidad del modelo y límites de la API. Los modelos locales suelen ser muy rápidos.",
+ isSafe: "¿Es seguro reconstruir mientras uso la aplicación?",
+ isSafeAns: "Sí, la reconstrucción es segura. No elimina contenido, solo reemplaza embeddings, y maneja errores de forma controlada.",
+ },
+ },
+ transformations: {
+ title: "Transformaciones",
+ desc: "Las transformaciones son prompts que el LLM usará para procesar una fuente y extraer análisis, resúmenes, etc.",
+ workspace: "Elige un espacio de trabajo",
+ playground: "Laboratorio",
+ defaultPrompt: "Prompt de transformación predeterminado",
+ defaultPromptDesc: "Esto se agregará a todos tus prompts de transformación",
+ defaultPromptPlaceholder: "Ingresa tus instrucciones de transformación predeterminadas...",
+ listTitle: "Transformaciones personalizadas",
+ createNew: "Crear nueva",
+ inputLabel: "Texto de entrada",
+ inputPlaceholder: "Ingresa algo de texto para transformar...",
+ outputLabel: "Resultado",
+ runTest: "Ejecutar transformación",
+ running: "Ejecutando...",
+ selectToStart: "Selecciona una transformación para comenzar",
+ name: "Nombre",
+ namePlaceholder: "Identificador único, ej. temas_clave",
+ titlePlaceholder: "Título mostrado, por defecto usa el nombre",
+ promptPlaceholder: "Escribe el prompt que impulsará esta transformación...",
+ descriptionPlaceholder: "Describe qué hace esta transformación.",
+ suggestDefault: "Sugerir por defecto en nuevas fuentes",
+ promptHint: "Los prompts deben escribirse pensando en el contenido de la fuente. Puedes pedirle al modelo que resuma, extraiga análisis o produzca resultados estructurados como tablas.",
+ createSuccess: "Transformación creada exitosamente",
+ updateSuccess: "Transformación actualizada exitosamente",
+ deleteSuccess: "Transformación eliminada exitosamente",
+ noTransformations: "Aún no hay transformaciones",
+ createOne: "Crea una transformación para comenzar",
+ selectModel: "Seleccionar un modelo",
+ deleteConfirm: "¿Estás seguro de que quieres eliminar esta transformación?",
+ model: "Modelo",
+ systemPrompt: "Prompt del sistema",
+ overrideModelDesc: "Anula el modelo predeterminado para esta sesión de chat. Déjalo vacío para usar el predeterminado del sistema.",
+ sessionUseReplacement: "Esta sesión usará {name} en lugar del modelo predeterminado.",
+ systemDefault: "Predeterminado del sistema",
+ },
+ models: {
+ embedding: "Modelos de embedding",
+ tts: "Texto a voz (TTS)",
+ stt: "Voz a texto (STT)",
+ apiKey: "Clave API",
+ deleteSuccess: "Modelo eliminado exitosamente",
+ saveSuccess: "Modelo guardado exitosamente",
+ noModels: "Sin modelos",
+ discoverModels: "Descubrir modelos",
+ noModelsFound: "No se encontraron modelos de este proveedor",
+ modelType: "Tipo de modelo",
+ modelTypeHint: "Selecciona el tipo para los modelos que quieres agregar. Si necesitas tipos diferentes, agrégalos en lotes separados.",
+ deleteModel: "Eliminar modelo",
+ defaultAssignments: "Asignaciones de modelos predeterminados",
+ defaultAssignmentsDesc: "Configura qué modelos usar para diferentes propósitos en Open Notebook",
+ missingRequiredModels: "Faltan modelos requeridos: {models}. Open Notebook puede no funcionar correctamente sin estos.",
+ selectModelPlaceholder: "Seleccionar un modelo",
+ requiredModelPlaceholder: "⚠️ Requerido - Seleccionar un modelo",
+ chatModelLabel: "Modelo de chat",
+ chatModelDesc: "Usado para conversaciones de chat",
+ transformationModelLabel: "Modelo de transformación",
+ transformationModelDesc: "Usado para resúmenes, análisis y transformaciones",
+ toolsModelLabel: "Modelo de herramientas",
+ toolsModelDesc: "Usado para llamadas a funciones - Se recomienda OpenAI o Anthropic",
+ largeContextModelLabel: "Modelo de contexto largo",
+ largeContextModelDesc: "Usado para procesar documentos grandes - Se recomienda Gemini",
+ embeddingModelLabel: "Modelo de embedding",
+ embeddingModelDesc: "Usado para búsqueda semántica y embeddings vectoriales",
+ ttsModelLabel: "Modelo de texto a voz",
+ ttsModelDesc: "Usado para generación de podcasts",
+ sttModelLabel: "Modelo de voz a texto",
+ sttModelDesc: "Usado para transcripción de audio",
+ embeddingChangeTitle: "Cambio de modelo de embedding",
+ embeddingChangeConfirm: "Estás a punto de cambiar tu modelo de embedding de {from} a {to}.",
+ rebuildRequired: "Importante: Reconstrucción requerida",
+ rebuildReason: "Cambiar tu modelo de embedding requiere reconstruir todos los embeddings existentes para mantener la consistencia. Sin reconstruir, tus búsquedas pueden devolver resultados incorrectos o incompletos.",
+ whatHappensNext: "Qué pasa después:",
+ step1: "Tu modelo de embedding predeterminado será actualizado",
+ step2: "Los embeddings existentes permanecerán sin cambios hasta la reconstrucción",
+ step3: "El nuevo contenido usará el nuevo modelo de embedding",
+ step4: "Deberías reconstruir los embeddings lo antes posible",
+ proceedToRebuildPrompt: "¿Te gustaría ir a la página Avanzado para iniciar la reconstrucción ahora?",
+ changeModelOnly: "Solo cambiar modelo",
+ changeAndRebuild: "Cambiar e ir a reconstruir",
+ autoAssign: "Auto-asignar predeterminados",
+ autoAssigning: "Asignando...",
+ autoAssignSuccess: "{count} modelos predeterminados asignados automáticamente",
+ autoAssignNoModels: "No hay modelos disponibles para asignar. Por favor, sincroniza modelos primero.",
+ autoAssignAlreadySet: "Todos los modelos predeterminados ya están configurados",
+ testModel: "Probar modelo",
+ testModelSuccess: "Prueba de modelo exitosa",
+ testModelFailed: "Prueba de modelo fallida",
+ searchOrAddModel: "Buscar o escribir nombre del modelo...",
+ addCustomModel: "Agregar \"{name}\"",
+ },
+ apiKeys: {
+ title: "Configura tu IA con tus propias claves API",
+ description: "Almacena claves API de forma segura en la base de datos para habilitar proveedores de IA en Open Notebook.",
+ encryptionRequired: "Clave de encriptación no configurada",
+ encryptionRequiredDescription: "Establece la variable de entorno OPEN_NOTEBOOK_ENCRYPTION_KEY con cualquier cadena secreta para habilitar el almacenamiento de claves API en la base de datos.",
+ configured: "Configurado",
+ notConfigured: "No configurado",
+ migrationAvailable: "Variables de entorno detectadas",
+ migrationDescription: "{count} clave(s) API están configuradas vía variables de entorno y pueden migrarse a la base de datos para una gestión más fácil.",
+ migrateToDatabase: "Migrar a la base de datos",
+ migrating: "Migrando...",
+ migrationSuccess: "{count} clave(s) API migradas exitosamente",
+ migrationErrors: "{count} clave(s) fallaron al migrar",
+ migrationNothingToMigrate: "Todas las claves ya están en la base de datos",
+ learnMore: "Aprende cómo configurar claves API →",
+ testConnection: "Probar conexión",
+ testSuccess: "Conexión exitosa",
+ testFailed: "La prueba de conexión falló",
+ syncModels: "Sincronizar modelos",
+ syncSuccess: "Se descubrieron {discovered} modelos, se agregaron {new} nuevos",
+ syncNoNew: "Se descubrieron {count} modelos, todos ya registrados",
+ syncFailed: "Error al sincronizar modelos",
+ getApiKey: "Obtener clave API",
+ vertexProject: "ID de proyecto GCP",
+ vertexLocation: "Región",
+ vertexCredentials: "Ruta del JSON de cuenta de servicio",
+ addConfig: "Agregar configuración",
+ editConfig: "Editar configuración",
+ deleteConfig: "Eliminar configuración",
+ configName: "Nombre de la configuración",
+ configNameHint: "Un nombre descriptivo para esta configuración (ej., 'Producción', 'Desarrollo')",
+ baseUrl: "URL base",
+ baseUrlOverrideHint: "Solo cambia esto si necesitas anular el endpoint API predeterminado del proveedor.",
+ deleteConfigConfirm: "¿Estás seguro de que quieres eliminar '{name}'? Esto no se puede deshacer.",
+ configSaveSuccess: "Configuración guardada exitosamente",
+ configUpdateSuccess: "Configuración actualizada exitosamente",
+ configDeleteSuccess: "Configuración eliminada exitosamente",
+ apiKeyEditHint: "Deja en blanco para mantener la clave API existente",
+ decryptionError: "Error de desencriptación",
+ decryptionErrorDescription: "La clave API de esta credencial no pudo ser desencriptada. La clave de encriptación puede haber cambiado. Elimina esta credencial y créala de nuevo con la clave correcta.",
+ },
+ setupBanner: {
+ encryptionRequired: "Clave de encriptación no configurada",
+ encryptionRequiredDescription: "Establece la variable de entorno OPEN_NOTEBOOK_ENCRYPTION_KEY para habilitar el almacenamiento seguro de credenciales.",
+ migrationAvailable: "Migración de claves API disponible",
+ migrationDescription: "{count} proveedor(es) tienen claves API configuradas vía variables de entorno. Mígralas a la base de datos para una gestión más fácil.",
+ goToSettings: "Ir a configuración",
+ viewDocs: "Ver documentación",
+ },
+}
diff --git a/frontend/src/lib/locales/fr-FR/index.ts b/frontend/src/lib/locales/fr-FR/index.ts
index 5d2cc8b..2dd641d 100644
--- a/frontend/src/lib/locales/fr-FR/index.ts
+++ b/frontend/src/lib/locales/fr-FR/index.ts
@@ -26,6 +26,7 @@ export const frFR = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "Source",
notebook: "Carnet",
podcast: "Podcast",
diff --git a/frontend/src/lib/locales/index.ts b/frontend/src/lib/locales/index.ts
index 0b4a6d8..0ace84c 100644
--- a/frontend/src/lib/locales/index.ts
+++ b/frontend/src/lib/locales/index.ts
@@ -7,6 +7,7 @@ import { itIT } from './it-IT';
import { frFR } from './fr-FR';
import { ruRU } from './ru-RU';
import { bnIN } from './bn-IN';
+import { esES } from './es-ES';
export const resources = {
'zh-CN': { translation: zhCN },
@@ -18,11 +19,12 @@ export const resources = {
'fr-FR': { translation: frFR },
'ru-RU': { translation: ruRU },
'bn-IN': { translation: bnIN },
+ 'es-ES': { translation: esES },
} as const;
export type TranslationKeys = typeof enUS;
-export type LanguageCode = 'zh-CN' | 'en-US' | 'zh-TW' | 'pt-BR' | 'ja-JP' | 'it-IT' | 'fr-FR' | 'ru-RU' | 'bn-IN';
+export type LanguageCode = 'zh-CN' | 'en-US' | 'zh-TW' | 'pt-BR' | 'ja-JP' | 'it-IT' | 'fr-FR' | 'ru-RU' | 'bn-IN' | 'es-ES';
export type Language = {
code: LanguageCode;
@@ -39,6 +41,7 @@ export const languages: Language[] = [
{ code: 'fr-FR', label: 'Français' },
{ code: 'ru-RU', label: 'Русский' },
{ code: 'bn-IN', label: 'বাংলা' },
+ { code: 'es-ES', label: 'Español' },
];
-export { zhCN, enUS, zhTW, ptBR, jaJP, itIT, frFR, ruRU, bnIN };
+export { zhCN, enUS, zhTW, ptBR, jaJP, itIT, frFR, ruRU, bnIN, esES };
diff --git a/frontend/src/lib/locales/it-IT/index.ts b/frontend/src/lib/locales/it-IT/index.ts
index 971f3ca..916846d 100644
--- a/frontend/src/lib/locales/it-IT/index.ts
+++ b/frontend/src/lib/locales/it-IT/index.ts
@@ -26,6 +26,7 @@ export const itIT = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "Fonte",
notebook: "Quaderno",
podcast: "Podcast",
diff --git a/frontend/src/lib/locales/ja-JP/index.ts b/frontend/src/lib/locales/ja-JP/index.ts
index 5f4fcf3..9a5dfa2 100644
--- a/frontend/src/lib/locales/ja-JP/index.ts
+++ b/frontend/src/lib/locales/ja-JP/index.ts
@@ -26,6 +26,7 @@ export const jaJP = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "ソース",
notebook: "ノートブック",
podcast: "ポッドキャスト",
diff --git a/frontend/src/lib/locales/pt-BR/index.ts b/frontend/src/lib/locales/pt-BR/index.ts
index 16acc33..d3f66c0 100644
--- a/frontend/src/lib/locales/pt-BR/index.ts
+++ b/frontend/src/lib/locales/pt-BR/index.ts
@@ -26,6 +26,7 @@ export const ptBR = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "Fonte",
notebook: "Caderno",
podcast: "Podcast",
diff --git a/frontend/src/lib/locales/ru-RU/index.ts b/frontend/src/lib/locales/ru-RU/index.ts
index c06f6b9..fece9fd 100644
--- a/frontend/src/lib/locales/ru-RU/index.ts
+++ b/frontend/src/lib/locales/ru-RU/index.ts
@@ -26,6 +26,7 @@ export const ruRU = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "Источник",
notebook: "Блокнот",
podcast: "Подкаст",
diff --git a/frontend/src/lib/locales/zh-CN/index.ts b/frontend/src/lib/locales/zh-CN/index.ts
index 3cedde2..fa0c4e4 100644
--- a/frontend/src/lib/locales/zh-CN/index.ts
+++ b/frontend/src/lib/locales/zh-CN/index.ts
@@ -26,6 +26,7 @@ export const zhCN = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "来源",
notebook: "笔记本",
podcast: "播客",
diff --git a/frontend/src/lib/locales/zh-TW/index.ts b/frontend/src/lib/locales/zh-TW/index.ts
index cb3e21c..25f26fb 100644
--- a/frontend/src/lib/locales/zh-TW/index.ts
+++ b/frontend/src/lib/locales/zh-TW/index.ts
@@ -26,6 +26,7 @@ export const zhTW = {
french: "Français",
russian: "Русский",
bengali: "বাংলা",
+ spanish: "Español",
source: "來源",
notebook: "筆記本",
podcast: "播客",
diff --git a/frontend/src/lib/utils/date-locale.ts b/frontend/src/lib/utils/date-locale.ts
index bd2e04f..5fa2f9b 100644
--- a/frontend/src/lib/utils/date-locale.ts
+++ b/frontend/src/lib/utils/date-locale.ts
@@ -1,4 +1,4 @@
-import { zhCN, enUS, zhTW, ptBR, ja, fr, ru, bn, Locale } from 'date-fns/locale'
+import { zhCN, enUS, zhTW, ptBR, ja, fr, ru, bn, es, Locale } from 'date-fns/locale'
/**
* Mapping of language codes to date-fns locales.
@@ -13,6 +13,7 @@ const LOCALE_MAP: Record = {
'fr-FR': fr,
'ru-RU': ru,
'bn-IN': bn,
+ 'es-ES': es,
}
/**