# i18n — visionA-local 多語系 > 支援語言:**繁體中文(台灣)+ 英文**(使用者決策 Q13) > 預設:跟隨系統語系;找不到對應時 fallback 英文 --- ## 1. 涵蓋範圍 | 層 | 是否要 i18n | 方式 | |----|-----------|------| | Next.js 業務前端 | ✅ 必要 | i18next / next-intl | | Wails app(安裝精靈、錯誤對話框) | ✅ 必要 | Go 端 i18n map | | Go server log 訊息 | ❌ 英文固定 | 給開發者看,不翻譯 | | API 錯誤訊息(JSON response) | ⚠️ 看情境 | code + message 英文;前端用 code 轉成對應語系 | | README / 說明文件 | ⚠️ | 英文優先,第二版補中文 | ## 2. 前端 i18n(Next.js) ### 2.1 原專案現況 edge-ai-platform 的 `frontend/` 已經有 `useTranslation` hook(見 Design round 1),沿用即可。推測是用 `react-i18next` 或 `next-intl`。**M2 階段**拆分時一併確認並整合。 ### 2.2 檔案結構 ``` frontend/src/locales/ ├── en/ │ ├── common.json │ ├── dashboard.json │ ├── devices.json │ ├── models.json │ ├── workspace.json │ ├── settings.json │ └── errors.json └── zh-TW/ ├── common.json ├── dashboard.json ├── devices.json ├── models.json ├── workspace.json ├── settings.json └── errors.json ``` ### 2.3 語系切換 - Settings 頁新增「語言」下拉:跟隨系統 / 中文 / English - 使用者選擇後存進 `localStorage.locale` - 下次啟動讀取此值;無值時讀 `navigator.language` - 跟隨系統模式下,`zh-*` 全歸到 `zh-TW`,其他歸 `en` ### 2.4 關鍵字決策(避免翻譯混亂) | 英文 | 繁中 | 備註 | |------|------|------| | Device | 裝置 | 不用「設備」 | | Model | 模型 | | | Inference | 推論 | 不用「推理」 | | Workspace | 工作區 | | | Mock mode | 模擬模式 | | | Dashboard | 儀表板 | | | Classification | 分類 | | | Detection | 偵測 | 不用「檢測」 | | Face recognition | 臉部辨識 | | | Settings | 設定 | | | Upload | 上傳 | | | Flash | 燒錄 | (已砍,但文案保留供未來重開) | --- ## 3. Wails app i18n > Tray 已砍(第三輪使用者決策 Q-A=A3),本節僅涵蓋安裝精靈與錯誤對話框。 ### 3.1 為何獨立做 Wails app 是 Go,在 Go server 還沒啟動前就需要顯示文字(安裝精靈、錯誤對話框)。不能依賴前端 i18n。 ### 3.2 實作 ```go // visiona-local/i18n.go package main import ( "embed" "encoding/json" "strings" "golang.org/x/text/language" ) //go:embed locales/*.json var localesFS embed.FS type Translator struct { current language.Tag messages map[language.Tag]map[string]string } func NewTranslator(preferred string) *Translator { t := &Translator{ messages: make(map[language.Tag]map[string]string), } t.load("en") t.load("zh-TW") t.setLocale(preferred) return t } func (t *Translator) load(code string) { data, _ := localesFS.ReadFile("locales/" + code + ".json") var m map[string]string json.Unmarshal(data, &m) tag, _ := language.Parse(code) t.messages[tag] = m } func (t *Translator) setLocale(preferred string) { if preferred == "" || preferred == "system" { preferred = detectSystemLocale() } if strings.HasPrefix(preferred, "zh") { t.current = language.TraditionalChinese } else { t.current = language.English } } func (t *Translator) T(key string) string { if msg, ok := t.messages[t.current][key]; ok { return msg } // fallback to English if msg, ok := t.messages[language.English][key]; ok { return msg } return key // last resort } ``` ### 3.3 檔案 ``` visiona-local/locales/ ├── en.json └── zh-TW.json ``` ### 3.4 關鍵 key 清單(給安裝精靈) ```json // en.json { "installer.welcome.title": "Welcome to visionA-local", "installer.welcome.subtitle": "Kneron KL520/KL720 local development tool", "installer.step.creating_dir": "Creating install directory", "installer.step.extract_binary": "Extracting server binary", "installer.step.extract_data": "Extracting model files", "installer.step.setup_python": "Setting up Python runtime", "installer.step.install_wheels": "Installing Python packages", "installer.step.install_driver": "Installing USB device driver", "installer.step.verify": "Verifying installation", "installer.error.no_python": "Python 3.12 is required but not found on your system.", "installer.error.driver_failed": "USB driver installation failed. You may need to allow the UAC prompt.", "error.already_running": "visionA-local is already running.", "error.port_in_use": "Port {port} is in use.", "error.server_unhealthy": "Server did not respond in time." } ``` ```json // zh-TW.json { "installer.welcome.title": "歡迎使用 visionA-local", "installer.welcome.subtitle": "Kneron KL520/KL720 本機開發工具", "installer.step.creating_dir": "建立安裝目錄", "installer.step.extract_binary": "解壓伺服器程式", "installer.step.extract_data": "解壓模型檔案", "installer.step.setup_python": "設定 Python 執行環境", "installer.step.install_wheels": "安裝 Python 套件", "installer.step.install_driver": "安裝 USB 裝置驅動", "installer.step.verify": "驗證安裝", "installer.error.no_python": "找不到 Python 3.12。請安裝後重試,或使用內建 Python 模式。", "installer.error.driver_failed": "USB 驅動安裝失敗,請確認已同意系統權限提示。", "error.already_running": "visionA-local 已在執行中。", "error.port_in_use": "連接埠 {port} 已被佔用。", "error.server_unhealthy": "伺服器未在預期時間內回應。" } ``` ### 3.5 參數化 簡單字串替換 `{key}` → `strings.Replace`,不做複數形 / 性別等進階 i18n(不值得)。 --- ## 4. 系統語系偵測 ### 4.1 各平台 API | 平台 | Go 偵測方式 | |------|-----------| | macOS | `defaults read -g AppleLocale` | | Windows | `GetUserDefaultLocaleName` via golang.org/x/sys | | Linux | 環境變數 `$LANG` / `$LC_MESSAGES` | ### 4.2 實作 ```go func detectSystemLocale() string { // 1. 先查 env(跨平台通用) for _, env := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} { if v := os.Getenv(env); v != "" { return normalizeLocale(v) } } // 2. 各平台特殊查詢 switch runtime.GOOS { case "darwin": out, _ := exec.Command("defaults", "read", "-g", "AppleLocale").Output() return normalizeLocale(strings.TrimSpace(string(out))) case "windows": // GetUserDefaultLocaleName via syscall return getWindowsLocale() } return "en" } func normalizeLocale(s string) string { // "zh_TW.UTF-8" → "zh-TW" s = strings.Split(s, ".")[0] s = strings.Replace(s, "_", "-", -1) if strings.HasPrefix(strings.ToLower(s), "zh") { return "zh-TW" } return "en" } ``` --- ## 5. 翻譯流程與維護 ### 5.1 新增字串的流程 1. 在 code 中使用 `t.T("some.new.key")` 2. 在 `en.json` 加上 key 3. 在 `zh-TW.json` 加上對應翻譯 4. lint 腳本檢查所有 key 在兩邊都存在(`scripts/check-i18n.sh`) ### 5.2 lint 腳本(建議加進 CI) ```bash #!/usr/bin/env bash # scripts/check-i18n.sh set -e for dir in frontend/src/locales visiona-local/locales; do en_keys=$(jq -r 'keys[]' "$dir/en.json" | sort) zh_keys=$(jq -r 'keys[]' "$dir/zh-TW.json" | sort) diff <(echo "$en_keys") <(echo "$zh_keys") || { echo "❌ i18n keys out of sync in $dir" exit 1 } done echo "✅ i18n keys are in sync" ``` --- ## 6. 第一版範圍(M2 階段實作) **M2 先做**: - [x] 英文 + 繁中 locale 檔建立 - [x] Wails app 端 i18n loader(安裝精靈 + 錯誤對話框) - [x] 前端整合現有 i18n(沿用 edge-ai-platform 的 hook) - [x] Settings 頁加入語言下拉 - [x] 切換語言後**需重啟 app 才生效**(首版妥協) **首版策略**:切換語言 → 顯示「已儲存,重啟後生效」→ 使用者手動關閉再開。這樣可以避開: - Next.js WebView 內的 i18n hot-reload 需要重跑所有 `useTranslation` hook - Wails 原生 menu bar 需要 `runtime.MenuSetApplicationMenu()` 重建 - 安裝精靈與錯誤對話框已經是 one-shot,重啟沒成本 **M2 以後才做**: - **動態語系切換(即時生效,不需重啟)**:對應 Design 規格 `10-i18n §10.7`,前端走 `i18n.changeLanguage()`、Wails 端 emit 事件重建 menu - API error code 對照表 — 先讓前端直接顯示英文 - 複數形處理 ### 6.1 Wails 原生 menu 快捷鍵清單(第四輪修訂) 對應第四輪決策 R4-6(⌘R → ⌘Shift+R、取消 ⌘Shift+W): | 功能 | 快捷鍵 | Wails menu API | |------|-------|---------------| | 前往 Dashboard | ⌘1 / Ctrl+1 | `keys.CmdOrCtrl("1")` | | 前往 Devices | ⌘2 / Ctrl+2 | `keys.CmdOrCtrl("2")` | | 前往 Models | ⌘3 / Ctrl+3 | `keys.CmdOrCtrl("3")` | | 前往 Workspace | ⌘4 / Ctrl+4 | `keys.CmdOrCtrl("4")` | | 設定 | ⌘, / Ctrl+, | `keys.CmdOrCtrl(",")` | | 重新整理裝置 | **⌘Shift+R / Ctrl+Shift+R** | `keys.Combo("r", keys.CmdOrCtrlKey, keys.ShiftKey)` | 避開 WebView 內建 ⌘R reload | | 結束 | ⌘Q / Ctrl+Q | `keys.CmdOrCtrl("q")` | | ~~切到 Workspace~~ | ~~⌘Shift+W~~ | **已移除**:與 macOS 「關閉所有視窗」衝突,且 ⌘4 已有相同功能 | **生產模式必須 disable WebView 預設的 ⌘R reload**(`wails.json` 或 `app.go` Assets middleware 攔截)。