4a16aad2ac
Introduce a new "check_app_updates" configuration (default: true) to allow users to enable/disable automatic YTSage update checks. Adds a styled checkbox to the Updater tab with getter, loads the setting (defaults to enabled for older configs), and saves it from the custom options dialog. The main update check now respects this setting and will skip checking when disabled. Also include English localization entries for the new UI strings.
302 lines
11 KiB
Python
302 lines
11 KiB
Python
"""
|
|
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 .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 .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 / "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"
|
|
},
|
|
"settings": {
|
|
"generic_mode": "Generic Mode",
|
|
"enable_generic_mode": "Enable Generic Mode (support non-YouTube sites)",
|
|
"generic_mode_help": "Allows downloading from Dailymotion, CBC Gem, and other sites supported by yt-dlp.",
|
|
"app_updates_title": "YTSage Updates",
|
|
"check_app_updates": "Check for YTSage updates on startup",
|
|
"check_beta_updates": "Receive Beta Updates"
|
|
},
|
|
"tabs": {
|
|
"cookies": "Login with Cookies",
|
|
"custom_command": "Custom Command",
|
|
"proxy": "Proxy",
|
|
"language": "Language"
|
|
},
|
|
"main_ui": {
|
|
"url_placeholder": "Enter YouTube video or playlist URL",
|
|
"url_placeholder_generic": "Enter video or playlist URL from any supported site",
|
|
"settings_tooltip": "Current Path: {path}\nSpeed Limit: {speed_limit}",
|
|
"speed_limit_none": "None"
|
|
},
|
|
"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)",
|
|
"portuguese": "Português (Portuguese)",
|
|
"russian": "Русский (Russian)",
|
|
"chinese": "中文 (简体) (Chinese Simplified)",
|
|
"german": "Deutsch (German)",
|
|
"french": "Français (French)",
|
|
"hindi": "हिन्दी (Hindi)",
|
|
"indonesian": "Bahasa Indonesia (Indonesian)",
|
|
"turkish": "Türkçe (Turkish)",
|
|
"polish": "Polski (Polish)",
|
|
"italian": "Italiano (Italian)",
|
|
"arabic": "العربية (Arabic)",
|
|
"japanese": "日本語 (Japanese)"
|
|
},
|
|
"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:"
|
|
},
|
|
"errors": {
|
|
"download_failed_return_code_conflict": "Download failed with return code {return_code}. This may be due to a conflict with multiple yt-dlp installations. Try uninstalling any system-installed yt-dlp (e.g. through snap or apt) and restart the application.",
|
|
"download_failed_return_code": "Download failed with return code {return_code}",
|
|
"direct_command_error": "Error in direct command: {error}"
|
|
},
|
|
"about": {
|
|
"open_logs": "📂 Logs",
|
|
"logs_tooltip": "Open application logs folder",
|
|
"refresh": "🔄"
|
|
}
|
|
}
|
|
|
|
@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) |