diff --git a/src/components/ai-edition/ExportDialog.cancel.test.tsx b/src/components/ai-edition/ExportDialog.cancel.test.tsx
index 308a76eee..25b02a132 100644
--- a/src/components/ai-edition/ExportDialog.cancel.test.tsx
+++ b/src/components/ai-edition/ExportDialog.cancel.test.tsx
@@ -17,6 +17,7 @@ vi.mock("@/native/sceneDescription", () => ({
import { toast } from "sonner";
import { I18nProvider } from "@/contexts/I18nContext";
+import { LOCALE_STORAGE_KEY } from "@/i18n/config";
import { type AxcutDocument, axcutSchemaVersion } from "@/lib/ai-edition/schema";
import { cancelExportNative, exportGifNative, exportMultiNative } from "@/native";
import { NativeBridgeRequestError } from "@/native/client";
@@ -144,7 +145,35 @@ describe("export cancellation", () => {
}),
} as unknown as Window["electronAPI"];
});
- afterEach(cleanup);
+ afterEach(() => {
+ cleanup();
+ localStorage.removeItem(LOCALE_STORAGE_KEY);
+ });
+
+ it("localizes native validation failures in both the panel and notification", async () => {
+ localStorage.setItem(LOCALE_STORAGE_KEY, "zh-CN");
+ const job = pendingMp4Export();
+ render(
+
+
+ ,
+ );
+ fireEvent.click(screen.getByRole("button", { name: "导出 MP4" }));
+ await waitFor(() => expect(exportMultiNative).toHaveBeenCalledOnce());
+ await act(async () =>
+ job.reject(
+ new NativeBridgeRequestError({
+ code: "INTERNAL_ERROR",
+ retryable: false,
+ message:
+ "validating exported video: The exported video has no decodable displayable frames. The destination has not been replaced.",
+ }),
+ ),
+ );
+ const message = "无法验证导出的视频。目标文件未被替换,请重试导出。";
+ expect(screen.getByText(message)).toBeVisible();
+ expect(toast.error).toHaveBeenCalledWith("导出失败", { description: message });
+ });
it("enables Cancel immediately, waits for cleanup and returns to the same options", async () => {
const job = pendingExport();
diff --git a/src/components/ai-edition/ExportDialog.tsx b/src/components/ai-edition/ExportDialog.tsx
index 024fe472e..a4c453c04 100644
--- a/src/components/ai-edition/ExportDialog.tsx
+++ b/src/components/ai-edition/ExportDialog.tsx
@@ -44,6 +44,7 @@ import {
import { NativeBridgeRequestError } from "@/native/client";
import type { CompositorClipInput } from "@/native/contracts";
import { buildSceneDescription, resolveVisibleClips } from "@/native/sceneDescription";
+import { exportErrorMessage } from "./exportError";
import { ModalShell } from "./Modals";
import styles from "./NewEditorShell.module.css";
import { Toggle } from "./RightPanes";
@@ -490,10 +491,11 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) {
setError(null);
return;
}
- setError(err instanceof Error ? err.message : String(err));
+ const message = exportErrorMessage(err instanceof Error ? err.message : String(err), t);
+ setError(message);
setPhase("error");
toast.error(t("exportDialog.exportFailed"), {
- description: err instanceof Error ? err.message : String(err),
+ description: message,
});
} finally {
unsubscribeProgress?.();
diff --git a/src/components/ai-edition/exportError.test.ts b/src/components/ai-edition/exportError.test.ts
new file mode 100644
index 000000000..a464413c8
--- /dev/null
+++ b/src/components/ai-edition/exportError.test.ts
@@ -0,0 +1,75 @@
+import { readFileSync } from "node:fs";
+import { describe, expect, it } from "vitest";
+import { getAvailableLocales, translate } from "@/i18n/loader";
+import { exportErrorMessage } from "./exportError";
+
+const cases = [
+ [
+ "No video frames were exported. Check that the clips contain readable video before exporting again.",
+ "noReadableFrames",
+ ],
+ [
+ "Cannot export clip 12: the camera video ends before this clip starts. Record the camera again or remove its track before exporting.",
+ "cameraEnded",
+ ],
+ [
+ "Cannot export clip 12: the camera video has no readable frame at the clip's start. Record the camera again or remove its track before exporting.",
+ "cameraUnreadable",
+ ],
+ [
+ "Cannot export clip 12: the screen video has no readable frame at the clip's start.",
+ "screenUnreadable",
+ ],
+ ["Cannot export clip 12: could not read the screen video.", "screenUnreadable"],
+] as const;
+
+describe("native export error presentation", () => {
+ it.each(cases)("localizes %s in every supported language", (message, key) => {
+ const native = readFileSync("crates/compositor/src/timeline_walk.rs", "utf8");
+ // The bridge currently carries anyhow text. Catch native wording drift here.
+ expect(native).toContain(message.replace("clip 12:", "clip {}:"));
+ for (const locale of getAvailableLocales()) {
+ const t = (key: string, vars?: Record) =>
+ translate(locale, "editor", key, vars);
+ const expected = t(`exportDialog.${key}`, { clip: 12 });
+ expect(exportErrorMessage(message, t)).toBe(expected);
+ expect(expected).not.toContain("exportDialog.");
+ expect(expected).not.toContain("{{");
+ if (key !== "noReadableFrames") expect(expected).toContain("12");
+ if (locale !== "en")
+ expect(expected).not.toBe(translate("en", "editor", `exportDialog.${key}`, { clip: 12 }));
+ }
+ });
+
+ it("translates recognized context chains without exposing decoder internals", () => {
+ const t = (key: string, vars?: Record) =>
+ translate("zh-CN", "editor", key, vars);
+ for (const [message, key] of cases.slice(2)) {
+ expect(exportErrorMessage(`${message}: decoder failed: invalid data`, t)).toBe(
+ t(`exportDialog.${key}`, { clip: 12 }),
+ );
+ }
+ for (const detail of [
+ "No decoder is available for the exported video.",
+ "open exported video: invalid data",
+ "The exported video has no decodable displayable frames. The destination has not been replaced.",
+ ]) {
+ for (const locale of getAvailableLocales()) {
+ const scoped = (key: string) => translate(locale, "editor", key);
+ expect(exportErrorMessage(`validating exported video: ${detail}`, scoped)).toBe(
+ scoped("exportDialog.validationFailed"),
+ );
+ }
+ }
+ });
+
+ it.each([
+ "disk full",
+ "Cannot export clip 12: unknown encoder error",
+ "cleanup failed: validating exported video: invalid data",
+ "[pipeline] warning: shortened clip",
+ "Cannot export clip 0: could not read the screen video.",
+ ])("preserves unrelated errors: %s", (message) => {
+ expect(exportErrorMessage(message, () => "wrong diagnosis")).toBe(message);
+ });
+});
diff --git a/src/components/ai-edition/exportError.ts b/src/components/ai-edition/exportError.ts
new file mode 100644
index 000000000..f951b212a
--- /dev/null
+++ b/src/components/ai-edition/exportError.ts
@@ -0,0 +1,41 @@
+type Translate = (key: string, vars?: Record) => string;
+
+/** Adapt the native exporter's existing error text at the presentation boundary. */
+export function exportErrorMessage(message: string, t: Translate): string {
+ if (message.startsWith("validating exported video: ")) {
+ return t("exportDialog.validationFailed");
+ }
+ if (
+ message ===
+ "No video frames were exported. Check that the clips contain readable video before exporting again."
+ ) {
+ return t("exportDialog.noReadableFrames");
+ }
+ const clip = /^Cannot export clip ([1-9]\d*): (.+)$/s.exec(message);
+ if (!clip) return message;
+ const vars = { clip: clip[1] };
+ const reason = clip[2];
+ if (
+ reason ===
+ "the camera video ends before this clip starts. Record the camera again or remove its track before exporting."
+ ) {
+ return t("exportDialog.cameraEnded", vars);
+ }
+ const cameraStart =
+ "the camera video has no readable frame at the clip's start. Record the camera again or remove its track before exporting.";
+ if (reason === cameraStart || reason.startsWith(`${cameraStart}: `)) {
+ return t("exportDialog.cameraUnreadable", vars);
+ }
+ const screenStart = "the screen video has no readable frame at the clip's start.";
+ const screenRead = "could not read the screen video.";
+ if (
+ reason === screenStart ||
+ reason.startsWith(`${screenStart}: `) ||
+ reason === screenRead ||
+ reason.startsWith(`${screenRead}: `)
+ ) {
+ return t("exportDialog.screenUnreadable", vars);
+ }
+ // Unknown failures retain their diagnostic detail instead of becoming a false diagnosis.
+ return message;
+}
diff --git a/src/i18n/locales/ar/editor.json b/src/i18n/locales/ar/editor.json
index 2095ac883..293429b03 100644
--- a/src/i18n/locales/ar/editor.json
+++ b/src/i18n/locales/ar/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "فشلت كتابة الملف",
"qualityUpscaleWarning": "تكبير الدقة",
"exportedVideoOf": "من الفيديو تم تصديره في",
- "nothingToExport": "لا يوجد شيء للتصدير — المخطط الزمني فارغ."
+ "nothingToExport": "لا يوجد شيء للتصدير — المخطط الزمني فارغ.",
+ "validationFailed": "تعذّر التحقق من الفيديو المُصدَّر. لم يُستبدل الملف الهدف. حاول التصدير مجددًا.",
+ "noReadableFrames": "لم تُصدَّر أي إطارات فيديو. تأكد من أن المقاطع تحتوي على فيديو قابل للقراءة قبل إعادة التصدير.",
+ "cameraEnded": "تعذّر تصدير المقطع {{clip}}: ينتهي فيديو الكاميرا قبل بداية هذا المقطع. أعد تسجيل الكاميرا أو أزل مسارها قبل التصدير.",
+ "cameraUnreadable": "تعذّر تصدير المقطع {{clip}}: لا يحتوي فيديو الكاميرا على إطار قابل للقراءة عند بداية المقطع. أعد تسجيل الكاميرا أو أزل مسارها قبل التصدير.",
+ "screenUnreadable": "تعذّر تصدير المقطع {{clip}}: لا يمكن قراءة فيديو الشاشة عند بداية المقطع. تحقق من التسجيل أو عدّل المقطع قبل إعادة التصدير."
},
"inspector": {
"resetFocusPoint": "إعادة تعيين نقطة التركيز",
diff --git a/src/i18n/locales/cs/editor.json b/src/i18n/locales/cs/editor.json
index 44a420a62..c4a15bc10 100644
--- a/src/i18n/locales/cs/editor.json
+++ b/src/i18n/locales/cs/editor.json
@@ -215,7 +215,12 @@
"exportFailed": "Export selhal",
"failedToWriteFile": "Nepodařilo se zapsat soubor",
"qualityUpscaleWarning": "Zvýšení rozlišení",
- "nothingToExport": "Není co exportovat, časová osa je prázdná."
+ "nothingToExport": "Není co exportovat, časová osa je prázdná.",
+ "validationFailed": "Exportované video se nepodařilo ověřit. Cílový soubor nebyl nahrazen. Zkuste export znovu.",
+ "noReadableFrames": "Nebyly exportovány žádné snímky videa. Před dalším exportem ověřte, že klipy obsahují čitelné video.",
+ "cameraEnded": "Klip {{clip}} nelze exportovat: video z kamery končí před začátkem klipu. Nahrajte kameru znovu nebo před exportem odstraňte její stopu.",
+ "cameraUnreadable": "Klip {{clip}} nelze exportovat: video z kamery nemá na začátku klipu čitelný snímek. Nahrajte kameru znovu nebo před exportem odstraňte její stopu.",
+ "screenUnreadable": "Klip {{clip}} nelze exportovat: video obrazovky nelze na začátku klipu přečíst. Před dalším exportem zkontrolujte nahrávku nebo upravte klip."
},
"inspector": {
"resetFocusPoint": "Obnovit bod zaostření",
diff --git a/src/i18n/locales/de/editor.json b/src/i18n/locales/de/editor.json
index a50418ac4..b2343eae0 100644
--- a/src/i18n/locales/de/editor.json
+++ b/src/i18n/locales/de/editor.json
@@ -215,7 +215,12 @@
"exportFailed": "Export fehlgeschlagen",
"failedToWriteFile": "Datei konnte nicht geschrieben werden",
"qualityUpscaleWarning": "Hochskalierung",
- "nothingToExport": "Nichts zu exportieren — die Timeline ist leer."
+ "nothingToExport": "Nichts zu exportieren — die Timeline ist leer.",
+ "validationFailed": "Das exportierte Video konnte nicht überprüft werden. Die Zieldatei wurde nicht ersetzt. Bitte erneut exportieren.",
+ "noReadableFrames": "Es wurden keine Videobilder exportiert. Prüfe vor dem erneuten Export, ob die Clips lesbare Videos enthalten.",
+ "cameraEnded": "Clip {{clip}} kann nicht exportiert werden: Das Kameravideo endet vor dem Beginn dieses Clips. Nimm das Kameravideo erneut auf oder entferne seine Spur vor dem Export.",
+ "cameraUnreadable": "Clip {{clip}} kann nicht exportiert werden: Das Kameravideo enthält am Clipanfang kein lesbares Bild. Nimm das Kameravideo erneut auf oder entferne seine Spur vor dem Export.",
+ "screenUnreadable": "Clip {{clip}} kann nicht exportiert werden: Das Bildschirmvideo ist am Clipanfang nicht lesbar. Prüfe die Aufnahme oder passe den Clip vor dem erneuten Export an."
},
"inspector": {
"resetFocusPoint": "Fokuspunkt zurücksetzen",
diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json
index 0954f73e6..654c987e6 100644
--- a/src/i18n/locales/en/editor.json
+++ b/src/i18n/locales/en/editor.json
@@ -215,7 +215,12 @@
"exportFailed": "Export failed",
"failedToWriteFile": "Failed to write file",
"qualityUpscaleWarning": "Upscale",
- "nothingToExport": "Nothing to export — the timeline is empty."
+ "nothingToExport": "Nothing to export — the timeline is empty.",
+ "validationFailed": "The exported video could not be validated. The destination was not replaced. Try exporting again.",
+ "noReadableFrames": "No video frames were exported. Check that the clips contain readable video before exporting again.",
+ "cameraEnded": "Cannot export clip {{clip}}: the camera video ends before this clip starts. Record the camera again or remove its track before exporting.",
+ "cameraUnreadable": "Cannot export clip {{clip}}: the camera video has no readable frame at the clip’s start. Record the camera again or remove its track before exporting.",
+ "screenUnreadable": "Cannot export clip {{clip}}: the screen video cannot be read at the clip’s start. Check the recording or adjust the clip before exporting again."
},
"inspector": {
"resetFocusPoint": "Reset focus point",
diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json
index 5bb7c2f7b..10ef581c6 100644
--- a/src/i18n/locales/es/editor.json
+++ b/src/i18n/locales/es/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "No se pudo escribir el archivo",
"qualityUpscaleWarning": "Upscaling",
"exportedVideoOf": "de vídeo exportado en",
- "nothingToExport": "Nada que exportar: la línea de tiempo está vacía."
+ "nothingToExport": "Nada que exportar: la línea de tiempo está vacía.",
+ "validationFailed": "No se pudo validar el vídeo exportado. El archivo de destino no se ha reemplazado. Vuelve a exportar.",
+ "noReadableFrames": "No se ha exportado ningún fotograma. Comprueba que los clips contengan vídeo legible antes de volver a exportar.",
+ "cameraEnded": "No se puede exportar el clip {{clip}}: el vídeo de la cámara termina antes del inicio del clip. Vuelve a grabar la cámara o elimina su pista antes de exportar.",
+ "cameraUnreadable": "No se puede exportar el clip {{clip}}: el vídeo de la cámara no tiene ningún fotograma legible al inicio del clip. Vuelve a grabar la cámara o elimina su pista antes de exportar.",
+ "screenUnreadable": "No se puede exportar el clip {{clip}}: no se puede leer el vídeo de pantalla al inicio del clip. Revisa la grabación o ajusta el clip antes de volver a exportar."
},
"inspector": {
"resetFocusPoint": "Restablecer punto de enfoque",
diff --git a/src/i18n/locales/fr/editor.json b/src/i18n/locales/fr/editor.json
index 9f595323f..6c145d57d 100644
--- a/src/i18n/locales/fr/editor.json
+++ b/src/i18n/locales/fr/editor.json
@@ -215,7 +215,12 @@
"exportFailed": "Échec de l'export",
"failedToWriteFile": "Échec de l'écriture du fichier",
"qualityUpscaleWarning": "Upscaling",
- "nothingToExport": "Rien à exporter — la timeline est vide."
+ "nothingToExport": "Rien à exporter — la timeline est vide.",
+ "validationFailed": "La vidéo exportée n’a pas pu être validée. Le fichier de destination n’a pas été remplacé. Réessayez l’exportation.",
+ "noReadableFrames": "Aucune image vidéo n’a été exportée. Vérifiez que les clips contiennent une vidéo lisible avant de réessayer.",
+ "cameraEnded": "Impossible d’exporter le clip {{clip}} : la vidéo de la caméra se termine avant le début de ce clip. Réenregistrez la caméra ou retirez sa piste avant d’exporter.",
+ "cameraUnreadable": "Impossible d’exporter le clip {{clip}} : la vidéo de la caméra ne contient aucune image lisible au début du clip. Réenregistrez la caméra ou retirez sa piste avant d’exporter.",
+ "screenUnreadable": "Impossible d’exporter le clip {{clip}} : la vidéo de l’écran est illisible au début du clip. Vérifiez l’enregistrement ou ajustez le clip avant de réessayer."
},
"inspector": {
"resetFocusPoint": "Réinitialiser le point de focus",
diff --git a/src/i18n/locales/it/editor.json b/src/i18n/locales/it/editor.json
index 66997ed7c..ef4a46b4c 100644
--- a/src/i18n/locales/it/editor.json
+++ b/src/i18n/locales/it/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "Impossibile scrivere il file",
"qualityUpscaleWarning": "Upscaling",
"exportedVideoOf": "di video esportato in",
- "nothingToExport": "Niente da esportare: la timeline è vuota."
+ "nothingToExport": "Niente da esportare: la timeline è vuota.",
+ "validationFailed": "Impossibile convalidare il video esportato. Il file di destinazione non è stato sostituito. Prova a esportare di nuovo.",
+ "noReadableFrames": "Non è stato esportato alcun fotogramma. Verifica che le clip contengano video leggibili prima di esportare di nuovo.",
+ "cameraEnded": "Impossibile esportare la clip {{clip}}: il video della videocamera termina prima dell’inizio della clip. Registra di nuovo la videocamera o rimuovi la sua traccia prima di esportare.",
+ "cameraUnreadable": "Impossibile esportare la clip {{clip}}: il video della videocamera non contiene fotogrammi leggibili all’inizio della clip. Registra di nuovo la videocamera o rimuovi la sua traccia prima di esportare.",
+ "screenUnreadable": "Impossibile esportare la clip {{clip}}: il video dello schermo non è leggibile all’inizio della clip. Controlla la registrazione o modifica la clip prima di esportare di nuovo."
},
"inspector": {
"resetFocusPoint": "Reimposta punto di messa a fuoco",
diff --git a/src/i18n/locales/ja-JP/editor.json b/src/i18n/locales/ja-JP/editor.json
index 439264b75..da28ac389 100644
--- a/src/i18n/locales/ja-JP/editor.json
+++ b/src/i18n/locales/ja-JP/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "ファイルの書き込みに失敗しました",
"qualityUpscaleWarning": "アップスケール",
"exportedVideoOf": "の動画を書き出しました。所要時間:",
- "nothingToExport": "書き出す内容がありません — タイムラインが空です。"
+ "nothingToExport": "書き出す内容がありません — タイムラインが空です。",
+ "validationFailed": "書き出した動画を検証できませんでした。保存先のファイルは置き換えられていません。もう一度書き出してください。",
+ "noReadableFrames": "動画フレームが書き出されませんでした。クリップに読み取り可能な動画が含まれているか確認してから、再度書き出してください。",
+ "cameraEnded": "クリップ {{clip}} を書き出せません。カメラ動画がクリップの開始前に終了しています。カメラ動画を録画し直すか、カメラトラックを削除してから書き出してください。",
+ "cameraUnreadable": "クリップ {{clip}} を書き出せません。クリップの開始位置に読み取り可能なカメラ映像がありません。カメラ動画を録画し直すか、カメラトラックを削除してから書き出してください。",
+ "screenUnreadable": "クリップ {{clip}} を書き出せません。クリップの開始位置の画面動画を読み取れません。録画を確認するか、クリップを調整してから再度書き出してください。"
},
"inspector": {
"resetFocusPoint": "フォーカスポイントをリセット",
diff --git a/src/i18n/locales/ko-KR/editor.json b/src/i18n/locales/ko-KR/editor.json
index 4e32c8013..e77b0e817 100644
--- a/src/i18n/locales/ko-KR/editor.json
+++ b/src/i18n/locales/ko-KR/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "파일 쓰기에 실패했습니다",
"qualityUpscaleWarning": "업스케일",
"exportedVideoOf": "분량의 영상을 내보냈습니다. 소요 시간:",
- "nothingToExport": "내보낼 항목이 없습니다 — 타임라인이 비어 있습니다."
+ "nothingToExport": "내보낼 항목이 없습니다 — 타임라인이 비어 있습니다.",
+ "validationFailed": "내보낸 동영상을 검증할 수 없습니다. 대상 파일은 교체되지 않았습니다. 다시 내보내세요.",
+ "noReadableFrames": "내보낸 동영상 프레임이 없습니다. 클립에 읽을 수 있는 동영상이 있는지 확인한 후 다시 내보내세요.",
+ "cameraEnded": "클립 {{clip}}을(를) 내보낼 수 없습니다. 클립 시작 전에 카메라 동영상이 끝납니다. 카메라를 다시 녹화하거나 카메라 트랙을 제거한 후 내보내세요.",
+ "cameraUnreadable": "클립 {{clip}}을(를) 내보낼 수 없습니다. 클립 시작 지점에 읽을 수 있는 카메라 프레임이 없습니다. 카메라를 다시 녹화하거나 카메라 트랙을 제거한 후 내보내세요.",
+ "screenUnreadable": "클립 {{clip}}을(를) 내보낼 수 없습니다. 클립 시작 지점의 화면 동영상을 읽을 수 없습니다. 녹화 파일을 확인하거나 클립을 조정한 후 다시 내보내세요."
},
"inspector": {
"resetFocusPoint": "포커스 지점 재설정",
diff --git a/src/i18n/locales/pt-BR/editor.json b/src/i18n/locales/pt-BR/editor.json
index 0834c0fa7..d79f9a2f1 100644
--- a/src/i18n/locales/pt-BR/editor.json
+++ b/src/i18n/locales/pt-BR/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "Falha ao gravar o arquivo",
"qualityUpscaleWarning": "Upscaling",
"exportedVideoOf": "de vídeo exportado em",
- "nothingToExport": "Nada para exportar — a linha do tempo está vazia."
+ "nothingToExport": "Nada para exportar — a linha do tempo está vazia.",
+ "validationFailed": "Não foi possível validar o vídeo exportado. O arquivo de destino não foi substituído. Tente exportar novamente.",
+ "noReadableFrames": "Nenhum quadro de vídeo foi exportado. Verifique se os clipes contêm vídeos legíveis antes de exportar novamente.",
+ "cameraEnded": "Não é possível exportar o clipe {{clip}}: o vídeo da câmera termina antes do início do clipe. Grave a câmera novamente ou remova sua faixa antes de exportar.",
+ "cameraUnreadable": "Não é possível exportar o clipe {{clip}}: o vídeo da câmera não contém um quadro legível no início do clipe. Grave a câmera novamente ou remova sua faixa antes de exportar.",
+ "screenUnreadable": "Não é possível exportar o clipe {{clip}}: não foi possível ler o vídeo da tela no início do clipe. Verifique a gravação ou ajuste o clipe antes de exportar novamente."
},
"inspector": {
"resetFocusPoint": "Redefinir ponto de foco",
diff --git a/src/i18n/locales/ru/editor.json b/src/i18n/locales/ru/editor.json
index d8e59773e..4b81e7597 100644
--- a/src/i18n/locales/ru/editor.json
+++ b/src/i18n/locales/ru/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "Не удалось записать файл",
"qualityUpscaleWarning": "Апскейл",
"exportedVideoOf": "видео экспортировано за",
- "nothingToExport": "Нечего экспортировать — таймлайн пуст."
+ "nothingToExport": "Нечего экспортировать — таймлайн пуст.",
+ "validationFailed": "Не удалось проверить экспортированное видео. Целевой файл не был заменён. Повторите экспорт.",
+ "noReadableFrames": "Не экспортировано ни одного видеокадра. Перед повторным экспортом убедитесь, что клипы содержат читаемое видео.",
+ "cameraEnded": "Не удалось экспортировать клип {{clip}}: видео с камеры заканчивается до начала клипа. Запишите видео с камеры заново или удалите её дорожку перед экспортом.",
+ "cameraUnreadable": "Не удалось экспортировать клип {{clip}}: в начале клипа нет читаемого кадра с камеры. Запишите видео с камеры заново или удалите её дорожку перед экспортом.",
+ "screenUnreadable": "Не удалось экспортировать клип {{clip}}: невозможно прочитать видео экрана в начале клипа. Проверьте запись или измените клип перед повторным экспортом."
},
"inspector": {
"resetFocusPoint": "Сбросить точку фокуса",
diff --git a/src/i18n/locales/tr/editor.json b/src/i18n/locales/tr/editor.json
index 116621ec5..bb248a352 100644
--- a/src/i18n/locales/tr/editor.json
+++ b/src/i18n/locales/tr/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "Dosya yazılamadı",
"qualityUpscaleWarning": "Upscale",
"exportedVideoOf": "video şu sürede dışa aktarıldı:",
- "nothingToExport": "Dışa aktarılacak bir şey yok — zaman çizelgesi boş."
+ "nothingToExport": "Dışa aktarılacak bir şey yok — zaman çizelgesi boş.",
+ "validationFailed": "Dışa aktarılan video doğrulanamadı. Hedef dosya değiştirilmedi. Yeniden dışa aktarmayı deneyin.",
+ "noReadableFrames": "Hiçbir video karesi dışa aktarılmadı. Yeniden dışa aktarmadan önce kliplerde okunabilir video bulunduğunu kontrol edin.",
+ "cameraEnded": "{{clip}} numaralı klip dışa aktarılamıyor: kamera videosu klip başlamadan önce bitiyor. Kamerayı yeniden kaydedin veya dışa aktarmadan önce kamera izini kaldırın.",
+ "cameraUnreadable": "{{clip}} numaralı klip dışa aktarılamıyor: klibin başlangıcında okunabilir kamera karesi yok. Kamerayı yeniden kaydedin veya dışa aktarmadan önce kamera izini kaldırın.",
+ "screenUnreadable": "{{clip}} numaralı klip dışa aktarılamıyor: klibin başlangıcındaki ekran videosu okunamıyor. Yeniden dışa aktarmadan önce kaydı kontrol edin veya klibi ayarlayın."
},
"inspector": {
"resetFocusPoint": "Odak noktasını sıfırla",
diff --git a/src/i18n/locales/vi/editor.json b/src/i18n/locales/vi/editor.json
index 7b0cb6cef..c9940ac9b 100644
--- a/src/i18n/locales/vi/editor.json
+++ b/src/i18n/locales/vi/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "Không thể ghi tệp",
"qualityUpscaleWarning": "Upscale",
"exportedVideoOf": "video đã xuất trong",
- "nothingToExport": "Không có gì để xuất — dòng thời gian trống."
+ "nothingToExport": "Không có gì để xuất — dòng thời gian trống.",
+ "validationFailed": "Không thể xác thực video đã xuất. Tệp đích chưa bị thay thế. Hãy thử xuất lại.",
+ "noReadableFrames": "Không có khung hình video nào được xuất. Hãy kiểm tra các đoạn có chứa video đọc được trước khi xuất lại.",
+ "cameraEnded": "Không thể xuất đoạn {{clip}}: video camera kết thúc trước khi đoạn này bắt đầu. Hãy ghi lại camera hoặc xóa rãnh camera trước khi xuất.",
+ "cameraUnreadable": "Không thể xuất đoạn {{clip}}: video camera không có khung hình đọc được ở đầu đoạn. Hãy ghi lại camera hoặc xóa rãnh camera trước khi xuất.",
+ "screenUnreadable": "Không thể xuất đoạn {{clip}}: không thể đọc video màn hình ở đầu đoạn. Hãy kiểm tra bản ghi hoặc điều chỉnh đoạn trước khi xuất lại."
},
"inspector": {
"resetFocusPoint": "Đặt lại điểm lấy nét",
diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json
index 713fd4fc0..322a2566c 100644
--- a/src/i18n/locales/zh-CN/editor.json
+++ b/src/i18n/locales/zh-CN/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "写入文件失败",
"qualityUpscaleWarning": "放大",
"exportedVideoOf": "的视频已导出,用时",
- "nothingToExport": "没有可导出的内容 — 时间轴为空。"
+ "nothingToExport": "没有可导出的内容 — 时间轴为空。",
+ "validationFailed": "无法验证导出的视频。目标文件未被替换,请重试导出。",
+ "noReadableFrames": "未导出任何视频帧。请确认片段包含可读取的视频后再导出。",
+ "cameraEnded": "无法导出第 {{clip}} 个片段:摄像头视频在该片段开始前已结束。请重新录制摄像头视频,或移除摄像头轨道后再导出。",
+ "cameraUnreadable": "无法导出第 {{clip}} 个片段:摄像头视频在片段起点没有可读取的画面。请重新录制摄像头视频,或移除摄像头轨道后再导出。",
+ "screenUnreadable": "无法导出第 {{clip}} 个片段:无法读取片段起点的屏幕视频。请检查录屏或调整片段后再导出。"
},
"inspector": {
"resetFocusPoint": "重置焦点",
diff --git a/src/i18n/locales/zh-TW/editor.json b/src/i18n/locales/zh-TW/editor.json
index 485bd9919..1e3bc73e0 100644
--- a/src/i18n/locales/zh-TW/editor.json
+++ b/src/i18n/locales/zh-TW/editor.json
@@ -215,7 +215,12 @@
"failedToWriteFile": "寫入檔案失敗",
"qualityUpscaleWarning": "放大",
"exportedVideoOf": "的影片已匯出,耗時",
- "nothingToExport": "沒有可匯出的內容 — 時間軸為空。"
+ "nothingToExport": "沒有可匯出的內容 — 時間軸為空。",
+ "validationFailed": "無法驗證匯出的影片。目標檔案未被取代,請重新匯出。",
+ "noReadableFrames": "未匯出任何影片影格。請確認片段包含可讀取的影片後再匯出。",
+ "cameraEnded": "無法匯出第 {{clip}} 個片段:攝影機影片在該片段開始前已結束。請重新錄製攝影機影片,或移除攝影機軌道後再匯出。",
+ "cameraUnreadable": "無法匯出第 {{clip}} 個片段:攝影機影片在片段起點沒有可讀取的畫面。請重新錄製攝影機影片,或移除攝影機軌道後再匯出。",
+ "screenUnreadable": "無法匯出第 {{clip}} 個片段:無法讀取片段起點的螢幕影片。請檢查螢幕錄影或調整片段後再匯出。"
},
"inspector": {
"resetFocusPoint": "重設焦點",
diff --git a/technical-documentation/architecture/export-pipeline.md b/technical-documentation/architecture/export-pipeline.md
index eb69eff62..dbf1a9306 100644
--- a/technical-documentation/architecture/export-pipeline.md
+++ b/technical-documentation/architecture/export-pipeline.md
@@ -168,6 +168,19 @@ and **one** encoder + muxer pair:
file and preserve the destination. This is a first-frame check, not a
full-file integrity scan; encoder timing for single-frame files is not repaired.
+ User-facing failures travel through the napi task error, the native bridge
+ (which logs the original error and redacts paths), and `NativeBridgeRequestError`
+ to `ExportDialog`. The dialog uses
+ [`exportErrorMessage`](../../src/components/ai-edition/exportError.ts) to present
+ these known frame/source/validation failures through the existing `editor`
+ translations in all supported locales, in both the error panel and notification.
+ Decoder context remains in the main-process log; unknown errors retain their
+ original bridge message. Cancellation still returns to the export options
+ without an error notification. The bridge currently carries error text, so
+ the presentation adapter's contract tests check the native wording as well as
+ localized messages and clip placeholders. Developer-only timeline warnings
+ are not translated.
+
- **Imported audio tracks** (voiceover / BGM / SFX, issue #350) are mixed
on top of the assembled programme by `audio.rs::mix_external_tracks`,
between `assemble_concatenated_pcm` and `finish_audio`. Each track's
diff --git a/technical-documentation/testing/manual-e2e-checklist.md b/technical-documentation/testing/manual-e2e-checklist.md
index 62d3ca855..6841efab0 100644
--- a/technical-documentation/testing/manual-e2e-checklist.md
+++ b/technical-documentation/testing/manual-e2e-checklist.md
@@ -914,6 +914,7 @@ These checks are the human slice: an agent cannot click Apple's picker (see abov
| Date | Build / tag | Platform | Pass/fail | Notes |
|------|-------------|----------|-----------|-------|
+| 2026-10-25 | `fix/localize-export-validation-errors` on `d433e7ce`; fresh `npm run build-vite`; release addon SHA256 `71304292f7d4406fc53e550ee90363cd6f087d6f030f3787343f54e4d9c76be0` | macOS 15.7.1, Apple M2; Electron 41.2.1; isolated synthetic-media profile | Partial — localized errors and MP4 retry passed | Computer-use switched the test profile to Simplified Chinese. An interior-gap export and a tail-only MP4 both rejected after confirming replacement of a seeded file. The error panel and notification used Chinese; the destination SHA256 remained `9db4e92b90f2b768062322a1c72408284f6f5caea473380d6e46e4090e2fcac5` after each failure, with no partial output. A normal four-second clip plus 10 ms tail then exported successfully in the same session at 1080p/30; independent ffprobe counted 120 frames / 4.000 s at 1752×1080, and full-file decode exited 0. Process file mappings confirmed the fresh addon and matching FFmpeg libraries were loaded. The standard addon script compiled Rust but failed during staging because the local SDK already contained `@loader_path`; test libraries were staged separately with that existing path preserved and ad-hoc signatures refreshed. This is not packaging validation. The profile also reported its missing optional whisper server; transcription was not tested. Skipped: physical capture/webcam, Presenter Overlay, unrelated editor features, native Windows/Linux execution, all-language visual review and release packaging. |
| 2026-10-24 | PR #1379 offset-origin follow-up; rebuilt addon SHA256 `0dd01525a163d8889dd2aaa7088ea0c1e923956b14c430ff70537cba5c24e9d5` | macOS 15.7.1, Apple Silicon | Partial — offset source exported; interior gap still rejected | Computer-use opened a synthetic video whose first decoded timestamp is 1.000 s, with a clip starting at 0. MP4 1080p/30 displayed Saved; independent probing counted 120 frames / 3.967 s at 1752×1080 and full-file decode exited 0. In the same session, opened the interior-gap project and confirmed replacement of that MP4: export reported the screen-start error, destination SHA256 stayed `d45192c6c319006849fee014bc3882141bead0f8b0c43e7c895c51cefe1b7d7b`, and no partial remained. Native regressions cover screen/camera offset sources and interior gaps with both zero and non-zero stream origins. Recording, Presenter Overlay compatibility, unrelated editor features, Windows/Linux runtime and release packaging were not exercised. |
| 2026-10-24 | dev post-merge follow-up to PR #1377 on `79c3aa24`; rebuilt addon SHA256 `53df86189b359d41b63a76e3eaafed5e9bf0e299fe0e89f8fd7950aa12bad6f3` | macOS 15.7.1, Apple Silicon | Partial — interior gap rejected; destination preserved; normal retry passed | Computer-use opened a synthetic screen clip starting at 1.5 s inside a timestamp gap from 0.983 to 3.000 s, then confirmed replacement of an existing synthetic MP4. Export reported that clip 1 had no readable screen frame at its start. Destination SHA256 stayed `854b0911778da18c2a456d3eb227da749c0af2138ad52092180156ed138a6a48`; no partial remained. In the same session, a normal four-second clip plus 10 ms tail exported successfully at 1080p/30: independent ffprobe counted 120 frames / 4.000 s at 1752×1080, and full-file decode exited 0. Native tests also cover camera gaps and failure after a valid preceding clip. Recording, Presenter Overlay compatibility, unrelated editor features, Windows/Linux runtime and release packaging were not exercised. |
| 2026-10-24 | dev CodeRabbit follow-up to PR #1377; rebuilt addon SHA256 `8fe1baab14e941a5ba6d73b5596a27190a75edcd8b49630b451868f829bdaf71` | macOS 15.7.1, Apple Silicon | Partial — undecodable output rejected; destination preserved; retry passed | Computer-use reopened the same synthetic tail-only project used in the previous run, selected an existing synthetic MP4, and confirmed the native replace prompt. Export now reports validation failure instead of Saved. Destination SHA256 remained `854b0911778da18c2a456d3eb227da749c0af2138ad52092180156ed138a6a48`; no partial output remained. Without restarting the app, loaded the four-second valid clip plus tail and retried MP4 1080p/30: Saved, native walk 121 frames, independent ffprobe 120 decoded frames / 4.000 s at 1752×1080, full-file decode exit 0. This closes the publication-safety gap observed in the previous row; single-frame encoder timing itself remains unchanged. Native tests also cover absent/existing destination, malformed output, cancellation and successful H264/HEVC export. Recording, Presenter Overlay, unrelated editor features, Windows/Linux runtime and release packaging were not exercised. |