diff --git a/src/renderer/i18n.ts b/src/renderer/i18n.ts index 467fe98..6dcbca9 100644 --- a/src/renderer/i18n.ts +++ b/src/renderer/i18n.ts @@ -254,6 +254,8 @@ function translateDynamic(value: string, language: AppLanguage): string { if (result) return `${result[1]} completed${result[2].replace(/(\d+) Fehler/g, "$1 errors").replace(/(\d+) abgebrochen/g, "$1 cancelled")}`; const extracting = value.match(/^Entpacken (\d+%)$/); if (extracting) return `Extracting ${extracting[1]}`; + const finalizing = value.match(/^Finalisieren - (\d+%)$/); + if (finalizing) return `Finalizing - ${finalizing[1]}`; const checkedUntil = value.match(/^Account geprüft — (.+) bis (.+)$/); if (checkedUntil) return `Account checked — ${checkedUntil[1]} until ${checkedUntil[2]}`; const checked = value.match(/^Account geprüft — (.+)$/); @@ -403,6 +405,8 @@ function translateDynamic(value: string, language: AppLanguage): string { if (result) return `${result[1]} fertig${result[2].replace(/(\d+) errors/g, "$1 Fehler").replace(/(\d+) cancelled/g, "$1 abgebrochen")}`; const extracting = value.match(/^Extracting (\d+%)$/); if (extracting) return `Entpacken ${extracting[1]}`; + const finalizing = value.match(/^Finalizing - (\d+%)$/); + if (finalizing) return `Finalisieren - ${finalizing[1]}`; const checkedUntil = value.match(/^Account checked — (.+) until (.+)$/); if (checkedUntil) return `Account geprüft — ${checkedUntil[1]} bis ${checkedUntil[2]}`; const credentialsFor = value.match(/^Credentials for (.+)$/); diff --git a/src/renderer/views/downloads/DownloadsTable.tsx b/src/renderer/views/downloads/DownloadsTable.tsx index 0e55247..af94fee 100644 --- a/src/renderer/views/downloads/DownloadsTable.tsx +++ b/src/renderer/views/downloads/DownloadsTable.tsx @@ -153,6 +153,20 @@ export function compactDownloadStatus(value: string): string { if (extracting) return `Entpacken - ${extracting[1]}%`; const extractingEnglish = status.match(/Extracting\s+(\d+)%/i); if (extractingEnglish) return `Extracting - ${extractingEnglish[1]}%`; + const finalizing = status.match(/^(Finalisieren|Finalizing)\b/i); + if (finalizing) { + const fraction = status.match(/\(([^)]*)\)/); + if (fraction) { + const [currentValue, totalValue] = fraction[1].split("/"); + const current = Number(currentValue?.trim()); + const total = Number(totalValue?.trim()); + if (Number.isFinite(current) && Number.isFinite(total) && total > 0) return `${finalizing[1]} - ${progress((current / total) * 100)}%`; + return finalizing[1]; + } + const percentage = status.match(/-\s*(-?\d+(?:\.\d+)?)%/); + if (percentage) return `${finalizing[1]} - ${progress(Number(percentage[1]))}%`; + return finalizing[1]; + } return status; } @@ -447,9 +461,10 @@ function packageCell(row: DownloadPackageRow, column: string, packageSpeedBps: n if (column === "status") { const audio = entry.audioStripSummary ? formatAudioStripSummary(entry.audioStripSummary) : null; const rawPostProcessLabel = entry.postProcessLabel?.trim() || ""; - const postProcessLabel = entry.status === "extracting" && /(?:^|[\\/])[^\\/]+\.(?:rar|zip|7z|tar|gz|bz2|xz)(?:\.\d+)?$/i.test(rawPostProcessLabel) + const compactPostProcessLabel = compactDownloadStatus(rawPostProcessLabel); + const postProcessLabel = entry.status === "extracting" && compactPostProcessLabel === rawPostProcessLabel && /(?:^|[\\/])[^\\/]+\.(?:rar|zip|7z|tar|gz|bz2|xz)(?:\.\d+)?$/i.test(rawPostProcessLabel) ? "Entpacken - Ausstehend" - : compactDownloadStatus(rawPostProcessLabel); + : compactPostProcessLabel; const extractFailure = row.allItems.find((item) => /^Entpack-Fehler\b/i.test(item.fullStatus || "")); const waitsForDisk = row.allItems.some((item) => compactDownloadStatus(item.fullStatus || "") === "Warte auf Festplatte"); const details = `${stats.done}/${stats.total}${stats.failed > 0 ? ` · ${stats.failed} Fehler` : ""}${stats.cancelled > 0 ? ` · ${stats.cancelled} abgebrochen` : ""}${postProcessLabel ? ` · ${postProcessLabel}` : ""}${extractFailure ? " · Entpack-Fehler" : ""}${audio ? ` · ${audio.text}` : ""}`; diff --git a/tests/downloads-view.test.tsx b/tests/downloads-view.test.tsx index 586ab08..7531d93 100644 --- a/tests/downloads-view.test.tsx +++ b/tests/downloads-view.test.tsx @@ -923,6 +923,51 @@ describe("download table row contracts", () => { expect(html).not.toContain(">release.part1.rar"); }); + it.each([ + ["package", "Finalisieren (1/2) · release.part1.rar", "Finalisieren - 50%"], + ["package", "Finalizing - 50% * release.part1.rar", "Finalizing - 50%"], + ["item", "Finalizing (3/4) · release.part1.rar", "Finalizing - 75%"], + ["item", "Finalisieren - 50% * release.part1.rar", "Finalisieren - 50%"] + ])("shows %s finalization status without archive details", (target, rawStatus, expectedStatus) => { + const html = target === "package" + ? renderToStaticMarkup(PackageCardContent({ + actions: createActions(), + columnOrder: ["status"], + editing: false, + editingName: "", + gridTemplate: "220px", + packageSpeedBps: 0, + row: { + package: { ...pkg("finalization-package", "Finalization", ["finalization-item"]), postProcessLabel: rawStatus, status: "extracting" } as PackageEntry, + items: [item("finalization-item", "finalization-package", "completed")], + allItems: [item("finalization-item", "finalization-package", "completed")], + collapsed: true + }, + selectedIds: new Set(), + selectedVersion: 0 + })) + : renderToStaticMarkup(ItemRowContent({ + actions: createActions(), + columnOrder: ["status"], + gridTemplate: "220px", + item: item("finalization-item", "finalization-package", "completed", { fullStatus: rawStatus }), + selected: false + })); + + expect(html).toContain(`aria-label="${expectedStatus}"`); + expect(html.match(new RegExp(`>${expectedStatus}`, "g"))).toHaveLength(2); + expect(html).not.toContain(">release.part1.rar"); + }); + + it.each([ + ["Finalisieren (3/2) · release.part1.rar", "Finalisieren - 100%"], + ["Finalizing (-1/2) · release.part1.rar", "Finalizing - 0%"], + ["Finalisieren (1/0) · release.part1.rar", "Finalisieren"], + ["Finalizing (invalid/2) · release.part1.rar", "Finalizing"] + ])("derives only finite finalization percentages", (rawStatus, expectedStatus) => { + expect(compactDownloadStatus(rawStatus)).toBe(expectedStatus); + }); + it("renders meter text in clipped track and fill layers", () => { const html = renderToStaticMarkup(ItemRowContent({ actions: createActions(), diff --git a/tests/i18n.test.ts b/tests/i18n.test.ts index 3973e19..5fc1239 100644 --- a/tests/i18n.test.ts +++ b/tests/i18n.test.ts @@ -22,6 +22,15 @@ describe("renderer localization", () => { expect(translateUiText("100.001–100.005 of 100.005", "de")).toBe("100.001–100.005 von 100.005"); }); + it.each([ + ["Finalisieren - 0%", "Finalizing - 0%"], + ["Finalisieren - 50%", "Finalizing - 50%"], + ["Finalisieren - 100%", "Finalizing - 100%"] + ])("translates compact finalization progress %s in both directions", (german, english) => { + expect(translateUiText(german, "en")).toBe(english); + expect(translateUiText(english, "de")).toBe(german); + }); + it("translates the complete history surface including status values", () => { const translations = new Map([ ["Alle Einträge", "All entries"],