📖 更新 README.md 和 README.zh-TW.md,新增多語言支援功能介紹及版本歷史,並更新介面截圖。新增 i18n.py 模組以支援多語系,並重構相關檔案結構以提升可維護性。

This commit is contained in:
Minidoracat 2025-05-31 06:53:35 +08:00
parent a35fec2a5b
commit c470f686a9
18 changed files with 2913 additions and 1114 deletions

View File

@ -1,4 +1,4 @@
# Interactive Feedback MCP
# Interactive Feedback MCP
**🌐 Language / 語言切換:** **English** | [繁體中文](README.zh-TW.md)
@ -31,13 +31,20 @@ A simple [MCP server](https://modelcontextprotocol.io/) for implementing human-i
- **Error Handling**: Enhanced error handling ensuring stable program operation
- **Output Isolation**: Strict isolation of debug output from MCP communication
### 🌏 Multi-language Support
- **Full Internationalization**: Complete multi-language support with structured JSON translation files
- **Supported Languages**: Traditional Chinese, English, Simplified Chinese
- **Smart Detection**: Automatic language detection based on system locale
- **Easy Extension**: Simple JSON-based translation system for adding new languages
- **Legacy Compatibility**: Fully backward compatible with existing code
## 🖥️ Interface Preview
### Qt GUI Interface (Local Environment)
![Qt GUI Interface](docs/images/qt-gui-interface.png)
![Qt GUI Interface - English](docs/images/gui-en.png)
### Web UI Interface (SSH Remote Environment)
![Web UI Interface](docs/images/web-ui-interface.jpeg)
![Web UI Interface - English](docs/images/web-en.png)
Both interfaces support:
- 💬 Text feedback input
@ -45,6 +52,7 @@ Both interfaces support:
- ⚡ Real-time command execution
- 🎨 Modern dark theme
- 📱 Responsive design (Web UI)
- 🌐 Multi-language support (Traditional Chinese, English, Simplified Chinese)
## 🎯 Why Use This Tool?
@ -205,6 +213,46 @@ uvx mcp-feedback-enhanced@latest version
MCP_DEBUG=true uvx mcp-feedback-enhanced@latest test
```
## 🆕 Version History
### v2.0.9 - Multi-language Architecture Enhancement (Latest)
- 🌏 **Complete Multi-language Architecture Restructuring**: Migrated from embedded translations to structured JSON-based system
- 📁 **Organized Language Files**: Separated language files into `src/mcp_feedback_enhanced/locales/` directory structure
- 🔧 **Enhanced Internationalization**: Dynamic loading with nested key structure and browser language detection
- 📚 **Comprehensive Documentation**: Added detailed README for translation contributors with examples and guidelines
- 🔄 **Backward Compatibility**: Maintained full compatibility with existing code while enabling modern features
- 🖼️ **Interface Screenshots Update**: Added comprehensive screenshots showcasing both English and Traditional Chinese interfaces
- 📝 **Documentation Enhancement**: Updated README files with multi-language screenshots and feature descriptions
### v2.0.7 - Stability and Performance Improvements
- 🛡️ **Enhanced Error Handling**: Improved stability and error recovery mechanisms
- 🚀 **Performance Optimizations**: Faster startup times and improved resource management
- 🔧 **Bug Fixes**: Various minor fixes and improvements
- 📦 **Package Optimization**: Better dependency management and build process
- 🎨 **Visual Improvements**: Enhanced button visibility and color consistency
- 🖱️ **Interaction Fixes**: Improved drag-and-drop indicators and empty state hints
- 📱 **Responsive Design**: Better layout adaptation for different screen sizes
- 🌐 **Language Switching**: Fixed Qt GUI language switching with proper checkmarks
- 🛡️ **Complete Chinese Character Encoding Fix**: Perfect Chinese character display support
- 🔧 **JSON Parsing Error Resolution**: Fixed MCP client "Unexpected token" errors
- 🎛️ **Controllable Debug Mode**: Debug output control via `MCP_DEBUG` environment variable
- 🖼️ **Enhanced Image Support**: Improved image processing and Base64 encoding
- 🚀 **Output Isolation**: Strict separation of debug output from MCP communication
- 📦 **Package Optimization**: Improved uvx installation experience and dependency management
### v2.0.0 - Web UI and Remote Support
- ✅ **Web UI Interface**: Added Web UI support for SSH remote development environments
- ✅ **Automatic Environment Detection**: Smart interface selection based on environment
- ✅ **WebSocket Real-time Communication**: Live command execution and feedback
- ✅ **Modern Dark Theme**: Beautiful dark theme with responsive design
- ✅ **Persistent Testing Mode**: Enhanced testing capabilities
### v1.0.0 - Foundation (Original Author)
- ✅ **Qt GUI Interface**: Native desktop interface
- ✅ **Command Execution**: Real-time command execution and output
- ✅ **MCP Protocol Support**: Full Model Context Protocol implementation
- ✅ **Cross-platform Support**: Windows, macOS, and Linux compatibility
## 🛠️ Environment Variables
### Core Environment Variables

View File

@ -31,13 +31,20 @@
- **錯誤處理**:強化錯誤處理,確保程序穩定運行
- **輸出隔離**:嚴格隔離調試輸出與 MCP 通信
### 🌏 多語言支援
- **完整國際化**:採用結構化 JSON 翻譯檔案的完整多語言支援
- **支援語言**:繁體中文、英文、簡體中文
- **智能偵測**:根據系統語言環境自動偵測
- **易於擴充**:基於 JSON 的簡單翻譯系統,方便新增語言
- **向後兼容**:與現有程式碼完全向後兼容
## 🖥️ 介面預覽
### Qt GUI 介面(本地環境)
![Qt GUI Interface](docs/images/qt-gui-interface.png)
![Qt GUI Interface - Traditional Chinese](docs/images/gui-zh-tw.png)
### Web UI 介面SSH Remote 環境)
![Web UI Interface](docs/images/web-ui-interface.jpeg)
![Web UI Interface - Traditional Chinese](docs/images/web-zh-tw.png)
兩種介面都支援:
- 💬 文字回饋輸入
@ -45,6 +52,7 @@
- ⚡ 即時命令執行
- 🎨 現代化深色主題
- 📱 響應式設計Web UI
- 🌐 多語言支援(繁體中文、英文、簡體中文)
## 🎯 為什麼使用這個工具?
@ -406,7 +414,24 @@ AI 助手會如此調用 `mcp-feedback-enhanced` 工具:
## 🆕 版本更新
### v2.0.3 - 穩定性改善
### v2.0.9 - 多語言架構重構(最新版)
- 🌏 **完整多語言架構重構**:從嵌入式翻譯遷移到結構化 JSON 基礎系統
- 📁 **語言檔案組織化**:將語言檔案分離到 `src/mcp_feedback_enhanced/locales/` 目錄結構
- 🔧 **增強國際化功能**:動態載入與嵌套鍵值結構,支援瀏覽器語言偵測
- 📚 **完整文件說明**:為翻譯貢獻者新增詳細 README包含範例和指南
- 🔄 **向後兼容性**:在啟用現代功能的同時保持與現有程式碼的完全相容性
- 🖼️ **介面截圖更新**:新增完整截圖展示英文和繁體中文介面
- 📝 **文件強化**:更新 README 檔案,加入多語言截圖和功能說明
- 🛡️ **增強錯誤處理**:改善穩定性和錯誤恢復機制
- 🚀 **效能優化**:更快的啟動時間和改善的資源管理
- 🔧 **錯誤修復**:各種小型修復和改善
- 📦 **套件優化**:更好的相依性管理和建置流程
- 🎨 **視覺改善**:增強按鈕可見性和顏色一致性
- 🖱️ **互動修復**:改善拖拽指示器和空狀態提示
- 📱 **響應式設計**:更好的不同螢幕尺寸適應
- 🌐 **語言切換**:修復 Qt GUI 語言切換,加入適當的勾選標記
### v2.0.3 - 編碼與通訊修復
- 🛡️ **完全修復中文字符編碼問題**:支援完美的中文顯示
- 🔧 **解決 JSON 解析錯誤**:修復 MCP 客戶端的 "Unexpected token" 錯誤
- 🎛️ **可控調試模式**:透過 `MCP_DEBUG` 環境變數控制調試輸出
@ -414,19 +439,18 @@ AI 助手會如此調用 `mcp-feedback-enhanced` 工具:
- 🚀 **輸出隔離**:嚴格分離調試輸出與 MCP 通信
- 📦 **套件優化**:改善 uvx 安裝體驗和依賴管理
### v2.0 - Web UI 支援
- ✅ 新增 Web UI 介面支援 SSH remote 開發
- ✅ 自動環境檢測和介面選擇
- ✅ WebSocket 即時通訊
- ✅ 現代化深色主題
- ✅ 響應式設計支援
- ✅ 持久化測試模式
### v2.0.0 - Web UI 與遠端支援
- ✅ **Web UI 介面**:新增 Web UI 支援 SSH remote 開發環境
- ✅ **自動環境檢測**:根據環境智能選擇介面
- ✅ **WebSocket 即時通訊**:即時命令執行和回饋
- ✅ **現代化深色主題**:美觀的深色主題與響應式設計
- ✅ **持久化測試模式**:增強的測試功能
### v1.0 - 基礎版本(原作者)
- ✅ Qt GUI 介面
- ✅ 命令執行功能
- ✅ MCP 協議支援
- ✅ 多平台支援
### v1.0.0 - 基礎版本(原作者)
- ✅ **Qt GUI 介面**:原生桌面介面
- ✅ **命令執行**:即時命令執行和輸出
- ✅ **MCP 協議支援**:完整的模型上下文協議實作
- ✅ **跨平台支援**Windows、macOS 和 Linux 相容性
## 🐛 故障排除

BIN
docs/images/gui-en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/images/gui-zh-tw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/images/web-en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

BIN
docs/images/web-zh-tw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -6,10 +6,12 @@
基於 PySide6 的圖形用戶介面提供直觀的回饋收集功能
支援文字輸入圖片上傳命令執行等功能
新增多語系支援繁體中文英文簡體中文
作者: Fábio Ferreira
靈感來源: dotcursorrules.com
增強功能: 圖片支援和現代化界面設計
多語系支援: Minidoracat
"""
import os
@ -25,10 +27,13 @@ from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QTextEdit, QGroupBox,
QScrollArea, QFrame, QGridLayout, QFileDialog, QMessageBox,
QTabWidget, QSizePolicy
QTabWidget, QSizePolicy, QComboBox, QMenuBar, QMenu
)
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtGui import QFont, QPixmap, QDragEnterEvent, QDropEvent, QKeySequence, QShortcut
from PySide6.QtGui import QFont, QPixmap, QDragEnterEvent, QDropEvent, QKeySequence, QShortcut, QAction
# 導入多語系支援
from .i18n import t, get_i18n_manager
# ===== 調試日誌函數 =====
def debug_log(message: str) -> None:
@ -115,22 +120,25 @@ class ImagePreviewWidget(QLabel):
self.delete_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
color: #ffffff;
border: none;
border-radius: 10px;
font-weight: bold;
font-size: 12px;
font-size: 14px;
}
QPushButton:hover {
background-color: #d32f2f;
color: #ffffff;
}
QPushButton:hover { background-color: #d32f2f; }
""")
self.delete_button.clicked.connect(self._on_delete_clicked)
self.delete_button.setToolTip("刪除圖片")
self.delete_button.setToolTip(t('images_clear'))
def _on_delete_clicked(self) -> None:
"""處理刪除按鈕點擊事件"""
reply = QMessageBox.question(
self, '確認刪除',
f'確定要移除圖片 "{os.path.basename(self.image_path)}" 嗎?',
self, t('images_delete_title'),
t('images_delete_confirm', filename=os.path.basename(self.image_path)),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
@ -158,10 +166,10 @@ class ImageUploadWidget(QWidget):
layout.setContentsMargins(12, 8, 12, 8)
# 標題
title = QLabel("🖼️ 圖片附件(可選)")
title.setFont(QFont("", 10, QFont.Bold))
title.setStyleSheet("color: #007acc; margin: 1px 0;")
layout.addWidget(title)
self.title = QLabel(t('images_title'))
self.title.setFont(QFont("", 10, QFont.Bold))
self.title.setStyleSheet("color: #007acc; margin: 1px 0;")
layout.addWidget(self.title)
# 操作按鈕
self._create_buttons(layout)
@ -170,7 +178,7 @@ class ImageUploadWidget(QWidget):
self._create_drop_zone(layout)
# 狀態標籤 - 移到預覽區域前面
self.status_label = QLabel("已選擇 0 張圖片")
self.status_label = QLabel(t('images_status', count=0))
self.status_label.setStyleSheet("color: #9e9e9e; font-size: 10px; margin: 5px 0;")
layout.addWidget(self.status_label)
@ -182,15 +190,15 @@ class ImageUploadWidget(QWidget):
button_layout = QHBoxLayout()
# 選擇文件按鈕
self.file_button = QPushButton("📁 選擇文件")
self.file_button = QPushButton(t('btn_select_files'))
self.file_button.clicked.connect(self.select_files)
# 剪貼板按鈕
self.paste_button = QPushButton("📋 剪貼板")
self.paste_button = QPushButton(t('btn_paste_clipboard'))
self.paste_button.clicked.connect(self.paste_from_clipboard)
# 清除按鈕
self.clear_button = QPushButton("❌ 清除")
self.clear_button = QPushButton(t('btn_clear_all'))
self.clear_button.clicked.connect(self.clear_all_images)
# 設置按鈕樣式
@ -203,11 +211,39 @@ class ImageUploadWidget(QWidget):
font-weight: bold;
font-size: 11px;
}
QPushButton:hover {
opacity: 0.8;
}
"""
self.file_button.setStyleSheet(button_style + "QPushButton { background-color: #0e639c; }")
self.paste_button.setStyleSheet(button_style + "QPushButton { background-color: #4caf50; }")
self.clear_button.setStyleSheet(button_style + "QPushButton { background-color: #f44336; }")
self.file_button.setStyleSheet(button_style + """
QPushButton {
background-color: #0e639c;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.paste_button.setStyleSheet(button_style + """
QPushButton {
background-color: #4caf50;
}
QPushButton:hover {
background-color: #45a049;
}
""")
self.clear_button.setStyleSheet(button_style + """
QPushButton {
background-color: #f44336;
color: #ffffff;
}
QPushButton:hover {
background-color: #d32f2f;
color: #ffffff;
}
""")
button_layout.addWidget(self.file_button)
button_layout.addWidget(self.paste_button)
@ -218,7 +254,7 @@ class ImageUploadWidget(QWidget):
def _create_drop_zone(self, layout: QVBoxLayout) -> None:
"""創建拖拽區域"""
self.drop_zone = QLabel("🎯 拖拽圖片到這裡 (PNG、JPG、JPEG、GIF、BMP、WebP)")
self.drop_zone = QLabel(t('images_drag_hint'))
self.drop_zone.setFixedHeight(50)
self.drop_zone.setAlignment(Qt.AlignCenter)
self.drop_zone.setStyleSheet("""
@ -260,9 +296,9 @@ class ImageUploadWidget(QWidget):
"""選擇文件對話框"""
files, _ = QFileDialog.getOpenFileNames(
self,
"選擇圖片文件",
t('images_select'),
"",
"圖片文件 (*.png *.jpg *.jpeg *.gif *.bmp *.webp);;所有文件 (*)"
"Image files (*.png *.jpg *.jpeg *.gif *.bmp *.webp);;All files (*)"
)
if files:
self._add_images(files)
@ -469,21 +505,19 @@ class ImageUploadWidget(QWidget):
"""更新狀態標籤"""
count = len(self.images)
if count == 0:
self.status_label.setText("已選擇 0 張圖片")
self.status_label.setText(t('images_status', count=0))
else:
total_size = sum(img["size"] for img in self.images.values())
# 智能單位顯示
if total_size < 1024:
size_str = f"{total_size} B"
elif total_size < 1024 * 1024:
size_kb = total_size / 1024
size_str = f"{size_kb:.1f} KB"
else:
# 格式化文件大小
if total_size > 1024 * 1024: # MB
size_mb = total_size / (1024 * 1024)
size_str = f"{size_mb:.1f} MB"
else: # KB
size_kb = total_size / 1024
size_str = f"{size_kb:.1f} KB"
self.status_label.setText(f"已選擇 {count} 張圖片 (總計 {size_str})")
self.status_label.setText(t('images_status_with_size', count=count, size=size_str))
# 詳細調試信息
debug_log(f"=== 圖片狀態更新 ===")
@ -579,197 +613,332 @@ class ImageUploadWidget(QWidget):
debug_log(f"清理了 {cleaned_count} 個舊的臨時文件")
except Exception as e:
debug_log(f"臨時文件清理過程出錯: {e}")
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
# 更新標題
if hasattr(self, 'title'):
self.title.setText(t('images_title'))
# 更新按鈕文字
if hasattr(self, 'file_button'):
self.file_button.setText(t('btn_select_files'))
if hasattr(self, 'paste_button'):
self.paste_button.setText(t('btn_paste_clipboard'))
if hasattr(self, 'clear_button'):
self.clear_button.setText(t('btn_clear_all'))
# 更新拖拽區域文字
if hasattr(self, 'drop_zone'):
self.drop_zone.setText(t('images_drag_hint'))
# 更新狀態文字
self._update_status()
# ===== 主要回饋介面 =====
class FeedbackWindow(QMainWindow):
"""主要的回饋收集視窗"""
"""回饋收集主窗口"""
language_changed = Signal()
def __init__(self, project_dir: str, summary: str):
super().__init__()
self.project_dir = project_dir
self.summary = summary
self.result: Optional[FeedbackResult] = None
self.process: Optional[subprocess.Popen] = None
self.accepted = False
self.result = None
self.command_process = None
self.i18n = get_i18n_manager()
self._setup_ui()
self._apply_dark_style()
# 連接語言變更信號
self.language_changed.connect(self._refresh_ui_texts)
def _setup_ui(self) -> None:
"""設置用戶介面"""
self.setWindowTitle("互動式回饋收集")
self.setMinimumSize(800, 600)
self.setWindowTitle(t('app_title'))
self.setMinimumSize(900, 700)
self.resize(1000, 800)
# 主要元件
# 創建菜單欄
self._create_menu_bar()
# 中央元件
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
layout.setSpacing(8)
layout.setContentsMargins(16, 8, 16, 12)
# 主要佈局
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(10)
main_layout.setContentsMargins(10, 10, 10, 10)
# AI 工作摘要區域
self._create_summary_section(layout)
# AI 工作摘要(適度參與拉伸)
self._create_summary_section(main_layout)
# 分頁區域
self._create_tabs(layout)
# 分頁標籤(主要工作區域)
self._create_tabs(main_layout)
# 操作按鈕(固定大小)
self._create_action_buttons(main_layout)
# 設置比例拉伸摘要區域佔1份分頁區域佔3份按鈕不拉伸
summary_widget = main_layout.itemAt(0).widget() # 摘要區域
main_layout.setStretchFactor(summary_widget, 1) # 適度拉伸
main_layout.setStretchFactor(self.tabs, 3) # 主要拉伸區域
# 操作按鈕
self._create_action_buttons(layout)
def _create_menu_bar(self) -> None:
"""創建菜單欄"""
menubar = self.menuBar()
# 語言菜單
self.language_menu = menubar.addMenu(t('language_selector'))
self.language_actions = {}
# 添加語言選項
for lang_code in self.i18n.get_supported_languages():
action = QAction(self.i18n.get_language_display_name(lang_code), self)
action.setCheckable(True)
action.setChecked(lang_code == self.i18n.get_current_language())
action.triggered.connect(lambda checked, lang=lang_code: self._change_language(lang))
self.language_menu.addAction(action)
self.language_actions[lang_code] = action
def _change_language(self, language: str) -> None:
"""更改語言"""
if self.i18n.set_language(language):
# 更新所有菜單項目的勾選狀態
for lang_code, action in self.language_actions.items():
action.setChecked(lang_code == language)
# 發送語言變更信號
self.language_changed.emit()
def _refresh_ui_texts(self) -> None:
"""刷新界面文字"""
# 更新窗口標題
self.setWindowTitle(t('app_title'))
# 更新菜單文字
self._update_menu_texts()
# 更新標籤和按鈕文字
self._update_widget_texts()
# 更新圖片上傳元件的文字
self._update_image_upload_texts()
def _update_menu_texts(self) -> None:
"""更新菜單文字"""
# 更新語言菜單標題
self.language_menu.setTitle(t('language_selector'))
# 更新語言選項文字
for lang_code, action in self.language_actions.items():
action.setText(self.i18n.get_language_display_name(lang_code))
def _update_widget_texts(self) -> None:
"""更新元件文字"""
# 更新摘要標題
if hasattr(self, 'summary_title'):
self.summary_title.setText(t('ai_summary'))
# 更新專案目錄標籤
if hasattr(self, 'project_label'):
self.project_label.setText(f"{t('project_directory')}: {self.project_dir}")
# 更新分頁標籤
if hasattr(self, 'tab_widget'):
self.tab_widget.setTabText(0, t('feedback_tab'))
self.tab_widget.setTabText(1, t('command_tab'))
# 更新按鈕文字
if hasattr(self, 'submit_button'):
self.submit_button.setText(t('btn_submit_feedback'))
if hasattr(self, 'cancel_button'):
self.cancel_button.setText(t('btn_cancel'))
if hasattr(self, 'run_button'):
self.run_button.setText(t('btn_run_command'))
# 更新回饋區域標籤
if hasattr(self, 'feedback_title'):
self.feedback_title.setText(t('feedback_title'))
if hasattr(self, 'feedback_description'):
self.feedback_description.setText(t('feedback_description'))
if hasattr(self, 'feedback_input'):
self.feedback_input.setPlaceholderText(t('feedback_placeholder'))
# 更新命令區域標籤
if hasattr(self, 'command_title'):
self.command_title.setText(t('command_title'))
if hasattr(self, 'command_description'):
self.command_description.setText(t('command_description'))
if hasattr(self, 'command_input'):
self.command_input.setPlaceholderText(t('command_placeholder'))
if hasattr(self, 'output_title'):
self.output_title.setText(t('command_output'))
def _update_image_upload_texts(self) -> None:
"""更新圖片上傳元件的文字"""
if hasattr(self, 'image_upload'):
self.image_upload.update_texts()
def _create_summary_section(self, layout: QVBoxLayout) -> None:
"""創建 AI 工作摘要區域"""
summary_group = QGroupBox("📋 AI 工作摘要")
summary_group = QGroupBox()
summary_group.setTitle("")
summary_group.setMaximumHeight(200)
summary_layout = QVBoxLayout(summary_group)
summary_layout.setSpacing(8)
summary_layout.setContentsMargins(12, 8, 12, 12)
self.summary_text = QTextEdit()
self.summary_text.setPlainText(self.summary)
# 設置合理的高度範圍,允許適度拉伸
self.summary_text.setMinimumHeight(80)
self.summary_text.setMaximumHeight(250) # 增加最大高度,允許更多拉伸
self.summary_text.setReadOnly(True)
self.summary_text.setStyleSheet("background-color: #2d2d30; border: 1px solid #464647;")
# 標題與項目信息
header_layout = QHBoxLayout()
# 設置大小策略:允許適度垂直擴展
self.summary_text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# AI 工作摘要標題
self.summary_title = QLabel(t('ai_summary'))
self.summary_title.setFont(QFont("", 12, QFont.Bold))
self.summary_title.setStyleSheet("color: #007acc; margin-bottom: 5px;")
header_layout.addWidget(self.summary_title)
# 設置群組框的大小策略:允許適度擴展
summary_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
header_layout.addStretch()
# 專案目錄信息
self.project_label = QLabel(f"{t('project_directory')}: {self.project_dir}")
self.project_label.setStyleSheet("color: #9e9e9e; font-size: 11px;")
header_layout.addWidget(self.project_label)
summary_layout.addLayout(header_layout)
# 摘要內容(可滾動的文本區域)
summary_text = QTextEdit()
summary_text.setPlainText(self.summary)
summary_text.setReadOnly(True)
summary_text.setMaximumHeight(120)
summary_layout.addWidget(summary_text)
summary_layout.addWidget(self.summary_text)
layout.addWidget(summary_group)
def _create_tabs(self, layout: QVBoxLayout) -> None:
"""創建分頁標籤"""
self.tabs = QTabWidget()
# 設置分頁標籤的大小策略,確保能夠獲得主要空間
self.tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.tab_widget = QTabWidget()
# 回饋分頁
self._create_feedback_tab()
# 命令分頁
# 命令分頁
self._create_command_tab()
layout.addWidget(self.tabs)
layout.addWidget(self.tab_widget)
def _create_feedback_tab(self) -> None:
"""創建回饋分頁"""
feedback_widget = QWidget()
feedback_layout = QVBoxLayout(feedback_widget)
layout = QVBoxLayout(feedback_widget)
layout.setSpacing(8)
layout.setContentsMargins(12, 12, 12, 12)
# 文字回饋區域
feedback_group = QGroupBox("💬 您的回饋")
feedback_group_layout = QVBoxLayout(feedback_group)
# 回饋輸入區域
feedback_group = QGroupBox()
feedback_layout = QVBoxLayout(feedback_group)
feedback_layout.setSpacing(8)
feedback_layout.setContentsMargins(12, 8, 12, 12)
self.feedback_text = QTextEdit()
self.feedback_text.setPlaceholderText("請在這裡輸入您的回饋、建議或問題...\n\n💡 小提示:按 Ctrl+Enter 可快速提交回饋")
self.feedback_text.setMinimumHeight(150)
# 確保文字輸入區域能夠擴展
self.feedback_text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 回饋標題和說明
self.feedback_title = QLabel(t('feedback_title'))
self.feedback_title.setFont(QFont("", 11, QFont.Bold))
self.feedback_title.setStyleSheet("color: #007acc; margin-bottom: 5px;")
feedback_layout.addWidget(self.feedback_title)
# 添加快捷鍵支援
submit_shortcut = QShortcut(QKeySequence("Ctrl+Return"), self.feedback_text)
submit_shortcut.activated.connect(self._submit_feedback)
# 說明文字
self.feedback_description = QLabel(t('feedback_description'))
self.feedback_description.setStyleSheet("color: #9e9e9e; font-size: 10px; margin-bottom: 8px;")
self.feedback_description.setWordWrap(True)
feedback_layout.addWidget(self.feedback_description)
feedback_group_layout.addWidget(self.feedback_text)
feedback_layout.addWidget(feedback_group)
# 文字輸入框
self.feedback_input = QTextEdit()
self.feedback_input.setPlaceholderText(t('feedback_placeholder'))
self.feedback_input.setMinimumHeight(120)
feedback_layout.addWidget(self.feedback_input)
# 圖片上傳區域(允許適度拉伸)
layout.addWidget(feedback_group, stretch=2) # 給更多空間
# 圖片上傳區域
self.image_upload = ImageUploadWidget()
self.image_upload.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.image_upload.setMinimumHeight(200) # 設置最小高度
self.image_upload.setMaximumHeight(400) # 增加最大高度限制
feedback_layout.addWidget(self.image_upload)
layout.addWidget(self.image_upload, stretch=3) # 給圖片區域更多空間
# 設置比例拉伸文字區域佔2份圖片區域佔1份
feedback_layout.setStretchFactor(feedback_group, 2)
feedback_layout.setStretchFactor(self.image_upload, 1)
self.tab_widget.addTab(feedback_widget, t('feedback_tab'))
self.tabs.addTab(feedback_widget, "💬 回饋")
def _create_command_tab(self) -> None:
"""創建命令分頁"""
command_widget = QWidget()
command_layout = QVBoxLayout(command_widget)
layout = QVBoxLayout(command_widget)
layout.setSpacing(8)
layout.setContentsMargins(12, 12, 12, 12)
# 命令輸入區域
command_group = QGroupBox("⚡ 命令執行")
command_group_layout = QVBoxLayout(command_group)
command_group = QGroupBox()
command_layout = QVBoxLayout(command_group)
command_layout.setSpacing(8)
command_layout.setContentsMargins(12, 8, 12, 12)
# 命令輸入
cmd_input_layout = QHBoxLayout()
# 命令標題
self.command_title = QLabel(t('command_title'))
self.command_title.setFont(QFont("", 11, QFont.Bold))
self.command_title.setStyleSheet("color: #007acc; margin-bottom: 5px;")
command_layout.addWidget(self.command_title)
# 說明文字
self.command_description = QLabel(t('command_description'))
self.command_description.setStyleSheet("color: #9e9e9e; font-size: 10px; margin-bottom: 8px;")
self.command_description.setWordWrap(True)
command_layout.addWidget(self.command_description)
# 命令輸入和執行按鈕
input_layout = QHBoxLayout()
self.command_input = QLineEdit()
self.command_input.setPlaceholderText("輸入要執行的命令...")
self.command_input.setPlaceholderText(t('command_placeholder'))
self.command_input.returnPressed.connect(self._run_command)
input_layout.addWidget(self.command_input)
self.run_button = QPushButton("▶️ 執行")
self.run_button = QPushButton(t('btn_run_command'))
self.run_button.clicked.connect(self._run_command)
self.run_button.setFixedWidth(100)
input_layout.addWidget(self.run_button)
cmd_input_layout.addWidget(self.command_input)
cmd_input_layout.addWidget(self.run_button)
command_group_layout.addLayout(cmd_input_layout)
command_layout.addLayout(input_layout)
layout.addWidget(command_group)
# 命令輸出區域
output_group = QGroupBox()
output_layout = QVBoxLayout(output_group)
output_layout.setSpacing(8)
output_layout.setContentsMargins(12, 8, 12, 12)
self.output_title = QLabel(t('command_output'))
self.output_title.setFont(QFont("", 11, QFont.Bold))
self.output_title.setStyleSheet("color: #007acc; margin-bottom: 5px;")
output_layout.addWidget(self.output_title)
# 命令輸出
self.command_output = QTextEdit()
self.command_output.setReadOnly(True)
self.command_output.setMinimumHeight(200)
self.command_output.setStyleSheet("background-color: #1e1e1e; color: #ffffff; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;")
# 確保命令輸出區域能夠擴展
self.command_output.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
command_group_layout.addWidget(self.command_output)
self.command_output.setFont(QFont("Consolas", 9))
output_layout.addWidget(self.command_output)
# 設置群組框的大小策略
command_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
layout.addWidget(output_group, stretch=1)
command_layout.addWidget(command_group)
self.tab_widget.addTab(command_widget, t('command_tab'))
self.tabs.addTab(command_widget, "⚡ 命令")
def _create_action_buttons(self, layout: QVBoxLayout) -> None:
"""創建操作按鈕"""
button_layout = QHBoxLayout()
self.submit_button = QPushButton("✅ 提交回饋")
self.submit_button.clicked.connect(self._submit_feedback)
self.submit_button.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover { background-color: #45a049; }
""")
self.cancel_button = QPushButton("❌ 取消")
self.cancel_button.clicked.connect(self._cancel_feedback)
self.cancel_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover { background-color: #d32f2f; }
""")
button_layout.addStretch()
# 取消按鈕
self.cancel_button = QPushButton(t('btn_cancel'))
self.cancel_button.clicked.connect(self._cancel_feedback)
self.cancel_button.setFixedSize(120, 35)
button_layout.addWidget(self.cancel_button)
# 提交按鈕
self.submit_button = QPushButton(t('btn_submit_feedback'))
self.submit_button.clicked.connect(self._submit_feedback)
self.submit_button.setFixedSize(140, 35)
self.submit_button.setDefault(True)
button_layout.addWidget(self.submit_button)
layout.addLayout(button_layout)
@ -833,7 +1002,7 @@ class FeedbackWindow(QMainWindow):
try:
# 在專案目錄中執行命令
self.process = subprocess.Popen(
self.command_process = subprocess.Popen(
command,
shell=True,
cwd=self.project_dir,
@ -854,9 +1023,9 @@ class FeedbackWindow(QMainWindow):
def _read_command_output(self) -> None:
"""讀取命令輸出"""
if self.process and self.process.poll() is None:
if self.command_process and self.command_process.poll() is None:
try:
output = self.process.stdout.readline()
output = self.command_process.stdout.readline()
if output:
self.command_output.insertPlainText(output)
# 自動滾動到底部
@ -869,32 +1038,30 @@ class FeedbackWindow(QMainWindow):
# 進程結束
if hasattr(self, 'timer'):
self.timer.stop()
if self.process:
return_code = self.process.returncode
if self.command_process:
return_code = self.command_process.returncode
self.command_output.append(f"\n進程結束,返回碼: {return_code}\n")
def _submit_feedback(self) -> None:
"""提交回饋"""
self.result = {
"interactive_feedback": self.feedback_text.toPlainText(),
"interactive_feedback": self.feedback_input.toPlainText(),
"command_logs": self.command_output.toPlainText(),
"images": self.image_upload.get_images_data()
}
self.accepted = True
self.close()
def _cancel_feedback(self) -> None:
"""取消回饋"""
self.accepted = False
self.close()
def closeEvent(self, event) -> None:
"""處理視窗關閉事件"""
if hasattr(self, 'timer'):
self.timer.stop()
if self.process:
if self.command_process:
try:
self.process.terminate()
self.command_process.terminate()
except:
pass
@ -945,7 +1112,7 @@ def feedback_ui(project_directory: str, summary: str) -> Optional[FeedbackResult
app.exec()
# 返回結果
if window.accepted:
if window.result:
return window.result
else:
return None

View File

@ -0,0 +1,346 @@
#!/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
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
print(f"[I18N] 成功載入語言 {lang_code}: {data.get('meta', {}).get('displayName', lang_code)}")
except Exception as e:
print(f"[I18N] 載入語言檔案失敗 {lang_code}: {e}")
# 如果載入失敗,使用空的翻譯
self._translations[lang_code] = {}
else:
print(f"[I18N] 找不到語言檔案: {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)
print(f"[I18N] 成功添加語言 {language_code}: {data.get('meta', {}).get('displayName', language_code)}")
return True
except Exception as e:
print(f"[I18N] 添加語言失敗 {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()

View File

@ -0,0 +1,172 @@
# 多語系檔案結構說明
## 📁 檔案結構
```
locales/
├── README.md # 此說明檔案
├── zh-TW/ # 繁體中文
│ └── translations.json
├── en/ # 英文
│ └── translations.json
└── zh-CN/ # 簡體中文
└── translations.json
```
## 🌐 翻譯檔案格式
每個語言的 `translations.json` 檔案都包含以下結構:
### 1. 元資料區塊 (meta)
```json
{
"meta": {
"language": "zh-TW",
"displayName": "繁體中文",
"author": "作者名稱",
"version": "1.0.0",
"lastUpdate": "2025-01-31"
}
}
```
### 2. 應用程式區塊 (app)
```json
{
"app": {
"title": "應用程式標題",
"projectDirectory": "專案目錄",
"language": "語言",
"settings": "設定"
}
}
```
### 3. 分頁區塊 (tabs)
```json
{
"tabs": {
"feedback": "💬 回饋",
"command": "⚡ 命令",
"images": "🖼️ 圖片"
}
}
```
### 4. 其他功能區塊
- `feedback`: 回饋相關文字
- `command`: 命令執行相關文字
- `images`: 圖片上傳相關文字
- `buttons`: 按鈕文字
- `status`: 狀態訊息
- `aiSummary`: AI 摘要標題
- `languageSelector`: 語言選擇器標題
- `languageNames`: 語言顯示名稱
## 🔧 新增新語言步驟
### 1. 建立語言目錄
```bash
mkdir src/mcp_feedback_enhanced/locales/[語言代碼]
```
### 2. 複製翻譯檔案
```bash
cp src/mcp_feedback_enhanced/locales/en/translations.json \
src/mcp_feedback_enhanced/locales/[語言代碼]/translations.json
```
### 3. 修改元資料
```json
{
"meta": {
"language": "[語言代碼]",
"displayName": "[語言顯示名稱]",
"author": "[翻譯者姓名]",
"version": "1.0.0",
"lastUpdate": "[日期]"
}
}
```
### 4. 翻譯內容
逐一翻譯各個區塊的內容,保持 JSON 結構不變。
### 5. 註冊新語言
`i18n.py` 中將新語言代碼加入支援列表:
```python
self._supported_languages = ['zh-TW', 'en', 'zh-CN', '[新語言代碼]']
```
`i18n.js` 中也要加入:
```javascript
this.supportedLanguages = ['zh-TW', 'en', 'zh-CN', '[新語言代碼]'];
```
## 🎯 使用方式
### Python 後端
```python
from .i18n import t
# 新格式(建議)
title = t('app.title')
button_text = t('buttons.submitFeedback')
# 舊格式(兼容)
title = t('app_title')
button_text = t('btn_submit_feedback')
```
### JavaScript 前端
```javascript
// 新格式(建議)
const title = t('app.title');
const buttonText = t('buttons.submitFeedback');
// 舊格式(兼容)
const title = t('app_title');
const buttonText = t('btn_submit_feedback');
```
## 📋 翻譯檢查清單
建議在新增或修改翻譯時檢查:
- [ ] JSON 格式正確,沒有語法錯誤
- [ ] 所有必要的鍵值都存在
- [ ] 佔位符 `{param}` 格式正確
- [ ] 特殊字符和 Emoji 顯示正常
- [ ] 文字長度適合 UI 顯示
- [ ] 語言顯示名稱在 `languageNames` 中正確設定
## 🔄 向後兼容
新的多語系系統完全向後兼容舊的鍵值格式:
| 舊格式 | 新格式 |
|--------|--------|
| `app_title` | `app.title` |
| `btn_submit_feedback` | `buttons.submitFeedback` |
| `images_status` | `images.status` |
| `command_output` | `command.output` |
## 🚀 優勢特色
1. **結構化組織**:按功能區域分組,易於維護
2. **元資料支援**:包含版本、作者等資訊
3. **巢狀鍵值**:更清晰的命名空間
4. **動態載入**:前端支援從 API 載入翻譯
5. **向後兼容**:舊程式碼無需修改
6. **易於擴充**:新增語言非常簡單
## 📝 貢獻指南
歡迎貢獻新的語言翻譯:
1. Fork 專案
2. 按照上述步驟新增語言
3. 測試翻譯是否正確顯示
4. 提交 Pull Request
需要幫助可以參考現有的翻譯檔案作為範本。

View File

@ -0,0 +1,81 @@
{
"meta": {
"language": "en",
"displayName": "English",
"author": "Minidoracat",
"version": "1.0.0",
"lastUpdate": "2025-01-31"
},
"app": {
"title": "Interactive Feedback Collection",
"projectDirectory": "Project Directory",
"language": "Language",
"settings": "Settings"
},
"tabs": {
"feedback": "💬 Feedback",
"command": "⚡ Command",
"images": "🖼️ Images"
},
"feedback": {
"title": "Your Feedback",
"description": "Please describe your thoughts, suggestions, or changes needed for the AI work results.",
"placeholder": "Please enter your feedback, suggestions, or questions here...\n\n💡 Tip: Press Ctrl+Enter to submit quickly"
},
"command": {
"title": "Command Execution",
"description": "You can execute commands to verify results or gather more information.",
"placeholder": "Enter command to execute...",
"output": "Command Output"
},
"images": {
"title": "🖼️ Image Attachments (Optional)",
"select": "Select Files",
"paste": "Clipboard",
"clear": "Clear",
"status": "{count} images selected",
"statusWithSize": "{count} images selected (Total {size})",
"dragHint": "🎯 Drag images here (PNG, JPG, JPEG, GIF, BMP, WebP)",
"deleteConfirm": "Are you sure you want to remove image \"{filename}\"?",
"deleteTitle": "Confirm Delete",
"sizeWarning": "Image file size cannot exceed 1MB",
"formatError": "Unsupported image format"
},
"buttons": {
"submit": "Submit Feedback",
"cancel": "Cancel",
"close": "Close",
"clear": "Clear",
"submitFeedback": "✅ Submit Feedback",
"selectFiles": "📁 Select Files",
"pasteClipboard": "📋 Clipboard",
"clearAll": "✕ Clear",
"runCommand": "▶️ Run"
},
"status": {
"feedbackSubmitted": "Feedback submitted successfully!",
"feedbackCancelled": "Feedback cancelled.",
"timeoutMessage": "Feedback timeout",
"errorOccurred": "Error occurred",
"loading": "Loading...",
"connecting": "Connecting...",
"connected": "Connected",
"disconnected": "Disconnected",
"uploading": "Uploading...",
"uploadSuccess": "Upload successful",
"uploadFailed": "Upload failed",
"commandRunning": "Command running...",
"commandFinished": "Command finished",
"pasteSuccess": "Image pasted from clipboard",
"pasteFailed": "Failed to get image from clipboard",
"invalidFileType": "Unsupported file type",
"fileTooLarge": "File too large (max 1MB)"
},
"aiSummary": "AI Work Summary",
"languageSelector": "🌐 Language",
"languageNames": {
"zhTw": "繁體中文",
"en": "English",
"zhCn": "简体中文"
}
}

View File

@ -0,0 +1,81 @@
{
"meta": {
"language": "zh-CN",
"displayName": "简体中文",
"author": "Minidoracat",
"version": "1.0.0",
"lastUpdate": "2025-01-31"
},
"app": {
"title": "交互式反馈收集",
"projectDirectory": "项目目录",
"language": "语言",
"settings": "设置"
},
"tabs": {
"feedback": "💬 反馈",
"command": "⚡ 命令",
"images": "🖼️ 图片"
},
"feedback": {
"title": "您的反馈",
"description": "请描述您对 AI 工作结果的想法、建议或需要修改的地方。",
"placeholder": "请在这里输入您的反馈、建议或问题...\n\n💡 小提示:按 Ctrl+Enter 可快速提交反馈"
},
"command": {
"title": "命令执行",
"description": "您可以执行命令来验证结果或收集更多信息。",
"placeholder": "输入要执行的命令...",
"output": "命令输出"
},
"images": {
"title": "🖼️ 图片附件(可选)",
"select": "选择文件",
"paste": "剪贴板",
"clear": "清除",
"status": "已选择 {count} 张图片",
"statusWithSize": "已选择 {count} 张图片 (总计 {size})",
"dragHint": "🎯 拖拽图片到这里 (PNG、JPG、JPEG、GIF、BMP、WebP)",
"deleteConfirm": "确定要移除图片 \"{filename}\" 吗?",
"deleteTitle": "确认删除",
"sizeWarning": "图片文件大小不能超过 1MB",
"formatError": "不支持的图片格式"
},
"buttons": {
"submit": "提交反馈",
"cancel": "取消",
"close": "关闭",
"clear": "清除",
"submitFeedback": "✅ 提交反馈",
"selectFiles": "📁 选择文件",
"pasteClipboard": "📋 剪贴板",
"clearAll": "✕ 清除",
"runCommand": "▶️ 执行"
},
"status": {
"feedbackSubmitted": "反馈已成功提交!",
"feedbackCancelled": "已取消反馈。",
"timeoutMessage": "等待反馈超时",
"errorOccurred": "发生错误",
"loading": "加载中...",
"connecting": "连接中...",
"connected": "已连接",
"disconnected": "连接中断",
"uploading": "上传中...",
"uploadSuccess": "上传成功",
"uploadFailed": "上传失败",
"commandRunning": "命令执行中...",
"commandFinished": "命令执行完成",
"pasteSuccess": "已从剪贴板粘贴图片",
"pasteFailed": "无法从剪贴板获取图片",
"invalidFileType": "不支持的文件类型",
"fileTooLarge": "文件过大(最大 1MB"
},
"aiSummary": "AI 工作摘要",
"languageSelector": "🌐 语言选择",
"languageNames": {
"zhTw": "繁體中文",
"en": "English",
"zhCn": "简体中文"
}
}

View File

@ -0,0 +1,81 @@
{
"meta": {
"language": "zh-TW",
"displayName": "繁體中文",
"author": "Minidoracat",
"version": "1.0.0",
"lastUpdate": "2025-01-31"
},
"app": {
"title": "互動式回饋收集",
"projectDirectory": "專案目錄",
"language": "語言",
"settings": "設定"
},
"tabs": {
"feedback": "💬 回饋",
"command": "⚡ 命令",
"images": "🖼️ 圖片"
},
"feedback": {
"title": "您的回饋",
"description": "請描述您對 AI 工作結果的想法、建議或需要修改的地方。",
"placeholder": "請在這裡輸入您的回饋、建議或問題...\n\n💡 小提示:按 Ctrl+Enter 可快速提交回饋"
},
"command": {
"title": "命令執行",
"description": "您可以執行命令來驗證結果或收集更多資訊。",
"placeholder": "輸入要執行的命令...",
"output": "命令輸出"
},
"images": {
"title": "🖼️ 圖片附件(可選)",
"select": "選擇文件",
"paste": "剪貼板",
"clear": "清除",
"status": "已選擇 {count} 張圖片",
"statusWithSize": "已選擇 {count} 張圖片 (總計 {size})",
"dragHint": "🎯 拖拽圖片到這裡 (PNG、JPG、JPEG、GIF、BMP、WebP)",
"deleteConfirm": "確定要移除圖片 \"{filename}\" 嗎?",
"deleteTitle": "確認刪除",
"sizeWarning": "圖片文件大小不能超過 1MB",
"formatError": "不支援的圖片格式"
},
"buttons": {
"submit": "提交回饋",
"cancel": "取消",
"close": "關閉",
"clear": "清除",
"submitFeedback": "✅ 提交回饋",
"selectFiles": "📁 選擇文件",
"pasteClipboard": "📋 剪貼板",
"clearAll": "✕ 清除",
"runCommand": "▶️ 執行"
},
"status": {
"feedbackSubmitted": "回饋已成功提交!",
"feedbackCancelled": "已取消回饋。",
"timeoutMessage": "等待回饋超時",
"errorOccurred": "發生錯誤",
"loading": "載入中...",
"connecting": "連接中...",
"connected": "已連接",
"disconnected": "連接中斷",
"uploading": "上傳中...",
"uploadSuccess": "上傳成功",
"uploadFailed": "上傳失敗",
"commandRunning": "命令執行中...",
"commandFinished": "命令執行完成",
"pasteSuccess": "已從剪貼板貼上圖片",
"pasteFailed": "無法從剪貼板獲取圖片",
"invalidFileType": "不支援的文件類型",
"fileTooLarge": "文件過大(最大 1MB"
},
"aiSummary": "AI 工作摘要",
"languageSelector": "🌐 語言",
"languageNames": {
"zhTw": "繁體中文",
"en": "English",
"zhCn": "简体中文"
}
}

View File

@ -0,0 +1,472 @@
/**
* 前端國際化支援 - 新架構版本
* =============================
*
* 提供 Web UI 的多語系支援支援繁體中文英文簡體中文
* 新特性
* - 支援從 API 動態載入翻譯檔案
* - 巢狀翻譯鍵值支援
* - 舊格式兼容
* - 自動偵測瀏覽器語言
*/
class I18nManager {
constructor() {
this.currentLanguage = null;
this.translations = {};
this.supportedLanguages = ['zh-TW', 'en', 'zh-CN'];
this.fallbackLanguage = 'en';
this.isLoaded = false;
// 內嵌的備用翻譯(防止 API 載入失敗)
this.fallbackTranslations = this._getEmbeddedTranslations();
// 初始化語言設定
this.currentLanguage = this.detectLanguage();
}
/**
* 獲取內嵌的備用翻譯
*/
_getEmbeddedTranslations() {
return {
'zh-TW': {
app: {
title: 'Interactive Feedback MCP',
projectDirectory: '專案目錄',
language: '語言'
},
tabs: {
feedback: '💬 回饋',
command: '⚡ 命令'
},
feedback: {
title: '💬 您的回饋',
description: '請在這裡輸入您的回饋、建議或問題。您的意見將幫助 AI 更好地理解您的需求。',
placeholder: '請在這裡輸入您的回饋、建議或問題...\n\n💡 小提示:按 Ctrl+Enter 可快速提交回饋'
},
command: {
title: '⚡ 命令執行',
description: '您可以在此執行系統命令來驗證結果或獲取更多資訊。',
placeholder: '輸入要執行的命令...',
output: '命令輸出'
},
images: {
title: '🖼️ 圖片附件(可選)',
status: '已選擇 {count} 張圖片',
statusWithSize: '已選擇 {count} 張圖片 (總計 {size})',
dragHint: '🎯 拖拽圖片到這裡 (PNG、JPG、JPEG、GIF、BMP、WebP)',
deleteConfirm: '確定要移除圖片 "{filename}" 嗎?',
deleteTitle: '確認刪除'
},
buttons: {
selectFiles: '📁 選擇文件',
pasteClipboard: '📋 剪貼板',
clearAll: '✕ 清除',
runCommand: '▶️ 執行',
submitFeedback: '✅ 提交回饋',
cancel: '❌ 取消'
},
status: {
uploading: '上傳中...',
uploadSuccess: '上傳成功',
uploadFailed: '上傳失敗',
commandRunning: '命令執行中...',
commandFinished: '命令執行完成',
pasteSuccess: '已從剪貼板貼上圖片',
pasteFailed: '無法從剪貼板獲取圖片',
invalidFileType: '不支援的文件類型',
fileTooLarge: '文件過大(最大 1MB'
},
aiSummary: '📋 AI 工作摘要',
languageSelector: '🌐 語言選擇',
languageNames: {
zhTw: '繁體中文',
en: 'English',
zhCn: '简体中文'
}
},
'en': {
app: {
title: 'Interactive Feedback MCP',
projectDirectory: 'Project Directory',
language: 'Language'
},
tabs: {
feedback: '💬 Feedback',
command: '⚡ Commands'
},
feedback: {
title: '💬 Your Feedback',
description: 'Please enter your feedback, suggestions, or questions here. Your input helps AI better understand your needs.',
placeholder: 'Please enter your feedback, suggestions, or questions here...\n\n💡 Tip: Press Ctrl+Enter to submit quickly'
},
command: {
title: '⚡ Command Execution',
description: 'You can execute system commands here to verify results or get additional information.',
placeholder: 'Enter command to execute...',
output: 'Command Output'
},
images: {
title: '🖼️ Image Attachments (Optional)',
status: '{count} images selected',
statusWithSize: '{count} images selected (Total {size})',
dragHint: '🎯 Drag images here (PNG, JPG, JPEG, GIF, BMP, WebP)',
deleteConfirm: 'Are you sure you want to remove image "{filename}"?',
deleteTitle: 'Confirm Delete'
},
buttons: {
selectFiles: '📁 Select Files',
pasteClipboard: '📋 Clipboard',
clearAll: '✕ Clear',
runCommand: '▶️ Run',
submitFeedback: '✅ Submit Feedback',
cancel: '❌ Cancel'
},
status: {
uploading: 'Uploading...',
uploadSuccess: 'Upload successful',
uploadFailed: 'Upload failed',
commandRunning: 'Command running...',
commandFinished: 'Command finished',
pasteSuccess: 'Image pasted from clipboard',
pasteFailed: 'Failed to get image from clipboard',
invalidFileType: 'Unsupported file type',
fileTooLarge: 'File too large (max 1MB)'
},
aiSummary: '📋 AI Work Summary',
languageSelector: '🌐 Language',
languageNames: {
zhTw: '繁體中文',
en: 'English',
zhCn: '简体中文'
}
},
'zh-CN': {
app: {
title: 'Interactive Feedback MCP',
projectDirectory: '项目目录',
language: '语言'
},
tabs: {
feedback: '💬 反馈',
command: '⚡ 命令'
},
feedback: {
title: '💬 您的反馈',
description: '请在这里输入您的反馈、建议或问题。您的意见将帮助 AI 更好地理解您的需求。',
placeholder: '请在这里输入您的反馈、建议或问题...\n\n💡 小提示:按 Ctrl+Enter 可快速提交反馈'
},
command: {
title: '⚡ 命令执行',
description: '您可以在此执行系统命令来验证结果或获取更多信息。',
placeholder: '输入要执行的命令...',
output: '命令输出'
},
images: {
title: '🖼️ 图片附件(可选)',
status: '已选择 {count} 张图片',
statusWithSize: '已选择 {count} 张图片 (总计 {size})',
dragHint: '🎯 拖拽图片到这里 (PNG、JPG、JPEG、GIF、BMP、WebP)',
deleteConfirm: '确定要移除图片 "{filename}" 吗?',
deleteTitle: '确认删除'
},
buttons: {
selectFiles: '📁 选择文件',
pasteClipboard: '📋 剪贴板',
clearAll: '✕ 清除',
runCommand: '▶️ 执行',
submitFeedback: '✅ 提交反馈',
cancel: '❌ 取消'
},
status: {
uploading: '上传中...',
uploadSuccess: '上传成功',
uploadFailed: '上传失败',
commandRunning: '命令执行中...',
commandFinished: '命令执行完成',
pasteSuccess: '已从剪贴板粘贴图片',
pasteFailed: '无法从剪贴板获取图片',
invalidFileType: '不支持的文件类型',
fileTooLarge: '文件过大(最大 1MB'
},
aiSummary: '📋 AI 工作摘要',
languageSelector: '🌐 语言选择',
languageNames: {
zhTw: '繁體中文',
en: 'English',
zhCn: '简体中文'
}
}
};
}
/**
* API 載入翻譯檔案
*/
async loadTranslations() {
try {
// 嘗試從 API 載入翻譯
const response = await fetch('/api/translations');
if (response.ok) {
const data = await response.json();
this.translations = data;
this.isLoaded = true;
console.log('[I18N] 成功從 API 載入翻譯');
return true;
}
} catch (error) {
console.warn('[I18N] 無法從 API 載入翻譯,使用內嵌翻譯:', error);
}
// 使用內嵌翻譯作為備用
this.translations = this.fallbackTranslations;
this.isLoaded = true;
console.log('[I18N] 使用內嵌翻譯');
return false;
}
/**
* 自動偵測語言
*/
detectLanguage() {
// 1. 先檢查 localStorage
const savedLang = localStorage.getItem('mcp-feedback-language');
if (savedLang && this.supportedLanguages.includes(savedLang)) {
return savedLang;
}
// 2. 檢查瀏覽器語言設定
const browserLang = navigator.language || navigator.userLanguage;
// 映射常見的語言代碼
const langMap = {
'zh-TW': 'zh-TW',
'zh-HK': 'zh-TW',
'zh-MO': 'zh-TW',
'zh-CN': 'zh-CN',
'zh-SG': 'zh-CN',
'zh': 'zh-TW', // 默認繁體中文
'en': 'en',
'en-US': 'en',
'en-GB': 'en',
'en-AU': 'en',
'en-CA': 'en'
};
if (langMap[browserLang]) {
return langMap[browserLang];
}
// 3. 檢查語言前綴
const prefix = browserLang.split('-')[0];
if (langMap[prefix]) {
return langMap[prefix];
}
// 4. 回退到默認語言
return this.fallbackLanguage;
}
/**
* 設定語言
*/
setLanguage(language) {
if (!this.supportedLanguages.includes(language)) {
console.warn(`Unsupported language: ${language}`);
return false;
}
this.currentLanguage = language;
localStorage.setItem('mcp-feedback-language', language);
// 觸發語言變更事件
document.dispatchEvent(new CustomEvent('languageChanged', {
detail: { language: language }
}));
return true;
}
/**
* 從巢狀物件中獲取值
*/
_getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : null;
}, obj);
}
/**
* 舊鍵到新鍵的映射
*/
_getLegacyMapping() {
return {
// 應用程式標題
'app_title': 'app.title',
'project_directory': 'app.projectDirectory',
'language_selector': 'languageSelector',
// 語言名稱
'lang_zh_tw': 'languageNames.zhTw',
'lang_en': 'languageNames.en',
'lang_zh_cn': 'languageNames.zhCn',
// AI 摘要區域
'ai_summary': 'aiSummary',
// 分頁標籤
'feedback_tab': 'tabs.feedback',
'command_tab': 'tabs.command',
// 回饋區域
'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',
// 按鈕
'btn_select_files': 'buttons.selectFiles',
'btn_paste_clipboard': 'buttons.pasteClipboard',
'btn_clear_all': 'buttons.clearAll',
'btn_run_command': 'buttons.runCommand',
'btn_submit_feedback': 'buttons.submitFeedback',
'btn_cancel': 'buttons.cancel',
// 狀態消息
'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'
};
}
/**
* 獲取翻譯文字
*/
t(key, params = {}) {
// 確保翻譯已載入
if (!this.isLoaded) {
// 如果還沒載入,先嘗試從備用翻譯獲取
this.translations = this.fallbackTranslations;
}
const currentTranslations = this.translations[this.currentLanguage] || {};
// 嘗試新格式(巢狀鍵)
let translation = this._getNestedValue(currentTranslations, key);
// 如果沒有找到,嘗試舊格式映射
if (translation === null) {
const legacyMapping = this._getLegacyMapping();
const newKey = legacyMapping[key];
if (newKey) {
translation = this._getNestedValue(currentTranslations, newKey);
}
}
// 如果還是沒有找到,嘗試回退語言
if (translation === null) {
const fallbackTranslations = this.translations[this.fallbackLanguage] || {};
translation = this._getNestedValue(fallbackTranslations, key);
if (translation === null) {
const legacyMapping = this._getLegacyMapping();
const newKey = legacyMapping[key];
if (newKey) {
translation = this._getNestedValue(fallbackTranslations, newKey);
}
}
}
// 最後回退到鍵本身
if (translation === null) {
translation = key;
}
// 替換參數
if (typeof translation === 'string') {
translation = translation.replace(/{(\w+)}/g, (match, param) => {
return params[param] !== undefined ? params[param] : match;
});
}
return translation;
}
/**
* 獲取語言顯示名稱
*/
getLanguageDisplayName(languageCode) {
const key = `languageNames.${languageCode.toLowerCase().replace('-', '')}`;
if (languageCode === 'zh-TW') {
return this.t('languageNames.zhTw');
} else if (languageCode === 'zh-CN') {
return this.t('languageNames.zhCn');
} else if (languageCode === 'en') {
return this.t('languageNames.en');
}
return this.t(key);
}
/**
* 獲取當前語言
*/
getCurrentLanguage() {
return this.currentLanguage;
}
/**
* 獲取支援的語言列表
*/
getSupportedLanguages() {
return [...this.supportedLanguages];
}
/**
* 初始化載入翻譯
*/
async init() {
await this.loadTranslations();
return this.isLoaded;
}
}
// 創建全域實例
window.i18n = new I18nManager();
// 翻譯函數的全域快捷方式
window.t = function(key, params = {}) {
return window.i18n.t(key, params);
};
// 初始化函數
window.initI18n = async function() {
await window.i18n.init();
return window.i18n.isLoaded;
};

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
基於 FastAPI Web 用戶介面專為 SSH 遠端開發環境設計
支援文字輸入圖片上傳命令執行等功能
作者: Fábio Ferreira
作者: Minidoracat
靈感來源: dotcursorrules.com
增強功能: 圖片支援和現代化界面設計
"""

742
uv.lock generated

File diff suppressed because it is too large Load Diff