/
Flipper
/
Marginal
Обзор
Документация
Войти
/
Flipper
/
Marginal
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
master
src/Translator.py
392 строки
14 KB
Alex
Marginal v1.0.0 RELEASE
11 янв 2026, 18:02
11 янв 2026, 18:02
5e1eff8
Код
Авторство
О чём код?
""" Localization system for PySide6 application using gettext. Manages language switching, translation loading, and manifest-based language discovery. """ import gettext import os import locale import json from PySide6.QtCore import QObject, Signal from utils import getBasePath class AppTranslator(QObject): """ Handles application localization and language switching using gettext. Integrates with Qt signals for UI language change notifications. Uses a manifest.json file to manage available languages and their metadata. """ # Signal emitted when language changes (connected to UI for retranslation) languageChanged = Signal() def __init__(self, appName: str = "marginal", logger=None): """ Initialize translator with application name and optional logger. Args: appName (str): Name of the application (used for .mo/.po files). logger (Logger, optional): Application logger instance. """ super().__init__() self.appName = appName self.logger = logger self.currentLanguage = "en" self.translations = {} # Cache loaded translations by language code # Buffer for log messages until logger is available self.logBuffer = [] # Set up locale directory paths self.localeDir = os.path.join(getBasePath(), "resources", "locales") self.manifestPath = os.path.join(self.localeDir, "manifest.json") # Buffer initialization message self.bufferLog("Translator", f"Locale directory set to: {self.localeDir}") # Ensure locales directory exists os.makedirs(self.localeDir, exist_ok=True) # Load or create manifest file self.manifest = self._loadManifest() # Initialize system locale locale.setlocale(locale.LC_ALL, "") self.initialize() def bufferLog(self, author: str, message: str, hideInConsole: bool = True): """ Buffer log messages until logger is available. Args: author (str): Source of the log message. message (str): Log message content. hideInConsole (bool): Whether to hide message in console output. Returns: None """ if self.logger: self.logger.log(author, message, hideInConsole) else: self.logBuffer.append((author, message, hideInConsole)) def flushLogBuffer(self): """ Flush buffered log messages when logger becomes available. Returns: None """ if not self.logger: return for author, message, hideInConsole in self.logBuffer: self.logger.log(author, message, hideInConsole) # Clear buffer after flushing self.logBuffer.clear() def _loadManifest(self) -> dict: """ Load and parse manifest.json file, creating default if missing. Returns: dict -> Manifest dictionary with available languages and default locale. Manifest structure: { "availableLocales": [ {"code": "en", "name": "English"}, {"code": "ru", "name": "Русский"} ], "defaultLocale": "en" } """ defaultManifest = { "availableLocales": [ {"code": "en", "name": "English"} ], "defaultLocale": "en" } # Create manifest if it doesn't exist if not os.path.exists(self.manifestPath): self.bufferLog("Translator", f"Manifest not found at: {self.manifestPath}") self.bufferLog("Translator", "Creating default manifest with English only") # Try to discover existing language folders if os.path.exists(self.localeDir): availableLangs = [] for item in os.listdir(self.localeDir): langDir = os.path.join(self.localeDir, item) if os.path.isdir(langDir): # Check for translation files moPath = os.path.join(langDir, "LC_MESSAGES", f"{self.appName}.mo") poPath = os.path.join(langDir, "LC_MESSAGES", f"{self.appName}.po") if os.path.exists(moPath) or os.path.exists(poPath): # Get language name from built-in dictionary langName = self._getBuiltinLanguageName(item) availableLangs.append({"code": item, "name": langName}) if availableLangs: defaultManifest["availableLocales"] = availableLangs # Set first language as default defaultManifest["defaultLocale"] = availableLangs[0]["code"] # Save default manifest try: with open(self.manifestPath, 'w', encoding='utf-8') as f: json.dump(defaultManifest, f, indent=2, ensure_ascii=False) self.bufferLog("Translator", f"Created default manifest at: {self.manifestPath}") except Exception as e: self.bufferLog("Translator", f"Failed to create manifest: {e}") return defaultManifest # Load existing manifest try: with open(self.manifestPath, 'r', encoding='utf-8') as f: manifest = json.load(f) # Validate manifest structure if "availableLocales" not in manifest: manifest["availableLocales"] = defaultManifest["availableLocales"] self.bufferLog("Translator", "Manifest missing 'availableLocales', using default") if "defaultLocale" not in manifest: manifest["defaultLocale"] = defaultManifest["defaultLocale"] self.bufferLog("Translator", "Manifest missing 'defaultLocale', using default") self.bufferLog("Translator", f"Loaded manifest with {len(manifest['availableLocales'])} languages") return manifest except json.JSONDecodeError as e: self.bufferLog("Translator", f"Failed to parse manifest.json: {e}") return defaultManifest except Exception as e: self.bufferLog("Translator", f"Failed to load manifest: {e}") return defaultManifest def getAvailableLanguages(self) -> list[str]: """ Get list of available language codes from manifest. Returns: list[str] -> List of language codes (e.g., ['en', 'ru']). """ if not self.manifest or "availableLocales" not in self.manifest: return ["en"] languages = [lang["code"] for lang in self.manifest["availableLocales"]] return sorted(languages) def getLanguageInfo(self) -> list[dict]: """ Get detailed information about available languages. Returns: list[dict] -> List of dictionaries with code and name for each language. """ if not self.manifest or "availableLocales" not in self.manifest: return [{"code": "en", "name": "English"}] return self.manifest["availableLocales"] def getLanguageName(self, languageCode: str) -> str: """ Get display name for a language code from manifest. Args: languageCode (str): Language code (e.g., 'en'). Returns: str -> Display name (e.g., 'en' -> 'English'). """ if not self.manifest or "availableLocales" not in self.manifest: return self._getBuiltinLanguageName(languageCode) for lang in self.manifest["availableLocales"]: if lang["code"] == languageCode: return lang["name"] return self._getBuiltinLanguageName(languageCode) def _getBuiltinLanguageName(self, languageCode: str) -> str: """ Fallback language names if not found in manifest. Args: languageCode (str): Language code. Returns: str -> Language display name. """ names = { "en": "English", "ru": "Русский" } return names.get(languageCode, languageCode) def setLanguage(self, languageCode: str) -> bool: """ Set application language and load appropriate translation files. Args: languageCode (str): Language code to set. Returns: bool -> True if language set successfully, False otherwise. """ if languageCode == "auto": languageCode = self.getSystemLanguage() # Check if language is available in manifest availableLanguages = self.getAvailableLanguages() if languageCode not in availableLanguages: self.bufferLog("Translator", f"Language {languageCode} not in available languages: {availableLanguages}") # Fall back to manifest's default if "defaultLocale" in self.manifest: languageCode = self.manifest["defaultLocale"] self.bufferLog("Translator", f"Falling back to default locale: {languageCode}") else: languageCode = "en" self.bufferLog("Translator", "Falling back to English") try: # For English, use NullTranslations (no-op translation) if languageCode == "en": translation = gettext.NullTranslations() self.bufferLog("Translator", "Using NullTranslations for English") else: # Construct path to language folder langDir = os.path.join(self.localeDir, languageCode) # Verify language folder exists if not os.path.exists(langDir): self.bufferLog("Translator", f"Language folder does not exist: {langDir}") raise FileNotFoundError(f"Language directory not found: {langDir}") # Load translation using gettext translation = gettext.translation( self.appName, localedir=self.localeDir, languages=[languageCode], fallback=True ) self.bufferLog("Translator", f"Successfully loaded translation for {languageCode}") # Install translation globally translation.install() # Set text domain explicitly gettext.bindtextdomain(self.appName, self.localeDir) gettext.textdomain(self.appName) # Update current state self.currentLanguage = languageCode self.translations[languageCode] = translation self.bufferLog("Translator", f"Language set to: {languageCode}") self.languageChanged.emit() return True except Exception as e: self.bufferLog("Translator", f"FAILED to set language to {languageCode}: {e}") # Fall back to English gettext.NullTranslations().install() self.currentLanguage = "en" return False def getCurrentLanguage(self) -> str: """ Get current language code. Returns: str -> Current language code. """ return self.currentLanguage def getSystemLanguage(self) -> str: """ Detect system language from locale settings. Returns: str -> Two-letter language code (e.g., 'en', 'ru'). """ try: localeInfo = locale.getlocale() if localeInfo and localeInfo[0]: userLang = localeInfo[0].split('_')[0] langCode = userLang[:2].lower() if userLang else 'en' return langCode except Exception as e: self.bufferLog("Translator", f"Failed to get system language: {e}") return "en" def initialize(self, defaultLanguage: str = None) -> bool: """ Initialize translator with default or system language. Args: defaultLanguage (str, optional): Language code to use. If None, uses system language or manifest default. Returns: bool -> True if initialization successful. """ if defaultLanguage is None: # Try system language first systemLang = self.getSystemLanguage() self.bufferLog("Translator", f"System language detected: {systemLang}") available = self.getAvailableLanguages() if systemLang in available: self.bufferLog("Translator", f"Using system language: {systemLang}") return self.setLanguage(systemLang) else: # Try manifest default if "defaultLocale" in self.manifest: manifestDefault = self.manifest["defaultLocale"] if manifestDefault in available: self.bufferLog("Translator", f"Using manifest default language: {manifestDefault}") return self.setLanguage(manifestDefault) # Fall back to English self.bufferLog("Translator", "Using English as fallback") return self.setLanguage("en") else: self.bufferLog("Translator", f"Using specified language: {defaultLanguage}") return self.setLanguage(defaultLanguage) def translate(self, text: str) -> str: """ Translate text using current language. Args: text (str): Text to translate. Returns: str -> Translated text. """ if len(self.translations) == 0: return gettext.gettext(text) return self.translations[self.currentLanguage].gettext(text) def setLogger(self, logger): """ Set logger instance and flush buffered messages. Args: logger (Logger): Application logger instance. Returns: None """ self.logger = logger if logger: self.flushLogBuffer()