jim800121chen c54f16fca0 Initial commit: visionA monorepo with local-tool subproject
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>
2026-04-11 22:10:38 +08:00

308 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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. 前端 i18nNext.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 攔截)。