2025-06-11 14:59:27 +08:00

380 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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-TW", "en", "zh-CN"]
self._fallback_language = "en"
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()