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()