diff --git a/languages/en.json b/languages/en.json
new file mode 100644
index 0000000..335cbca
--- /dev/null
+++ b/languages/en.json
@@ -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.
For the full list of options and usage examples, click here to view the official yt-dlp documentation.
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..."
+ }
+}
\ No newline at end of file
diff --git a/languages/es.json b/languages/es.json
new file mode 100644
index 0000000..b5d2941
--- /dev/null
+++ b/languages/es.json
@@ -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.
Para ver la lista completa de opciones y ejemplos de uso, haz clic aquí para ver la documentación oficial de yt-dlp.
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..."
+ }
+}
\ No newline at end of file
diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py
index e625171..712b7ed 100644
--- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py
+++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py
@@ -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"Version {getattr(self._parent, 'version', '4.9.0b')}"
+ f"{_('about.version', version=getattr(self._parent, 'version', '4.9.0b'))}"
)
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: oop7"
+ f"{_('about.author', author='oop7')}"
)
author_label.setOpenExternalLinks(True)
info_layout.addWidget(author_label)
repo_label = QLabel(
- "GitHub: YTSage"
+ f"{_('about.github', repo='YTSage')}"
)
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 = (
- "✓ Detected" if ytdlp_found else "✗ Missing"
+ f"{_('about.detected')}" if ytdlp_found else f"{_('about.missing')}"
)
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 = (
- "✓ Detected"
+ f"{_('about.detected')}"
if ffmpeg_found
- else "✗ Missing"
+ else f"{_('about.missing')}"
)
- 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(
"""
diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py
index 61218d0..b3ef419 100644
--- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py
+++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py
@@ -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.
"
- "For complete list of options and usage examples, "
- f'click here to view the official yt-dlp documentation.
'
- "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)
diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py
index 876c606..946ea76 100644
--- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py
+++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py
@@ -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)
diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py
index bac857d..958b03e 100644
--- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py
+++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py
@@ -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("