jim800121chen 06ff2fe987 feat(local-tool): M9-4 — Frontend FW badge + 升級 modal + WS hot-fix
A 階段第四個 milestone、完整 Frontend FW UI(badge / modal / 8 種 reason 復原)+ backend WS hot-fix(補對稱於 flash 的 firmware WS endpoint)。

Frontend(13 修改 / 7 新檔):
- 新 firmware/ component group (badge / upgrade-button / upgrade-dialog 4-phase / progress-view / error-view 8-reason / index)
- Zustand store (firmware-store.ts) + WS hook (use-firmware-progress.ts) 對齊既有 useFlashProgress pattern
- DeviceCard 整合 FirmwareBadge + FirmwareUpgradeButton
- i18n: settings.firmware.* namespace (對齊 Design Spec §9 SoT) + devices.card.fwBadge.* (zh-TW + en, 57 leaf keys × 2 lang = 114 strings)
- toast.ts ToastOptions interface (duration param)
- types/device.ts: FW 衍生欄位 + FirmwareStage/Reason/ProgressEvent/ActiveTask types

Backend WS hot-fix (3 檔):
- ws/firmware_ws.go (50 行、純對稱 flash_ws.go)
- ws/firmware_ws_test.go (165 行、2 smoke tests: broadcast + room isolation)
- router.go: GET /ws/devices/:id/firmware-progress

關鍵設計:
- R-FW-11 緩解: upgrading phase modal 不可關 (onInteractOutside/onEscapeKeyDown preventDefault + 隱藏 X)
- 多裝置隔離 defense in depth: store handleEvent activeDeviceId mismatch 直接 return
- 8 種 reason → 4 種 UX (recoverable/destructive/brick 警告/contactSupport)
- ContactSupport mailto handler (RFC 6068 + encodeURIComponent)

Reviewer 兩輪審查:
- Round 1: 0 Critical / 3 Major / 8 Minor / 5 Suggestion
- Round 2: 0 Critical / 0 Major / 0 Minor / 2 Suggestion(接受方案 A、不需 frontend 第 3 輪)
- MJ1 i18n namespace 採方案 A (settings.firmware.*)、Design SoT 優先、Reviewer 同意

測試:
- pnpm test --run: 60 tests pass (32 firmware: 22 store + 10 badge + 新 9 error-view + 19 既有)
- npx tsc --noEmit: 0 error
- pnpm build: production build 成功
- go test ./internal/api/ws/... -race: 1.964s 全綠
- pnpm lint firmware/: 0 hit (17 既有 lint 問題不屬 M9-4、follow-up)

未做(範圍外):
- Settings 韌體面板 (M9-12 B 階段)
- 手動降版 UI (M9-12)
- 版本切換 dropdown (B 階段)
- Wails 控制台 force-quit modal (M9-4.5)

A 階段 MVP 後端 + 前端開發全部完成、剩 M9-4.5 (SIGTERM + Wails OnBeforeClose) + M9-5 (三平台實機驗證)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:57:21 +08:00

506 lines
18 KiB
TypeScript

