| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- # Copyright (C) 2025 AIDC-AI
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- # http://www.apache.org/licenses/LICENSE-2.0
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """
- International language support for Pixelle-Video Web UI
- """
- import json
- import locale
- from pathlib import Path
- from typing import Dict, Optional
- from loguru import logger
- _locales: Dict[str, dict] = {}
- _current_language: str = "en_US" # Default fallback to English
- def load_locales() -> Dict[str, dict]:
- """Load all locale files from locales directory"""
- global _locales
-
- locales_dir = Path(__file__).parent / "locales"
-
- if not locales_dir.exists():
- logger.warning(f"Locales directory not found: {locales_dir}")
- return _locales
-
- for json_file in locales_dir.glob("*.json"):
- lang_code = json_file.stem
- try:
- with open(json_file, "r", encoding="utf-8") as f:
- _locales[lang_code] = json.load(f)
- logger.debug(f"Loaded locale: {lang_code}")
- except Exception as e:
- logger.error(f"Failed to load locale {lang_code}: {e}")
-
- logger.info(f"Loaded {len(_locales)} locales: {list(_locales.keys())}")
- return _locales
- def set_language(lang_code: str):
- """Set current language"""
- global _current_language
- if lang_code in _locales:
- _current_language = lang_code
- logger.debug(f"Language set to: {lang_code}")
- else:
- logger.warning(f"Language {lang_code} not found, keeping {_current_language}")
- def get_language() -> str:
- """Get current language"""
- return _current_language
- def tr(key: str, fallback: Optional[str] = None, **kwargs) -> str:
- """
- Translate a key to current language
-
- Args:
- key: Translation key (e.g., "app.title")
- fallback: Fallback text if key not found
- **kwargs: Format parameters for string interpolation
-
- Returns:
- Translated text
-
- Example:
- tr("app.title") # => "Pixelle-Video"
- tr("error.missing_field", field="API Key") # => "请填写 API Key"
- """
- locale = _locales.get(_current_language, {})
- translations = locale.get("t", {})
-
- result = translations.get(key)
-
- if result is None:
- # Try fallback parameter
- if fallback is not None:
- result = fallback
- # Try English fallback
- elif _current_language != "en_US" and "en_US" in _locales:
- en_locale = _locales["en_US"]
- result = en_locale.get("t", {}).get(key)
-
- # Last resort: return the key itself
- if result is None:
- result = key
- logger.debug(f"Translation missing: {key}")
-
- # Apply string interpolation if kwargs provided
- if kwargs:
- try:
- result = result.format(**kwargs)
- except (KeyError, ValueError) as e:
- logger.warning(f"Failed to format translation '{key}': {e}")
-
- return result
- def get_language_name(lang_code: Optional[str] = None) -> str:
- """Get display name of a language"""
- if lang_code is None:
- lang_code = _current_language
-
- locale = _locales.get(lang_code, {})
- return locale.get("language_name", lang_code)
- def get_available_languages() -> Dict[str, str]:
- """Get all available languages with their display names"""
- return {
- code: locale.get("language_name", code)
- for code, locale in _locales.items()
- }
- def detect_system_language() -> str:
- """
- Detect system/OS language and return the best matching locale code.
- Falls back to English if no match found.
-
- This is designed for self-hosted scenarios where the server and browser
- are typically on the same machine.
-
- Returns:
- Language code (e.g., "zh_CN", "en_US")
- """
- try:
- import os
- import platform
- import subprocess
-
- system_locale = None
-
- # Method 1: macOS-specific detection (most reliable for macOS)
- if platform.system() == "Darwin": # macOS
- try:
- # Get AppleLocale which reflects system language preference
- result = subprocess.run(
- ["defaults", "read", "-g", "AppleLocale"],
- capture_output=True,
- text=True,
- timeout=2
- )
- if result.returncode == 0:
- system_locale = result.stdout.strip()
- logger.debug(f"System locale from macOS AppleLocale: {system_locale}")
- except Exception as e:
- logger.debug(f"macOS AppleLocale detection failed: {e}")
-
- # Fallback: try AppleLanguages
- if not system_locale:
- try:
- result = subprocess.run(
- ["defaults", "read", "-g", "AppleLanguages"],
- capture_output=True,
- text=True,
- timeout=2
- )
- if result.returncode == 0:
- # Parse array output like: ( "zh-Hans-CN", "en-CN" )
- output = result.stdout.strip()
- # Extract first language
- import re
- match = re.search(r'"([^"]+)"', output)
- if match:
- lang = match.group(1)
- # Convert zh-Hans-CN to zh_CN
- if lang.startswith("zh-Hans"):
- system_locale = "zh_CN"
- elif lang.startswith("zh-Hant"):
- system_locale = "zh_TW"
- else:
- system_locale = lang.replace("-", "_")
- logger.debug(f"System locale from macOS AppleLanguages: {system_locale}")
- except Exception as e:
- logger.debug(f"macOS AppleLanguages detection failed: {e}")
-
- # Method 2: Get from environment locale (cross-platform)
- if not system_locale:
- try:
- system_locale = locale.getdefaultlocale()[0]
- logger.debug(f"System locale from getdefaultlocale(): {system_locale}")
- except Exception as e:
- logger.debug(f"getdefaultlocale() failed: {e}")
-
- # Method 3: Get from current locale
- if not system_locale:
- try:
- system_locale = locale.getlocale()[0]
- logger.debug(f"System locale from getlocale(): {system_locale}")
- except Exception as e:
- logger.debug(f"getlocale() failed: {e}")
-
- # Method 4: Try to get from environment variables
- if not system_locale:
- for env_var in ['LC_ALL', 'LC_MESSAGES', 'LANG', 'LANGUAGE']:
- env_value = os.environ.get(env_var)
- if env_value:
- # Extract language code from formats like "zh_CN.UTF-8"
- system_locale = env_value.split('.')[0]
- logger.debug(f"System locale from {env_var}: {system_locale}")
- break
-
- if system_locale:
- # Normalize the locale string
- # Handle formats: zh_CN, zh-CN, zh_CN.UTF-8, etc.
- system_locale = system_locale.replace('-', '_').split('.')[0]
-
- # Direct match (e.g., "zh_CN")
- for locale_code in _locales.keys():
- if locale_code.lower() == system_locale.lower():
- logger.info(f"System language matched: {locale_code}")
- return locale_code
-
- # Partial match (e.g., "zh" matches "zh_CN")
- lang_prefix = system_locale.split('_')[0].lower()
- for locale_code in _locales.keys():
- if locale_code.lower().startswith(lang_prefix):
- logger.info(f"System language partially matched: {locale_code} (from {system_locale})")
- return locale_code
-
- logger.info("No system language detected, using fallback")
- except Exception as e:
- logger.warning(f"Failed to detect system language: {e}")
-
- # Fallback to English
- return "en_US"
- # Auto-load locales on import
- load_locales()
- # Auto-detect and set system language
- _detected_language = detect_system_language()
- _current_language = _detected_language
- logger.info(f"Default language initialized to: {_current_language}")
|