mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
348 lines
13 KiB
Python
348 lines
13 KiB
Python
#!/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() |