Skip to main content
Glama
i18n.py14 kB
#!/usr/bin/env python3 """ 國際化支援模組 =============== 提供統一的多語系支援功能,支援繁體中文、英文等語言。 自動偵測系統語言,並提供語言切換功能。 新架構: - 使用分離的 JSON 翻譯檔案 - 支援巢狀翻譯鍵值 - 元資料支援 - 易於擴充新語言 作者: Minidoracat """ import json import locale import os from pathlib import Path from typing import Any from .debug import i18n_debug_log as debug_log class I18nManager: """國際化管理器 - 新架構版本""" def __init__(self): self._current_language = None self._translations = {} self._supported_languages = ["zh-CN", "zh-TW", "en"] self._fallback_language = "zh-TW" self._config_file = self._get_config_file_path() self._locales_dir = Path(__file__).parent / "web" / "locales" # 載入翻譯 self._load_all_translations() # 設定語言 self._current_language = self._detect_language() def _get_config_file_path(self) -> Path: """獲取配置文件路徑""" config_dir = Path.home() / ".config" / "mcp-feedback-enhanced" config_dir.mkdir(parents=True, exist_ok=True) return config_dir / "language.json" def _load_all_translations(self) -> None: """載入所有語言的翻譯檔案""" self._translations = {} for lang_code in self._supported_languages: lang_dir = self._locales_dir / lang_code translation_file = lang_dir / "translation.json" if translation_file.exists(): try: with open(translation_file, encoding="utf-8") as f: data = json.load(f) self._translations[lang_code] = data debug_log( f"成功載入語言 {lang_code}: {data.get('meta', {}).get('displayName', lang_code)}" ) except Exception as e: debug_log(f"載入語言檔案失敗 {lang_code}: {e}") # 如果載入失敗,使用空的翻譯 self._translations[lang_code] = {} else: debug_log(f"找不到語言檔案: {translation_file}") self._translations[lang_code] = {} def _detect_language(self) -> str: """自動偵測語言""" # 1. 優先使用用戶保存的語言設定 saved_lang = self._load_saved_language() if saved_lang and saved_lang in self._supported_languages: return saved_lang # 2. 檢查環境變數 env_lang = os.getenv("MCP_LANGUAGE", "").strip() if env_lang and env_lang in self._supported_languages: return env_lang # 3. 檢查其他環境變數(LANG, LC_ALL 等) for env_var in ["LANG", "LC_ALL", "LC_MESSAGES", "LANGUAGE"]: env_value = os.getenv(env_var, "").strip() if env_value: if env_value.startswith("zh_TW") or env_value.startswith("zh_Hant"): return "zh-TW" if env_value.startswith("zh_CN") or env_value.startswith("zh_Hans"): return "zh-CN" if env_value.startswith("en"): return "en" # 4. 自動偵測系統語言(僅在非測試模式下) if not os.getenv("MCP_TEST_MODE"): try: # 獲取系統語言 system_locale = locale.getdefaultlocale()[0] if system_locale: if system_locale.startswith("zh_TW") or system_locale.startswith( "zh_Hant" ): return "zh-TW" if system_locale.startswith("zh_CN") or system_locale.startswith( "zh_Hans" ): return "zh-CN" if system_locale.startswith("en"): return "en" except Exception: pass # 5. 回退到默認語言 return self._fallback_language def _load_saved_language(self) -> str | None: """載入保存的語言設定""" try: if self._config_file.exists(): with open(self._config_file, encoding="utf-8") as f: config = json.load(f) language = config.get("language") return language if isinstance(language, str) else None except Exception: pass return None def save_language(self, language: str) -> None: """保存語言設定""" try: config = {"language": language} with open(self._config_file, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2) except Exception: pass def get_current_language(self) -> str: """獲取當前語言""" return self._current_language or "zh-TW" def set_language(self, language: str) -> bool: """設定語言""" if language in self._supported_languages: self._current_language = language self.save_language(language) return True return False def get_supported_languages(self) -> list[str]: """獲取支援的語言列表""" return self._supported_languages.copy() def get_language_info(self, language_code: str) -> dict[str, Any]: """獲取語言的元資料信息""" if language_code in self._translations: meta = self._translations[language_code].get("meta", {}) return meta if isinstance(meta, dict) else {} return {} def _get_nested_value(self, data: dict[str, Any], key_path: str) -> str | None: """從巢狀字典中獲取值,支援點分隔的鍵路徑""" keys = key_path.split(".") current: Any = data for key in keys: if isinstance(current, dict) and key in current: current = current[key] else: return None return str(current) if isinstance(current, str) else None def t(self, key: str, **kwargs) -> str: """ 翻譯函數 - 支援新舊兩種鍵值格式 新格式: 'buttons.submit' -> data['buttons']['submit'] 舊格式: 'btn_submit_feedback' -> 兼容舊的鍵值 """ # 獲取當前語言的翻譯 current_translations = self._translations.get(self._current_language, {}) # 嘗試新格式(巢狀鍵) text = self._get_nested_value(current_translations, key) # 如果沒有找到,嘗試舊格式的兼容映射 if text is None: text = self._get_legacy_translation(current_translations, key) # 如果還是沒有找到,嘗試使用回退語言 if text is None: fallback_translations = self._translations.get(self._fallback_language, {}) text = self._get_nested_value(fallback_translations, key) if text is None: text = self._get_legacy_translation(fallback_translations, key) # 最後回退到鍵本身 if text is None: text = key # 處理格式化參數 if kwargs: try: text = text.format(**kwargs) except (KeyError, ValueError): pass return text def _get_legacy_translation( self, translations: dict[str, Any], key: str ) -> str | None: """獲取舊格式翻譯的兼容方法""" # 舊鍵到新鍵的映射 legacy_mapping = { # 應用程式 "app_title": "app.title", "project_directory": "app.projectDirectory", "language": "app.language", "settings": "app.settings", # 分頁 "feedback_tab": "tabs.feedback", "command_tab": "tabs.command", "images_tab": "tabs.images", # 回饋 "feedback_title": "feedback.title", "feedback_description": "feedback.description", "feedback_placeholder": "feedback.placeholder", # 命令 "command_title": "command.title", "command_description": "command.description", "command_placeholder": "command.placeholder", "command_output": "command.output", # 圖片 "images_title": "images.title", "images_select": "images.select", "images_paste": "images.paste", "images_clear": "images.clear", "images_status": "images.status", "images_status_with_size": "images.statusWithSize", "images_drag_hint": "images.dragHint", "images_delete_confirm": "images.deleteConfirm", "images_delete_title": "images.deleteTitle", "images_size_warning": "images.sizeWarning", "images_format_error": "images.formatError", # 按鈕 "submit": "buttons.submit", "cancel": "buttons.cancel", "close": "buttons.close", "clear": "buttons.clear", "btn_submit_feedback": "buttons.submitFeedback", "btn_cancel": "buttons.cancel", "btn_select_files": "buttons.selectFiles", "btn_paste_clipboard": "buttons.pasteClipboard", "btn_clear_all": "buttons.clearAll", "btn_run_command": "buttons.runCommand", # 狀態 "feedback_submitted": "status.feedbackSubmitted", "feedback_cancelled": "status.feedbackCancelled", "timeout_message": "status.timeoutMessage", "error_occurred": "status.errorOccurred", "loading": "status.loading", "connecting": "status.connecting", "connected": "status.connected", "disconnected": "status.disconnected", "uploading": "status.uploading", "upload_success": "status.uploadSuccess", "upload_failed": "status.uploadFailed", "command_running": "status.commandRunning", "command_finished": "status.commandFinished", "paste_success": "status.pasteSuccess", "paste_failed": "status.pasteFailed", "invalid_file_type": "status.invalidFileType", "file_too_large": "status.fileTooLarge", # 其他 "ai_summary": "aiSummary", "language_selector": "languageSelector", "language_zh_tw": "languageNames.zhTw", "language_en": "languageNames.en", "language_zh_cn": "languageNames.zhCn", # 測試 "test_web_ui_summary": "test.webUiSummary", } # 檢查是否有對應的新鍵 new_key = legacy_mapping.get(key) if new_key: return self._get_nested_value(translations, new_key) return None def get_language_display_name(self, language_code: str) -> str: """獲取語言的顯示名稱""" # 直接從當前語言的翻譯中獲取,避免遞歸 current_translations = self._translations.get(self._current_language, {}) # 根據語言代碼構建鍵值 lang_key = None if language_code == "zh-TW": lang_key = "languageNames.zhTw" elif language_code == "zh-CN": lang_key = "languageNames.zhCn" elif language_code == "en": lang_key = "languageNames.en" else: # 通用格式 lang_key = f"languageNames.{language_code.replace('-', '').lower()}" # 直接獲取翻譯,避免調用 self.t() 產生遞歸 if lang_key: display_name = self._get_nested_value(current_translations, lang_key) if display_name: return display_name # 回退到元資料中的顯示名稱 meta = self.get_language_info(language_code) display_name = meta.get("displayName", language_code) return str(display_name) if display_name else language_code def reload_translations(self) -> None: """重新載入所有翻譯檔案(開發時使用)""" self._load_all_translations() def add_language(self, language_code: str, translation_file_path: str) -> bool: """動態添加新語言支援""" try: translation_file = Path(translation_file_path) if not translation_file.exists(): return False with open(translation_file, encoding="utf-8") as f: data = json.load(f) self._translations[language_code] = data if language_code not in self._supported_languages: self._supported_languages.append(language_code) debug_log( f"成功添加語言 {language_code}: {data.get('meta', {}).get('displayName', language_code)}" ) return True except Exception as e: debug_log(f"添加語言失敗 {language_code}: {e}") return False # 全域的國際化管理器實例 _i18n_manager = None def get_i18n_manager() -> I18nManager: """獲取全域的國際化管理器實例""" global _i18n_manager if _i18n_manager is None: _i18n_manager = I18nManager() return _i18n_manager def t(key: str, **kwargs) -> str: """便捷的翻譯函數""" return get_i18n_manager().t(key, **kwargs) def set_language(language: str) -> bool: """設定語言""" return get_i18n_manager().set_language(language) def get_current_language() -> str: """獲取當前語言""" return get_i18n_manager().get_current_language() def reload_translations() -> None: """重新載入翻譯(開發用)""" get_i18n_manager().reload_translations()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/find-xposed-magisk/mcp-feedback'

If you have feedback or need assistance with the MCP directory API, please join our Discord server