jim800121chen 53e8ab4ae1 feat(visionA-frontend): Phase 0.9 模型庫下載 — 前端對接 FAA delegated download
對齊 ADR-017 v1.2 決策 2:model-card 下載按鈕 → 打 visionA endpoint 拿 url+token
→ fetch 跨 origin 直連 FAA + Authorization Bearer → blob 觸發下載(不經 visionA、不經 AWS)。

- src/lib/api/model-download.ts: getModelDownload(打 GET /api/models/:id/download)+
  downloadModelFile(fetch + Bearer header + blob,credentials:"omit" 不帶 visionA cookie)+
  triggerBlobDownload(延遲 revoke + finally 釋放)+ deriveDownloadFilename + ModelDownloadError
- model-store: downloadingId 互斥(同時只一個下載、finally 必清)+ downloadModel action +
  isModelDownloadable(source==converted && status==ready)
- model-card: 下載按鈕(converted+ready 才顯示、上傳類隱藏;preventDefault+stopPropagation
  防觸發外層 Link 導航;loading;toast 錯誤)
- i18n zh/en: models.action.download.* / models.download.*(各 error code 對應訊息)

關鍵差異:模型庫下載跨 origin + 需 Bearer header,不能用 <a href> navigation(無法帶 header),
必須 fetch+blob。與既有 conversion download(同 origin navigation)分流。

測試: 43 unit + 互動 test(mock fetch / 按鈕互動 / 顯示條件 / error 分流);tsc/lint/build 全綠。
Reviewer: 0 Critical / 0 Major / 3 Minor / 4 Suggestion,通過(ADR 決策 2 規格 7/7 符合)。

待 stage 實測: FAA 端須設 CORS(允許 visionA origin + preflight Allow-Headers: Authorization),
撞到會落 network_error。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 04:50:17 +08:00

656 lines
32 KiB
TypeScript
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.

