__init__.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. # Copyright (C) 2025 AIDC-AI
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. """
  13. International language support for Pixelle-Video Web UI
  14. """
  15. import json
  16. import locale
  17. from pathlib import Path
  18. from typing import Dict, Optional
  19. from loguru import logger
  20. _locales: Dict[str, dict] = {}
  21. _current_language: str = "en_US" # Default fallback to English
  22. def load_locales() -> Dict[str, dict]:
  23. """Load all locale files from locales directory"""
  24. global _locales
  25. locales_dir = Path(__file__).parent / "locales"
  26. if not locales_dir.exists():
  27. logger.warning(f"Locales directory not found: {locales_dir}")
  28. return _locales
  29. for json_file in locales_dir.glob("*.json"):
  30. lang_code = json_file.stem
  31. try:
  32. with open(json_file, "r", encoding="utf-8") as f:
  33. _locales[lang_code] = json.load(f)
  34. logger.debug(f"Loaded locale: {lang_code}")
  35. except Exception as e:
  36. logger.error(f"Failed to load locale {lang_code}: {e}")
  37. logger.info(f"Loaded {len(_locales)} locales: {list(_locales.keys())}")
  38. return _locales
  39. def set_language(lang_code: str):
  40. """Set current language"""
  41. global _current_language
  42. if lang_code in _locales:
  43. _current_language = lang_code
  44. logger.debug(f"Language set to: {lang_code}")
  45. else:
  46. logger.warning(f"Language {lang_code} not found, keeping {_current_language}")
  47. def get_language() -> str:
  48. """Get current language"""
  49. return _current_language
  50. def tr(key: str, fallback: Optional[str] = None, **kwargs) -> str:
  51. """
  52. Translate a key to current language
  53. Args:
  54. key: Translation key (e.g., "app.title")
  55. fallback: Fallback text if key not found
  56. **kwargs: Format parameters for string interpolation
  57. Returns:
  58. Translated text
  59. Example:
  60. tr("app.title") # => "Pixelle-Video"
  61. tr("error.missing_field", field="API Key") # => "请填写 API Key"
  62. """
  63. locale = _locales.get(_current_language, {})
  64. translations = locale.get("t", {})
  65. result = translations.get(key)
  66. if result is None:
  67. # Try fallback parameter
  68. if fallback is not None:
  69. result = fallback
  70. # Try English fallback
  71. elif _current_language != "en_US" and "en_US" in _locales:
  72. en_locale = _locales["en_US"]
  73. result = en_locale.get("t", {}).get(key)
  74. # Last resort: return the key itself
  75. if result is None:
  76. result = key
  77. logger.debug(f"Translation missing: {key}")
  78. # Apply string interpolation if kwargs provided
  79. if kwargs:
  80. try:
  81. result = result.format(**kwargs)
  82. except (KeyError, ValueError) as e:
  83. logger.warning(f"Failed to format translation '{key}': {e}")
  84. return result
  85. def get_language_name(lang_code: Optional[str] = None) -> str:
  86. """Get display name of a language"""
  87. if lang_code is None:
  88. lang_code = _current_language
  89. locale = _locales.get(lang_code, {})
  90. return locale.get("language_name", lang_code)
  91. def get_available_languages() -> Dict[str, str]:
  92. """Get all available languages with their display names"""
  93. return {
  94. code: locale.get("language_name", code)
  95. for code, locale in _locales.items()
  96. }
  97. def detect_system_language() -> str:
  98. """
  99. Detect system/OS language and return the best matching locale code.
  100. Falls back to English if no match found.
  101. This is designed for self-hosted scenarios where the server and browser
  102. are typically on the same machine.
  103. Returns:
  104. Language code (e.g., "zh_CN", "en_US")
  105. """
  106. try:
  107. import os
  108. import platform
  109. import subprocess
  110. system_locale = None
  111. # Method 1: macOS-specific detection (most reliable for macOS)
  112. if platform.system() == "Darwin": # macOS
  113. try:
  114. # Get AppleLocale which reflects system language preference
  115. result = subprocess.run(
  116. ["defaults", "read", "-g", "AppleLocale"],
  117. capture_output=True,
  118. text=True,
  119. timeout=2
  120. )
  121. if result.returncode == 0:
  122. system_locale = result.stdout.strip()
  123. logger.debug(f"System locale from macOS AppleLocale: {system_locale}")
  124. except Exception as e:
  125. logger.debug(f"macOS AppleLocale detection failed: {e}")
  126. # Fallback: try AppleLanguages
  127. if not system_locale:
  128. try:
  129. result = subprocess.run(
  130. ["defaults", "read", "-g", "AppleLanguages"],
  131. capture_output=True,
  132. text=True,
  133. timeout=2
  134. )
  135. if result.returncode == 0:
  136. # Parse array output like: ( "zh-Hans-CN", "en-CN" )
  137. output = result.stdout.strip()
  138. # Extract first language
  139. import re
  140. match = re.search(r'"([^"]+)"', output)
  141. if match:
  142. lang = match.group(1)
  143. # Convert zh-Hans-CN to zh_CN
  144. if lang.startswith("zh-Hans"):
  145. system_locale = "zh_CN"
  146. elif lang.startswith("zh-Hant"):
  147. system_locale = "zh_TW"
  148. else:
  149. system_locale = lang.replace("-", "_")
  150. logger.debug(f"System locale from macOS AppleLanguages: {system_locale}")
  151. except Exception as e:
  152. logger.debug(f"macOS AppleLanguages detection failed: {e}")
  153. # Method 2: Get from environment locale (cross-platform)
  154. if not system_locale:
  155. try:
  156. system_locale = locale.getdefaultlocale()[0]
  157. logger.debug(f"System locale from getdefaultlocale(): {system_locale}")
  158. except Exception as e:
  159. logger.debug(f"getdefaultlocale() failed: {e}")
  160. # Method 3: Get from current locale
  161. if not system_locale:
  162. try:
  163. system_locale = locale.getlocale()[0]
  164. logger.debug(f"System locale from getlocale(): {system_locale}")
  165. except Exception as e:
  166. logger.debug(f"getlocale() failed: {e}")
  167. # Method 4: Try to get from environment variables
  168. if not system_locale:
  169. for env_var in ['LC_ALL', 'LC_MESSAGES', 'LANG', 'LANGUAGE']:
  170. env_value = os.environ.get(env_var)
  171. if env_value:
  172. # Extract language code from formats like "zh_CN.UTF-8"
  173. system_locale = env_value.split('.')[0]
  174. logger.debug(f"System locale from {env_var}: {system_locale}")
  175. break
  176. if system_locale:
  177. # Normalize the locale string
  178. # Handle formats: zh_CN, zh-CN, zh_CN.UTF-8, etc.
  179. system_locale = system_locale.replace('-', '_').split('.')[0]
  180. # Direct match (e.g., "zh_CN")
  181. for locale_code in _locales.keys():
  182. if locale_code.lower() == system_locale.lower():
  183. logger.info(f"System language matched: {locale_code}")
  184. return locale_code
  185. # Partial match (e.g., "zh" matches "zh_CN")
  186. lang_prefix = system_locale.split('_')[0].lower()
  187. for locale_code in _locales.keys():
  188. if locale_code.lower().startswith(lang_prefix):
  189. logger.info(f"System language partially matched: {locale_code} (from {system_locale})")
  190. return locale_code
  191. logger.info("No system language detected, using fallback")
  192. except Exception as e:
  193. logger.warning(f"Failed to detect system language: {e}")
  194. # Fallback to English
  195. return "en_US"
  196. # Auto-load locales on import
  197. load_locales()
  198. # Auto-detect and set system language
  199. _detected_language = detect_system_language()
  200. _current_language = _detected_language
  201. logger.info(f"Default language initialized to: {_current_language}")