import type { TranslationDict } from './types';
export const en: TranslationDict = {
common: {
save: 'Save',
cancel: 'Cancel',
delete: 'Delete',
back: 'Back',
close: 'Close',
done: 'Done',
retry: 'Retry',
connect: 'Connect',
disconnect: 'Disconnect',
manage: 'Manage',
view: 'View',
upload: 'Upload',
uploading: 'Uploading...',
loading: 'Loading...',
search: 'Search',
reset: 'Reset',
start: 'Start',
stop: 'Stop',
na: 'N/A',
custom: 'Custom',
},
nav: {
dashboard: 'Dashboard',
modelLibrary: 'Model Library',
devices: 'Devices',
workspace: 'Workspace',
settings: 'Settings',
appName: 'visionA Local',
version: 'visionA Local v0.1.0',
platformTitle: 'visionA Local — Edge AI Workspace',
serverConnected: 'Server Connected',
serverDisconnected: 'Server Disconnected',
},
workspace: {
title: 'Workspace',
subtitle: 'Select a device to begin',
noConnectedDevice: 'No connected device',
noConnectedDeviceDesc: 'Connect a device to start inference.',
goToDevices: 'Go to Devices',
},
dashboard: {
title: 'Dashboard',
subtitle: 'Overview of your visionA workspace',
models: 'Models',
devices: 'Devices',
connected: 'Connected',
flashes: 'Flashes',
quickActions: 'Quick Actions',
browseModels: 'Browse Models',
manageDevices: 'Manage Devices',
uploadModel: 'Upload Model',
recentActivity: 'Recent Activity',
noActivity: 'No activity recorded yet.',
connectedDevices: 'Connected Devices',
noConnectedDevices: 'No connected devices.',
justNow: 'Just now',
minutesAgo: '{n}m ago',
hoursAgo: '{n}h ago',
daysAgo: '{n}d ago',
},
devices: {
title: 'Devices',
subtitle: 'Manage your edge AI devices',
scan: 'Scan Devices',
scanning: 'Scanning...',
noDevices: 'No Kneron devices detected. Please connect a KL520 / KL720 device and click "Scan".',
type: 'Type',
firmware: 'Firmware',
flashedModel: 'Flashed Model',
status: {
detected: 'Detected',
connecting: 'Connecting...',
connected: 'Connected',
disconnecting: 'Disconnecting...',
flashing: 'Flashing',
inferencing: 'Inferring',
error: 'Error',
disconnected: 'Disconnected',
},
health: {
title: 'Device Health',
status: 'Status',
firmwareVersion: 'Firmware Version',
uptime: 'Uptime',
lastSeen: 'Last Seen',
},
connectionLog: {
title: 'Connection History',
noEvents: 'No connection events recorded.',
connected: 'Connected',
disconnected: 'Disconnected',
},
settings: {
title: 'Device Settings',
alias: 'Custom Name',
aliasPlaceholder: 'e.g., Lab Device #1',
notes: 'Notes',
notesPlaceholder: 'Add notes about this device...',
saveSettings: 'Save Settings',
settingsSaved: 'Device settings saved',
},
detail: {
deviceInfo: 'Device Info',
id: 'ID',
type: 'Type',
firmware: 'Firmware',
port: 'Port',
modelStatus: 'Model Status',
readyForInference: 'Device is ready for inference. Open the workspace to start.',
noModelFlashed: 'No model has been flashed to this device yet. Use the Flash Model button to deploy a model.',
openWorkspace: 'Open Workspace',
},
flash: {
flashModel: 'Flash Model',
flashToDevice: 'Flash Model to Device',
selectModel: 'Select a model',
hardwareIncompatible: 'Hardware Incompatible',
incompatibleDesc: 'This model does not support {device}.',
incompatibleCannotFlash: 'Incompatible \u2014 Cannot Flash',
startFlash: 'Start Flash',
flashFailed: 'Flash Failed',
preparingFlash: 'Preparing flash...',
flashComplete: 'Flash complete!',
},
card: {
fwBadge: {
tooltipCurrent: 'Firmware is up to date ({version}).',
tooltipOlder: 'Firmware is older. Upgrade recommended.',
tooltipLegacy: 'Legacy KDP1 firmware. Upgrade to KDP2 for full feature support.',
tooltipUnknown: 'Firmware status unknown. Please rescan the device.',
deepLinkA11y: 'Firmware status \u2014 {deviceName}',
},
},
},
models: {
title: 'Model Library',
subtitle: 'Browse and select AI models for deployment',
noModelsFound: 'No models found matching your criteria.',
compareModels: 'Compare Models',
exitCompare: 'Exit Compare',
uploadModel: 'Upload Model',
accuracy: 'Accuracy',
fps: 'FPS',
size: 'Size',
hardware: 'Hardware',
latency: 'Latency',
framework: 'Framework',
inputSize: 'Input Size',
quantization: 'Quantization',
version: 'Version',
author: 'Author',
license: 'License',
labels: 'Labels',
performance: 'Performance',
specifications: 'Specifications',
hardwareCompatibility: 'Hardware Compatibility',
metadata: 'Metadata',
deployToDevice: 'Deploy to Device',
deleteCustomModel: 'Delete Custom Model',
deleteConfirm: 'Are you sure you want to delete "{name}"? This action cannot be undone.',
comparison: {
title: 'Model Comparison',
metric: 'Metric',
selected: '{n} models selected',
compare: 'Compare',
clear: 'Clear',
taskType: 'Task Type',
modelSize: 'Model Size',
},
filters: {
searchPlaceholder: 'Search models...',
taskType: 'Task Type',
allTypes: 'All Types',
allHardware: 'All Hardware',
},
upload: {
title: 'Upload Custom Model',
modelFile: 'Model File (.nef) *',
clickToSelect: 'Click to select .nef file',
modelName: 'Model Name *',
modelNamePlaceholder: 'e.g., My Custom YOLOv5',
description: 'Description',
descriptionPlaceholder: 'Optional description',
taskType: 'Task Type *',
taskTypePlaceholder: 'Select task type',
objectDetection: 'Object Detection',
classification: 'Classification',
segmentation: 'Segmentation',
poseEstimation: 'Pose Estimation',
labels: 'Labels * (comma separated)',
labelsPlaceholder: 'e.g., person, car, dog, cat',
inputWidth: 'Input Width',
inputHeight: 'Input Height',
quantization: 'Quantization',
errors: {
noFile: 'Please select a .nef file',
noName: 'Model name is required',
noTaskType: 'Task type is required',
noLabels: 'At least one label is required',
uploadFailed: 'Upload failed. Please try again.',
},
},
},
camera: {
camera: 'Camera',
image: 'Image',
video: 'Video',
startCamera: 'Start Camera',
stopCamera: 'Stop Camera',
stopImage: 'Stop Image',
stopVideo: 'Stop Video',
stopBatch: 'Stop Batch',
selectImage: 'Select Image',
selectImages: 'Select Images',
selectVideo: 'Select Video',
jpgPng: 'JPG, PNG',
jpgPngMultiple: 'JPG, PNG (multiple)',
mp4AviMov: 'MP4 / AVI / MOV / MPEG / MPG',
noInputSource: 'No input source',
selectSourceHint: 'Select a camera, image, or video above',
uploadedImage: 'Uploaded Image',
videoPlayback: 'Video Playback',
cameraFeed: 'Camera Feed',
dropImagesHere: 'Drop images here',
batchProcessing: 'Processing',
batchImages: 'Batch Images',
noCameraDetected: 'No camera detected. Use image or video for inference.',
},
inference: {
confidenceFilter: 'Confidence Filter',
confidenceThreshold: 'Confidence Threshold',
classificationResults: 'Classification Results',
noResultsAboveThreshold: 'No results above threshold',
details: 'Details',
model: 'Model',
task: 'Task',
latency: 'Latency',
fps: 'FPS',
workspace: 'Workspace',
startInference: 'Start Inference',
stopInference: 'Stop Inference',
batchProgress: 'Batch Progress',
processed: 'Processed',
currentImage: 'Viewing',
totalImages: 'Total',
videoProgress: 'Video Progress',
frames: 'frames',
framesProcessed: 'Frames Processed',
},
settings: {
title: 'Settings',
subtitle: 'Application settings',
tabs: {
general: 'General',
hardware: 'Hardware',
models: 'Models',
advanced: 'Advanced',
},
general: {
title: 'General',
language: 'Language',
languageFollowSystem: 'Follow system',
languageFollowSystemLabel: 'Follow system ({detected})',
languageRestartHint: 'Language changes apply immediately in the app, but native OS menus may need a restart.',
theme: 'Theme',
themeFollowSystem: 'Follow system',
themeFollowSystemHint: 'Dark mode follows your system setting.',
themeCurrentLight: 'Currently: Light',
themeCurrentDark: 'Currently: Dark',
dataDirectory: 'Data Directory',
dataDirectoryHint: 'Logs, settings and custom models are stored here.',
},
hardware: {
title: 'Hardware',
pythonMode: 'Python Runtime',
pythonModeAuto: 'Auto (prefer bundled)',
pythonModeBundled: 'Bundled (recommended)',
pythonModeSystem: 'System Python',
pythonModeHint: 'Read-only in this milestone. Switching will be available in a later release.',
serverPort: 'Server Port',
serverPortHint: 'Actual port the backend is listening on.',
},
models: {
title: 'Models',
presetModels: 'Preset Models',
presetModelsEmpty: 'No preset models available.',
presetModelsLoadFailed: 'Could not load model list.',
uploadPath: 'Upload Path',
uploadPathHint: 'Custom models you upload are stored here.',
},
advanced: {
title: 'Advanced',
viewLogs: 'View Server Logs',
viewLogsHint: 'Inspect recent server log entries.',
restartServer: 'Restart Server',
restartServerHint: 'All active inference sessions will stop. The UI reconnects automatically.',
about: 'About',
resetAll: 'Reset All Settings',
resetAllHint: 'Restore all preferences to their defaults. This does not delete data.',
},
firmware: {
badge: {
current: 'Latest',
older: 'Upgrade available',
legacy: 'KDP1',
unknown: 'Unknown',
},
button: {
upgrade: 'Upgrade firmware',
upgradeTo: 'Upgrade to {version}',
},
upgradeModal: {
title: 'Upgrade Firmware',
heading: 'Upgrade firmware on {deviceName}',
from: 'Current version',
to: 'Upgrading to',
warning: 'Upgrade takes ~{duration}. Do not unplug the device or close the application. The device will be re-detected automatically when done.',
actionStart: 'Start upgrade',
actionCancel: 'Cancel',
},
progress: {
upgradeTitle: 'Upgrading Firmware',
deviceHeading: 'Upgrading firmware on {deviceName}',
stage: {
preparing: 'Stage {n} / {total}: Preparing (detect + connect)',
loading: 'Stage {n} / {total}: Loading bootloader',
flashing: 'Stage {n} / {total}: Flashing firmware',
verifying: 'Stage {n} / {total}: Verifying',
done: 'Upgrade complete',
},
warningUpgrade: '⚠ Do not unplug the device',
estimatedRemaining: '~{seconds}s remaining',
elapsed: 'Elapsed {seconds}s',
},
success: {
upgradeToast: '{deviceName} upgrade succeeded',
upgradeDetail: 'From {from} to {to} (took {duration})',
partialSuccess: 'Firmware may have been written but verification failed. Please unplug, reconnect, and rescan.',
},
error: {
modalTitle: 'Firmware operation failed',
message: {
scanNotFound: 'Device not found, possibly disconnected. Please unplug and retry.',
connectFailed: 'Cannot connect to device. Please check the USB connection and retry.',
loaderWriteFailed: 'Bootloader load failed. Please unplug, reconnect, and retry.',
upgradeMidFailed: 'Firmware write failed. Possible hardware issue, please contact technical support.',
disconnect: 'Device disconnected during operation. State unknown; the device may be damaged. Please contact technical support.',
timeout: 'Operation timed out. The operation may or may not have completed. Please unplug, reconnect, and rescan.',
verifyMismatch: 'Firmware written but verification failed. The device may be damaged. Please contact technical support.',
verifyNotFound: 'Device not found after upgrade. The device may be damaged. Please contact technical support.',
},
errorCode: 'Error code: {code}',
technicalInfo: 'Technical info',
copyError: 'Copy error info',
copied: 'Copied ✓',
action: {
close: 'Close',
retry: 'Retry',
replugRetry: 'Unplug and retry',
rescan: 'Unplug and rescan',
contactSupport: 'Contact technical support',
},
contactSupportTooltip: 'Please contact technical support',
brickWarning: '⚠ The device may be damaged. Please contact technical support.',
},
},
serverConfig: 'Server Configuration',
backendUrl: 'Backend URL',
backendUrlPlaceholder: 'e.g., http://192.168.1.100:3721',
backendUrlHint: 'Leave empty for same-origin mode (frontend embedded in Go binary).',
backendUrlSaved: 'Backend URL updated',
saveBackendUrl: 'Save',
apiUrl: 'API URL',
wsUrl: 'WebSocket URL',
serverNote: 'Server addresses are configured at build time.',
appearance: 'Appearance',
theme: 'Theme',
themeLight: 'Light',
themeDark: 'Dark',
language: 'Language',
displayLanguage: 'Display Language',
languageZhTW: 'Traditional Chinese',
languageEn: 'English',
about: 'About',
versionLabel: 'Version',
platform: 'visionA Local — Edge AI Workspace',
resetToDefaults: 'Reset to Defaults',
resetSuccess: 'Settings restored to defaults',
serverLogs: {
title: 'Server Logs',
clear: 'Clear',
filter: 'Filter',
autoScroll: 'Auto-scroll',
noLogs: 'No log entries yet. Logs will appear as the server produces them.',
entries: 'entries',
},
serverStatus: {
title: 'Server Status',
uptimeLabel: 'Uptime',
goroutinesLabel: 'Goroutines',
heapAllocLabel: 'Heap Alloc',
sysMemLabel: 'System Mem',
gcCyclesLabel: 'GC Cycles',
nextGcLabel: 'Next GC',
versionLabel: 'Version',
buildTimeLabel: 'Build Time',
platformLabel: 'Platform',
goVersionLabel: 'Go Version',
depsTitle: 'Dependencies',
depAvailable: 'Available',
depNotFound: 'Not Found',
restart: 'Restart Server',
restarting: 'Restarting...',
waitingForServer: 'Waiting for server...',
serverBack: 'Server back online',
restartConfirmTitle: 'Restart Server?',
restartConfirmDesc: 'The server will restart. All active inference sessions will be stopped. The UI will reconnect automatically.',
restartConfirm: 'Restart',
offline: 'Server Offline',
offlineDesc: 'Cannot connect to the backend server. Make sure the server is running.',
backendAddress: 'Backend address',
reconnect: 'Reconnect',
reconnecting: 'Reconnecting...',
},
},
onboarding: {
welcome: 'Welcome to visionA Local!',
step1Title: 'Connect Your Device',
step1Desc: 'Plug in your Kneron USB Dongle and scan for devices. The platform will automatically detect supported hardware.',
step2Title: 'Choose an AI Model',
step2Desc: 'Browse the model library and flash a pre-trained model to your device. Models are optimized for edge inference.',
step3Title: 'Start Inference!',
step3Desc: 'Open the workspace, select a camera, image, or video, and run real-time AI inference on your edge device.',
next: 'Next',
previous: 'Previous',
getStarted: 'Get Started',
skipTour: 'Skip Tour',
},
emptyState: {
devicesTitle: 'No Devices Detected',
devicesDesc: 'Plug in your Kneron USB Dongle and click Scan to detect devices.',
devicesScan: 'Scan Devices',
modelsTitle: 'Explore AI Models',
modelsDesc: 'Browse pre-trained models or upload your own custom model.',
modelsBrowse: 'Browse Models',
modelsUpload: 'Upload Model',
},
tour: {
step1Title: 'Scan for Devices',
step1Desc: 'Click this button to scan for connected Kneron USB devices.',
step1Pending: 'Please click "Scan Devices" first. No devices detected yet.',
step2Title: 'Connect a Device',
step2Desc: 'Click Connect to establish a connection with the detected device.',
step2Pending: 'Please click "Connect" on a device first.',
step3Title: 'Manage Device',
step3Desc: 'Click Manage to view device details, flash models, and open the workspace.',
step3Pending: 'Please connect a device first to see the Manage button.',
step4Title: 'Flash a Model',
step4Desc: 'Select an AI model from the library and flash it to your device.',
step4Pending: 'Please flash a model to the device first.',
step5Title: 'Open Workspace',
step5Desc: 'Open the workspace to configure input sources and run inference.',
step5Pending: 'Please flash a model first — the workspace button will appear after.',
step6Title: 'Start Inference',
step6Desc: 'Select a camera or upload an image/video, then start real-time AI inference!',
step6Pending: 'Please start inference to complete the tour.',
next: 'Next',
prev: 'Previous',
done: 'Done',
exit: 'Exit Tour',
of: 'of',
helpTooltip: 'Start guided tour',
},
errors: {
serverDisconnected: 'Server connection lost',
serverReconnected: 'Server reconnected',
failedToLoadDevices: 'Failed to load device list',
deviceConnected: 'Device connected',
deviceDisconnected: 'Device disconnected',
modelUploadSuccess: 'Model uploaded successfully',
modelUploadFailed: 'Upload failed, please check network connection',
modelDeleted: 'Model deleted',
modelDeleteFailed: 'Delete failed',
cannotStopStream: 'Cannot stop stream',
imageUploadFailed: 'Image upload failed',
videoUploadFailed: 'Video upload failed',
batchUploadFailed: 'Batch image upload failed',
unexpectedError: 'An unexpected error occurred',
},
offline: {
title: 'Local Server Offline',
subtitle: {
quit: 'visionA Local has stopped. Please relaunch the application.',
restart: 'Server is restarting, please wait…',
healthcheck: 'The server is not responding. Reconnecting…',
},
retryButton: 'Retry connection',
retrying: 'Retrying…',
helpText: 'To leave this page, simply close this browser tab.',
},
};