From 16644787701a6e85f957d7182ff2d97c64846915 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 12 Aug 2026 00:09:18 +0200 Subject: [PATCH] fix(downloads): compact finalization statuses Normalize German and English finalization producer labels before they reach package and item status cells. Derive percentages from valid fractions, clamp them to the visible 0-100 range, and reduce malformed fractions or zero totals to the phase name. Preserve legacy percentage labels while preventing finalization lines ending in archive names from being classified as bare extraction archives. Cover current and legacy forms, both languages, package and item rendering, localization, and invalid numeric input. --- src/renderer/i18n.ts | 4 ++ .../views/downloads/DownloadsTable.tsx | 19 +++++++- tests/downloads-view.test.tsx | 45 +++++++++++++++++++ tests/i18n.test.ts | 9 ++++ 4 files changed, 75 insertions(+), 2 deletions(-) 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"],