Add localization support and language selection

Introduces English and Spanish translation files, integrates localization throughout dialogs, and adds a language selection tab to the custom options dialog. UI strings are now loaded via the localization manager, enabling dynamic language switching and improved internationalization.
This commit is contained in:
Your Name
2025-09-30 20:53:59 +03:00
parent 0376c1c4cd
commit c5916d57c0
12 changed files with 1293 additions and 311 deletions
+316
View File
@@ -0,0 +1,316 @@
{
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Ready"
},
"formats": {
"show_formats": "Show Formats:",
"video_format": "Video Format:",
"audio_format": "Audio Format:",
"no_formats": "No formats available",
"loading": "Loading formats...",
"select": "Select",
"quality": "Quality",
"extension": "Extension",
"resolution": "Resolution",
"file_size": "File Size",
"codec": "Codec",
"audio": "Audio",
"notes": "Notes",
"will_merge_audio": "Will merge audio",
"has_audio": "✓ Has Audio",
"audio_only": "Audio Only",
"large_size": "Large size",
"medium_size": "Medium size",
"standard_size": "Standard size",
"compatible": "Compatible",
"efficient": "Efficient",
"high_quality": "High quality",
"best_4k": "Best (4K)",
"best_2k": "Best (2K)",
"high_1080p": "High (1080p)",
"high_720p": "High (720p)",
"medium_480p": "Medium (480p)",
"low_quality": "Low Quality",
"best_audio": "Best Audio",
"high_audio": "High Audio",
"medium_audio": "Medium Audio",
"low_audio": "Low Audio",
"audio_only_resolution": "Audio only"
},
"buttons": {
"download": "Download",
"pause": "Pause",
"resume": "Resume",
"cancel": "Cancel",
"browse": "Browse",
"clear": "Clear",
"ok": "OK",
"apply": "Apply",
"close": "Close",
"run_command": "Run Command",
"custom_command_help": "Help",
"about": "About",
"analyze": "Analyze",
"paste_url": "Paste URL",
"select_videos": "Select Videos...",
"video": "Video",
"audio_only": "Audio Only",
"custom_options": "Custom Options",
"trim_video": "Trim Video",
"download_settings": "Download Settings",
"update": "Update yt-dlp",
"select_defaults": "Select Defaults",
"select_all": "Select All",
"deselect_all": "Deselect All"
},
"dialogs": {
"custom_options": "Custom Options",
"settings": "Settings",
"select_folder": "Select Download Folder",
"sponsorblock_categories": "SponsorBlock Categories",
"sponsorblock_description": "Select which types of video segments to automatically remove during download.\nSponsorBlock uses community-submitted data to identify these segments.",
"select_subtitles": "Select Subtitles",
"filter_languages_placeholder": "Filter languages (e.g., en, es)...",
"no_subtitles_available": "No subtitles available",
"matching": "matching"
},
"tabs": {
"cookies": "Login with Cookies",
"custom_command": "Custom Command",
"proxy": "Proxy",
"language": "Language"
},
"cookies": {
"help_text": "Choose how to provide cookies for logging in.\nThis allows downloading of private videos and premium quality audio.",
"cookie_source": "Cookie Source",
"use_cookie_file": "Use cookie file",
"extract_from_browser": "Extract from browser",
"cookie_file": "Cookie File",
"cookie_file_placeholder": "Path to cookies.txt file...",
"browser_selection": "Browser Selection",
"browser_help": "Select the browser to extract cookies from:",
"browser_label": "Browser:",
"profile_label": "Profile:",
"profile_placeholder": "default",
"browser_extract_message": "Browser cookies will be extracted when applied",
"file_selected_message": "Cookie file selected - Click OK to apply",
"select_file_title": "Select Cookie File",
"file_filter": "Cookies files (*.txt *.lwp)"
},
"custom_command": {
"help_text": "Enter your custom yt-dlp command below. The current URL will be appended automatically.<br><br>For the full list of options and usage examples, <a href=\"{docs_url}\">click here to view the official yt-dlp documentation</a>.<br><br>Note: Download path and filename template will be handled automatically.",
"input_label": "yt-dlp Arguments:",
"input_placeholder": "Enter yt-dlp arguments here...\n\ne.g. --extract-audio --audio-format mp3",
"output_label": "Command Output:",
"output_placeholder": "Command output will appear here...",
"command_placeholder": "Enter custom yt-dlp command...",
"command_help": "Available placeholders:\n{url} - Video URL\n{output} - Output directory\n\nExample: --write-info-json --write-thumbnail",
"full_command": "🔧 Full command: {command}",
"command_success": "✅ Custom command completed successfully!",
"command_failed": "❌ Command failed with exit code {code}",
"command_error": "❌ Error running custom command: {error}"
},
"proxy": {
"help_text": "Configure proxy settings for downloading. Leave empty to use direct connection.",
"main_proxy": "Main Proxy",
"main_proxy_help": "Primary proxy server for all downloads. Supports HTTP/HTTPS and SOCKS5 protocols.",
"proxy_url_label": "Proxy URL:",
"proxy_url_placeholder": "http://proxy:port or socks5://proxy:port",
"proxy_examples": "Examples:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Geo-bypass Proxy",
"geo_proxy_help": "Secondary proxy specifically for bypassing geographic restrictions.",
"geo_proxy_url_label": "Geo-bypass Proxy URL:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Clear Main Proxy",
"clear_geo_proxy": "Clear Geo Proxy",
"proxy_url": "Proxy URL",
"proxy_placeholder": "http://proxy:port or socks5://proxy:port",
"geo_bypass": "Geo-bypass proxy (for geographic restrictions)",
"geo_bypass_placeholder": "http://proxy:port for bypassing geo-blocks",
"invalid_main_url": "Invalid main proxy URL format",
"invalid_geo_url": "Invalid geo proxy URL format",
"main_configured": "Main proxy configured",
"geo_configured": "Geo proxy configured"
},
"language": {
"help_text": "Select your preferred language for the interface.",
"current_language": "Current Language:",
"restart_notice": "Language change will take effect after restarting the application."
},
"download": {
"preparing": "Preparing download...",
"please_enter_url": "Please enter a URL first.",
"please_set_path": "Please set a download path first.",
"please_enter_url_and_path": "Please enter a URL and set download path.",
"please_select_format": "Please select a format first."
},
"update": {
"title": "Update yt-dlp",
"checking": "Checking for updates...",
"update_available": "Update available!\nCurrent version: {current}\nLatest version: {latest}",
"up_to_date": "yt-dlp is up to date (version {version})",
"could_not_determine": "Could not determine versions.",
"error_comparing": "Error comparing versions: {error}",
"update_available_failed": "Update available! (Comparison failed)\nCurrent: {current}\nLatest: {latest}",
"updating": "Updating...",
"initializing": "🚀 Initializing update process...",
"checking_current": "🔍 Checking current installation...",
"found_at": "📍 Found yt-dlp at: {path}",
"error_getting_path": "❌ Error getting yt-dlp path: {error}",
"updating_binary": "📦 Updating app-managed yt-dlp binary...",
"updating_pip": "🐍 Updating system yt-dlp via pip...",
"update_failed": "❌ Failed to update yt-dlp. Please try again or check your internet connection.",
"binary_updated": "✅ Binary successfully updated!",
"update_failed_stderr": "❌ yt-dlp update failed: {error}",
"update_timeout": "❌ yt-dlp update timed out.",
"unexpected_error": "❌ Unexpected error during update: {error}",
"checking_pip": "🔍 Checking current pip installation...",
"current_version": "📋 Current version: {version}",
"not_found_pip": "⚠️ yt-dlp not found via pip, attempting installation...",
"checking_latest": "🌐 Checking for latest version...",
"failed_check_updates": "❌ Failed to check for updates",
"latest_version": "🆕 Latest version: {version}",
"updating_from_to": "⬆️ Updating from {current} to {latest}...",
"running_pip_install": "📦 Running pip install --upgrade...",
"pip_completed": "✅ Pip update completed successfully!",
"pip_failed": "❌ Pip update failed: {error}"
},
"about": {
"title": "About YTSage",
"version": "Version: {version}",
"description": "A modern YouTube downloader with advanced features",
"author": "Created with ❤️",
"license": "License: MIT",
"system_info": "System Information",
"python_version": "Python Version: {version}",
"pyside_version": "PySide6 Version: {version}",
"ytdlp_version": "yt-dlp Version: {version}",
"ffmpeg_status": "FFmpeg Status: {status}",
"ffmpeg_available": "Available",
"ffmpeg_not_available": "Not Available",
"error_getting_info": "Error getting system information: {error}"
},
"time_range": {
"title": "Trim Video",
"start_time": "Start Time (HH:MM:SS)",
"end_time": "End Time (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"help_text": "Set the start and end times to trim the video.\nLeave empty to download the full video.",
"invalid_format": "Invalid time format. Use HH:MM:SS format.",
"start_after_end": "Start time cannot be after end time."
},
"settings": {
"title": "Download Settings",
"download_path": "Download Path",
"browse": "Browse...",
"speed_limit": "Speed Limit",
"speed_limit_placeholder": "None",
"auto_update_ytdlp": "Auto-Update yt-dlp",
"enable_auto_updates": "Enable automatic yt-dlp updates",
"update_frequency": "Update frequency:",
"check_startup": "Check on every startup (minimum 1 hour between checks)",
"check_daily": "Check daily",
"check_weekly": "Check weekly",
"check_updates_now": "Check for Updates Now",
"update_check_title": "Update Check",
"could_not_determine_version": "Could not determine current yt-dlp version.",
"update_available_dialog": "Update available!\n\nCurrent: {current}\nLatest: {latest}\n\nUse the 'Update yt-dlp' button in the main window to update.",
"up_to_date_dialog": "yt-dlp is up to date!\n\nCurrent version: {version}",
"error_checking_updates": "Error checking for updates: {error}",
"settings_saved_title": "Settings Saved",
"settings_saved_message": "Auto-update settings have been saved successfully!",
"error_title": "Error",
"failed_save_settings": "Failed to save auto-update settings.",
"error_saving_settings": "Error saving auto-update settings: {error}",
"auto_update_title": "Auto-Update Settings",
"auto_update_header": "🔄 Auto-Update Settings",
"auto_update_description": "Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.",
"current_status": "Current Status",
"current_version_label": "Current yt-dlp version: Checking...",
"last_check_label": "Last update check: Never",
"next_check_label": "Next check: Based on settings",
"manual_check_button": "🔍 Check for Updates Now",
"update_frequency_group": "Update Frequency",
"save_settings": "Save Settings",
"settings_saved_successfully": "✅ Settings saved successfully!",
"error_saving": "❌ Error saving settings: {error}"
},
"main_ui": {
"url_placeholder": "Enter YouTube video or playlist URL",
"merge_subtitles": "Merge Subtitles",
"save_thumbnail": "Save Thumbnail",
"save_description": "Save Description",
"embed_chapters": "Embed Chapters",
"subtitles_selected": "{count} selected",
"all_selected": "All selected",
"select_videos_all": "Select Videos... (All selected)",
"please_enter_url": "Please enter a URL first",
"cookie_file_selected_title": "Cookie File Selected",
"cookie_file_selected_message": "Cookie file selected: {path}",
"browser_cookies_selected_title": "Browser Cookies Selected",
"browser_cookies_selected_message": "Browser cookies will be extracted from: {browser}",
"error_no_format_info": "Error: No format information available.",
"error_extract_info": "Error: Could not extract basic video information. Please check your link.",
"analyzing_preparing": "Analyzing (0%)... Preparing request",
"analyzing_extracting_basic": "Analyzing (15%)... Extracting basic info",
"analyzing_extracting_detailed": "Analyzing (30%)... Extracting detailed info",
"analyzing_processing_video": "Analyzing (45%)... Processing video data",
"analyzing_processing_formats": "Analyzing (60%)... Processing formats",
"analyzing_loading_thumbnail": "Analyzing (75%)... Loading thumbnail",
"analyzing_processing_subtitles": "Analyzing (85%)... Processing subtitles",
"analyzing_updating_table": "Analyzing (95%)... Updating format table",
"analysis_complete": "Analysis complete!",
"analyzing_extracting_ytdlp": "Analyzing (30%)... Extracting info with yt-dlp executable",
"analyzing_processing_data": "Analyzing (60%)... Processing data",
"analyzing_processing_formats_ytdlp": "Analyzing (75%)... Processing formats",
"analyzing_loading_thumbnail_ytdlp": "Analyzing (85%)... Loading thumbnail",
"analyzing_processing_subtitles_ytdlp": "Analyzing (90%)... Processing subtitles",
"select_subtitles": "Select Subtitles...",
"sponsorblock_categories": "SponsorBlock Categories..."
},
"sponsorblock": {
"sponsor": "Sponsor",
"sponsor_desc": "Paid promotion, paid referrals and direct advertisements",
"selfpromo": "Unpaid/Self Promotion",
"selfpromo_desc": "Unpaid promotion of creators' own content",
"interaction": "Interaction Reminder",
"interaction_desc": "Asking viewers to like, subscribe, or follow social media",
"intro": "Intro",
"intro_desc": "Video introduction that can be skipped",
"outro": "Outro/End Cards",
"outro_desc": "Credits or when the video ends",
"preview": "Preview/Recap",
"preview_desc": "Quick recap of previous videos or preview of what's coming up",
"music_offtopic": "Non-Music Section",
"music_offtopic_desc": "Only for music videos. Marks non-music sections",
"filler": "Filler Tangent",
"filler_desc": "Tangential scenes added only for filler or humor"
},
"video_info": {
"channel": "Channel",
"views": "Views",
"likes": "Likes",
"upload_date": "Upload date",
"duration": "Duration",
"unknown_channel": "Unknown channel",
"unknown_date": "Unknown date",
"unknown_title": "Unknown title"
},
"command": {
"running": "Running...",
"run_command": "Run Command"
},
"selection": {
"none_selected": "0 selected",
"one_selected": "1 category selected",
"count_selected": "{count} selected"
},
"status": {
"ready": "Ready",
"file_exists": "⚠️ File already exists",
"cancelling": "Cancelling download..."
}
}
+313
View File
@@ -0,0 +1,313 @@
{
"language": {
"display_name": "Español (Spanish)",
"select_language": "Seleccionar idioma:",
"current_language": "Idioma actual: {language}",
"restart_required": "Los cambios de idioma tendrán efecto después de reiniciar la aplicación.",
"english": "Inglés",
"spanish": "Español (Spanish)"
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Listo"
},
"buttons": {
"download": "Descargar",
"pause": "Pausar",
"resume": "Reanudar",
"cancel": "Cancelar",
"browse": "Examinar",
"clear": "Limpiar",
"ok": "Aceptar",
"apply": "Aplicar",
"close": "Cerrar",
"run_command": "Ejecutar Comando",
"about": "Acerca de",
"update": "Actualizar",
"analyze": "Analizar",
"paste_url": "Pegar URL",
"select_videos": "Seleccionar Videos...",
"video": "Video",
"audio_only": "Solo Audio",
"custom_options": "Opciones Personalizadas",
"trim_video": "Recortar Video",
"download_settings": "Configuración de Descarga",
"select_defaults": "Seleccionar Predeterminados",
"select_all": "Seleccionar Todos",
"deselect_all": "Deseleccionar Todos"
},
"dialogs": {
"custom_options": "Opciones Personalizadas",
"settings": "Configuración",
"select_folder": "Seleccionar Carpeta de Descarga",
"sponsorblock_categories": "Categorías SponsorBlock",
"sponsorblock_description": "Selecciona qué tipos de segmentos de video se eliminarán automáticamente durante la descarga.\nSponsorBlock utiliza datos enviados por la comunidad para identificar estos segmentos.",
"select_subtitles": "Seleccionar Subtítulos",
"filter_languages_placeholder": "Filtrar idiomas (ej., en, es)...",
"no_subtitles_available": "No hay subtítulos disponibles",
"matching": "que coincidan con"
},
"tabs": {
"cookies": "Iniciar sesión con Cookies",
"custom_command": "Comando Personalizado",
"proxy": "Proxy",
"language": "Idioma"
},
"cookies": {
"help_text": "Elige cómo proporcionar cookies para iniciar sesión.\nEsto permite la descarga de videos privados y audio de calidad premium.",
"cookie_source": "Origen de Cookie",
"use_cookie_file": "Usar archivo de cookie (formato Netscape)",
"extract_from_browser": "Extraer cookies del navegador",
"cookie_file": "Archivo de Cookie",
"cookie_file_placeholder": "Ruta al archivo de cookies (formato Netscape)",
"browser_selection": "Selección de Navegador",
"browser_help": "Selecciona el navegador del cual extraer cookies. Asegúrate de que el navegador esté cerrado antes de la extracción.",
"browser_label": "Navegador:",
"profile_label": "Perfil (opcional):",
"profile_placeholder": "Nombre o ruta del perfil (dejar vacío para el predeterminado)",
"browser_extract_message": "Las cookies del navegador se extraerán al aplicar",
"file_selected_message": "Archivo de cookies seleccionado - Haz clic en Aceptar para aplicar",
"select_file_title": "Seleccionar Archivo de Cookies",
"file_filter": "Archivos de cookies (*.txt *.lwp)"
},
"custom_command": {
"help_text": "Ingresa tu comando yt-dlp personalizado a continuación. La URL actual se añadirá automáticamente.<br><br>Para ver la lista completa de opciones y ejemplos de uso, <a href=\"{docs_url}\">haz clic aquí para ver la documentación oficial de yt-dlp</a>.<br><br>Nota: La ruta de descarga y la plantilla de nombre de archivo se manejarán automáticamente.",
"input_label": "Argumentos de yt-dlp:",
"input_placeholder": "Ingresa argumentos de yt-dlp aquí...\n\nej. --extract-audio --audio-format mp3",
"output_label": "Salida del Comando:",
"output_placeholder": "La salida del comando aparecerá aquí...",
"full_command": "🔧 Comando completo: {command}",
"command_failed": "❌ El comando falló con código de salida {code}",
"command_success": "✅ ¡Comando completado exitosamente!",
"command_error": "❌ Error ejecutando comando: {error}"
},
"proxy": {
"help_text": "Configurar ajustes de proxy para conexiones de red y geo-verificación.\nEl proxy puede ayudar a evitar restricciones regionales y mejorar el rendimiento de descarga.",
"main_proxy": "Proxy Principal",
"main_proxy_help": "Usar el proxy HTTP/HTTPS/SOCKS especificado para todas las conexiones.",
"proxy_url_label": "URL del Proxy:",
"proxy_url_placeholder": "ej., http://proxy.example.com:8080 o socks5://user:pass@127.0.0.1:1080",
"proxy_examples": "Ejemplos: http://proxy.com:8080, https://proxy.com:8080, socks5://127.0.0.1:1080",
"geo_proxy": "Proxy de Geo-verificación",
"geo_proxy_help": "Usar este proxy para verificar dirección IP para sitios con restricciones geográficas. El proxy principal (si está configurado) se usa para la descarga real.",
"geo_proxy_url_label": "URL del Geo Proxy:",
"geo_proxy_url_placeholder": "ej., http://us-proxy.example.com:8080",
"clear_main_proxy": "Limpiar Proxy Principal",
"clear_geo_proxy": "Limpiar Geo Proxy",
"invalid_main_url": "Formato de URL de proxy principal inválido",
"invalid_geo_url": "Formato de URL de geo proxy inválido",
"main_configured": "Proxy principal configurado",
"geo_configured": "Geo proxy configurado"
},
"download": {
"preparing": "🚀 Preparando tu descarga...",
"completed": "✅ ¡Descarga completada!",
"video_completed": "✅ ¡Descarga de video completada!",
"audio_completed": "✅ ¡Descarga de audio completada!",
"subtitle_completed": "✅ ¡Descarga de subtítulos completada!",
"please_set_path": "Por favor establece una ruta de descarga usando 'Cambiar Ruta'",
"please_enter_url": "Por favor ingresa una URL",
"please_enter_url_and_path": "Por favor ingresa URL y establece ruta de descarga",
"please_select_format": "Por favor selecciona un formato"
},
"formats": {
"show_formats": "Mostrar formatos:",
"select": "Seleccionar",
"quality": "Calidad",
"extension": "Extensión",
"resolution": "Resolución",
"file_size": "Tamaño",
"codec": "Códec",
"audio": "Audio",
"notes": "Notas",
"will_merge_audio": "Se combinará audio",
"has_audio": "✓ Tiene Audio",
"audio_only": "Solo Audio",
"large_size": "Tamaño grande",
"medium_size": "Tamaño mediano",
"standard_size": "Tamaño estándar",
"compatible": "Compatible",
"efficient": "Eficiente",
"high_quality": "Alta calidad",
"best_4k": "Óptima (4K)",
"best_2k": "Óptima (2K)",
"high_1080p": "Alta (1080p)",
"high_720p": "Alta (720p)",
"medium_480p": "Media (480p)",
"low_quality": "Baja Calidad",
"best_audio": "Mejor Audio",
"high_audio": "Audio Alto",
"medium_audio": "Audio Medio",
"low_audio": "Audio Bajo",
"audio_only_resolution": "Solo audio"
},
"about": {
"title": "Acerca de YTSage",
"version": "Versión {version}",
"description": "Descargador moderno de YouTube con una interfaz limpia de PySide6.",
"author": "Por: {author}",
"github": "GitHub: {repo}",
"system_info": "Información del Sistema",
"loading": "🔄 Cargando información del sistema...",
"refresh": "🔄",
"refreshing": "🔄 Actualizando...",
"refresh_failed": "Actualización Fallida",
"refresh_failed_message": "No se pudo actualizar la información de versión.",
"detected": "✓ Detectado",
"missing": "✗ Faltante",
"not_available": "No Disponible"
},
"time_range": {
"title": "Descargar Sección del Video",
"help_text": "Descarga solo partes específicas de un video especificando rangos de tiempo.\nUsa formato HH:MM:SS o segundos. Deja inicio o fin vacío para descargar desde el principio o hasta el final.",
"time_range_group": "Rango de Tiempo",
"start_time": "Tiempo de Inicio:",
"end_time": "Tiempo de Fin:",
"start_time_placeholder": "00:00:00 (o dejar vacío para inicio)",
"end_time_placeholder": "00:10:00 (o dejar vacío para fin)",
"force_keyframes": "Forzar fotogramas clave en cortes (mejor precisión, más lento)"
},
"update": {
"title": "Actualizar yt-dlp",
"checking": "Verificando actualizaciones...",
"update_available": "¡Actualización disponible!\nVersión actual: {current}\nÚltima versión: {latest}",
"up_to_date": "yt-dlp está actualizado (versión {version})",
"could_not_determine": "No se pudieron determinar las versiones.",
"error_comparing": "Error al comparar versiones: {error}",
"update_available_failed": "¡Actualización disponible! (Comparación fallida)\nActual: {current}\nÚltima: {latest}",
"updating": "Actualizando...",
"initializing": "🚀 Inicializando proceso de actualización...",
"checking_current": "🔍 Verificando instalación actual...",
"found_at": "📍 Se encontró yt-dlp en: {path}",
"error_getting_path": "❌ Error al obtener la ruta de yt-dlp: {error}",
"updating_binary": "📦 Actualizando binario de yt-dlp administrado por la app...",
"updating_pip": "🐍 Actualizando yt-dlp del sistema vía pip...",
"update_failed": "❌ Falló la actualización de yt-dlp. Inténtalo de nuevo o verifica tu conexión a internet.",
"binary_updated": "✅ ¡Binario actualizado exitosamente!",
"update_failed_stderr": "❌ Falló la actualización de yt-dlp: {error}",
"update_timeout": "❌ Tiempo agotado en la actualización de yt-dlp.",
"unexpected_error": "❌ Error inesperado durante la actualización: {error}",
"checking_pip": "🔍 Verificando instalación actual de pip...",
"current_version": "📋 Versión actual: {version}",
"not_found_pip": "⚠️ yt-dlp no encontrado vía pip, intentando instalación...",
"checking_latest": "🌐 Verificando última versión...",
"failed_check_updates": "❌ Falló la verificación de actualizaciones",
"latest_version": "🆕 Última versión: {version}",
"updating_from_to": "⬆️ Actualizando de {current} a {latest}...",
"running_pip_install": "📦 Ejecutando pip install --upgrade...",
"pip_completed": "✅ ¡Actualización de pip completada exitosamente!",
"pip_failed": "❌ Actualización de pip falló: {error}"
},
"settings": {
"title": "Configuración de Descarga",
"download_path": "Ruta de Descarga",
"browse": "Examinar...",
"speed_limit": "Límite de Velocidad",
"speed_limit_placeholder": "Ninguno",
"auto_update_ytdlp": "Auto-Actualizar yt-dlp",
"enable_auto_updates": "Habilitar actualizaciones automáticas de yt-dlp",
"update_frequency": "Frecuencia de actualización:",
"check_startup": "Verificar en cada inicio (mínimo 1 hora entre verificaciones)",
"check_daily": "Verificar diariamente",
"check_weekly": "Verificar semanalmente",
"check_updates_now": "Verificar Actualizaciones Ahora",
"update_check_title": "Verificación de Actualizaciones",
"could_not_determine_version": "No se pudo determinar la versión actual de yt-dlp.",
"update_available_dialog": "¡Actualización disponible!\n\nActual: {current}\nÚltima: {latest}\n\nUsa el botón 'Actualizar yt-dlp' en la ventana principal para actualizar.",
"up_to_date_dialog": "¡yt-dlp está actualizado!\n\nVersión actual: {version}",
"error_checking_updates": "Error al verificar actualizaciones: {error}",
"settings_saved_title": "Configuración Guardada",
"settings_saved_message": "¡La configuración de auto-actualización se ha guardado exitosamente!",
"error_title": "Error",
"failed_save_settings": "Falló al guardar la configuración de auto-actualización.",
"error_saving_settings": "Error al guardar la configuración de auto-actualización: {error}",
"auto_update_title": "Configuración de Auto-Actualización",
"auto_update_header": "🔄 Configuración de Auto-Actualización",
"auto_update_description": "Configura las actualizaciones automáticas de yt-dlp para asegurar que siempre tengas las últimas funciones y correcciones de errores.",
"current_status": "Estado Actual",
"current_version_label": "Versión actual de yt-dlp: Verificando...",
"last_check_label": "Última verificación de actualización: Nunca",
"next_check_label": "Próxima verificación: Basado en configuración",
"manual_check_button": "🔍 Verificar Actualizaciones Ahora",
"update_frequency_group": "Frecuencia de Actualización",
"save_settings": "Guardar Configuración",
"settings_saved_successfully": "✅ ¡Configuración guardada exitosamente!",
"error_saving": "❌ Error al guardar configuración: {error}"
},
"main_ui": {
"url_placeholder": "Ingresa URL de video o lista de YouTube",
"merge_subtitles": "Combinar Subtítulos",
"save_thumbnail": "Guardar Miniatura",
"save_description": "Guardar Descripción",
"embed_chapters": "Incrustar Capítulos",
"subtitles_selected": "{count} seleccionados",
"all_selected": "Todos seleccionados",
"select_videos_all": "Seleccionar Videos... (Todos seleccionados)",
"please_enter_url": "Por favor ingresa primero una URL",
"cookie_file_selected_title": "Archivo de Cookies Seleccionado",
"cookie_file_selected_message": "Archivo de cookies seleccionado: {path}",
"browser_cookies_selected_title": "Cookies de Navegador Seleccionadas",
"browser_cookies_selected_message": "Las cookies del navegador serán extraídas de: {browser}",
"error_no_format_info": "Error: No hay información de formato disponible.",
"error_extract_info": "Error: No se pudo extraer la información básica del video. Por favor verifica tu enlace.",
"analyzing_preparing": "Analizando (0%)... Preparando solicitud",
"analyzing_extracting_basic": "Analizando (15%)... Extrayendo información básica",
"analyzing_extracting_detailed": "Analizando (30%)... Extrayendo información detallada",
"analyzing_processing_video": "Analizando (45%)... Procesando datos de video",
"analyzing_processing_formats": "Analizando (60%)... Procesando formatos",
"analyzing_loading_thumbnail": "Analizando (75%)... Cargando miniatura",
"analyzing_processing_subtitles": "Analizando (85%)... Procesando subtítulos",
"analyzing_updating_table": "Analizando (95%)... Actualizando tabla de formatos",
"analysis_complete": "¡Análisis completo!",
"analyzing_extracting_ytdlp": "Analizando (30%)... Extrayendo información con ejecutable yt-dlp",
"analyzing_processing_data": "Analizando (60%)... Procesando datos",
"analyzing_processing_formats_ytdlp": "Analizando (75%)... Procesando formatos",
"analyzing_loading_thumbnail_ytdlp": "Analizando (85%)... Cargando miniatura",
"analyzing_processing_subtitles_ytdlp": "Analizando (90%)... Procesando subtítulos",
"select_subtitles": "Seleccionar Subtítulos...",
"sponsorblock_categories": "Categorías SponsorBlock..."
},
"sponsorblock": {
"sponsor": "Patrocinador",
"sponsor_desc": "Promoción pagada, referencias pagadas y anuncios directos",
"selfpromo": "Autopromoción No Pagada",
"selfpromo_desc": "Promoción no pagada del propio contenido del creador",
"interaction": "Recordatorio de Interacción",
"interaction_desc": "Pedir a los espectadores que den like, se suscriban o sigan en redes sociales",
"intro": "Introducción",
"intro_desc": "Introducción del video que se puede omitir",
"outro": "Outro/Tarjetas Finales",
"outro_desc": "Créditos o cuando termina el video",
"preview": "Vista Previa/Resumen",
"preview_desc": "Resumen rápido de videos anteriores o vista previa de lo que viene",
"music_offtopic": "Sección No Musical",
"music_offtopic_desc": "Solo para videos musicales. Marca secciones no musicales",
"filler": "Relleno/Tangente",
"filler_desc": "Escenas tangenciales agregadas solo como relleno o humor"
},
"video_info": {
"channel": "Canal",
"views": "Visualizaciones",
"likes": "Me gusta",
"upload_date": "Fecha de subida",
"duration": "Duración",
"unknown_channel": "Canal desconocido",
"unknown_date": "Fecha desconocida",
"unknown_title": "Título desconocido"
},
"command": {
"running": "Ejecutando...",
"run_command": "Ejecutar Comando"
},
"selection": {
"none_selected": "0 seleccionados",
"one_selected": "1 categoría seleccionada",
"count_selected": "{count} seleccionados"
},
"status": {
"ready": "Listo",
"file_exists": "⚠️ El archivo ya existe",
"cancelling": "Cancelando descarga..."
}
}
@@ -19,6 +19,8 @@ from PySide6.QtWidgets import (
QWidget,
)
from src.utils.ytsage_localization import _
from src.core.ytsage_ffmpeg import get_ffmpeg_path
from src.core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, refresh_version_cache
from src.core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path
@@ -60,7 +62,7 @@ class AboutDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._parent = parent # Store parent to access version etc.
self.setWindowTitle("About YTSage")
self.setWindowTitle(_("about.title"))
self.setMinimumSize(460, 420) # Slightly increased to accommodate paths
self.resize(460, 440) # Slightly increased initial size
self.setMaximumSize(500, 480) # Reasonable maximum size
@@ -157,13 +159,13 @@ class AboutDialog(QDialog):
layout.addWidget(title_label)
version_label = QLabel(
f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>Version {getattr(self._parent, 'version', '4.9.0b')}</span>"
f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>{_('about.version', version=getattr(self._parent, 'version', '4.9.0b'))}</span>"
)
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(version_label)
# Description - more compact
description_label = QLabel("Modern YouTube downloader with a clean PySide6 interface.")
description_label = QLabel(_("about.description"))
description_label.setWordWrap(True)
description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;")
@@ -174,13 +176,13 @@ class AboutDialog(QDialog):
info_layout.setSpacing(15)
author_label = QLabel(
"By: <a href='https://github.com/oop7/' style='color: #c90000; text-decoration: none; font-size: 10px;'>oop7</a>"
f"{_('about.author', author='<a href=\'https://github.com/oop7/\' style=\'color: #c90000; text-decoration: none; font-size: 10px;\'>oop7</a>')}"
)
author_label.setOpenExternalLinks(True)
info_layout.addWidget(author_label)
repo_label = QLabel(
"GitHub: <a href='https://github.com/oop7/YTSage/' style='color: #c90000; text-decoration: none; font-size: 10px;'>YTSage</a>"
f"{_('about.github', repo='<a href=\'https://github.com/oop7/YTSage/\' style=\'color: #c90000; text-decoration: none; font-size: 10px;\'>YTSage</a>')}"
)
repo_label.setOpenExternalLinks(True)
info_layout.addWidget(repo_label)
@@ -219,7 +221,7 @@ class AboutDialog(QDialog):
header_layout.setContentsMargins(0, 0, 0, 5)
# System Information title
title_label = QLabel("System Information")
title_label = QLabel(_("about.system_info"))
title_label.setStyleSheet(
"""
QLabel {
@@ -237,7 +239,7 @@ class AboutDialog(QDialog):
header_layout.addStretch()
# Create refresh button
self.refresh_btn = QPushButton("🔄")
self.refresh_btn = QPushButton(_("about.refresh"))
self.refresh_btn.setFixedSize(16, 16)
self.refresh_btn.setStyleSheet(
"""
@@ -283,7 +285,7 @@ class AboutDialog(QDialog):
def _show_loading_message(self) -> None:
"""Show a compact loading message while system information is being gathered."""
loading_label = QLabel("🔄 Loading system information...")
loading_label = QLabel(_("about.loading"))
loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
loading_label.setStyleSheet(
"""
@@ -381,7 +383,7 @@ class AboutDialog(QDialog):
# yt-dlp Status - compact version with path
ytdlp_found = check_ytdlp_installed()
ytdlp_status_text = (
"<span style='color: #4CAF50;'>✓ Detected</span>" if ytdlp_found else "<span style='color: #F44336;'>✗ Missing</span>"
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if ytdlp_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
)
ytdlp_version = get_ytdlp_version()
@@ -409,11 +411,11 @@ class AboutDialog(QDialog):
# FFmpeg Status - compact version with path
ffmpeg_found = check_ffmpeg()
ffmpeg_status_text = (
"<span style='color: #4CAF50;'>✓ Detected</span>"
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>"
if ffmpeg_found
else "<span style='color: #F44336;'>✗ Missing</span>"
else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
)
ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else "Not Available"
ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else _('about.not_available')
# Get FFmpeg path
ffmpeg_path_text = None
@@ -440,7 +442,7 @@ class AboutDialog(QDialog):
def refresh_version_info(self) -> None:
"""Refresh version information manually."""
self.refresh_btn.setText("🔄 Refreshing...")
self.refresh_btn.setText(_('about.refreshing'))
self.refresh_btn.setEnabled(False)
# Perform refresh in a separate thread to avoid blocking UI
@@ -457,7 +459,7 @@ class AboutDialog(QDialog):
def on_refresh_finished(self, success) -> None:
"""Handle refresh completion."""
self.refresh_btn.setText("🔄 Refresh")
self.refresh_btn.setText(_('about.refresh'))
self.refresh_btn.setEnabled(True)
if success:
@@ -466,8 +468,8 @@ class AboutDialog(QDialog):
# Show error message with proper styling
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle("Refresh Failed")
msg_box.setText("Failed to refresh version information.")
msg_box.setWindowTitle(_('about.refresh_failed'))
msg_box.setText(_('about.refresh_failed_message'))
msg_box.setWindowIcon(self.windowIcon())
msg_box.setStyleSheet(
"""
@@ -31,6 +31,8 @@ from PySide6.QtWidgets import (
from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import YTDLP_DOCS_URL
from src.utils.ytsage_config_manager import ConfigManager
from src.utils.ytsage_localization import LocalizationManager, _
from src.utils.ytsage_logger import logger
if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
@@ -68,7 +70,7 @@ class CommandWorker(QObject):
base_cmd.append(self.url)
# Emit the full command
self.output_received.emit(f"🔧 Full command: {' '.join(str(cmd) for cmd in base_cmd)}")
self.output_received.emit(_('custom_command.full_command', command=' '.join(str(cmd) for cmd in base_cmd)))
self.output_received.emit("=" * 50)
# Run the command
@@ -90,22 +92,22 @@ class CommandWorker(QObject):
self.output_received.emit("=" * 50)
if ret != 0:
self.output_received.emit(f"❌ Command failed with exit code {ret}")
self.output_received.emit(_('custom_command.command_failed', code=ret))
self.command_finished.emit(False, ret)
else:
self.output_received.emit("✅ Command completed successfully!")
self.output_received.emit(_('custom_command.command_success'))
self.command_finished.emit(True, ret)
except Exception as e:
self.output_received.emit("=" * 50)
self.error_occurred.emit(f"❌ Error executing command: {str(e)}")
self.error_occurred.emit(_('custom_command.command_error', error=str(e)))
class CustomOptionsDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._parent: YTSageApp = cast("YTSageApp", self.parent()) # cast will help with auto complete and type hint checking.
self.setWindowTitle("Custom Options")
self.setWindowTitle(_("dialogs.custom_options"))
self.setMinimumSize(550, 400) # Made even shorter
layout = QVBoxLayout(self)
@@ -118,38 +120,35 @@ class CustomOptionsDialog(QDialog):
cookies_layout = QVBoxLayout(cookies_tab)
# Help text
help_text = QLabel(
"Choose how to provide cookies for logging in.\n"
"This allows downloading of private videos and premium quality audio."
)
help_text = QLabel(_('cookies.help_text'))
help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;")
cookies_layout.addWidget(help_text)
# Cookie source selection
cookie_source_group = QGroupBox("Cookie Source")
cookie_source_group = QGroupBox(_('cookies.cookie_source'))
cookie_source_layout = QVBoxLayout(cookie_source_group)
# Radio buttons for cookie source
self.cookie_file_radio = QRadioButton("Use cookie file (Netscape format)")
self.cookie_file_radio = QRadioButton(_('cookies.use_cookie_file'))
self.cookie_file_radio.setChecked(True)
self.cookie_file_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_file_radio)
self.cookie_browser_radio = QRadioButton("Extract cookies from browser")
self.cookie_browser_radio = QRadioButton(_('cookies.extract_from_browser'))
self.cookie_browser_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_browser_radio)
cookies_layout.addWidget(cookie_source_group)
# Cookie file section
self.cookie_file_group = QGroupBox("Cookie File")
self.cookie_file_group = QGroupBox(_('cookies.cookie_file'))
file_layout = QVBoxLayout(self.cookie_file_group)
# File path input and browse button
path_layout = QHBoxLayout()
self.cookie_path_input = QLineEdit()
self.cookie_path_input.setPlaceholderText("Path to cookies file (Netscape format)")
self.cookie_path_input.setPlaceholderText(_('cookies.cookie_file_placeholder'))
if hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
# Convert Path to string properly and validate
cookie_path_str = str(self._parent.cookie_file_path)
@@ -158,7 +157,7 @@ class CustomOptionsDialog(QDialog):
self.cookie_path_input.setText(cookie_path_str)
path_layout.addWidget(self.cookie_path_input)
self.browse_button = QPushButton("Browse")
self.browse_button = QPushButton(_('buttons.browse'))
self.browse_button.clicked.connect(self.browse_cookie_file)
path_layout.addWidget(self.browse_button)
file_layout.addLayout(path_layout)
@@ -166,16 +165,16 @@ class CustomOptionsDialog(QDialog):
cookies_layout.addWidget(self.cookie_file_group)
# Browser selection section
self.cookie_browser_group = QGroupBox("Browser Selection")
self.cookie_browser_group = QGroupBox(_('cookies.browser_selection'))
browser_layout = QVBoxLayout(self.cookie_browser_group)
browser_help = QLabel("Select the browser to extract cookies from. Make sure the browser is closed before extraction.")
browser_help = QLabel(_('cookies.browser_help'))
browser_help.setWordWrap(True)
browser_help.setStyleSheet("color: #999999; font-size: 11px;")
browser_layout.addWidget(browser_help)
browser_select_layout = QHBoxLayout()
browser_select_layout.addWidget(QLabel("Browser:"))
browser_select_layout.addWidget(QLabel(_('cookies.browser_label')))
self.browser_combo = QComboBox()
self.browser_combo.addItems(["chrome", "firefox", "safari", "edge", "opera", "brave", "chromium", "vivaldi"])
@@ -184,9 +183,9 @@ class CustomOptionsDialog(QDialog):
# Optional profile field
profile_layout = QHBoxLayout()
profile_layout.addWidget(QLabel("Profile (optional):"))
profile_layout.addWidget(QLabel(_('cookies.profile_label')))
self.profile_input = QLineEdit()
self.profile_input.setPlaceholderText("Profile name or path (leave empty for default)")
self.profile_input.setPlaceholderText(_('cookies.profile_placeholder'))
profile_layout.addWidget(self.profile_input)
browser_layout.addLayout(profile_layout)
@@ -207,12 +206,7 @@ class CustomOptionsDialog(QDialog):
command_layout = QVBoxLayout(command_tab)
# Improved help text
cmd_help_text = QLabel(
"Enter your custom yt-dlp command below. The current URL will be automatically appended.<br><br>"
"For complete list of options and usage examples, "
f'<a href="{YTDLP_DOCS_URL}">click here to view the official yt-dlp documentation</a>.<br><br>'
"Note: Download path and output filename template will be automatically handled."
)
cmd_help_text = QLabel(_('custom_command.help_text', docs_url=YTDLP_DOCS_URL))
cmd_help_text.setWordWrap(True)
cmd_help_text.setOpenExternalLinks(True) # Enable clicking links
cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
@@ -238,13 +232,13 @@ class CustomOptionsDialog(QDialog):
command_layout.addWidget(cmd_help_text)
# Command input label
input_label = QLabel("yt-dlp Arguments:")
input_label = QLabel(_('custom_command.input_label'))
input_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 10px;")
command_layout.addWidget(input_label)
# Command input
self.command_input = QPlainTextEdit()
self.command_input.setPlaceholderText("Enter yt-dlp arguments here...\n\n" "e.g. --extract-audio --audio-format mp3")
self.command_input.setPlaceholderText(_('custom_command.input_placeholder'))
self.command_input.setMinimumHeight(80) # Reduced further from 100
self.command_input.setStyleSheet(
"""
@@ -269,7 +263,7 @@ class CustomOptionsDialog(QDialog):
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
clear_btn = QPushButton("Clear")
clear_btn = QPushButton(_('buttons.clear'))
clear_btn.clicked.connect(lambda: self.command_input.clear())
clear_btn.setStyleSheet(
"""
@@ -292,7 +286,7 @@ class CustomOptionsDialog(QDialog):
button_layout.addStretch() # Push run button to the right
# Run command button
self.run_btn = QPushButton("Run Command")
self.run_btn = QPushButton(_('buttons.run_command'))
self.run_btn.clicked.connect(self.run_custom_command)
self.run_btn.setDefault(True)
button_layout.addWidget(self.run_btn)
@@ -300,14 +294,14 @@ class CustomOptionsDialog(QDialog):
command_layout.addLayout(button_layout)
# Output label
output_label = QLabel("Command Output:")
output_label = QLabel(_('custom_command.output_label'))
output_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 15px;")
command_layout.addWidget(output_label)
# Log output
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setPlaceholderText("Command output will appear here...")
self.log_output.setPlaceholderText(_('custom_command.output_placeholder'))
self.log_output.setMinimumHeight(100) # Reduced further from 120
self.log_output.setStyleSheet(
"""
@@ -332,56 +326,50 @@ class CustomOptionsDialog(QDialog):
proxy_layout = QVBoxLayout(proxy_tab)
# Help text
proxy_help_text = QLabel(
"Configure proxy settings for network connections and geo-verification.\n"
"Proxy can help bypass regional restrictions and improve download performance."
)
proxy_help_text = QLabel(_('proxy.help_text'))
proxy_help_text.setWordWrap(True)
proxy_help_text.setStyleSheet("color: #999999; padding: 10px;")
proxy_layout.addWidget(proxy_help_text)
# Main Proxy section
main_proxy_group = QGroupBox("Main Proxy")
main_proxy_group = QGroupBox(_('proxy.main_proxy'))
main_proxy_layout = QVBoxLayout(main_proxy_group)
main_proxy_help = QLabel("Use the specified HTTP/HTTPS/SOCKS proxy for all connections.")
main_proxy_help = QLabel(_('proxy.main_proxy_help'))
main_proxy_help.setWordWrap(True)
main_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
main_proxy_layout.addWidget(main_proxy_help)
# Main proxy input
main_proxy_input_layout = QHBoxLayout()
main_proxy_input_layout.addWidget(QLabel("Proxy URL:"))
main_proxy_input_layout.addWidget(QLabel(_('proxy.proxy_url_label')))
self.proxy_url_input = QLineEdit()
self.proxy_url_input.setPlaceholderText("e.g., http://proxy.example.com:8080 or socks5://user:pass@127.0.0.1:1080")
self.proxy_url_input.setPlaceholderText(_('proxy.proxy_url_placeholder'))
self.proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
main_proxy_input_layout.addWidget(self.proxy_url_input)
main_proxy_layout.addLayout(main_proxy_input_layout)
# Example text
example_label = QLabel("Examples: http://proxy.com:8080, https://proxy.com:8080, socks5://127.0.0.1:1080")
example_label = QLabel(_('proxy.proxy_examples'))
example_label.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;")
main_proxy_layout.addWidget(example_label)
proxy_layout.addWidget(main_proxy_group)
# Geo-verification Proxy section
geo_proxy_group = QGroupBox("Geo-verification Proxy")
geo_proxy_group = QGroupBox(_('proxy.geo_proxy'))
geo_proxy_layout = QVBoxLayout(geo_proxy_group)
geo_proxy_help = QLabel(
"Use this proxy to verify IP address for geo-restricted sites. "
"The main proxy (if set) is used for actual downloading."
)
geo_proxy_help = QLabel(_('proxy.geo_proxy_help'))
geo_proxy_help.setWordWrap(True)
geo_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
geo_proxy_layout.addWidget(geo_proxy_help)
# Geo proxy input
geo_proxy_input_layout = QHBoxLayout()
geo_proxy_input_layout.addWidget(QLabel("Geo Proxy URL:"))
geo_proxy_input_layout.addWidget(QLabel(_('proxy.geo_proxy_url_label')))
self.geo_proxy_url_input = QLineEdit()
self.geo_proxy_url_input.setPlaceholderText("e.g., http://us-proxy.example.com:8080")
self.geo_proxy_url_input.setPlaceholderText(_('proxy.geo_proxy_url_placeholder'))
self.geo_proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
geo_proxy_input_layout.addWidget(self.geo_proxy_url_input)
geo_proxy_layout.addLayout(geo_proxy_input_layout)
@@ -395,7 +383,7 @@ class CustomOptionsDialog(QDialog):
# Clear buttons
clear_layout = QHBoxLayout()
clear_main_proxy_btn = QPushButton("Clear Main Proxy")
clear_main_proxy_btn = QPushButton(_('proxy.clear_main_proxy'))
clear_main_proxy_btn.clicked.connect(lambda: self.proxy_url_input.clear())
clear_main_proxy_btn.setStyleSheet(
"""
@@ -414,7 +402,7 @@ class CustomOptionsDialog(QDialog):
)
clear_layout.addWidget(clear_main_proxy_btn)
clear_geo_proxy_btn = QPushButton("Clear Geo Proxy")
clear_geo_proxy_btn = QPushButton(_('proxy.clear_geo_proxy'))
clear_geo_proxy_btn.clicked.connect(lambda: self.geo_proxy_url_input.clear())
clear_geo_proxy_btn.setStyleSheet(
"""
@@ -438,13 +426,75 @@ class CustomOptionsDialog(QDialog):
proxy_layout.addStretch()
# === Language Tab ===
language_tab = QWidget()
language_layout = QVBoxLayout(language_tab)
# Help text
language_help_text = QLabel(_("language.select_language"))
language_help_text.setWordWrap(True)
language_help_text.setStyleSheet("color: #ffffff; font-size: 14px; font-weight: bold; padding: 10px;")
language_layout.addWidget(language_help_text)
# Current language info
current_lang = ConfigManager.get("language") or "en"
available_languages = LocalizationManager.get_available_languages()
current_lang_display = available_languages.get(current_lang, current_lang.upper())
current_lang_label = QLabel(_("language.current_language", language=current_lang_display))
current_lang_label.setWordWrap(True)
current_lang_label.setStyleSheet("color: #999999; padding: 10px;")
language_layout.addWidget(current_lang_label)
# Language selection group
language_group = QGroupBox(_("language.select_language"))
language_group_layout = QVBoxLayout(language_group)
# Language selection combo box
language_select_layout = QHBoxLayout()
language_select_layout.addWidget(QLabel(_("language.select_language") + ":"))
self.language_combo = QComboBox()
# Populate language combo with available languages
for lang_code, display_name in available_languages.items():
self.language_combo.addItem(display_name, lang_code)
# Set current selection
current_index = self.language_combo.findData(current_lang)
if current_index >= 0:
self.language_combo.setCurrentIndex(current_index)
# Connect language change event
self.language_combo.currentIndexChanged.connect(self.on_language_changed)
language_select_layout.addWidget(self.language_combo)
language_group_layout.addLayout(language_select_layout)
language_layout.addWidget(language_group)
# Restart notice
self.restart_notice = QLabel(_("language.restart_required"))
self.restart_notice.setWordWrap(True)
self.restart_notice.setStyleSheet(
"color: #ffaa00; font-style: italic; padding: 10px; "
"background-color: #2a2d36; border-radius: 6px; margin: 10px;"
)
self.restart_notice.setVisible(False) # Initially hidden
language_layout.addWidget(self.restart_notice)
language_layout.addStretch()
# Add tabs to the tab widget
self.tab_widget.addTab(cookies_tab, "Login with Cookies")
self.tab_widget.addTab(command_tab, "Custom Command")
self.tab_widget.addTab(proxy_tab, "Proxy")
self.tab_widget.addTab(cookies_tab, _("tabs.cookies"))
self.tab_widget.addTab(command_tab, _("tabs.custom_command"))
self.tab_widget.addTab(proxy_tab, _("tabs.proxy"))
self.tab_widget.addTab(language_tab, _("tabs.language"))
# Dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
@@ -524,7 +574,6 @@ class CustomOptionsDialog(QDialog):
border: none;
width: 12px;
height: 12px;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgNEw2IDdMOSA0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
}
QComboBox QAbstractItemView {
background-color: #1d1e22;
@@ -610,18 +659,18 @@ class CustomOptionsDialog(QDialog):
else:
self.cookie_file_group.setVisible(False)
self.cookie_browser_group.setVisible(True)
self.cookie_status.setText("Browser cookies will be extracted when applied")
self.cookie_status.setText(_("cookies.browser_extract_message"))
self.cookie_status.setStyleSheet("color: #ffaa00; font-style: italic;")
def browse_cookie_file(self) -> None:
# Open file dialog to select cookie file
selected_files, _ = QFileDialog.getOpenFileName(self, "Select Cookie File", "", "Cookies files (*.txt *.lwp)")
selected_files, _ = QFileDialog.getOpenFileName(self, _("cookies.select_file_title"), "", _("cookies.file_filter"))
if selected_files:
# Ensure we have a valid full path
cookie_path = Path(selected_files).resolve()
self.cookie_path_input.setText(str(cookie_path))
self.cookie_status.setText("Cookie file selected - Click OK to apply")
self.cookie_status.setText(_("cookies.file_selected_message"))
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
def get_cookie_file_path(self) -> Path | None:
@@ -760,7 +809,7 @@ class CustomOptionsDialog(QDialog):
self.log_output.append(f"📁 Download path: {path}")
self.log_output.append("=" * 50)
self.run_btn.setEnabled(False)
self.run_btn.setText("Running...")
self.run_btn.setText(_("command.running"))
# Create worker and thread
self.worker = CommandWorker(command, url, path)
@@ -781,49 +830,63 @@ class CustomOptionsDialog(QDialog):
def on_command_finished(self, success: bool, exit_code: int):
"""Slot for when command finishes"""
self.run_btn.setEnabled(True)
self.run_btn.setText("Run Command")
self.run_btn.setText(_("command.run_command"))
def on_error_occurred(self, error_msg: str):
"""Slot for handling errors"""
self.log_output.append(error_msg)
self.run_btn.setEnabled(True)
self.run_btn.setText("Run Command")
self.run_btn.setText(_("buttons.run_command"))
def on_language_changed(self) -> None:
"""Handle language selection change"""
selected_lang_code = self.language_combo.currentData()
if selected_lang_code:
current_lang = ConfigManager.get("language") or "en"
if selected_lang_code != current_lang:
# Save the new language preference
ConfigManager.set("language", selected_lang_code)
# Update LocalizationManager
LocalizationManager.set_language(selected_lang_code)
# Show restart notice
self.restart_notice.setVisible(True)
logger.info(f"Language changed to: {selected_lang_code}")
class TimeRangeDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Download Video Section")
self.setWindowTitle(_('time_range.title'))
self.setMinimumWidth(400)
layout = QVBoxLayout(self)
# Help text explaining the feature
help_text = QLabel(
"Download only specific parts of a video by specifying time ranges.\n"
"Use HH:MM:SS format or seconds. Leave start or end empty to download from beginning or to end."
)
help_text = QLabel(_('time_range.help_text'))
help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;")
layout.addWidget(help_text)
# Time range section
time_group = QGroupBox("Time Range")
time_group = QGroupBox(_('time_range.time_range_group'))
time_layout = QVBoxLayout()
# Start time row
start_layout = QHBoxLayout()
start_layout.addWidget(QLabel("Start Time:"))
start_layout.addWidget(QLabel(_('time_range.start_time')))
self.start_time_input = QLineEdit()
self.start_time_input.setPlaceholderText("00:00:00 (or leave empty for start)")
self.start_time_input.setPlaceholderText(_('time_range.start_time_placeholder'))
start_layout.addWidget(self.start_time_input)
time_layout.addLayout(start_layout)
# End time row
end_layout = QHBoxLayout()
end_layout.addWidget(QLabel("End Time:"))
end_layout.addWidget(QLabel(_('time_range.end_time')))
self.end_time_input = QLineEdit()
self.end_time_input.setPlaceholderText("00:10:00 (or leave empty for end)")
self.end_time_input.setPlaceholderText(_('time_range.end_time_placeholder'))
end_layout.addWidget(self.end_time_input)
time_layout.addLayout(end_layout)
@@ -831,7 +894,7 @@ class TimeRangeDialog(QDialog):
layout.addWidget(time_group)
# Force keyframes option
self.force_keyframes = QCheckBox("Force keyframes at cuts (better accuracy, slower)")
self.force_keyframes = QCheckBox(_('time_range.force_keyframes'))
self.force_keyframes.setChecked(True)
self.force_keyframes.setStyleSheet(
"""
@@ -859,7 +922,9 @@ class TimeRangeDialog(QDialog):
layout.addWidget(self.force_keyframes)
# Buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
@@ -17,11 +17,13 @@ from PySide6.QtWidgets import (
QWidget,
)
from src.utils.ytsage_localization import _
class SubtitleSelectionDialog(QDialog):
def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Select Subtitles")
self.setWindowTitle(_("dialogs.select_subtitles"))
self.setMinimumWidth(400)
self.setMinimumHeight(300)
@@ -35,7 +37,7 @@ class SubtitleSelectionDialog(QDialog):
# Filter input
self.filter_input = QLineEdit()
self.filter_input.setPlaceholderText("Filter languages (e.g., en, es)...")
self.filter_input.setPlaceholderText(_("dialogs.filter_languages_placeholder"))
self.filter_input.textChanged.connect(self.filter_list)
self.filter_input.setStyleSheet(
"""
@@ -72,7 +74,9 @@ class SubtitleSelectionDialog(QDialog):
self.populate_list()
# OK and Cancel buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
@@ -128,7 +132,8 @@ class SubtitleSelectionDialog(QDialog):
combined_subs[lang_code] = f"{lang_code} - Auto-generated"
if not combined_subs:
no_subs_label = QLabel("No subtitles available" + (f" matching '{filter_text}'" if filter_text else ""))
matching_text = _("dialogs.matching") if filter_text else ""
no_subs_label = QLabel(_("dialogs.no_subtitles_available") + (f" {matching_text} '{filter_text}'" if filter_text else ""))
no_subs_label.setStyleSheet("color: #aaaaaa; padding: 10px;")
self.list_layout.addWidget(no_subs_label)
return
@@ -205,8 +210,8 @@ class PlaylistSelectionDialog(QDialog):
# Top buttons (Select/Deselect All)
button_layout = QHBoxLayout()
select_all_btn = QPushButton("Select All")
deselect_all_btn = QPushButton("Deselect All")
select_all_btn = QPushButton(_("buttons.select_all"))
deselect_all_btn = QPushButton(_("buttons.deselect_all"))
select_all_btn.clicked.connect(self._select_all)
deselect_all_btn.clicked.connect(self._deselect_all)
# Style the buttons to match the subtitle dialog
@@ -250,7 +255,9 @@ class PlaylistSelectionDialog(QDialog):
self._populate_list(previously_selected_string)
# Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
@@ -428,50 +435,50 @@ class SponsorBlockCategoryDialog(QDialog):
# Default SponsorBlock categories with descriptions
SPONSORBLOCK_CATEGORIES = {
"sponsor": {
"name": "Sponsor",
"description": "Paid promotion, paid referrals and direct advertisements",
"name_key": "sponsorblock.sponsor",
"description_key": "sponsorblock.sponsor_desc",
"default": True,
},
"selfpromo": {
"name": "Unpaid/Self Promotion",
"description": "Unpaid promotion of creators' own content",
"name_key": "sponsorblock.selfpromo",
"description_key": "sponsorblock.selfpromo_desc",
"default": True,
},
"interaction": {
"name": "Interaction Reminder",
"description": "Asking viewers to like, subscribe, or follow social media",
"name_key": "sponsorblock.interaction",
"description_key": "sponsorblock.interaction_desc",
"default": True,
},
"intro": {
"name": "Intro",
"description": "Video introduction that can be skipped",
"name_key": "sponsorblock.intro",
"description_key": "sponsorblock.intro_desc",
"default": False,
},
"outro": {
"name": "Outro/End Cards",
"description": "Credits or when the video ends",
"name_key": "sponsorblock.outro",
"description_key": "sponsorblock.outro_desc",
"default": False,
},
"preview": {
"name": "Preview/Recap",
"description": "Quick recap of previous videos or preview of what's coming up",
"name_key": "sponsorblock.preview",
"description_key": "sponsorblock.preview_desc",
"default": False,
},
"music_offtopic": {
"name": "Non-Music Section",
"description": "Only for music videos. Marks non-music sections",
"name_key": "sponsorblock.music_offtopic",
"description_key": "sponsorblock.music_offtopic_desc",
"default": False,
},
"filler": {
"name": "Filler Tangent",
"description": "Tangential scenes added only for filler or humor",
"name_key": "sponsorblock.filler",
"description_key": "sponsorblock.filler_desc",
"default": False,
},
}
def __init__(self, previously_selected=None, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("SponsorBlock Categories")
self.setWindowTitle(_("dialogs.sponsorblock_categories"))
self.setMinimumWidth(500)
self.setMinimumHeight(400)
@@ -489,15 +496,12 @@ class SponsorBlockCategoryDialog(QDialog):
layout = QVBoxLayout(self)
# Title and description
title_label = QLabel("SponsorBlock Categories")
title_label = QLabel(_("dialogs.sponsorblock_categories"))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;")
layout.addWidget(title_label)
desc_label = QLabel(
"Select which types of video segments to automatically remove during download.\n"
"SponsorBlock uses community-submitted data to identify these segments."
)
desc_label = QLabel(_("dialogs.sponsorblock_description"))
desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
@@ -521,8 +525,8 @@ class SponsorBlockCategoryDialog(QDialog):
category_layout.setContentsMargins(0, 0, 0, 0)
category_layout.setSpacing(2)
# Create checkbox with just the name
checkbox = QCheckBox(category_info["name"])
# Create checkbox with localized name
checkbox = QCheckBox(_(category_info["name_key"]))
checkbox.setProperty("category_id", category_id)
# Determine if this category should be checked
@@ -559,8 +563,8 @@ class SponsorBlockCategoryDialog(QDialog):
"""
)
# Create description label
desc_label = QLabel(category_info["description"])
# Create description label with localized text
desc_label = QLabel(_(category_info["description_key"]))
desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;")
desc_label.setWordWrap(True)
@@ -577,15 +581,15 @@ class SponsorBlockCategoryDialog(QDialog):
# Quick selection buttons
button_layout = QHBoxLayout()
select_defaults_btn = QPushButton("Select Defaults")
select_defaults_btn = QPushButton(_("buttons.select_defaults"))
select_defaults_btn.clicked.connect(self.select_defaults)
select_defaults_btn.setStyleSheet(self._get_button_style())
select_all_btn = QPushButton("Select All")
select_all_btn = QPushButton(_("buttons.select_all"))
select_all_btn.clicked.connect(self.select_all)
select_all_btn.setStyleSheet(self._get_button_style())
deselect_all_btn = QPushButton("Deselect All")
deselect_all_btn = QPushButton(_("buttons.deselect_all"))
deselect_all_btn.clicked.connect(self.deselect_all)
deselect_all_btn.setStyleSheet(self._get_button_style())
@@ -597,7 +601,9 @@ class SponsorBlockCategoryDialog(QDialog):
layout.addLayout(button_layout)
# Dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
@@ -34,12 +34,13 @@ from src.core.ytsage_utils import (
update_auto_update_settings,
)
from src.utils.ytsage_logger import logger
from src.utils.ytsage_localization import _
class DownloadSettingsDialog(QDialog):
def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Download Settings")
self.setWindowTitle(_("settings.title"))
self.setMinimumWidth(450)
self.setMinimumHeight(400)
self.current_path = current_path
@@ -163,7 +164,7 @@ class DownloadSettingsDialog(QDialog):
layout = QVBoxLayout(self)
# --- Download Path Section ---
path_group_box = QGroupBox("Download Path")
path_group_box = QGroupBox(_("settings.download_path"))
path_layout = QVBoxLayout()
self.path_display = QLabel(str(self.current_path))
@@ -173,7 +174,8 @@ class DownloadSettingsDialog(QDialog):
)
path_layout.addWidget(self.path_display)
browse_button = QPushButton("Browse...")
browse_button = QPushButton(_("settings.browse"))
browse_button.clicked.connect(self.browse_new_path)
path_layout.addWidget(browse_button)
@@ -181,11 +183,11 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(path_group_box)
# --- Speed Limit Section ---
speed_group_box = QGroupBox("Speed Limit")
speed_group_box = QGroupBox(_("settings.speed_limit"))
speed_layout = QHBoxLayout()
self.speed_limit_input = QLineEdit(str(self.current_limit))
self.speed_limit_input.setPlaceholderText("None")
self.speed_limit_input.setPlaceholderText(_("settings.speed_limit_placeholder"))
speed_layout.addWidget(self.speed_limit_input)
self.speed_limit_unit = QComboBox()
@@ -197,25 +199,25 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(speed_group_box)
# --- Auto-Update yt-dlp Section ---
auto_update_group_box = QGroupBox("Auto-Update yt-dlp")
auto_update_group_box = QGroupBox(_("settings.auto_update_ytdlp"))
auto_update_layout = QVBoxLayout()
# Load current auto-update settings
auto_settings = get_auto_update_settings()
# Enable/Disable auto-update checkbox
self.auto_update_enabled = QCheckBox("Enable automatic yt-dlp updates")
self.auto_update_enabled = QCheckBox(_("settings.enable_auto_updates"))
self.auto_update_enabled.setChecked(auto_settings["enabled"])
auto_update_layout.addWidget(self.auto_update_enabled)
# Frequency options
frequency_label = QLabel("Update frequency:")
frequency_label = QLabel(_("settings.update_frequency"))
frequency_label.setStyleSheet("color: #ffffff; margin-top: 10px;")
auto_update_layout.addWidget(frequency_label)
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)")
self.daily_radio = QRadioButton("Check daily")
self.weekly_radio = QRadioButton("Check weekly")
self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton(_("settings.check_weekly"))
# Set current selection based on saved settings
current_frequency = auto_settings["frequency"]
@@ -232,7 +234,7 @@ class DownloadSettingsDialog(QDialog):
# Test update button
test_update_layout = QHBoxLayout()
test_update_button = QPushButton("Check for Updates Now")
test_update_button = QPushButton(_("settings.check_updates_now"))
test_update_button.clicked.connect(self.test_update_check)
test_update_layout.addWidget(test_update_button)
test_update_layout.addStretch()
@@ -242,13 +244,15 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(auto_update_group_box)
# Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
def browse_new_path(self) -> None:
new_path = QFileDialog.getExistingDirectory(self, "Select Download Directory", str(self.current_path))
new_path = QFileDialog.getExistingDirectory(self, _("dialogs.select_folder"), str(self.current_path))
if new_path:
self.current_path = new_path
self.path_display.setText(self.current_path)
@@ -316,8 +320,8 @@ class DownloadSettingsDialog(QDialog):
if "Error" in current_version:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning,
"Update Check",
"Could not determine current yt-dlp version.",
_("settings.update_check_title"),
_("settings.could_not_determine_version"),
)
msg_box.exec()
return
@@ -334,22 +338,22 @@ class DownloadSettingsDialog(QDialog):
if version_parser.parse(latest_version) > version_parser.parse(current_version):
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information,
"Update Check",
f"Update available!\n\nCurrent: {current_version}\nLatest: {latest_version}\n\nUse the 'Update yt-dlp' button in the main window to update.",
_("settings.update_check_title"),
_("settings.update_available_dialog", current=current_version, latest=latest_version),
)
msg_box.exec()
else:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information,
"Update Check",
f"yt-dlp is up to date!\n\nCurrent version: {current_version}",
_("settings.update_check_title"),
_("settings.up_to_date_dialog", version=current_version),
)
msg_box.exec()
except Exception as e:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning,
"Update Check",
f"Error checking for updates: {e}",
_("settings.update_check_title"),
_("settings.error_checking_updates", error=str(e)),
)
msg_box.exec()
@@ -375,13 +379,13 @@ class DownloadSettingsDialog(QDialog):
if update_auto_update_settings(enabled, frequency):
QMessageBox.information(
self,
"Settings Saved",
"Auto-update settings have been saved successfully!",
_("settings.settings_saved_title"),
_("settings.settings_saved_message"),
)
else:
QMessageBox.warning(self, "Error", "Failed to save auto-update settings.")
QMessageBox.warning(self, _("settings.error_title"), _("settings.failed_save_settings"))
except Exception as e:
QMessageBox.critical(self, "Error", f"Error saving auto-update settings: {e}")
QMessageBox.critical(self, _("settings.error_title"), _("settings.error_saving_settings", error=str(e)))
# Call the parent accept method to close the dialog
super().accept()
@@ -390,7 +394,7 @@ class DownloadSettingsDialog(QDialog):
class AutoUpdateSettingsDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Auto-Update Settings")
self.setWindowTitle(_("settings.auto_update_title"))
self.setMinimumWidth(400)
self.setMinimumHeight(300)
@@ -406,32 +410,32 @@ class AutoUpdateSettingsDialog(QDialog):
layout = QVBoxLayout(self)
# Title
title_label = QLabel("<h2>🔄 Auto-Update Settings</h2>")
title_label = QLabel(f"<h2>{_("settings.auto_update_header")}</h2>")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
# Description
desc_label = QLabel("Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.")
desc_label = QLabel(_("settings.auto_update_description"))
desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
layout.addWidget(desc_label)
# Enable/Disable auto-update
self.enable_checkbox = QCheckBox("Enable automatic yt-dlp updates")
self.enable_checkbox = QCheckBox(_("settings.enable_auto_updates"))
self.enable_checkbox.setChecked(True) # Default enabled
self.enable_checkbox.toggled.connect(self.on_enable_toggled)
layout.addWidget(self.enable_checkbox)
# Frequency options
frequency_group = QGroupBox("Update Frequency")
frequency_group = QGroupBox(_("settings.update_frequency_group"))
frequency_layout = QVBoxLayout()
self.frequency_group = QButtonGroup(self)
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)")
self.daily_radio = QRadioButton("Check daily")
self.weekly_radio = QRadioButton("Check weekly")
self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton(_("settings.check_weekly"))
self.daily_radio.setChecked(True) # Default to daily
@@ -447,12 +451,12 @@ class AutoUpdateSettingsDialog(QDialog):
layout.addWidget(frequency_group)
# Current status
status_group = QGroupBox("Current Status")
status_group = QGroupBox(_("settings.current_status"))
status_layout = QVBoxLayout()
self.current_version_label = QLabel("Current yt-dlp version: Checking...")
self.last_check_label = QLabel("Last update check: Never")
self.next_check_label = QLabel("Next check: Based on settings")
self.current_version_label = QLabel(_("settings.current_version_label"))
self.last_check_label = QLabel(_("settings.last_check_label"))
self.next_check_label = QLabel(_("settings.next_check_label"))
status_layout.addWidget(self.current_version_label)
status_layout.addWidget(self.last_check_label)
@@ -462,17 +466,17 @@ class AutoUpdateSettingsDialog(QDialog):
layout.addWidget(status_group)
# Manual check button
self.manual_check_btn = QPushButton("🔍 Check for Updates Now")
self.manual_check_btn = QPushButton(_("settings.manual_check_button"))
self.manual_check_btn.clicked.connect(self.manual_check)
layout.addWidget(self.manual_check_btn)
# Buttons
button_layout = QHBoxLayout()
self.save_btn = QPushButton("Save Settings")
self.save_btn = QPushButton(_("settings.save_settings"))
self.save_btn.clicked.connect(self.save_settings)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn = QPushButton(_("buttons.cancel"))
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn)
@@ -721,19 +725,19 @@ class AutoUpdateSettingsDialog(QDialog):
if update_auto_update_settings(enabled, frequency):
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information,
"Settings Saved",
"✅ Auto-update settings have been saved successfully!",
_("settings.settings_saved_title"),
_("settings.settings_saved_successfully"),
)
msg_box.exec()
self.accept()
else:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning,
"Error",
"❌ Failed to save auto-update settings.\nPlease try again.",
_("settings.error_title"),
_("settings.failed_save_settings"),
)
msg_box.exec()
except Exception as e:
logger.exception(f"Error saving auto-update settings: {e}")
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, "Error", f"❌ Error saving settings: {e}")
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, _("settings.error_title"), _("settings", "error_saving", error=str(e)))
msg_box.exec()
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushB
from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config
from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL
from src.utils.ytsage_localization import _
from src.utils.ytsage_logger import logger
try:
@@ -118,15 +119,15 @@ class UpdateThread(QThread):
error_message = ""
success = False
try:
self.update_status.emit("🔍 Checking current installation...")
self.update_status.emit(_('update.checking_current'))
self.update_progress.emit(10)
# Get the yt-dlp path
try:
yt_dlp_path = get_yt_dlp_path()
self.update_status.emit(f"📍 Found yt-dlp at: {yt_dlp_path}")
self.update_status.emit(_('update.found_at', path=yt_dlp_path))
except Exception as e:
self.update_status.emit(f"❌ Error getting yt-dlp path: {e}")
self.update_status.emit(_('update.error_getting_path', error=e))
self.update_finished.emit(False, f"❌ Error getting yt-dlp path: {e}")
return
@@ -152,17 +153,17 @@ class UpdateThread(QThread):
is_app_managed = False
if is_app_managed:
self.update_status.emit("📦 Updating app-managed yt-dlp binary...")
self.update_status.emit(_('update.updating_binary'))
success = self._update_binary(yt_dlp_path)
else:
self.update_status.emit("🐍 Updating system yt-dlp via pip...")
self.update_status.emit(_('update.updating_pip'))
success = self._update_via_pip()
if success:
self.update_progress.emit(100)
error_message = "✅ yt-dlp has been successfully updated!"
else:
error_message = "❌ Failed to update yt-dlp. Please try again or check your internet connection."
error_message = _('update.update_failed')
except requests.RequestException as e:
error_message = f"❌ Network error during update: {e}"
@@ -196,61 +197,61 @@ class UpdateThread(QThread):
logger.info("UpdateThread: yt-dlp update completed successfully.")
if result.stdout:
logger.debug(f"yt-dlp output: {result.stdout.strip()}")
self.update_status.emit("✅ Binary successfully updated!")
self.update_status.emit(_('update.binary_updated'))
self.update_progress.emit(95)
return True
else:
logger.error(f"UpdateThread: yt-dlp update failed. {result.stderr.strip()}")
self.update_status.emit(f"❌ yt-dlp update failed: {result.stderr.strip()}")
self.update_status.emit(_('update.update_failed_stderr', error=result.stderr.strip()))
return False
except subprocess.TimeoutExpired:
logger.error("UpdateThread: yt-dlp update timed out.")
self.update_status.emit("❌ yt-dlp update timed out.")
self.update_status.emit(_('update.update_timeout'))
return False
except Exception as e:
logger.exception(f"UpdateThread: Unexpected error during update: {e}")
self.update_status.emit(f"❌ Unexpected error during update: {e}")
self.update_status.emit(_('update.unexpected_error', error=e))
return False
def _update_via_pip(self) -> bool:
"""Update yt-dlp via pip."""
try:
self.update_status.emit("🔍 Checking current pip installation...")
self.update_status.emit(_('update.checking_pip'))
self.update_progress.emit(30)
# Get current version
try:
current_version = get_version("yt-dlp")
self.update_status.emit(f"📋 Current version: {current_version}")
self.update_status.emit(_('update.current_version', version=current_version))
except PackageNotFoundError:
self.update_status.emit("⚠️ yt-dlp not found via pip, attempting installation...")
self.update_status.emit(_('update.not_found_pip'))
current_version = "0.0.0"
self.update_progress.emit(40)
# Get the latest version from PyPI
self.update_status.emit("🌐 Checking for latest version...")
self.update_status.emit(_('update.checking_latest'))
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
if response.status_code != 200:
self.update_status.emit("❌ Failed to check for updates")
self.update_status.emit(_('update.failed_check_updates'))
return False
data = response.json()
latest_version = data["info"]["version"]
self.update_status.emit(f"🆕 Latest version: {latest_version}")
self.update_status.emit(_('update.latest_version', version=latest_version))
self.update_progress.emit(50)
# Compare versions
if version.parse(latest_version) > version.parse(current_version):
self.update_status.emit(f"⬆️ Updating from {current_version} to {latest_version}...")
self.update_status.emit(_('update.updating_from_to', current=current_version, latest=latest_version))
self.update_progress.emit(60)
try:
# Run pip update with timeout
self.update_status.emit("📦 Running pip install --upgrade...")
self.update_status.emit(_('update.running_pip_install'))
update_result = subprocess.run(
[sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
capture_output=True,
@@ -263,11 +264,11 @@ class UpdateThread(QThread):
self.update_progress.emit(85)
if update_result.returncode == 0:
self.update_status.emit("✅ Pip update completed successfully!")
self.update_status.emit(_('update.pip_completed'))
self.update_progress.emit(95)
return True
else:
self.update_status.emit(f"❌ Pip update failed: {update_result.stderr}")
self.update_status.emit(_('update.pip_failed', error=update_result.stderr))
return False
except subprocess.TimeoutExpired:
@@ -289,7 +290,7 @@ class UpdateThread(QThread):
class YTDLPUpdateDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Update yt-dlp")
self.setWindowTitle(_('update.title'))
self.setMinimumWidth(450)
self.setMinimumHeight(200)
self._closing = False # Flag to track if dialog is closing
@@ -297,7 +298,7 @@ class YTDLPUpdateDialog(QDialog):
layout = QVBoxLayout(self)
# Status label
self.status_label = QLabel("Checking for updates...")
self.status_label = QLabel(_('update.checking'))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setWordWrap(True)
self.status_label.setMinimumHeight(60)
@@ -310,11 +311,11 @@ class YTDLPUpdateDialog(QDialog):
# Buttons
button_layout = QHBoxLayout()
self.update_btn = QPushButton("Update")
self.update_btn = QPushButton(_('buttons.update'))
self.update_btn.clicked.connect(self.perform_update)
self.update_btn.setEnabled(False)
self.close_btn = QPushButton("Close")
self.close_btn = QPushButton(_('buttons.close'))
self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.update_btn)
@@ -370,7 +371,7 @@ class YTDLPUpdateDialog(QDialog):
self.check_version()
def check_version(self) -> None:
self.status_label.setText("Checking for updates...")
self.status_label.setText(_('update.checking'))
self.update_btn.setEnabled(False)
self.version_check_thread = VersionCheckThread()
self.version_check_thread.finished.connect(self.on_version_check_finished)
@@ -387,7 +388,7 @@ class YTDLPUpdateDialog(QDialog):
return
if not current_version or not latest_version:
self.status_label.setText("Could not determine versions.")
self.status_label.setText(_('update.could_not_determine'))
self.update_btn.setEnabled(False)
return
@@ -398,7 +399,7 @@ class YTDLPUpdateDialog(QDialog):
if current_ver < latest_ver:
self.status_label.setText(
f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}"
_('update.update_available', current=current_version, latest=latest_version)
)
self.update_btn.setEnabled(True)
else:
@@ -408,22 +409,22 @@ class YTDLPUpdateDialog(QDialog):
# If version parsing fails, do a simple string comparison
if current_version != latest_version:
self.status_label.setText(
f"Update available! (Comparison failed)\nCurrent: {current_version}\nLatest: {latest_version}"
_('update.update_available_failed', current=current_version, latest=latest_version)
)
self.update_btn.setEnabled(True)
else:
self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
self.status_label.setText(_('update.up_to_date', version=current_version))
self.update_btn.setEnabled(False)
except Exception as e:
self.status_label.setText(f"Error comparing versions: {e}")
self.status_label.setText(_('update.error_comparing', error=e))
self.update_btn.setEnabled(False)
def perform_update(self) -> None:
# Immediate visual feedback
self.update_btn.setEnabled(False)
self.close_btn.setEnabled(False)
self.update_btn.setText("Updating...")
self.status_label.setText("🚀 Initializing update process...")
self.update_btn.setText(_('update.updating'))
self.status_label.setText(_('update.initializing'))
# Show progress bar immediately
self.progress_bar.setRange(0, 100)
@@ -461,7 +462,7 @@ class YTDLPUpdateDialog(QDialog):
self.progress_bar.setValue(100)
self.status_label.setText(message)
self.close_btn.setEnabled(True)
self.update_btn.setText("Update") # Reset button text
self.update_btn.setText(_('buttons.update')) # Reset button text
if success:
# Show success briefly then auto-check version
+43 -41
View File
@@ -4,6 +4,8 @@ from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtGui import QColor
from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QHeaderView, QSizePolicy, QTableWidget, QTableWidgetItem, QWidget
from src.utils.ytsage_localization import _
if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp
@@ -22,14 +24,14 @@ class FormatTableMixin:
self.format_table.setColumnCount(8)
self.format_table.setHorizontalHeaderLabels(
[
"Select",
"Quality",
"Extension",
"Resolution",
"File Size",
"Codec",
"Audio",
"Notes",
_("formats.select"),
_("formats.quality"),
_("formats.extension"),
_("formats.resolution"),
_("formats.file_size"),
_("formats.codec"),
_("formats.audio"),
_("formats.notes"),
]
)
@@ -183,7 +185,7 @@ class FormatTableMixin:
# Configure columns based on mode
if is_playlist_mode:
self.format_table.setColumnCount(5)
self.format_table.setHorizontalHeaderLabels(["Select", "Quality", "Resolution", "Notes", "Audio"])
self.format_table.setHorizontalHeaderLabels([_("formats.select"), _("formats.quality"), _("formats.resolution"), _("formats.notes"), _("formats.audio")])
# Configure column visibility and resizing for playlist mode
self.format_table.setColumnHidden(5, True)
@@ -202,14 +204,14 @@ class FormatTableMixin:
self.format_table.setColumnCount(8)
self.format_table.setHorizontalHeaderLabels(
[
"Select",
"Quality",
"Extension",
"Resolution",
"File Size",
"Codec",
"Audio",
"Notes",
_("formats.select"),
_("formats.quality"),
_("formats.extension"),
_("formats.resolution"),
_("formats.file_size"),
_("formats.codec"),
_("formats.audio"),
_("formats.notes"),
]
)
# Ensure all columns are visible
@@ -262,14 +264,14 @@ class FormatTableMixin:
# Column 1: Quality (Always shown)
quality_text = self.get_quality_label(f)
quality_item = QTableWidgetItem(quality_text)
# Set color based on quality
if "Best" in quality_text:
# Set color based on quality (check both English and Spanish terms)
if any(term in quality_text for term in ["Best", "Óptima", "Mejor"]):
quality_item.setForeground(QColor("#00ff00")) # Green for best quality
elif "High" in quality_text:
elif any(term in quality_text for term in ["High", "Alta", "Alto"]):
quality_item.setForeground(QColor("#00cc00")) # Light green for high quality
elif "Medium" in quality_text:
elif any(term in quality_text for term in ["Medium", "Media", "Medio"]):
quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality
elif "Low" in quality_text:
elif any(term in quality_text for term in ["Low", "Baja", "Bajo"]):
quality_item.setForeground(QColor("#ff5555")) # Red for low quality
self.format_table.setItem(row, 1, quality_item)
@@ -278,7 +280,7 @@ class FormatTableMixin:
# Column 2: Resolution (Always shown)
resolution = f.get("resolution", "N/A")
if f.get("vcodec") == "none":
resolution = "Audio only"
resolution = _("formats.audio_only_resolution")
self.format_table.setItem(row, 2, QTableWidgetItem(resolution))
# Column 3: Notes for playlist mode, Extension for normal mode
@@ -299,11 +301,11 @@ class FormatTableMixin:
# Column 4 in playlist mode, Column 6 in normal mode: Audio Status
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge
audio_status = "Will merge audio" if needs_audio else ("✓ Has Audio" if f.get("vcodec") != "none" else "Audio Only")
audio_status = _("formats.will_merge_audio") if needs_audio else (_("formats.has_audio") if f.get("vcodec") != "none" else _("formats.audio_only"))
audio_item = QTableWidgetItem(audio_status)
if needs_audio:
audio_item.setForeground(QColor("#ffa500"))
elif audio_status == "Audio Only":
elif audio_status == _("formats.audio_only"):
audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only
else: # Has Audio (Video+Audio)
audio_item.setForeground(QColor("#00cc00")) # Green for included audio
@@ -369,13 +371,13 @@ class FormatTableMixin:
# Audio quality
abr = format_info.get("abr", 0)
if abr >= 256:
return "Best Audio"
return _("formats.best_audio")
elif abr >= 192:
return "High Audio"
return _("formats.high_audio")
elif abr >= 128:
return "Medium Audio"
return _("formats.medium_audio")
else:
return "Low Audio"
return _("formats.low_audio")
else:
# Video quality
height = 0
@@ -387,17 +389,17 @@ class FormatTableMixin:
pass
if height >= 2160:
return "Best (4K)"
return _("formats.best_4k")
elif height >= 1440:
return "Best (2K)"
return _("formats.best_2k")
elif height >= 1080:
return "High (1080p)"
return _("formats.high_1080p")
elif height >= 720:
return "High (720p)"
return _("formats.high_720p")
elif height >= 480:
return "Medium (480p)"
return _("formats.medium_480p")
else:
return "Low Quality"
return _("formats.low_quality")
def _get_format_notes(self, format_info) -> str:
"""Generate helpful format notes based on format info."""
@@ -410,11 +412,11 @@ class FormatTableMixin:
# Better file size categories
if file_size > 50 * 1024 * 1024: # Over 50MB
notes.append("Large size")
notes.append(_("formats.large_size"))
elif file_size > 15 * 1024 * 1024: # 15-50MB
notes.append("Medium size")
notes.append(_("formats.medium_size"))
elif file_size > 5 * 1024 * 1024: # 5-15MB
notes.append("Standard size")
notes.append(_("formats.standard_size"))
else: # Under 5MB
notes.append("Small size")
@@ -422,11 +424,11 @@ class FormatTableMixin:
vcodec = format_info.get("vcodec", "")
if vcodec != "none":
if "avc1" in vcodec: # H.264
notes.append("Compatible")
notes.append(_("formats.compatible"))
elif "av01" in vcodec: # AV1
notes.append("Efficient")
notes.append(_("formats.efficient"))
elif "vp9" in vcodec: # VP9
notes.append("High quality")
notes.append(_("formats.high_quality"))
# Add quick mobile compatibility check
if "avc1" in vcodec and file_size < 8 * 1024 * 1024:
+55 -50
View File
@@ -46,6 +46,7 @@ from src.gui.ytsage_gui_video_info import VideoInfoMixin
from src.utils.ytsage_constants import ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS
from src.utils.ytsage_logger import logger
from src.utils.ytsage_config_manager import ConfigManager
from src.utils.ytsage_localization import LocalizationManager, _
try:
import yt_dlp
@@ -60,6 +61,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
def __init__(self) -> None:
super().__init__()
# Initialize localization system
saved_language = ConfigManager.get("language") or "en"
LocalizationManager.initialize(saved_language)
# Log startup warnings for missing dependencies
if not YT_DLP_AVAILABLE:
logger.warning("yt-dlp not available at startup, will be downloaded at runtime")
@@ -316,7 +321,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
logger.exception(f"Error playing notification sound: {e}")
def init_ui(self) -> None:
self.setWindowTitle(f"YTSage v{self.version}")
self.setWindowTitle(f"{_('app.title')} {_('app.version', version=self.version)}")
self.setMinimumSize(900, 750)
# Main widget and layout
@@ -329,13 +334,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# URL input section
url_layout = QHBoxLayout()
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("Enter YouTube video or playlist URL")
self.url_input.setPlaceholderText(_("main_ui.url_placeholder"))
self.url_input.returnPressed.connect(self.analyze_url) # Analyze on Enter key
self.analyze_button = QPushButton("Analyze")
self.analyze_button = QPushButton(_("buttons.analyze"))
self.analyze_button.clicked.connect(self.analyze_url)
self.paste_button = QPushButton("Paste URL")
self.paste_button = QPushButton(_("buttons.paste_url"))
self.paste_button.clicked.connect(self.paste_url)
url_layout.addWidget(self.url_input, 1)
@@ -363,7 +368,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
layout.addWidget(self.playlist_info_label)
# Add playlist selection BUTTON (initially hidden) - REPLACED QLineEdit
self.playlist_select_btn = QPushButton("Select Videos...")
self.playlist_select_btn = QPushButton(_("buttons.select_videos"))
self.playlist_select_btn.clicked.connect(self.open_playlist_selection_dialog)
self.playlist_select_btn.setVisible(False)
self.playlist_select_btn.setStyleSheet(
@@ -394,7 +399,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout = QHBoxLayout()
# Show formats label
self.show_formats_label = QLabel("Show formats:")
self.show_formats_label = QLabel(_("formats.show_formats"))
self.show_formats_label.setStyleSheet("color: white;")
self.format_layout.addWidget(self.show_formats_label)
@@ -403,7 +408,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_buttons.setExclusive(True)
# Video button
self.video_button = QPushButton("Video")
self.video_button = QPushButton(_("buttons.video"))
self.video_button.setCheckable(True)
self.video_button.setChecked(True) # Set video as default
self.video_button.setStyleSheet(
@@ -431,7 +436,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.video_button)
# Audio button
self.audio_button = QPushButton("Audio Only")
self.audio_button = QPushButton(_("buttons.audio_only"))
self.audio_button.setCheckable(True)
self.audio_button.setStyleSheet(
"""
@@ -458,7 +463,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.audio_button)
# Add Merge Subtitles checkbox (Moved here)
self.merge_subs_checkbox = QCheckBox("Merge Subtitles")
self.merge_subs_checkbox = QCheckBox(_("main_ui.merge_subtitles"))
self.merge_subs_checkbox.setStyleSheet(
"""
QCheckBox {
@@ -491,7 +496,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.merge_subs_checkbox)
# Add Save Thumbnail Checkbox (Moved here)
self.save_thumbnail_checkbox = QCheckBox("Save Thumbnail")
self.save_thumbnail_checkbox = QCheckBox(_("main_ui.save_thumbnail"))
self.save_thumbnail_checkbox.setChecked(False)
self.save_thumbnail_checkbox.stateChanged.connect(self.toggle_save_thumbnail)
self.save_thumbnail_checkbox.setStyleSheet(
@@ -511,7 +516,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.save_thumbnail_checkbox)
# Add Save Description Checkbox (Moved here)
self.save_description_checkbox = QCheckBox("Save Description")
self.save_description_checkbox = QCheckBox(_("main_ui.save_description"))
self.save_description_checkbox.setChecked(False)
self.save_description_checkbox.stateChanged.connect(self.toggle_save_description)
self.save_description_checkbox.setStyleSheet(
@@ -531,7 +536,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.save_description_checkbox)
# Add Embed Chapters Checkbox
self.embed_chapters_checkbox = QCheckBox("Embed Chapters")
self.embed_chapters_checkbox = QCheckBox(_("main_ui.embed_chapters"))
self.embed_chapters_checkbox.setChecked(False)
self.embed_chapters_checkbox.stateChanged.connect(self.toggle_embed_chapters)
self.embed_chapters_checkbox.setStyleSheet(
@@ -562,34 +567,34 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
download_layout = QHBoxLayout()
# Replace the two separate buttons with a single Custom Options button
self.custom_options_btn = QPushButton("Custom Options")
self.custom_options_btn = QPushButton(_("buttons.custom_options"))
self.custom_options_btn.clicked.connect(self.show_custom_options)
self.about_btn = QPushButton("About")
self.about_btn = QPushButton(_("buttons.about"))
self.about_btn.clicked.connect(self.show_about_dialog)
# Add new Time Range button
self.time_range_btn = QPushButton("Trim Video")
self.time_range_btn = QPushButton(_("buttons.trim_video"))
self.time_range_btn.clicked.connect(self.show_time_range_dialog)
self.update_ytdlp_btn = QPushButton("Update yt-dlp")
self.update_ytdlp_btn = QPushButton(_("buttons.update"))
self.update_ytdlp_btn.clicked.connect(self.update_ytdlp)
# --- Rename Path Button to Settings Button ---
self.settings_button = QPushButton("Download Settings") # Renamed button
self.settings_button = QPushButton(_("buttons.download_settings")) # Renamed button
self.settings_button.clicked.connect(self.show_download_settings_dialog) # Renamed method
self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: None") # Update initial tooltip
# --- End Settings Button ---
self.download_btn = QPushButton("Download")
self.download_btn = QPushButton(_("buttons.download"))
self.download_btn.clicked.connect(self.start_download)
# Add pause and cancel buttons
self.pause_btn = QPushButton("Pause")
self.pause_btn = QPushButton(_("buttons.pause"))
self.pause_btn.clicked.connect(self.toggle_pause)
self.pause_btn.setVisible(False) # Hidden initially
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn = QPushButton(_("buttons.cancel"))
self.cancel_btn.clicked.connect(self.cancel_download)
self.cancel_btn.setVisible(False) # Hidden initially
@@ -640,7 +645,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
)
progress_layout.addWidget(self.download_details_label)
self.status_label = QLabel("Ready")
self.status_label = QLabel(_("app.ready"))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet(
"""
@@ -673,12 +678,12 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.update_status.emit("Invalid URL or please enter a URL.")
return
self.signals.update_status.emit("Analyzing (0%)... Preparing request")
self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start()
def _analyze_url_thread(self, url) -> None:
try:
self.signals.update_status.emit("Analyzing (15%)... Extracting basic info")
self.signals.update_status.emit(_("main_ui.analyzing_extracting_basic"))
# Clean up the URL to handle both playlist and video URLs
if "list=" in url and "watch?v=" in url:
@@ -719,7 +724,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
if not basic_info:
logger.error("Could not extract basic video information")
self.signals.update_status.emit(
"Error: Could not extract basic video information. Please check your link."
_("main_ui.error_extract_info")
)
# Hide playlist UI on error
self.signals.playlist_info_label_visible.emit(False)
@@ -733,7 +738,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.update_status.emit(user_friendly_error)
return
self.signals.update_status.emit("Analyzing (30%)... Extracting detailed info")
self.signals.update_status.emit(_("main_ui.analyzing_extracting_detailed"))
# Configure options for detailed extraction (keep other options)
# Add no_warnings here as well, as this is where detailed info is fetched
ydl_opts_detail = {
@@ -762,7 +767,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Use a separate options dict for the detailed extraction
with yt_dlp.YoutubeDL(ydl_opts_detail) as ydl_detail:
try:
self.signals.update_status.emit("Analyzing (45%)... Processing video data")
self.signals.update_status.emit(_("main_ui.analyzing_processing_video"))
if basic_info.get("_type") == "playlist":
self.is_playlist = True
self.playlist_info = basic_info
@@ -806,7 +811,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Show playlist selection BUTTON
# update signal method from QMetaObject.invokeMethod to signals
self.signals.playlist_select_btn_text.emit("Select Videos... (All selected)")
self.signals.playlist_select_btn_text.emit(_("main_ui.select_videos_all"))
self.signals.playlist_select_btn_visible.emit(True)
else: # Single video
@@ -824,19 +829,19 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Verify we have format information
if not self.video_info or "formats" not in self.video_info:
logger.debug(f"video_info keys: {self.video_info.keys() if self.video_info else 'None'}")
self.signals.update_status.emit("Error: No format information available.")
self.signals.update_status.emit(_("main_ui.error_no_format_info"))
self.signals.playlist_info_label_visible.emit(False)
self.signals.playlist_select_btn_visible.emit(False)
return
self.signals.update_status.emit("Analyzing (60%)... Processing formats")
self.signals.update_status.emit(_("main_ui.analyzing_processing_formats"))
self.all_formats = self.video_info["formats"]
# Update UI
self.update_video_info(self.video_info)
# Update thumbnail
self.signals.update_status.emit("Analyzing (75%)... Loading thumbnail")
self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail"))
# Try to get thumbnail from playlist info first
# Fallback to video thumbnail if playlist thumbnail not found or not a playlist
thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail")
@@ -849,14 +854,14 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
)
# --- Subtitle Handling ---
self.signals.update_status.emit("Analyzing (85%)... Processing subtitles")
self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles"))
# Clear previous selections when analyzing a new video
self.selected_subtitles = []
self.available_subtitles = self.video_info.get("subtitles", {})
self.available_automatic_subtitles = self.video_info.get("automatic_captions", {})
# Update the UI elements related to subtitle selection state
# update signal method from QMetaObject.invokeMethod to signals
self.signals.selected_subs_label_text.emit("0 selected")
self.signals.selected_subs_label_text.emit(_("main_ui.subtitles_selected", count=0))
# QMetaObject.invokeMethod(
# self.subtitle_select_btn,
@@ -875,11 +880,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# )
# Update format table
self.signals.update_status.emit("Analyzing (95%)... Updating format table")
self.signals.update_status.emit(_("main_ui.analyzing_updating_table"))
self.video_button.setChecked(True)
self.audio_button.setChecked(False)
self.filter_formats()
self.signals.update_status.emit("Analysis complete!")
self.signals.update_status.emit(_("main_ui.analysis_complete"))
except Exception as e:
logger.exception(f"Detailed extraction failed: {e}")
@@ -948,22 +953,22 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
if not url or not path:
# More specific error message if path is missing
if not path:
self.status_label.setText("Please set a download path using 'Change Path'")
self.status_label.setText(_('download.please_set_path'))
elif not url:
self.status_label.setText("Please enter a URL")
self.status_label.setText(_('download.please_enter_url'))
else:
self.status_label.setText("Please enter URL and set download path")
self.status_label.setText(_('download.please_enter_url_and_path'))
return
# --- End Path Change ---
# Get selected format
format_id = self.get_selected_format()
if not format_id:
self.status_label.setText("Please select a format")
self.status_label.setText(_('download.please_select_format'))
return
# Show preparation message
self.status_label.setText("🚀 Preparing your download...")
self.status_label.setText(_('download.preparing'))
self.progress_bar.setValue(0)
# Get resolution for filename
@@ -1044,7 +1049,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.download_cancelled = False
# Show pause/cancel buttons
self.pause_btn.setText("Pause")
self.pause_btn.setText(_("buttons.pause"))
self.pause_btn.setVisible(True)
self.cancel_btn.setVisible(True)
@@ -1066,16 +1071,16 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Video file extensions
if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]:
self.status_label.setText(f"✅ Video download completed!")
self.status_label.setText(_('download.video_completed'))
# Audio file extensions
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]:
self.status_label.setText(f"✅ Audio download completed!")
self.status_label.setText(_('download.audio_completed'))
# Subtitle file extensions
elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
self.status_label.setText(f"✅ Subtitle download completed!")
self.status_label.setText(_('download.subtitle_completed'))
# Default case
else:
self.status_label.setText("✅ Download completed!")
self.status_label.setText(_('download.completed'))
# Play notification sound when download completes
self.play_notification_sound()
@@ -1102,7 +1107,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.pause_btn.setText("Resume")
self.signals.update_status.emit("Download paused")
else:
self.pause_btn.setText("Pause")
self.pause_btn.setText(_("buttons.pause"))
self.signals.update_status.emit("Download resumed")
def check_for_updates(self) -> None:
@@ -1735,7 +1740,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.playlist_select_btn_visible.emit(False)
return
self.signals.update_status.emit("Analyzing (30%)... Extracting info with yt-dlp executable")
self.signals.update_status.emit(_("main_ui.analyzing_extracting_ytdlp"))
# Clean up the URL to handle both playlist and video URLs
if "list=" in url and "watch?v=" in url:
@@ -1787,7 +1792,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.playlist_select_btn_visible.emit(False)
return
self.signals.update_status.emit("Analyzing (60%)... Processing data")
self.signals.update_status.emit(_("main_ui.analyzing_processing_data"))
if first_info.get("_type") == "playlist" or len(json_lines) > 1:
# Handle playlist
@@ -1852,14 +1857,14 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.playlist_select_btn_visible.emit(False)
return
self.signals.update_status.emit("Analyzing (75%)... Processing formats")
self.signals.update_status.emit(_("main_ui.analyzing_processing_formats_ytdlp"))
self.all_formats = self.video_info["formats"]
# Update UI
self.update_video_info(self.video_info)
# Update thumbnail
self.signals.update_status.emit("Analyzing (85%)... Loading thumbnail")
self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp"))
# Try to get thumbnail from playlist info first
# Fallback to video thumbnail if playlist thumbnail not found or not a playlist
thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail")
@@ -1871,7 +1876,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.download_thumbnail_file(self.video_url, self.path_input.text()) # type: ignore[reportAttributeAccessIssue]
# Handle subtitles
self.signals.update_status.emit("Analyzing (90%)... Processing subtitles")
self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles_ytdlp"))
self.selected_subtitles = []
self.available_subtitles = self.video_info.get("subtitles", {})
self.available_automatic_subtitles = self.video_info.get("automatic_captions", {})
+16 -15
View File
@@ -14,6 +14,7 @@ from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__
SponsorBlockCategoryDialog,
SubtitleSelectionDialog,
)
from src.utils.ytsage_localization import _
from src.utils.ytsage_logger import logger
if TYPE_CHECKING:
@@ -95,7 +96,7 @@ class VideoInfoMixin:
subtitle_layout.setSpacing(10)
# Subtitle selection button
self.subtitle_select_btn = QPushButton("Select Subtitles...") # Renamed & changed text
self.subtitle_select_btn = QPushButton(_("main_ui.select_subtitles")) # Renamed & changed text
self.subtitle_select_btn.setFixedHeight(30)
# self.subtitle_select_btn.setFixedWidth(150) # Let it size naturally or adjust as needed
self.subtitle_select_btn.clicked.connect(self.open_subtitle_dialog)
@@ -124,7 +125,7 @@ class VideoInfoMixin:
subtitle_layout.addWidget(self.subtitle_select_btn)
# Label to show number of selected subtitles
self.selected_subs_label = QLabel("0 selected")
self.selected_subs_label = QLabel(_("selection.none_selected"))
self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
subtitle_layout.addWidget(self.selected_subs_label)
@@ -138,7 +139,7 @@ class VideoInfoMixin:
# --- SponsorBlock Section ---
sponsorblock_layout = QHBoxLayout()
self.sponsorblock_select_btn = QPushButton("SponsorBlock Categories...")
self.sponsorblock_select_btn = QPushButton(_("main_ui.sponsorblock_categories"))
self.sponsorblock_select_btn.setFixedHeight(30)
self.sponsorblock_select_btn.clicked.connect(self.open_sponsorblock_dialog)
self.sponsorblock_select_btn.setStyleSheet(
@@ -167,7 +168,7 @@ class VideoInfoMixin:
sponsorblock_layout.addWidget(self.sponsorblock_select_btn)
# Label to show selection count
self.selected_sponsorblock_label = QLabel("0 selected")
self.selected_sponsorblock_label = QLabel(_("selection.none_selected"))
self.selected_sponsorblock_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
sponsorblock_layout.addWidget(self.selected_sponsorblock_label)
@@ -253,7 +254,7 @@ class VideoInfoMixin:
date_obj = datetime.strptime(upload_date, "%Y%m%d")
formatted_date = date_obj.strftime("%B %d, %Y")
else:
formatted_date = "Unknown date"
formatted_date = _("video_info.unknown_date")
# Format duration
duration = info.get("duration", 0)
@@ -261,13 +262,13 @@ class VideoInfoMixin:
seconds = duration % 60
duration_str = f"{minutes}:{seconds:02d}"
# Update labels
self.title_label.setText(info.get("title", "Unknown title"))
self.channel_label.setText(f"Channel: {info.get('uploader', 'Unknown channel')}")
self.views_label.setText(f"Views: {formatted_views}")
self.like_count_label.setText(f"Likes: {formatted_likes}")
self.date_label.setText(f"Upload date: {formatted_date}")
self.duration_label.setText(f"Duration: {duration_str}")
# Update labels with localized text
self.title_label.setText(info.get("title", _("video_info.unknown_title")))
self.channel_label.setText(f"{_("video_info.channel")}: {info.get('uploader', _("video_info.unknown_channel"))}")
self.views_label.setText(f"{_("video_info.views")}: {formatted_views}")
self.like_count_label.setText(f"{_("video_info.likes")}: {formatted_likes}")
self.date_label.setText(f"{_("video_info.upload_date")}: {formatted_date}")
self.duration_label.setText(f"{_("video_info.duration")}: {duration_str}")
def open_subtitle_dialog(self) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
@@ -341,11 +342,11 @@ class VideoInfoMixin:
# Update label text
if count == 0:
self.selected_sponsorblock_label.setText("0 selected")
self.selected_sponsorblock_label.setText(_("selection.none_selected"))
elif count == 1:
self.selected_sponsorblock_label.setText("1 category selected")
self.selected_sponsorblock_label.setText(_("selection.one_selected"))
else:
self.selected_sponsorblock_label.setText(f"{count} categories selected")
self.selected_sponsorblock_label.setText(_("selection.count_selected", count=count))
# Update button property for styling
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", count > 0)
+1
View File
@@ -79,6 +79,7 @@ class ConfigManager:
"auto_update_ytdlp": True,
"auto_update_frequency": "daily",
"last_update_check": 0,
"language": "en",
"cached_versions": {
"ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
"ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
+266
View File
@@ -0,0 +1,266 @@
"""
Localization Manager Module
==========================
This module provides centralized localization support for YTSage application.
It handles loading language files, switching languages, and retrieving localized strings.
Features
--------
- Thread-safe operations for getting localized text
- Fallback to English when translation is missing
- Support for multiple languages via JSON files
- Dynamic language switching without restart
- Nested key support with dot notation
Usage
-----
from src.utils.ytsage_localization import LocalizationManager
# Get localized text
text = LocalizationManager.get_text("download.ready")
button_text = LocalizationManager.get_text("buttons.download")
# Change language
LocalizationManager.set_language("es")
# Get available languages
languages = LocalizationManager.get_available_languages()
"""
import json
import threading
from pathlib import Path
from typing import Any, Dict
from src.utils.ytsage_logger import logger
class LocalizationManager:
"""
Thread-safe localization manager for YTSage.
Handles loading, caching, and retrieving localized strings from JSON language files.
"""
_lock = threading.RLock()
_current_language = "en"
_languages: Dict[str, Dict[str, Any]] = {}
_languages_dir = Path(__file__).parent.parent.parent / "languages"
# Fallback English strings embedded in code
_fallback_strings = {
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Ready"
},
"buttons": {
"download": "Download",
"pause": "Pause",
"resume": "Resume",
"cancel": "Cancel",
"browse": "Browse",
"clear": "Clear",
"ok": "OK",
"apply": "Apply",
"close": "Close"
},
"dialogs": {
"custom_options": "Custom Options",
"settings": "Settings"
},
"tabs": {
"cookies": "Login with Cookies",
"custom_command": "Custom Command",
"proxy": "Proxy",
"language": "Language"
},
"language": {
"select_language": "Select Language:",
"current_language": "Current language: {language}",
"restart_required": "Language changes will take effect after restarting the application.",
"english": "English",
"spanish": "Español (Spanish)"
},
"download": {
"preparing": "🚀 Preparing your download...",
"completed": "✅ Download completed!",
"video_completed": "✅ Video download completed!",
"audio_completed": "✅ Audio download completed!",
"subtitle_completed": "✅ Subtitle download completed!",
"please_set_path": "Please set a download path using 'Change Path'",
"please_enter_url": "Please enter a URL",
"please_enter_url_and_path": "Please enter URL and set download path",
"please_select_format": "Please select a format"
},
"formats": {
"show_formats": "Show formats:"
}
}
@classmethod
def _ensure_languages_dir(cls) -> None:
"""Ensure the languages directory exists."""
cls._languages_dir.mkdir(exist_ok=True)
@classmethod
def _load_language(cls, language_code: str) -> Dict[str, Any]:
"""
Load a language file from disk.
Args:
language_code: The language code (e.g., 'en', 'es')
Returns:
Dictionary containing the language strings, or empty dict if not found
"""
language_file = cls._languages_dir / f"{language_code}.json"
if not language_file.exists():
logger.warning(f"Language file not found: {language_file}")
return {}
try:
with open(language_file, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
logger.error(f"Failed to load language file {language_file}: {e}")
return {}
@classmethod
def _get_nested_value(cls, data: Dict[str, Any], key: str) -> Any:
"""
Get a nested value from dictionary using dot notation.
Args:
data: Dictionary to search in
key: Dot-separated key (e.g., "app.title")
Returns:
The value if found, None otherwise
"""
parts = key.split(".")
value = data
for part in parts:
if isinstance(value, dict) and part in value:
value = value[part]
else:
return None
return value
@classmethod
def get_text(cls, key: str, **kwargs) -> str:
"""
Get localized text for the given key.
Args:
key: Dot-separated key for the text (e.g., "app.title")
**kwargs: Format parameters for the text
Returns:
Localized text, with fallback to English if not found
"""
with cls._lock:
# Load current language if not cached
if cls._current_language not in cls._languages:
cls._languages[cls._current_language] = cls._load_language(cls._current_language)
# Try to get from current language
current_lang_data = cls._languages.get(cls._current_language, {})
text = cls._get_nested_value(current_lang_data, key)
# Fallback to embedded English strings
if text is None:
text = cls._get_nested_value(cls._fallback_strings, key)
# Final fallback to key itself
if text is None:
logger.warning(f"Localization key not found: {key}")
text = key
# Format the text with provided parameters
if kwargs and isinstance(text, str):
try:
text = text.format(**kwargs)
except (KeyError, ValueError) as e:
logger.warning(f"Failed to format localized text '{key}': {e}")
return str(text)
@classmethod
def set_language(cls, language_code: str) -> None:
"""
Set the current language.
Args:
language_code: The language code to set (e.g., 'en', 'es')
"""
with cls._lock:
if language_code != cls._current_language:
cls._current_language = language_code
# Clear cache to force reload
cls._languages.clear()
logger.info(f"Language set to: {language_code}")
@classmethod
def get_current_language(cls) -> str:
"""Get the current language code."""
return cls._current_language
@classmethod
def get_available_languages(cls) -> Dict[str, str]:
"""
Get available languages from the languages directory.
Returns:
Dictionary mapping language codes to display names
"""
cls._ensure_languages_dir()
available_languages = {"en": cls.get_text("language.english")}
# Scan for language files
for language_file in cls._languages_dir.glob("*.json"):
lang_code = language_file.stem
if lang_code != "en": # Skip English as it's already added
# Try to get language display name from the file
lang_data = cls._load_language(lang_code)
display_name = cls._get_nested_value(lang_data, "language.display_name")
if display_name:
available_languages[lang_code] = display_name
else:
# Fallback display name
available_languages[lang_code] = lang_code.upper()
return available_languages
@classmethod
def initialize(cls, language_code: str = "en") -> None:
"""
Initialize the localization system.
Args:
language_code: Initial language code to use
"""
with cls._lock:
cls._ensure_languages_dir()
cls.set_language(language_code)
logger.info(f"Localization system initialized with language: {language_code}")
# Convenience function for getting localized text
def _(key: str, **kwargs) -> str:
"""
Convenience function to get localized text.
Args:
key: Dot-separated key for the text
**kwargs: Format parameters
Returns:
Localized text
"""
return LocalizationManager.get_text(key, **kwargs)