#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 國際化支援模組 =============== 提供統一的多語系支援功能,支援繁體中文、英文等語言。 自動偵測系統語言,並提供語言切換功能。 新架構: - 使用分離的 JSON 翻譯檔案 - 支援巢狀翻譯鍵值 - 元資料支援 - 易於擴充新語言 作者: Minidoracat """ import os import sys import locale import json from typing import Dict, Any, Optional, Union from pathlib import Path from .debug import i18n_debug_log as debug_log class I18nManager: """國際化管理器 - 新架構版本""" def __init__(self): self._current_language = None self._translations = {} self._supported_languages = ['zh-TW', 'en', 'zh-CN'] self._fallback_language = 'en' self._config_file = self._get_config_file_path() self._locales_dir = Path(__file__).parent / "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 / "translations.json" if translation_file.exists(): try: with open(translation_file, 'r', 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. 自動偵測系統語言 try: # 獲取系統語言 system_locale = locale.getdefaultlocale()[0] if system_locale: if system_locale.startswith('zh_TW') or system_locale.startswith('zh_Hant'): return 'zh-TW' elif system_locale.startswith('zh_CN') or system_locale.startswith('zh_Hans'): return 'zh-CN' elif system_locale.startswith('en'): return 'en' except Exception: pass # 4. 回退到默認語言 return self._fallback_language def _load_saved_language(self) -> Optional[str]: """載入保存的語言設定""" try: if self._config_file.exists(): with open(self._config_file, 'r', encoding='utf-8') as f: config = json.load(f) return config.get('language') 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 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: """獲取支援的語言列表""" return self._supported_languages.copy() def get_language_info(self, language_code: str) -> Dict[str, Any]: """獲取語言的元資料信息""" if language_code in self._translations: return self._translations[language_code].get('meta', {}) return {} def _get_nested_value(self, data: Dict[str, Any], key_path: str) -> Optional[str]: """從巢狀字典中獲取值,支援點分隔的鍵路徑""" keys = key_path.split('.') current = data for key in keys: if isinstance(current, dict) and key in current: current = current[key] else: return None return 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) -> Optional[str]: """獲取舊格式翻譯的兼容方法""" # 舊鍵到新鍵的映射 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', } # 檢查是否有對應的新鍵 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: """獲取語言的顯示名稱""" # 從當前語言的翻譯中獲取 lang_key = f"languageNames.{language_code.replace('-', '').lower()}" 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' display_name = self.t(lang_key) if display_name != lang_key: # 如果找到了翻譯 return display_name # 回退到元資料中的顯示名稱 meta = self.get_language_info(language_code) return meta.get('displayName', 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, 'r', 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()