local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
308 lines
9.5 KiB
Markdown
308 lines
9.5 KiB
Markdown
# 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 攔截)。
|