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, } /**