/**
* English dictionary — visionA Cloud
*
* Must contain the exact same set of keys as zh-Hant.ts (the unit test enforces this).
*/
import type { Dictionary } from "../types";
export const en: Dictionary = {
// ── App ──
"app.title": "visionA Cloud",
"app.tagline": "Extending the local-tool experience to the cloud",
// ── Common ──
"common.loading": "Loading…",
"common.error": "Something went wrong",
"common.cancel": "Cancel",
"common.confirm": "Confirm",
"common.save": "Save",
"common.close": "Close",
"common.retry": "Retry",
"common.view": "View",
"common.manage": "Manage",
"common.connect": "Connect",
"common.disconnect": "Disconnect",
"common.delete": "Delete",
"common.edit": "Edit",
"common.back": "Back",
"common.na": "—",
// ── Sidebar navigation ──
"nav.dashboard": "Dashboard",
"nav.devices": "Devices",
"nav.models": "Models",
"nav.conversion": "Convert",
"nav.workspace": "Workspace",
"nav.clusters": "Clusters",
"nav.settings": "Settings",
// ── Prototype banner ──
// Phase 0.7 stage deployment fix: OIDC is wired to Innovedus Account Center; remove "no real auth" wording.
"banner.prototype":
"🚧 Prototype build · Member Center login enabled, but some flows are UI mockups; sample data only.",
"banner.prototype.short": "🚧 Prototype (demo)",
"banner.prototype.ariaLabel": "Prototype notice",
// ── Header ──
"header.toggleTheme": "Toggle theme",
"header.toggleLocale": "Toggle language",
"header.userMenu.open": "Open user menu",
"header.userMenu.profile": "Account settings",
"header.userMenu.logout": "Sign out",
"header.userMenu.fallbackName": "User",
"header.breadcrumb.home": "Home",
// ── Tunnel / remote status ──
"tunnel.status.online": "Connected",
"tunnel.status.offline": "Offline",
"tunnel.status.reconnecting": "Reconnecting",
"tunnel.rttLabel": "RTT {rtt}ms",
// ── Theme toggle ──
"theme.light": "Light",
"theme.dark": "Dark",
"theme.system": "System",
// ── Auth (Phase 0.6 OIDC redirect mode) ──
// Design notes:
// - All email/password form keys removed; visionA delegates auth to Innovedus Member Center
// - Shared sign-in/out copy lives under `auth.action.*` so non-form callsites (e.g. header
// dropdown) no longer borrow `auth.login.submit`
"auth.action.signIn": "Sign in",
"auth.action.signOut": "Sign out",
// Login page
"auth.login.title": "Sign in to visionA Cloud",
"auth.login.subtitle": "Welcome back to visionA Cloud",
"auth.login.welcomeBack": "Welcome back",
"auth.login.signInWithMC": "Sign in with your Innovedus account",
"auth.login.button": "Sign in",
"auth.login.noAccount": "Don't have an account?",
"auth.login.registerLink": "Register now",
"auth.login.prototypeHint":
"Phase 0.6 prototype — sign-in redirects to the Innovedus Account Center for verification.",
// Register page (Phase 0.6: visionA no longer hosts a sign-up form; redirects to Member Center)
"auth.register.title": "Register for visionA",
"auth.register.description":
"visionA uses the Innovedus unified account system.",
"auth.register.howTo":
"Please register at the Innovedus Account Center. Once done you can sign in to visionA.",
"auth.register.button": "Go to Innovedus Account Center",
"auth.register.disabledHint":
"Innovedus Account Center URL is not configured. Please contact the administrator.",
"auth.register.alreadyHaveAccount": "Already have an account?",
"auth.register.loginLink": "Sign in",
// ── Home / Dashboard ──
"home.welcome": "Welcome back, {name}",
"home.placeholder":
"This will be the Dashboard (StatCard / ActivityTimeline / ConnectedDevicesList); F6 will port it from local-tool.",
"dashboard.title": "Dashboard",
"dashboard.subtitle": "Manage your cloud-based Edge AI assets",
"dashboard.models": "Models",
"dashboard.devices": "Devices",
"dashboard.connected": "Online devices",
"dashboard.flashes": "Flashes",
"dashboard.connectedDevices": "Online devices",
"dashboard.noConnectedDevices":
"No devices are online. Pair a Kneron device to start cloud inference.",
"dashboard.recentActivity": "Recent activity",
"dashboard.noActivity":
"Nothing here yet. Activity appears after pairing, uploads, or inference runs.",
"dashboard.quickActions": "Quick actions",
"dashboard.browseModels": "Browse models",
"dashboard.manageDevices": "Manage devices",
"dashboard.uploadModel": "Upload model",
"dashboard.pairDevice": "Pair device",
"dashboard.empty.title": "No devices yet",
"dashboard.empty.description":
"Pair your first Kneron device to start running inference from anywhere.",
"dashboard.empty.action": "Pair a device",
"dashboard.activity.justNow": "just now",
"dashboard.activity.minutesAgo": "{n} minutes ago",
"dashboard.activity.hoursAgo": "{n} hours ago",
"dashboard.activity.daysAgo": "{n} days ago",
// ── Devices ──
"devices.title": "Devices",
"devices.subtitle": "Manage your Edge AI devices",
"devices.type": "Type",
"devices.firmware": "Firmware",
"devices.flashedModel": "Flashed model",
"devices.openWorkspace": "Open workspace",
"devices.addMore": "Pair a new device",
"devices.pairAction": "Pair a new device",
"devices.empty.title": "No devices paired yet",
"devices.empty.description":
"Run local agent on your computer and complete pairing to access your Kneron devices from anywhere.",
"devices.empty.action": "Pair your first device",
"devices.empty.secondaryAction": "How pairing works",
"devices.detail.id": "ID",
"devices.detail.type": "Type",
"devices.detail.firmware": "Firmware",
"devices.detail.port": "Port",
"devices.detail.deviceInfo": "Device info",
"devices.detail.modelStatus": "Model status",
"devices.detail.readyForInference": "Ready for inference",
"devices.detail.noModelFlashed": "No model has been flashed",
"devices.detail.pairedAt": "Paired at",
"devices.detail.hostName": "Host",
"devices.detail.lastSeen": "Last seen",
"devices.detail.offlineBanner.title": "This device is offline",
"devices.detail.offlineBanner.description":
"Some actions are unavailable until local agent reconnects.",
"devices.status.detected": "Detected",
"devices.status.connecting": "Connecting",
"devices.status.connected": "Connected",
"devices.status.flashing": "Flashing",
"devices.status.inferencing": "Inferencing",
"devices.status.error": "Error",
"devices.status.disconnected": "Disconnected",
// ── Remote Device Badge ──
"remote.status.online": "Online",
"remote.status.offline": "Offline",
"remote.status.reconnecting": "Reconnecting",
"remote.status.error": "Connection error",
"remote.status.unknown": "Unknown",
"remote.lastSeenNever": "Never connected",
"remote.lastSeen.justNow": "just now",
"remote.lastSeen.minutesAgo": "{n} min ago",
"remote.lastSeen.hoursAgo": "{n} h ago",
// ── Models ──
"models.title": "Models",
"models.subtitle": "Manage Kneron models on the cloud",
"models.size": "Size",
"models.createdAt": "Created",
"models.status.uploading": "Uploading",
"models.status.scanning": "Scanning",
"models.status.ready": "Ready",
"models.status.rejected": "Rejected",
"models.source.uploaded": "Uploaded",
"models.source.preset": "Preset",
"models.source.converted": "Converted",
"models.filters.label": "Model filters",
"models.filters.hardware": "Hardware",
"models.filters.source": "Source",
"models.filters.all": "All",
"models.empty.title": "No models yet",
"models.empty.description":
"Upload your first .nef model to deploy it to any paired Kneron device.",
"models.empty.action": "Upload your first model",
"models.detail.description": "Description",
"models.detail.version": "Version",
"models.detail.checksum": "Checksum",
"models.detail.supportedChips": "Supported chips",
"models.detail.deployToDevice": "Deploy to device",
// ── Model Upload Dialog ──
"models.upload.button": "Upload model",
"models.upload.dialog.title": "Upload model",
"models.upload.dropzone.hint": "Format: .nef · Max 100 MB",
"models.upload.selectedFile": "Choose file",
"models.upload.field.name": "Model name",
"models.upload.field.version": "Version",
"models.upload.field.notes": "Notes (optional)",
"models.upload.field.targetChip": "Target chip",
"models.upload.action.cancel": "Cancel",
"models.upload.action.start": "Start upload",
"models.upload.action.remove": "Remove",
"models.upload.uploading.title": "Uploading",
"models.upload.uploading.hint": "Do not close this window or navigate away.",
"models.upload.success.title": "Upload complete",
"models.upload.success.scanHint":
"The model is entering safety scan; it will be available once ready.",
"models.upload.error.invalidType":
"Only .nef files are supported, got {type}.",
"models.upload.error.tooLarge":
"File too large ({size}); max allowed is 100 MB.",
"models.upload.error.requiredField": "{field} is required.",
"models.upload.error.urlFailed": "Server is busy, please try again.",
"models.upload.error.networkLost": "Network lost, upload paused.",
"models.upload.toast.uploaded": "Model \"{name}\" uploaded.",
// ── Model download (Phase 0.9 FAA delegated download) ──
"models.action.download": "Download",
"models.action.downloading": "Downloading…",
"models.action.download.aria": "Download the model file to your device",
"models.action.download.unsupportedTooltip":
"Only converted models are downloadable in this phase",
"models.download.toast.start": "Download started",
"models.download.toast.hint":
"If you don't see a download prompt, check your browser settings",
"models.download.error.title": "Download failed",
"models.download.error.model_not_found": "Model not found.",
"models.download.error.forbidden": "You don't have permission to download this model.",
"models.download.error.upload_not_supported":
"Uploaded models are not downloadable in this phase.",
"models.download.error.sign_failed":
"Failed to obtain a download grant, please try again later.",
"models.download.error.download_failed":
"Failed to download from the file server, please try again later.",
"models.download.error.network_error":
"Network error, please check your connection and retry.",
"models.download.error.timeout": "Download timed out, please try again later.",
"models.download.error.busy": "A download is already in progress, please wait.",
"models.download.error.unknown": "Download failed, please try again later.",
// ── Workspace ──
"workspace.title": "Workspace",
"workspace.subtitle": "Select an online device to start inference",
"workspace.empty.title": "No devices are online",
"workspace.empty.description":
"Pair a device and make sure the local agent is connected to the cloud.",
"workspace.empty.action": "Go to devices",
"workspace.header.backToDevices": "Back to devices",
"workspace.header.title": "Workspace",
"workspace.inference.start": "Start inference",
"workspace.inference.stop": "Stop inference",
"workspace.placeholder.cameraComingSoon":
"Camera inference preview — F8 will wire up the MJPEG stream through the tunnel from local agent.",
"workspace.offline.title": "Device went offline",
"workspace.offline.description":
"The connection to {deviceName} was lost; inference has been stopped.",
"workspace.offline.backToList": "Back to devices",
"workspace.tabs.camera": "Camera",
"workspace.tabs.image": "Image (Phase 1)",
"workspace.tabs.video": "Video (Phase 1)",
"workspace.tabs.batch": "Batch (Phase 1)",
// ── Settings ──
"settings.title": "Settings",
"settings.subtitle": "Manage preferences and cloud endpoints",
"settings.tabs.general": "General",
"settings.tabs.advanced": "Advanced",
"settings.general.title": "Preferences",
"settings.general.language": "Language",
"settings.general.theme": "Theme",
"settings.general.themeHint":
"Automatically follow your system light / dark mode.",
"settings.advanced.title": "Cloud endpoints",
"settings.advanced.apiEndpoint": "API endpoint",
"settings.advanced.apiEndpointHint":
"Normal users should not change this; developers can switch to staging or localhost.",
"settings.advanced.apiUrl": "Current API URL",
"settings.advanced.wsUrl": "Current WebSocket URL",
"settings.advanced.about": "About",
"settings.advanced.version": "Version",
"settings.advanced.platform": "Platform",
// ── Pairing (F7) ──
"pairing.title": "Pair a new device",
"pairing.subtitle":
"Connect your Kneron device to the cloud so you can operate it from anywhere.",
"pairing.token.title": "Your pairing token",
"pairing.step1.description":
"Copy the token below and paste it into your local agent within 15 minutes.",
"pairing.copy": "Copy",
"pairing.copied": "Copied",
"pairing.regenerate": "Regenerate",
"pairing.timeRemaining": "{time} remaining",
"pairing.generatedAt": "Generated at {time}",
"pairing.token.expired.label": "This token has expired — please regenerate.",
"pairing.regenerateConfirm.title": "Regenerate token?",
"pairing.regenerateConfirm.description":
"The old token will be invalidated immediately; the new one is valid for 15 minutes.",
"pairing.security.warning":
"This token is valid for 15 minutes — complete pairing now.",
"pairing.security.oneTime":
"Tokens are single-use and expire automatically after pairing.",
"pairing.toast.copied": "Token copied — valid for 15 minutes.",
"pairing.toast.generateFailed": "Could not generate token — please retry.",
"pairing.toast.expiringSoon":
"Token expiring soon — complete pairing or regenerate.",
"pairing.toast.pairedSuccess": "Device {deviceName} paired successfully.",
"pairing.toast.cliCopied": "CLI command copied.",
"pairing.device.unknown": "Unknown device",
"pairing.cli.title": "CLI example",
"pairing.cli.description":
"Start local agent on your computer and pass the token to the --relay-token flag.",
"pairing.cli.copy": "Copy command",
"pairing.cli.hint":
"Once local agent connects to the cloud, this page detects it and forwards you to the device list.",
"pairing.step3.waiting": "Waiting for local agent to connect…",
"pairing.step3.elapsed": "Elapsed {time} (max 3 minutes)",
"pairing.step3.hints.running": "Confirm local agent is running",
"pairing.step3.hints.token":
"Confirm the token was pasted without missing or extra characters",
"pairing.step3.hints.network":
"Confirm your network can reach the cloud endpoint",
"pairing.step3.success": "Connected!",
"pairing.step3.success.detected": "Detected device",
"pairing.step3.failure.timeout": "Connection timeout",
"pairing.step3.failure.reason":
"No local agent connection within 3 minutes — agent may not be running.",
"pairing.step3.failure.retry": "Check again",
// ── Account (Phase 0.6 OIDC) ──
// Design notes:
// - Profile is read-only; edits go through the Innovedus Account Center
// - "Delete account" stays as a disabled stub; copy directs users to Member Center
"account.title": "Account settings",
"account.subtitle": "Profile is managed by the Innovedus Account Center",
"account.profile.title": "Profile",
"account.profile.userId": "User ID",
"account.profile.email": "Email",
"account.profile.name": "Display name",
"account.profile.namePlaceholder": "—",
"account.profile.managedBy":
"Your profile is managed by the Innovedus Account Center. To make changes, please go to the Innovedus Account Center.",
"account.profile.editAtMemberCenter": "Open Innovedus Account Center",
"account.profile.editDisabledHint":
"Innovedus Account Center URL is not configured. Please contact the administrator.",
"account.session.title": "Session",
"account.session.description":
"Signing out returns you to the login page. You will need to sign in again through the Innovedus Account Center.",
"account.danger.title": "Danger zone",
"account.danger.description":
"Account deletion is handled at the Innovedus Account Center; visionA does not perform deletion directly.",
"account.danger.deleteAccount": "Delete account",
"account.danger.deleteAccount.tooltip":
"Please handle this at the Innovedus Account Center.",
// ── Conversion (Phase 0.8 — feature-converter-integration) ──
// Spec: .autoflow/03-design/wireframes/wireframe-conversion.md §11
// PRD: .autoflow/02-prd/features/feature-converter-integration.md
// Scope: 5 states (idle / uploading / processing / success / failed) + 6 error code translations + expired
"conversion.title": "Convert",
"conversion.subtitle":
"Turn ONNX / TFLite models into .nef so they can run on Kneron edge devices.",
// idle empty state
"conversion.idle.heading": "No conversion in progress",
"conversion.idle.description":
"Upload an ONNX / TFLite model, pick a target Kneron chip, and we'll produce a flashable .nef file for you.",
"conversion.idle.cta": "Start conversion",
"conversion.idle.formats": "Supports .onnx / .tflite · max 500 MB",
"conversion.idle.about.title": "About conversion",
"conversion.idle.about.line1":
"Only one conversion job per user at a time (across tabs).",
"conversion.idle.about.line2":
"Results are downloadable for 7 days, then automatically cleared.",
"conversion.idle.about.line3":
"Conversion typically takes 110 minutes depending on model size.",
// Upload Dialog
"conversion.upload.title": "Start conversion",
"conversion.upload.description":
"Upload a model, choose a target chip, and optionally attach reference images for better accuracy.",
"conversion.upload.source.label": "Source model",
"conversion.upload.source.dropzone": "Drag .onnx / .tflite here",
"conversion.upload.source.or": "or",
"conversion.upload.source.browse": "Choose file",
"conversion.upload.source.formatHint":
"Format: .onnx · .tflite · max 500 MB",
"conversion.upload.source.remove": "Remove",
"conversion.upload.name.label": "Job name (optional)",
"conversion.upload.name.hint": "Display only; does not change output filename.",
"conversion.upload.chip.label": "Target chip",
"conversion.upload.refImages.label": "Reference images (optional)",
"conversion.upload.refImages.dropzone": "Drag images here (optional)",
"conversion.upload.refImages.hint":
"Reference images can improve quantized accuracy (max 100 images, ≤ 10 MB each).",
"conversion.upload.refImages.summary":
"{count} reference images selected ({totalSize})",
"conversion.upload.refImages.removeAll": "Remove all",
"conversion.upload.cancel": "Cancel",
"conversion.upload.start": "Start conversion",
"conversion.upload.error.noFile": "Please choose a .onnx or .tflite file.",
"conversion.upload.error.unsupported":
"Unsupported format — please use ONNX or TFLite.",
"conversion.upload.error.modelTooLarge":
"Model exceeds the 500 MB limit; please use a smaller model.",
"conversion.upload.error.noChip": "Please pick a target chip.",
"conversion.upload.error.refTooLarge":
"{filename} is over 10 MB — please remove or compress it.",
"conversion.upload.error.refTooMany":
"Reference images cannot exceed 100 files.",
// uploading stage (inside Dialog)
"conversion.uploading.title": "Uploading",
"conversion.uploading.heading": "Uploading to visionA…",
"conversion.uploading.progress": "{loaded} / {total} uploaded · {eta} remaining",
"conversion.uploading.almostDone": "Almost done…",
"conversion.uploading.warning":
"Please don't close this tab — the upload will be aborted.",
"conversion.uploading.cancel": "Cancel upload",
"conversion.uploading.cancelConfirm":
"Upload not finished — cancel anyway?",
"conversion.uploading.tabTitle": "visionA Cloud · Uploading ({pct}%)",
"conversion.uploading.toastCanceled": "Upload canceled.",
"conversion.uploading.toastFailed": "Upload failed: {reason}",
// F-T9 sub-3 / M1: upload failure toast hint
// M1 fix: File objects can't live in the store (non-serializable / unsafe across unmounts);
// chip / task name are preserved via store.formDraft, but the file itself must be re-selected.
"conversion.toast.retryHint":
"Your settings (chip / task name) are preserved — please re-select the file to retry.",
"conversion.toast.networkError":
"Network error — please check your connection and retry.",
"conversion.uploading.toastStarted":
"Conversion started (job #{shortJobId}).",
// F-T5: ETA / cancel confirm / beforeunload (detailed keys)
"conversion.uploading.subtitle": "Uploading model to visionA backend",
"conversion.uploading.eta.computing": "Estimating time remaining…",
"conversion.uploading.eta.almostDone": "Almost done…",
"conversion.uploading.eta.format": "{eta} remaining",
"conversion.uploading.progress.format": "{loaded} / {total}",
"conversion.uploading.info":
"Conversion will start automatically (typically 1030 seconds).",
"conversion.uploading.cancel.confirm.title": "Cancel conversion?",
"conversion.uploading.cancel.confirm.message":
"Upload is not finished — uploaded progress will be lost. Cancel anyway?",
"conversion.uploading.cancel.confirm.yes": "Yes, cancel",
"conversion.uploading.cancel.confirm.no": "Keep uploading",
"conversion.uploading.warning.beforeunload":
"Upload not finished — leaving will abort the upload.",
"conversion.uploading.target": "Target: {chip}",
"conversion.uploading.aria.cancel": "Cancel upload",
"conversion.uploading.aria.progress": "Upload progress",
"conversion.uploading.aria.status": "Uploaded {pct}%, {eta}",
// processing stage
"conversion.processing.title": "Convert",
"conversion.processing.cardHeading": "In progress",
"conversion.processing.statusBadge": "Converting",
"conversion.processing.startedAgo": "Started {time}",
"conversion.processing.stage1": "Upload complete",
"conversion.processing.stage2": "Parsing model",
"conversion.processing.stage3": "Compiling NEF",
"conversion.processing.processing": "Processing…",
"conversion.processing.hint":
"Typically 110 minutes · You can leave this page; progress will resume when you return.",
"conversion.processing.background.title": "Feel free to walk away",
"conversion.processing.background.l1":
"We poll progress in the background (every 510 seconds).",
"conversion.processing.background.l2":
"The tab title will notify you on completion.",
"conversion.processing.background.l3":
"Closing this page is fine — it will resume automatically.",
"conversion.processing.queueLong":
"Queue is currently long — you can come back later.",
"conversion.processing.runLong":
"This conversion is taking a while; still running.",
"conversion.processing.pollFailed":
"Couldn't fetch conversion status — please retry.",
"conversion.processing.bannerActive":
"A conversion was still running when you left.",
"conversion.processing.bannerExisting":
"You already have a conversion in progress — switched to that job.",
// F-T9 sub-2: banner dismiss button a11y label
"conversion.processing.bannerDismiss": "Dismiss notice",
// F-T6: ProcessingView (queued / running shared UI)
"conversion.processing.subtitle":
"Conversion in progress — please keep this page open",
"conversion.processing.queued": "Queued…",
"conversion.processing.stage.onnx": "Parsing model",
"conversion.processing.stage.bie": "Quantize & compile",
"conversion.processing.stage.nef": "Compile NEF",
"conversion.processing.stage.status.completed": "Done",
"conversion.processing.stage.status.current": "In progress",
"conversion.processing.stage.status.pending": "Pending",
"conversion.processing.stage.aria.completed": "{name} (completed)",
"conversion.processing.stage.aria.current": "{name} (in progress)",
"conversion.processing.stage.aria.pending": "{name} (pending)",
"conversion.processing.eta.pending": "Please wait",
"conversion.processing.eta.computing": "Estimating time remaining",
"conversion.processing.expiryHint":
"Job will be auto-deleted in 7 days — please download or add it to your model library before then.",
"conversion.processing.tabTitle.prefix": "(Converting) ",
"conversion.processing.aria.progressIndeterminate":
"Conversion in progress; remaining time unknown",
"conversion.processing.aria.queueProgress":
"Queued, waiting for conversion to start",
"conversion.processing.targetChipPrefix": "→",
// active job hint (also shown on idle)
"conversion.busy.title": "You already have a conversion in progress",
"conversion.busy.cta": "View progress",
// success
"conversion.success.heading": "Conversion complete",
"conversion.success.summary.chip": "Target chip",
"conversion.success.summary.size": "Output size",
"conversion.success.summary.duration": "Duration",
"conversion.success.summary.checksum": "checksum",
"conversion.success.summary.jobId": "Job",
"conversion.success.nextStep": "What's next?",
"conversion.success.import.title": "Add to model library",
"conversion.success.import.description":
"You can deploy it from the model library to any {chip} device.",
"conversion.success.import.cta": "Add to library",
"conversion.success.import.dialog.title": "Add to model library",
"conversion.success.import.dialog.description":
"Add this conversion result to the model library so you can deploy it to {chip} devices.",
"conversion.success.import.dialog.nameLabel": "Model name",
"conversion.success.import.dialog.nameHint":
"Display name in the model library; up to 100 characters; cannot contain / or \\.",
"conversion.success.import.dialog.nameError.required": "Please enter a model name.",
"conversion.success.import.dialog.nameError.tooLong":
"Model name must be 100 characters or fewer.",
"conversion.success.import.dialog.nameError.invalidChars":
"Model name cannot contain / or \\.",
"conversion.success.import.dialog.descLabel": "Description (optional)",
"conversion.success.import.dialog.sourceLabel": "Source",
"conversion.success.import.dialog.sourceValue":
"Conversion (job #{shortJobId})",
"conversion.success.import.dialog.confirm": "Add to library",
"conversion.success.import.dialog.cancel": "Cancel",
"conversion.success.import.processing": "Processing…",
"conversion.success.import.toastDone": "Added to model library.",
"conversion.success.import.toastDoneAction": "Open model library →",
"conversion.success.import.toastDup":
"This job has already been added to the library.",
"conversion.success.import.toastDupAction": "View existing model →",
"conversion.success.import.statusDone": "✓ Added (open it →)",
"conversion.success.import.errorGeneric":
"Couldn't add to model library; please try again.",
"conversion.success.import.aria.cta":
"Add to model library (opens a confirmation dialog)",
"conversion.success.download.title": "Download .nef",
"conversion.success.description":
"{source} has been converted into a .nef compatible with {chip}.",
"conversion.success.summary.outputFile": "Output file",
"conversion.success.summary.notAvailable": "—",
"conversion.success.download.description":
"Save it locally to use elsewhere.",
"conversion.success.download.cta": "Download",
"conversion.success.download.preparing": "Preparing download…",
"conversion.success.download.toastStart": "Download started.",
"conversion.success.download.toastHint":
"If no download prompt appears, check your browser settings.",
"conversion.success.download.toastFail": "Couldn't fetch download link.",
"conversion.success.download.aria.cta": "Download .nef file to your computer",
"conversion.success.download.aria.disabled":
"Download link expired; start a new conversion",
"conversion.success.expiry":
"This result will be auto-deleted in {time}; please finish using it before then.",
"conversion.success.expiry.expired": "This conversion result has expired.",
"conversion.success.expiry.remaining.daysHours": "{days}d {hours}h",
"conversion.success.expiry.remaining.daysOnly": "{days}d",
"conversion.success.expiry.remaining.hoursMinutes": "{hours}h {minutes}m",
"conversion.success.expiry.remaining.hoursOnly": "{hours}h",
"conversion.success.expiry.remaining.minutes": "{minutes}m",
"conversion.success.startNew": "Start new conversion",
// failed
"conversion.failed.heading": "Conversion failed",
"conversion.failed.errorCode": "Error code",
"conversion.failed.suggestionsTitle": "Things you can try:",
"conversion.failed.retry": "Try again",
"conversion.failed.backToModels": "Back to models",
"conversion.failed.contactSupport":
"If this keeps happening, copy the job ID and contact support.",
"conversion.failed.copyJobId": "Copy job ID",
"conversion.failed.toastJobIdCopied": "Job ID copied.",
// F-T8 — FailedView
"conversion.failed.aria.alert": "Conversion failure notice",
"conversion.failed.jobIdLabel": "Job ID",
"conversion.failed.summary.failedSuffix": " (failed)",
"conversion.failed.aria.retry": "Reset and start a new conversion",
// error code translations (mirrors zh-Hant; one key per suggestion line)
"conversion.error.UNSUPPORTED_FORMAT.message":
"This model format isn't supported — please use ONNX / TFLite.",
"conversion.error.UNSUPPORTED_FORMAT.suggestion1": "Verify file extension.",
"conversion.error.UNSUPPORTED_FORMAT.suggestion2":
"Use a standard export tool.",
"conversion.error.INVALID_CHECKSUM.message":
"File was corrupted in transit — please re-upload.",
"conversion.error.INVALID_CHECKSUM.suggestion1": "Restart the upload.",
"conversion.error.QUANTIZATION_FAILED.message":
"Model contains unsupported operators and can't be quantized for the target chip.",
"conversion.error.QUANTIZATION_FAILED.suggestion1": "Simplify the model.",
"conversion.error.QUANTIZATION_FAILED.suggestion2": "Remove custom ops.",
"conversion.error.QUANTIZATION_FAILED.suggestion3":
"Use a smaller input shape.",
"conversion.error.MODEL_TOO_LARGE.message": "Model exceeds the 500 MB limit.",
"conversion.error.MODEL_TOO_LARGE.suggestion1": "Use a smaller model.",
"conversion.error.MODEL_TOO_LARGE.suggestion2":
"Try pruning / quantization first.",
"conversion.error.QUOTA_EXCEEDED.message":
"System is busy right now — please try again later.",
"conversion.error.QUOTA_EXCEEDED.suggestion1": "Wait 5 minutes and retry.",
"conversion.error.unknown.message":
"Conversion failed. If it keeps happening, contact support.",
"conversion.error.unknown.suggestion1":
"Copy the job ID and report it to support.",
// expired / not-found
"conversion.expired.heading": "This conversion result has expired",
"conversion.expired.description":
"Conversion results are kept for 7 days; this one has been auto-cleared.",
// F-T9 sub-1: ExpiredView extra description — explain how to recover
"conversion.expired.subDescription":
"To get a new conversion result, please submit the model again.",
// F-T9 sub-1: ExpiredView a11y
"conversion.expired.aria.alert": "Conversion result has expired notice",
"conversion.expired.aria.startNew": "Reset and start a new conversion",
"conversion.expired.startNew": "Start new conversion",
// mobile hint
"conversion.mobileHint":
"Uploading large models on mobile can be unreliable; we recommend a desktop browser.",
// ── Clusters (F7 stub) ──
"clusters.title": "Clusters",
"clusters.subtitle":
"Combine multiple Kneron devices into a parallel inference cluster.",
"clusters.create": "Create cluster",
"clusters.empty.title": "Cluster inference — coming in Phase 1",
"clusters.empty.description":
"Phase 1 will port the POCs multi-device parallel inference and degrade handling to the cloud.",
"clusters.phase1Badge": "Coming in Phase 1",
"clusters.phase1Toast": "Cluster creation arrives in Phase 1.",
};