From 522bbddccf5620224c5412a3a4bb36d974403bfc Mon Sep 17 00:00:00 2001 From: Palash Debnath Date: Wed, 1 Jul 2026 11:16:13 +0530 Subject: [PATCH] feat(translate): highlighted Install affordance for uninstalled engines + dismissable/auto-clearing error banner (#847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related Dub-tab translation-flow fixes, one PR. TASK 1 — proactive, highlighted Install affordance in the translate engine selector (replaces "find out only via a translate-time 400"): - FROM-SOURCE lane (activeEngineUnavailable && !enginesSandboxed): the muted install chip is promoted to a HIGHLIGHTED brand-accent Install button, still wired to handleInstallEngine(translateProvider) with the installing/disabled state. Selecting any uninstalled engine surfaces it immediately. - FROZEN lane (enginesSandboxed): pip install is impossible in the read-only, signed packaged env, so the disabled "needs dev install" span becomes an equally highlighted button opening a popover with (1) the exact install command + copy-to-clipboard, (2) one-click "Switch to Argos (bundled, offline)" — the guaranteed importable escape hatch, and (3) a Docs link via the existing Tauri shell.open path. Gated on the existing `sandboxed` flag, not platform. - Single-source install command: new translation_engines.install_command() is the one source of truth; list_engines() stamps `install_command` per engine and BOTH the argos + deep_translator translate-time 400 messages build their command from it, so the proactive button and the 400 can't drift. engines.ts gains `install_command: string | null`. TASK 2 — the translation error banner now dismisses and clears (class fix): - Root cause: handleTranslateAll never cleared dubError, so a stale 400 survived even a successful retry. It now clears at the start of every attempt. - Corrective-action clears (whole class): changing the engine and installing the package both clear dubError (wrapped setTranslateProvider + handleInstallEngine in DubTab). - DubFooter's banner gains a × dismiss and a guarded auto-timeout (skipped while generating so live per-segment errors persist). i18n: 8 new dub.* keys translated across all 21 locales. Docs: new docs/dubbing/translation-engines.md (from-source vs packaged build) linked from the popover Docs button + a troubleshooting cross-reference. Tests: FE regression for both lanes + never-installs-when-sandboxed + banner dismiss/auto-clear; BE regression that list_engines() install_command is embedded verbatim in the dub_translate 400s. Co-authored-by: mergetest Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 20 +++ backend/api/routers/dub_translate.py | 18 +- backend/services/translation_engines.py | 18 ++ docs/dubbing/translation-engines.md | 92 ++++++++++ docs/install/troubleshooting.md | 21 +++ frontend/src/api/engines.ts | 3 + frontend/src/components/dub/DubFooter.jsx | 40 ++++- frontend/src/components/dub/DubLeftColumn.jsx | 139 +++++++++++++-- frontend/src/hooks/useDubWorkflow.js | 5 + frontend/src/i18n/locales/ar.json | 8 + frontend/src/i18n/locales/de.json | 8 + frontend/src/i18n/locales/en.json | 8 + frontend/src/i18n/locales/es.json | 8 + frontend/src/i18n/locales/fr.json | 8 + frontend/src/i18n/locales/hi.json | 8 + frontend/src/i18n/locales/id.json | 8 + frontend/src/i18n/locales/it.json | 8 + frontend/src/i18n/locales/ja.json | 8 + frontend/src/i18n/locales/ko.json | 8 + frontend/src/i18n/locales/nl.json | 8 + frontend/src/i18n/locales/pl.json | 8 + frontend/src/i18n/locales/pt.json | 8 + frontend/src/i18n/locales/ru.json | 8 + frontend/src/i18n/locales/sv.json | 8 + frontend/src/i18n/locales/th.json | 8 + frontend/src/i18n/locales/tr.json | 8 + frontend/src/i18n/locales/uk.json | 8 + frontend/src/i18n/locales/vi.json | 8 + frontend/src/i18n/locales/zh-CN.json | 8 + frontend/src/i18n/locales/zh-TW.json | 8 + frontend/src/pages/DubTab.jsx | 18 +- frontend/src/test/DubErrorBanner.test.jsx | 68 ++++++++ .../test/DubTranslateEngineInstall.test.jsx | 161 ++++++++++++++++++ frontend/src/utils/errorDocsMap.ts | 4 + tests/test_translation_install_command.py | 71 ++++++++ 35 files changed, 827 insertions(+), 19 deletions(-) create mode 100644 docs/dubbing/translation-engines.md create mode 100644 frontend/src/test/DubErrorBanner.test.jsx create mode 100644 frontend/src/test/DubTranslateEngineInstall.test.jsx create mode 100644 tests/test_translation_install_command.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bc12dd73..acadd6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,26 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently. - **Factory reset in Storage.** A confirm-dialog-guarded action that clears the locally-saved UI preferences and reloads — without touching your voices, projects, or generated audio on disk. +- **Proactive, highlighted "Install" affordance for translation engines.** When + you pick a Dub translation engine whose optional package isn't installed yet + (e.g. Google / DeepL via `deep_translator`), the Engine selector now surfaces a + bright accent **Install** button *before* you hit Translate — no more + discovering the missing package only via a translate-time 400. On a from-source + install it one-click installs into the backend's own interpreter; on a + read-only **packaged build** it opens a popover with the exact `uv pip install …` + command (copy-to-clipboard), a one-click **Switch to Argos (bundled, offline)** + escape hatch, and a docs link. The install command is single-sourced in the + backend registry, so the button and the 400 error can never disagree. New guide: + `docs/dubbing/translation-engines.md`. + +### Fixed + +- **The "TRANSLATION FAILED" banner now dismisses and clears itself.** The Dub + translation-error banner used to be sticky — it survived a successful re-try and + never went away. It now has a close (×), auto-clears on the next corrective + action (re-translating, changing the engine, or installing the package), and + self-clears after a short timeout — fixing the whole class of translate/pipeline + banners that outlived the state that caused them. ## [0.3.8] — 2026-06-29 diff --git a/backend/api/routers/dub_translate.py b/backend/api/routers/dub_translate.py index dcec00a8..0c807ab0 100644 --- a/backend/api/routers/dub_translate.py +++ b/backend/api/routers/dub_translate.py @@ -413,11 +413,16 @@ async def dub_translate(req: TranslateRequest): try: import argostranslate # noqa: F401 except ImportError: + # Single-source the install command from the engine registry so + # this 400 and the proactive Install button in the Engine + # selector can never drift (see translation_engines.install_command). + from services.translation_engines import install_command + cmd = install_command("argos") or "uv pip install argostranslate" friendly = ( f"The '{provider}' translation engine needs the optional " f"`argostranslate` Python package, which isn't installed in " - f"this backend. Install it with `uv pip install argostranslate` " - f"(or `pip install argostranslate`) and restart the server, or " + f"this backend. Install it with `{cmd}` " + f"and restart the server, or " f"switch the Engine dropdown to another provider." ) return JSONResponse(status_code=400, content={"error": friendly}) @@ -470,11 +475,16 @@ async def dub_translate(req: TranslateRequest): try: import deep_translator # noqa: F401 except ImportError: + # Same single-source install command as the Engine selector's Install + # button (translation_engines.install_command) — google/deepl/ + # microsoft/mymemory all share the deep_translator package. + from services.translation_engines import install_command + cmd = install_command(provider) or "uv pip install deep_translator" friendly = ( f"The '{provider}' translation engine needs the optional " f"`deep_translator` Python package, which isn't installed in " - f"this backend. Install it with `uv pip install deep_translator` " - f"(or `pip install deep_translator`) and restart the server, or " + f"this backend. Install it with `{cmd}` " + f"and restart the server, or " f"switch the Engine dropdown to Argos (local, bundled), NLLB " f"(local, heavier), or OpenAI (LLM)." ) diff --git a/backend/services/translation_engines.py b/backend/services/translation_engines.py index e12db556..8d50e79e 100644 --- a/backend/services/translation_engines.py +++ b/backend/services/translation_engines.py @@ -120,6 +120,23 @@ def _probe(entry: dict) -> tuple[bool, str]: return False, f"import {mod!r} failed: {e}" +def install_command(engine: "str | dict | None") -> str | None: + """The exact shell command that makes this engine importable, or None. + + Single source of truth for the install string. BOTH the proactive Install + affordance in the Engine selector (via list_engines' ``install_command`` + field) AND the translate-time 400 error (dub_translate.py) read from here, + so the command a user is told to run can never drift between the two + surfaces. Returns None when the engine needs no separate install — either + it's unknown or its dependency is a core dep already pinned in + ``pyproject.toml`` (e.g. NLLB → transformers), in which case a + ``uv pip install`` line would be misleading. + """ + entry = engine if isinstance(engine, dict) else REGISTRY.get(engine) if engine else None + pkg = entry.get("pip_package") if entry else None + return f"uv pip install {pkg}" if pkg else None + + def list_engines() -> list[dict]: """Return a UI-ready list with per-engine availability stamped in.""" out = [] @@ -129,6 +146,7 @@ def list_engines() -> list[dict]: **e, "installed": installed, "availability_reason": reason, + "install_command": install_command(e), }) return out diff --git a/docs/dubbing/translation-engines.md b/docs/dubbing/translation-engines.md new file mode 100644 index 00000000..e0a2322b --- /dev/null +++ b/docs/dubbing/translation-engines.md @@ -0,0 +1,92 @@ +# Translation engines (Dub tab) + +OmniVoice dubs in two steps: **transcribe → translate → speak**. The *translate* +step is pluggable — pick the engine in the Dub tab's **Engine** dropdown. Two +engines are **built in** and always available offline; the rest need a small +optional Python package. + +| Engine | Category | Needs a package? | Key needed? | +|--------|----------|------------------|-------------| +| **Argos** (Local, Fast) | offline | `argostranslate` (bundled) | no | +| **NLLB-200** (Local, Heavy) | offline | none (uses core `transformers`) | no | +| Google Translate (Free) | online | `deep_translator` | no | +| DeepL | online | `deep_translator` | yes (`DEEPL_API_KEY`) | +| Microsoft Translator | online | `deep_translator` | yes (`MICROSOFT_API_KEY`) | +| MyMemory | online | `deep_translator` | no | +| LLM (OpenAI-compatible) | llm | `openai` | usually yes | + +If you pick an engine whose package isn't importable yet, the Engine label shows +a **highlighted Install affordance**, and — if you try to translate anyway — the +backend returns a single, actionable error telling you exactly what to install +(the install command is single-sourced, so the button and the error never +disagree). + +## Installing optional translation engines (from-source vs packaged build) + +How you add an engine depends on **how you installed OmniVoice**. + +### From-source / dev install (one-click) + +If you cloned the repo and run OmniVoice from source (`uv sync` + the dev +launcher) or via Docker, the app can install engines for you: + +1. In the Dub tab, open the translation settings and pick the engine you want + (e.g. **Google Translate**) from the **Engine** dropdown. +2. A highlighted **Install** button appears next to the *Engine* label. Click it. +3. OmniVoice runs the install into the **same** Python environment the backend + is using (`uv pip install --python `), then + re-probes. When it reports *"restart the backend to load it"*, restart so the + freshly-installed module is importable. + +You can also install by hand into the backend venv: + +``` +uv pip install deep_translator # Google / DeepL / Microsoft / MyMemory +uv pip install argostranslate # Argos (already bundled; rarely needed) +uv pip install openai # LLM (OpenAI-compatible) provider +``` + +Then restart the backend. + +### Packaged / installer build (read-only — use the popover) + +The signed desktop installers (`.dmg`, `.msi`, AppImage, `.deb`) ship a +**read-only, code-signed Python environment**. Installing extra packages into it +would break the signature, so **in-app install is intentionally disabled** on +these builds. Selecting an uninstalled engine there shows a highlighted button +that opens a small popover with everything you need: + +- **The exact command** to run (with a copy-to-clipboard button) if you *do* + have a from-source checkout somewhere and want the online engines there. +- **Switch to Argos (bundled, offline)** — one click. Argos and NLLB are always + importable in every build, so this is the guaranteed escape hatch: you can + keep dubbing immediately, fully offline, no install required. +- A link back to this page. + +**Recommendation for packaged builds:** just use **Argos** (fast, offline) or +**NLLB-200** (heavier, higher quality, offline). They need nothing installed and +never leave your machine. Reach for the online engines only from a from-source +install where you can add their package. + +## API keys (online engines) + +Some online engines need a key, set as an environment variable before launching +the backend (or in **Settings → Credentials**): + +- **DeepL:** `DEEPL_API_KEY` (optionally `DEEPL_BASE_URL` for a self-hosted / + pro endpoint). +- **Microsoft Translator:** `MICROSOFT_API_KEY` (optionally `MICROSOFT_BASE_URL`). +- **LLM provider:** `TRANSLATE_BASE_URL` + `TRANSLATE_API_KEY` + `TRANSLATE_MODEL` + (Ollama / LM Studio run locally and need no real key). + +## Troubleshooting + +- **"The 'google' translation engine needs the optional deep_translator Python + package…"** — the package isn't installed. On a from-source install, click the + Install button (or run the command above) and restart. On a packaged build, + switch to Argos/NLLB via the popover. +- **Install button does nothing / says "disabled in packaged builds"** — you're + on a signed installer build (expected). Use Argos/NLLB, or add the package in a + from-source checkout. +- **Installed it but still "needs install"** — restart the backend so Python + picks up the newly-installed module. diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index fdeae302..55d1182e 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -345,6 +345,27 @@ after a timeout with this exact guidance. Tune the bound with `OMNIVOICE_ASR_TRANSCRIBE_TIMEOUT_S` (seconds; default 300) — **raise** it for very long single files, **lower** it to fail faster on a small machine. +## Dub: "translation engine needs the optional … package" + +**Symptom:** in the Dub tab, translating fails with e.g. *"The 'google' +translation engine needs the optional `deep_translator` Python package, which +isn't installed in this backend."* + +**Cause:** the online translation engines (Google / DeepL / Microsoft / MyMemory +via `deep_translator`, and the LLM provider via `openai`) are **optional** and +not bundled. Only **Argos** and **NLLB** work out of the box. + +**Fix:** +- **From-source / Docker install:** click the highlighted **Install** button next + to the *Engine* label in the Dub tab (or run `uv pip install deep_translator` + in the backend venv) and restart the backend. +- **Packaged installer build:** in-app install is disabled (read-only signed + environment). Click the highlighted button to open the popover and **Switch to + Argos (bundled, offline)** — or copy the command to run it in a from-source + checkout. + +Full guide: [dubbing/translation-engines.md](../dubbing/translation-engines.md#installing-optional-translation-engines-from-source-vs-packaged-build). + ## First-run setup fails on a restricted network (GitHub/PyPI blocked) On networks that block or can't resolve **GitHub**, the first-run bootstrap may diff --git a/frontend/src/api/engines.ts b/frontend/src/api/engines.ts index f68db153..cea87d2a 100644 --- a/frontend/src/api/engines.ts +++ b/frontend/src/api/engines.ts @@ -17,6 +17,9 @@ interface TranslationEngine { notes?: string; installed: boolean; availability_reason: string; + /** `uv pip install ` (single-sourced by the backend registry), or + * null when the engine needs no separate install (builtin/core dep). */ + install_command: string | null; } export interface TranslationEnginesResponse { engines: TranslationEngine[]; diff --git a/frontend/src/components/dub/DubFooter.jsx b/frontend/src/components/dub/DubFooter.jsx index 9ff97d72..a0014ea6 100644 --- a/frontend/src/components/dub/DubFooter.jsx +++ b/frontend/src/components/dub/DubFooter.jsx @@ -1,7 +1,13 @@ -import { Check, AlertCircle } from 'lucide-react'; +import { useEffect } from 'react'; +import { Check, AlertCircle, X } from 'lucide-react'; import { Badge } from '../../ui'; import DubFailureNotice from './DubFailureNotice'; +// How long a translate/pipeline error banner lingers before it self-clears. +// Long enough to read a short message; the × and corrective-action clears are +// the primary escape hatches — this is the belt-and-suspenders timeout. +const ERROR_AUTOCLEAR_MS = 12000; + // Export-track toggle chips: flat pill outline, tinted by on/off/success state. const TRACK_LABEL = 'inline-flex items-center gap-[4px] px-[8px] py-[2px] border border-transparent rounded-[var(--chrome-radius-pill)] cursor-pointer transition-colors'; @@ -18,11 +24,24 @@ export default function DubFooter({ incrementalPlan, dubError, dubFailure, + onDismissError, exportTracks, setExportTracks, dubSegments, translateQuality, }) { + // Auto-clear the error banner after a grace period so it can't get stuck + // forever (issue: "TRANSLATION FAILED banner never goes away"). Skipped + // while generating/stopping, where the banner accumulates live per-segment + // errors the user needs to keep reading until the run ends. + const canAutoClear = + !!dubError && !!onDismissError && dubStep !== 'generating' && dubStep !== 'stopping'; + useEffect(() => { + if (!canAutoClear) return undefined; + const id = setTimeout(() => onDismissError(), ERROR_AUTOCLEAR_MS); + return () => clearTimeout(id); + }, [canAutoClear, dubError, onDismissError]); + return (
{dubStep === 'done' && ( @@ -46,9 +65,22 @@ export default function DubFooter({ )} {dubError && (
- - {dubError} - + + + {dubError} + + {onDismissError && ( + + )} +
)} diff --git a/frontend/src/components/dub/DubLeftColumn.jsx b/frontend/src/components/dub/DubLeftColumn.jsx index f3c6c0e0..e55b5f45 100644 --- a/frontend/src/components/dub/DubLeftColumn.jsx +++ b/frontend/src/components/dub/DubLeftColumn.jsx @@ -1,3 +1,4 @@ +import { useEffect, useRef, useState } from 'react'; import { Sparkles, Loader, @@ -7,6 +8,10 @@ import { UserSquare2, Languages, Wand2, + Download, + Copy, + ExternalLink, + ArrowRightLeft, } from 'lucide-react'; import { Button, Segmented, Progress } from '../../ui'; import WaveformTimeline from '../WaveformTimeline'; @@ -16,6 +21,9 @@ import { LANG_CODES } from '../../utils/languages'; import ALL_LANGUAGES from '../../languages.json'; import { POPULAR_LANGS, PRESETS } from '../../utils/constants'; import { dialectOptionsFor, dialectLabel, dialectMatchesLang } from '../../api/dialects'; +import { copyText } from '../../utils/copyText'; +import { openExternal } from '../../api/external'; +import { TRANSLATION_ENGINES_DOCS } from '../../utils/errorDocsMap'; import toast from 'react-hot-toast'; // ── Translation-settings bar utility class clusters ────────────────────── @@ -32,6 +40,11 @@ const FIELD_LABEL = const FIELD_INPUT = 'input-base !w-full !text-[0.65rem] !px-[5px] !py-[3px]'; const ENGINE_CHIP = 'ml-[6px] px-[6px] py-[1px] text-[0.55rem] leading-[1.4] bg-[rgba(211,134,155,0.14)] border border-[rgba(211,134,155,0.35)] text-[#d3869b] rounded-[999px] whitespace-nowrap transition-colors'; +// Highlighted accent Install affordance — brand accent (#d3869b) filled pill, +// deliberately louder than ENGINE_CHIP so an uninstalled selected engine is an +// obvious call to action rather than a muted footnote. +const ENGINE_INSTALL_BTN = + 'inline-flex items-center gap-[3px] ml-[6px] px-[7px] py-[1px] text-[0.55rem] font-semibold leading-[1.5] bg-[#d3869b] hover:bg-[#e0a0b3] text-[#1d2021] border border-[#d3869b] rounded-[999px] whitespace-nowrap cursor-pointer transition-colors shadow-[0_0_0_2px_rgba(211,134,155,0.25)] disabled:opacity-60 disabled:cursor-default'; export default function DubLeftColumn({ hasDubbedTrack, @@ -90,6 +103,41 @@ export default function DubLeftColumn({ setMultiLangs, editSegments, }) { + // Frozen-build (packaged/signed, read-only site-packages) escape-hatch + // popover: pip install is impossible, so we surface the copyable command + + // a one-click switch to the always-bundled Argos engine + a docs deeplink. + const [installPopoverOpen, setInstallPopoverOpen] = useState(false); + const installPopoverRef = useRef(null); + useEffect(() => { + if (!installPopoverOpen) return undefined; + const onDown = (e) => { + if (installPopoverRef.current && !installPopoverRef.current.contains(e.target)) { + setInstallPopoverOpen(false); + } + }; + const onKey = (e) => { + if (e.key === 'Escape') setInstallPopoverOpen(false); + }; + document.addEventListener('mousedown', onDown); + document.addEventListener('keydown', onKey); + return () => { + document.removeEventListener('mousedown', onDown); + document.removeEventListener('keydown', onKey); + }; + }, [installPopoverOpen]); + // Command shown/copied in the frozen popover — single-sourced from the + // backend registry (activeEngineEntry.install_command), with a defensive + // fallback so the popover is never empty for a known-uninstalled engine. + const installCmd = + activeEngineEntry?.install_command || + (activeEngineEntry?.pip_package ? `uv pip install ${activeEngineEntry.pip_package}` : ''); + const copyInstallCmd = async () => { + if (!installCmd) return; + const ok = await copyText(installCmd); + if (ok) toast.success(t('dub.install_cmd_copied')); + else toast.error(t('dub.copy_failed')); + }; + return (
{hasDubbedTrack && ( @@ -390,27 +438,98 @@ export default function DubLeftColumn({
)}
-
+
{t('dub.engine_label')} + {/* FROM-SOURCE lane: pip install works (uv pip install runs + in-process). Promote the muted chip to a highlighted accent + Install button so an uninstalled selected engine is an + obvious call to action. Keys off translateProvider, so + picking any uninstalled engine surfaces it immediately. */} {activeEngineUnavailable && !enginesSandboxed && ( )} + {/* FROZEN lane: packaged build, site-packages is read-only + + signed, so pip install is impossible. Offer a highlighted + button that opens a popover with the copyable command, a + one-click switch to bundled Argos, and a docs deeplink. */} {activeEngineUnavailable && enginesSandboxed && ( - - {t('dub.needs_dev_install')} + + + {installPopoverOpen && ( +
+
+ {t('dub.install_popover_title')} +
+

+ {t('dub.install_popover_frozen_body')} +

+ {installCmd && ( +
+ + {installCmd} + + +
+ )} + + +
+ )}
)}
diff --git a/frontend/src/hooks/useDubWorkflow.js b/frontend/src/hooks/useDubWorkflow.js index bfb6b1c1..4e0ba7bf 100644 --- a/frontend/src/hooks/useDubWorkflow.js +++ b/frontend/src/hooks/useDubWorkflow.js @@ -690,6 +690,11 @@ export default function useDubWorkflow({ const handleTranslateAll = useCallback(async () => { if (!dubSegments.length || !dubLangCode) return; setIsTranslating(true); + // Root cause of the "sticky TRANSLATION FAILED banner": a new translate + // attempt never cleared the previous failure, so a stale 400 survived even + // a successful retry. Clear it up front — the whole class of translate/ + // pipeline error banners should reset on the next relevant action. + setDubError(''); try { const data = await dubTranslate({ segments: dubSegments.map((s) => ({ diff --git a/frontend/src/i18n/locales/ar.json b/frontend/src/i18n/locales/ar.json index 74a2a9d0..f2b156d7 100644 --- a/frontend/src/i18n/locales/ar.json +++ b/frontend/src/i18n/locales/ar.json @@ -659,6 +659,14 @@ "consent_revoked": "تم إلغاء التحقق" }, "dub": { + "install_engine_pkg": "تثبيت {{pkg}}", + "needs_install_short": "يتطلب التثبيت", + "install_popover_title": "تثبيت هذا المحرك", + "install_popover_frozen_body": "لا يمكن لهذه النسخة المحزومة تثبيت المحركات — فبيئة Python الخاصة بها للقراءة فقط وموقّعة. شغّل الأمر أدناه في تثبيت من المصدر، أو بدّل إلى محرك Argos المضمّن.", + "switch_to_argos": "التبديل إلى Argos (مضمّن، دون اتصال)", + "copy_command": "نسخ الأمر", + "install_cmd_copied": "تم نسخ الأمر", + "dismiss_error": "إغلاق", "transcribe": "نسخ", "translate": "ترجمة", "generate": "إنشاء", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index b49d0be7..633b24a2 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -659,6 +659,14 @@ "consent_revoked": "Verifizierung widerrufen" }, "dub": { + "install_engine_pkg": "{{pkg}} installieren", + "needs_install_short": "Installation nötig", + "install_popover_title": "Diese Engine installieren", + "install_popover_frozen_body": "Diese paketierte Version kann keine Engines installieren – ihre Python-Umgebung ist schreibgeschützt und signiert. Führe den Befehl unten in einer Quellcode-Installation aus oder wechsle zur mitgelieferten Argos-Engine.", + "switch_to_argos": "Zu Argos wechseln (integriert, offline)", + "copy_command": "Befehl kopieren", + "install_cmd_copied": "Befehl kopiert", + "dismiss_error": "Schließen", "transcribe": "Transkribieren", "translate": "Übersetzen", "generate": "Generieren", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 07999cc0..4890feb8 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -638,6 +638,14 @@ "dubbed_segments_other": "{{count}} dubbed segments" }, "dub": { + "install_engine_pkg": "Install {{pkg}}", + "needs_install_short": "Needs install", + "install_popover_title": "Install this engine", + "install_popover_frozen_body": "This packaged build can't install engines — its Python environment is read-only and signed. Run the command below in a from-source install, or switch to the bundled Argos engine.", + "switch_to_argos": "Switch to Argos (bundled, offline)", + "copy_command": "Copy command", + "install_cmd_copied": "Command copied", + "dismiss_error": "Dismiss", "transcribe": "Transcribe", "translate": "Translate", "generate": "Generate", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 84249131..b35a1474 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -659,6 +659,14 @@ "consent_revoked": "Verificación revocada" }, "dub": { + "install_engine_pkg": "Instalar {{pkg}}", + "needs_install_short": "Requiere instalación", + "install_popover_title": "Instalar este motor", + "install_popover_frozen_body": "Esta versión empaquetada no puede instalar motores: su entorno de Python es de solo lectura y está firmado. Ejecuta el comando de abajo en una instalación desde el código fuente, o cambia al motor Argos incluido.", + "switch_to_argos": "Cambiar a Argos (incluido, sin conexión)", + "copy_command": "Copiar comando", + "install_cmd_copied": "Comando copiado", + "dismiss_error": "Descartar", "transcribe": "Transcribir", "translate": "Traducir", "generate": "generar", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 26d2d629..188632ff 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -659,6 +659,14 @@ "consent_revoked": "Vérification révoquée" }, "dub": { + "install_engine_pkg": "Installer {{pkg}}", + "needs_install_short": "Installation requise", + "install_popover_title": "Installer ce moteur", + "install_popover_frozen_body": "Cette version packagée ne peut pas installer de moteurs : son environnement Python est en lecture seule et signé. Exécutez la commande ci-dessous dans une installation depuis les sources, ou passez au moteur Argos intégré.", + "switch_to_argos": "Passer à Argos (intégré, hors ligne)", + "copy_command": "Copier la commande", + "install_cmd_copied": "Commande copiée", + "dismiss_error": "Ignorer", "transcribe": "Transcrire", "translate": "Traduire", "generate": "Générer", diff --git a/frontend/src/i18n/locales/hi.json b/frontend/src/i18n/locales/hi.json index bf09c39e..977afe8d 100644 --- a/frontend/src/i18n/locales/hi.json +++ b/frontend/src/i18n/locales/hi.json @@ -659,6 +659,14 @@ "consent_revoked": "सत्यापन निरस्त कर दिया गया" }, "dub": { + "install_engine_pkg": "{{pkg}} इंस्टॉल करें", + "needs_install_short": "इंस्टॉल आवश्यक", + "install_popover_title": "यह इंजन इंस्टॉल करें", + "install_popover_frozen_body": "यह पैकेज्ड बिल्ड इंजन इंस्टॉल नहीं कर सकता — इसका Python एनवायरनमेंट केवल-पढ़ने योग्य और साइन किया हुआ है। नीचे दिया गया कमांड सोर्स-से इंस्टॉल में चलाएँ, या साथ में आने वाले Argos इंजन पर स्विच करें।", + "switch_to_argos": "Argos पर स्विच करें (अंतर्निहित, ऑफ़लाइन)", + "copy_command": "कमांड कॉपी करें", + "install_cmd_copied": "कमांड कॉपी हो गया", + "dismiss_error": "खारिज करें", "transcribe": "प्रतिलेखन", "translate": "अनुवाद करें", "generate": "उत्पन्न करें", diff --git a/frontend/src/i18n/locales/id.json b/frontend/src/i18n/locales/id.json index 1234d19c..7cdc870f 100644 --- a/frontend/src/i18n/locales/id.json +++ b/frontend/src/i18n/locales/id.json @@ -659,6 +659,14 @@ "consent_revoked": "Verifikasi dicabut" }, "dub": { + "install_engine_pkg": "Instal {{pkg}}", + "needs_install_short": "Perlu instalasi", + "install_popover_title": "Instal mesin ini", + "install_popover_frozen_body": "Build paket ini tidak dapat memasang mesin — lingkungan Python-nya bersifat hanya-baca dan bertanda tangan. Jalankan perintah di bawah pada instalasi dari sumber, atau beralih ke mesin Argos bawaan.", + "switch_to_argos": "Beralih ke Argos (bawaan, offline)", + "copy_command": "Salin perintah", + "install_cmd_copied": "Perintah disalin", + "dismiss_error": "Tutup", "transcribe": "Transkripsikan", "translate": "Terjemahkan", "generate": "Hasilkan", diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index 84cb688a..00e4bb46 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -659,6 +659,14 @@ "consent_revoked": "Verifica revocata" }, "dub": { + "install_engine_pkg": "Installa {{pkg}}", + "needs_install_short": "Richiede installazione", + "install_popover_title": "Installa questo motore", + "install_popover_frozen_body": "Questa build pacchettizzata non può installare motori: il suo ambiente Python è di sola lettura e firmato. Esegui il comando qui sotto in un'installazione da sorgente, oppure passa al motore Argos incluso.", + "switch_to_argos": "Passa ad Argos (incluso, offline)", + "copy_command": "Copia comando", + "install_cmd_copied": "Comando copiato", + "dismiss_error": "Ignora", "transcribe": "Trascrivere", "translate": "Traduci", "generate": "Genera", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 409c7546..8c827dc9 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -659,6 +659,14 @@ "consent_revoked": "検証が取り消されました" }, "dub": { + "install_engine_pkg": "{{pkg}} をインストール", + "needs_install_short": "インストールが必要", + "install_popover_title": "このエンジンをインストール", + "install_popover_frozen_body": "このパッケージ版はエンジンをインストールできません(Python 環境が読み取り専用で署名済みのため)。下のコマンドをソースからのインストールで実行するか、同梱の Argos エンジンに切り替えてください。", + "switch_to_argos": "Argos に切り替え(同梱・オフライン)", + "copy_command": "コマンドをコピー", + "install_cmd_copied": "コマンドをコピーしました", + "dismiss_error": "閉じる", "transcribe": "転写する", "translate": "翻訳する", "generate": "生成する", diff --git a/frontend/src/i18n/locales/ko.json b/frontend/src/i18n/locales/ko.json index ca6f324e..82cbd5ce 100644 --- a/frontend/src/i18n/locales/ko.json +++ b/frontend/src/i18n/locales/ko.json @@ -659,6 +659,14 @@ "consent_revoked": "인증이 취소되었습니다." }, "dub": { + "install_engine_pkg": "{{pkg}} 설치", + "needs_install_short": "설치 필요", + "install_popover_title": "이 엔진 설치", + "install_popover_frozen_body": "이 패키지 빌드는 엔진을 설치할 수 없습니다. Python 환경이 읽기 전용이며 서명되어 있기 때문입니다. 아래 명령을 소스 설치에서 실행하거나 기본 제공되는 Argos 엔진으로 전환하세요.", + "switch_to_argos": "Argos로 전환(기본 제공, 오프라인)", + "copy_command": "명령 복사", + "install_cmd_copied": "명령이 복사됨", + "dismiss_error": "닫기", "transcribe": "전사", "translate": "번역하다", "generate": "생성", diff --git a/frontend/src/i18n/locales/nl.json b/frontend/src/i18n/locales/nl.json index b4c98961..6d6b4a8a 100644 --- a/frontend/src/i18n/locales/nl.json +++ b/frontend/src/i18n/locales/nl.json @@ -659,6 +659,14 @@ "consent_revoked": "Verificatie ingetrokken" }, "dub": { + "install_engine_pkg": "{{pkg}} installeren", + "needs_install_short": "Installatie vereist", + "install_popover_title": "Deze engine installeren", + "install_popover_frozen_body": "Deze verpakte build kan geen engines installeren — de Python-omgeving is alleen-lezen en ondertekend. Voer de onderstaande opdracht uit in een broncode-installatie, of schakel over naar de meegeleverde Argos-engine.", + "switch_to_argos": "Overschakelen naar Argos (meegeleverd, offline)", + "copy_command": "Opdracht kopiëren", + "install_cmd_copied": "Opdracht gekopieerd", + "dismiss_error": "Sluiten", "transcribe": "Transcriberen", "translate": "Vertalen", "generate": "Genereer", diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index 58d28a00..5640b200 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -659,6 +659,14 @@ "consent_revoked": "Weryfikacja odwołana" }, "dub": { + "install_engine_pkg": "Zainstaluj {{pkg}}", + "needs_install_short": "Wymaga instalacji", + "install_popover_title": "Zainstaluj ten silnik", + "install_popover_frozen_body": "Ta spakowana wersja nie może instalować silników — jej środowisko Python jest tylko do odczytu i podpisane. Uruchom poniższe polecenie w instalacji ze źródeł lub przełącz się na dołączony silnik Argos.", + "switch_to_argos": "Przełącz na Argos (wbudowany, offline)", + "copy_command": "Kopiuj polecenie", + "install_cmd_copied": "Skopiowano polecenie", + "dismiss_error": "Odrzuć", "transcribe": "Transkrypcja", "translate": "Przetłumacz", "generate": "Wygeneruj", diff --git a/frontend/src/i18n/locales/pt.json b/frontend/src/i18n/locales/pt.json index 1af34b11..db2abaef 100644 --- a/frontend/src/i18n/locales/pt.json +++ b/frontend/src/i18n/locales/pt.json @@ -659,6 +659,14 @@ "consent_revoked": "Verificação revogada" }, "dub": { + "install_engine_pkg": "Instalar {{pkg}}", + "needs_install_short": "Requer instalação", + "install_popover_title": "Instalar este motor", + "install_popover_frozen_body": "Esta versão empacotada não consegue instalar motores — o seu ambiente Python é somente leitura e assinado. Execute o comando abaixo numa instalação a partir do código-fonte, ou mude para o motor Argos incluído.", + "switch_to_argos": "Mudar para o Argos (incluído, offline)", + "copy_command": "Copiar comando", + "install_cmd_copied": "Comando copiado", + "dismiss_error": "Dispensar", "transcribe": "Transcrever", "translate": "Traduzir", "generate": "Gerar", diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index 2a78bd47..2174b9f8 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -659,6 +659,14 @@ "consent_revoked": "Проверка отменена" }, "dub": { + "install_engine_pkg": "Установить {{pkg}}", + "needs_install_short": "Требуется установка", + "install_popover_title": "Установить этот движок", + "install_popover_frozen_body": "Эта упакованная сборка не может устанавливать движки — её окружение Python доступно только для чтения и подписано. Выполните команду ниже в установке из исходного кода или переключитесь на встроенный движок Argos.", + "switch_to_argos": "Переключиться на Argos (встроенный, офлайн)", + "copy_command": "Копировать команду", + "install_cmd_copied": "Команда скопирована", + "dismiss_error": "Закрыть", "transcribe": "Расшифровать", "translate": "Переводить", "generate": "Генерировать", diff --git a/frontend/src/i18n/locales/sv.json b/frontend/src/i18n/locales/sv.json index 86a5ef2b..ecf7f83f 100644 --- a/frontend/src/i18n/locales/sv.json +++ b/frontend/src/i18n/locales/sv.json @@ -659,6 +659,14 @@ "consent_revoked": "Verifieringen återkallad" }, "dub": { + "install_engine_pkg": "Installera {{pkg}}", + "needs_install_short": "Kräver installation", + "install_popover_title": "Installera denna motor", + "install_popover_frozen_body": "Den här paketerade versionen kan inte installera motorer – dess Python-miljö är skrivskyddad och signerad. Kör kommandot nedan i en källkodsinstallation, eller växla till den medföljande Argos-motorn.", + "switch_to_argos": "Växla till Argos (medföljer, offline)", + "copy_command": "Kopiera kommando", + "install_cmd_copied": "Kommando kopierat", + "dismiss_error": "Avfärda", "transcribe": "Transkribera", "translate": "Översätt", "generate": "Generera", diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index 189eda65..7336952f 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -659,6 +659,14 @@ "consent_revoked": "เพิกถอนการยืนยันแล้ว" }, "dub": { + "install_engine_pkg": "ติดตั้ง {{pkg}}", + "needs_install_short": "ต้องติดตั้ง", + "install_popover_title": "ติดตั้งเอนจินนี้", + "install_popover_frozen_body": "บิลด์แบบแพ็กเกจนี้ติดตั้งเอนจินไม่ได้ เนื่องจากสภาพแวดล้อม Python เป็นแบบอ่านอย่างเดียวและมีลายเซ็น ให้รันคำสั่งด้านล่างในการติดตั้งแบบซอร์สโค้ด หรือสลับไปใช้เอนจิน Argos ที่มาพร้อมกับโปรแกรม", + "switch_to_argos": "สลับไปใช้ Argos (มาพร้อมกับโปรแกรม ออฟไลน์)", + "copy_command": "คัดลอกคำสั่ง", + "install_cmd_copied": "คัดลอกคำสั่งแล้ว", + "dismiss_error": "ปิด", "transcribe": "ถอดเสียง", "translate": "แปล", "generate": "สร้าง", diff --git a/frontend/src/i18n/locales/tr.json b/frontend/src/i18n/locales/tr.json index 2e2fe1ee..67a03bc4 100644 --- a/frontend/src/i18n/locales/tr.json +++ b/frontend/src/i18n/locales/tr.json @@ -659,6 +659,14 @@ "consent_revoked": "Doğrulama iptal edildi" }, "dub": { + "install_engine_pkg": "{{pkg}} yükle", + "needs_install_short": "Kurulum gerekli", + "install_popover_title": "Bu motoru yükle", + "install_popover_frozen_body": "Bu paketlenmiş sürüm motor yükleyemez — Python ortamı salt okunur ve imzalıdır. Aşağıdaki komutu kaynaktan kurulumda çalıştırın ya da yerleşik Argos motoruna geçin.", + "switch_to_argos": "Argos'a geç (yerleşik, çevrimdışı)", + "copy_command": "Komutu kopyala", + "install_cmd_copied": "Komut kopyalandı", + "dismiss_error": "Kapat", "transcribe": "Metne dönüştür", "translate": "Çevir", "generate": "Oluştur", diff --git a/frontend/src/i18n/locales/uk.json b/frontend/src/i18n/locales/uk.json index 979c0509..dc36ef94 100644 --- a/frontend/src/i18n/locales/uk.json +++ b/frontend/src/i18n/locales/uk.json @@ -659,6 +659,14 @@ "consent_revoked": "Перевірку скасовано" }, "dub": { + "install_engine_pkg": "Встановити {{pkg}}", + "needs_install_short": "Потрібне встановлення", + "install_popover_title": "Встановити цей рушій", + "install_popover_frozen_body": "Ця пакетна збірка не може встановлювати рушії — її середовище Python лише для читання та підписане. Виконайте команду нижче у встановленні з вихідного коду або перемкніться на вбудований рушій Argos.", + "switch_to_argos": "Перемкнутися на Argos (вбудований, офлайн)", + "copy_command": "Копіювати команду", + "install_cmd_copied": "Команду скопійовано", + "dismiss_error": "Закрити", "transcribe": "Транскрибувати", "translate": "Перекласти", "generate": "Генерувати", diff --git a/frontend/src/i18n/locales/vi.json b/frontend/src/i18n/locales/vi.json index 053407de..f75988b1 100644 --- a/frontend/src/i18n/locales/vi.json +++ b/frontend/src/i18n/locales/vi.json @@ -659,6 +659,14 @@ "consent_revoked": "Đã thu hồi xác minh" }, "dub": { + "install_engine_pkg": "Cài đặt {{pkg}}", + "needs_install_short": "Cần cài đặt", + "install_popover_title": "Cài đặt công cụ này", + "install_popover_frozen_body": "Bản đóng gói này không thể cài công cụ — môi trường Python của nó chỉ đọc và đã ký. Hãy chạy lệnh bên dưới trong bản cài từ mã nguồn, hoặc chuyển sang công cụ Argos tích hợp sẵn.", + "switch_to_argos": "Chuyển sang Argos (tích hợp, ngoại tuyến)", + "copy_command": "Sao chép lệnh", + "install_cmd_copied": "Đã sao chép lệnh", + "dismiss_error": "Bỏ qua", "transcribe": "Phiên âm", "translate": "Dịch", "generate": "Tạo", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 8d4f8689..5caad428 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -617,6 +617,14 @@ "consent_revoked": "验证已撤销" }, "dub": { + "install_engine_pkg": "安装 {{pkg}}", + "needs_install_short": "需要安装", + "install_popover_title": "安装此引擎", + "install_popover_frozen_body": "此打包版本无法安装引擎——其 Python 环境为只读且已签名。请在源码安装环境中运行下面的命令,或切换到内置的 Argos 引擎。", + "switch_to_argos": "切换到 Argos(内置,离线)", + "copy_command": "复制命令", + "install_cmd_copied": "已复制命令", + "dismiss_error": "关闭", "transcribe": "转录", "translate": "翻译", "generate": "生成", diff --git a/frontend/src/i18n/locales/zh-TW.json b/frontend/src/i18n/locales/zh-TW.json index d14ac2e2..78aab1bc 100644 --- a/frontend/src/i18n/locales/zh-TW.json +++ b/frontend/src/i18n/locales/zh-TW.json @@ -659,6 +659,14 @@ "consent_revoked": "驗證已撤銷" }, "dub": { + "install_engine_pkg": "安裝 {{pkg}}", + "needs_install_short": "需要安裝", + "install_popover_title": "安裝此引擎", + "install_popover_frozen_body": "此打包版本無法安裝引擎——其 Python 環境為唯讀且已簽章。請在原始碼安裝環境中執行下方指令,或切換至內建的 Argos 引擎。", + "switch_to_argos": "切換至 Argos(內建,離線)", + "copy_command": "複製指令", + "install_cmd_copied": "已複製指令", + "dismiss_error": "關閉", "transcribe": "轉錄", "translate": "翻譯", "generate": "產生", diff --git a/frontend/src/pages/DubTab.jsx b/frontend/src/pages/DubTab.jsx index 77088834..4bf3cad3 100644 --- a/frontend/src/pages/DubTab.jsx +++ b/frontend/src/pages/DubTab.jsx @@ -93,6 +93,7 @@ export default function DubTab(props) { const setDubInstruct = useAppStore((s) => s.setDubInstruct); const dubTracks = useAppStore((s) => s.dubTracks); const dubError = useAppStore((s) => s.dubError); + const setDubError = useAppStore((s) => s.setDubError); const dubFailure = useAppStore((s) => s.dubFailure); const dubProgress = useAppStore((s) => s.dubProgress); const isTranslating = useAppStore((s) => s.isTranslating); @@ -242,8 +243,22 @@ export default function DubTab(props) { }, [refreshEngines]); const activeEngineEntry = engines.find((e) => e.id === translateProvider); const activeEngineUnavailable = activeEngineEntry && !activeEngineEntry.installed; + // Changing the translation engine is a corrective action — clear any stale + // translate/pipeline error banner so it doesn't outlive the choice that + // caused it (same class-fix as clearing on a new translate attempt). Covers + // both the fires setTranslateProvider (the error-clearing corrective action)', () => { + const setTranslateProvider = vi.fn(); + const engines = [ + GOOGLE, + { id: 'argos', display_name: 'Argos', installed: true, install_command: null }, + ]; + render(); + // The engine