From 44c9173f10710d6e01240fb1c344f10936d2d721 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Sun, 10 May 2026 12:33:18 +0200 Subject: [PATCH] feat: backend i18n for user-visible errors + light-theme color vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related Phase-4 changes. 1. main.ts: tBackend(key, params) helper with DE/EN tables for every user-visible error / status string produced server-side. Previously every backend message was hardcoded German, so EN-mode users saw German errors in the queue (last_error), in download progress status, in clip-download responses, and in the preflight panel. ~30 keys covered: invalidVodUrl, streamlinkMissing, fileTooSmall, integrity*, downloadCancelled / downloadPaused, attemptFailed, retryingIn, statusBytesDownloaded, mergeGroupFileMissing, notAllPartsDownloaded / notAllClipPartsDownloaded, ffmpegMerge/ SplitFailed, diskSpaceShortFor, all preflight* messages, etc. classifyDownloadError extended to recognize EN equivalents (streamlink not found, no video stream, folder) so the retry classification still works correctly when the language is EN. The hand-rolled translation table in renderer.ts:downloadClip is gone — backend strings are already locale-correct. 2. styles.css: --border-soft CSS var added to :root and the theme-light override. Inline styles in index.html for the VOD filter input / sort select / bulk bar were referencing --bg-secondary / --text-primary / --border-color (which don't exist) and falling through to dark hex fallbacks (#222 / #fff / #444), producing a dark patch in light theme. Now uses var(--bg-card) / var(--text) / var(--border-soft) which both themes define. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/index.html | 16 ++--- src/main.ts | 181 +++++++++++++++++++++++++++++++++++++----------- src/renderer.ts | 16 +---- src/styles.css | 5 ++ 4 files changed, 155 insertions(+), 63 deletions(-) diff --git a/src/index.html b/src/index.html index 5f102d4..1b16ef6 100644 --- a/src/index.html +++ b/src/index.html @@ -246,23 +246,23 @@
- - - - + + + - +
diff --git a/src/main.ts b/src/main.ts index bb066db..32016e7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -80,6 +80,103 @@ function getMergeGroupPhaseText(phase: string): string { } } +// ========================================== +// BACKEND I18N +// ========================================== +// User-visible messages produced in main.ts. Keep keys stable — the renderer +// no longer translates these (renderer.ts:downloadClip used to translate a +// hardcoded set, which was brittle as the strings drifted). Internal +// debug log messages stay English-only since they're developer-facing. +const BACKEND_MESSAGES = { + de: { + invalidVodUrl: 'Ungueltige VOD-URL', + invalidClipUrl: 'Ungueltige Clip-URL', + clipNotFound: 'Clip nicht gefunden', + streamlinkAutoInstallFailed: 'Streamlink fehlt und konnte nicht automatisch installiert werden. Siehe debug.log.', + streamlinkMissing: 'Streamlink fehlt.', + streamlinkNotFound: 'Streamlink nicht gefunden. Installiere Streamlink oder Python+streamlink (py -3 -m pip install streamlink).', + streamlinkExitCode: 'Streamlink Fehlercode {code}', + ffmpegMissing: 'FFmpeg fehlt.', + ffmpegMergeFailed: 'FFmpeg Merge fehlgeschlagen.', + ffmpegSplitFailed: 'FFmpeg Split fehlgeschlagen.', + fileTooSmall: 'Datei zu klein ({bytes} Bytes)', + clipFileTooSmall: 'Clip-Datei zu klein ({bytes} Bytes) - Twitch hat den Stream evtl. nicht ausgeliefert.', + integrityNoVideo: 'Integritaetspruefung fehlgeschlagen: Kein Videostream gefunden.', + integrityTooShort: 'Integritaetspruefung fehlgeschlagen: Dauer zu kurz ({duration}s).', + integrityDurationMismatch: 'Integritaetspruefung fehlgeschlagen: {actual}s statt erwarteter ~{expected}s.', + integrityFailedGeneric: 'Integritaetspruefung fehlgeschlagen.', + downloadCancelled: 'Download wurde abgebrochen.', + downloadPaused: 'Download wurde pausiert.', + downloadFailedExitCode: 'Download fehlgeschlagen (Exit-Code {code})', + unknownDownloadError: 'Unbekannter Fehler beim Download', + notAllClipPartsDownloaded: 'Nicht alle Clip-Teile konnten heruntergeladen werden.', + notAllPartsDownloaded: 'Nicht alle Teile konnten heruntergeladen werden.', + mergeGroupFileMissing: 'Heruntergeladene Datei {index} fehlt.', + diskSpaceShortFor: 'Zu wenig Speicherplatz fur {context}: frei {free}, benoetigt ~{required}.', + diskSpaceShortGeneric: 'Zu wenig Speicherplatz.', + attemptFailed: 'Versuch {attempt}/{max} fehlgeschlagen ({errorClass}): {error}', + retryingIn: 'Neuer Versuch in {seconds}s ({errorClass})...', + statusCheckingTools: 'Prufe Download-Tools...', + statusDownloadStarted: 'Download gestartet', + statusBytesDownloaded: '{bytes} heruntergeladen', + preflightNoInternet: 'Keine Internetverbindung erkannt.', + preflightStreamlinkMissing: 'Streamlink fehlt oder ist nicht startbar.', + preflightFfmpegMissing: 'FFmpeg fehlt oder ist nicht startbar.', + preflightFfprobeMissing: 'FFprobe fehlt oder ist nicht startbar.', + preflightDownloadPathNotWritable: 'Download-Ordner ist nicht beschreibbar.' + }, + en: { + invalidVodUrl: 'Invalid VOD URL', + invalidClipUrl: 'Invalid clip URL', + clipNotFound: 'Clip not found', + streamlinkAutoInstallFailed: 'Streamlink is missing and could not be auto-installed. See debug.log.', + streamlinkMissing: 'Streamlink is missing.', + streamlinkNotFound: 'Streamlink not found. Install streamlink or Python+streamlink (py -3 -m pip install streamlink).', + streamlinkExitCode: 'Streamlink exit code {code}', + ffmpegMissing: 'FFmpeg is missing.', + ffmpegMergeFailed: 'FFmpeg merge failed.', + ffmpegSplitFailed: 'FFmpeg split failed.', + fileTooSmall: 'File too small ({bytes} bytes)', + clipFileTooSmall: 'Clip file too small ({bytes} bytes) - Twitch may not have served the stream.', + integrityNoVideo: 'Integrity check failed: no video stream found.', + integrityTooShort: 'Integrity check failed: duration too short ({duration}s).', + integrityDurationMismatch: 'Integrity check failed: {actual}s instead of expected ~{expected}s.', + integrityFailedGeneric: 'Integrity check failed.', + downloadCancelled: 'Download was cancelled.', + downloadPaused: 'Download was paused.', + downloadFailedExitCode: 'Download failed (exit code {code})', + unknownDownloadError: 'Unknown download error', + notAllClipPartsDownloaded: 'Not all clip parts could be downloaded.', + notAllPartsDownloaded: 'Not all parts could be downloaded.', + mergeGroupFileMissing: 'Downloaded file {index} is missing.', + diskSpaceShortFor: 'Not enough disk space for {context}: free {free}, need ~{required}.', + diskSpaceShortGeneric: 'Not enough disk space.', + attemptFailed: 'Attempt {attempt}/{max} failed ({errorClass}): {error}', + retryingIn: 'Retrying in {seconds}s ({errorClass})...', + statusCheckingTools: 'Checking download tools...', + statusDownloadStarted: 'Download started', + statusBytesDownloaded: '{bytes} downloaded', + preflightNoInternet: 'No internet connection detected.', + preflightStreamlinkMissing: 'Streamlink is missing or not runnable.', + preflightFfmpegMissing: 'FFmpeg is missing or not runnable.', + preflightFfprobeMissing: 'FFprobe is missing or not runnable.', + preflightDownloadPathNotWritable: 'Download folder is not writable.' + } +} as const; + +type BackendMessageKey = keyof typeof BACKEND_MESSAGES.de; + +function tBackend(key: BackendMessageKey, params?: Record): string { + const lang: 'de' | 'en' = config.language === 'en' ? 'en' : 'de'; + let template: string = BACKEND_MESSAGES[lang][key]; + if (params) { + for (const [k, v] of Object.entries(params)) { + template = template.replace(`{${k}}`, String(v)); + } + } + return template; +} + // Ensure directories exist if (!fs.existsSync(APPDATA_DIR)) { fs.mkdirSync(APPDATA_DIR, { recursive: true }); @@ -650,11 +747,11 @@ async function runPreflight(autoFix = false): Promise { } const messages: string[] = []; - if (!checks.internet) messages.push('Keine Internetverbindung erkannt.'); - if (!checks.streamlink) messages.push('Streamlink fehlt oder ist nicht startbar.'); - if (!checks.ffmpeg) messages.push('FFmpeg fehlt oder ist nicht startbar.'); - if (!checks.ffprobe) messages.push('FFprobe fehlt oder ist nicht startbar.'); - if (!checks.downloadPathWritable) messages.push('Download-Ordner ist nicht beschreibbar.'); + if (!checks.internet) messages.push(tBackend('preflightNoInternet')); + if (!checks.streamlink) messages.push(tBackend('preflightStreamlinkMissing')); + if (!checks.ffmpeg) messages.push(tBackend('preflightFfmpegMissing')); + if (!checks.ffprobe) messages.push(tBackend('preflightFfprobeMissing')); + if (!checks.downloadPathWritable) messages.push(tBackend('preflightDownloadPathNotWritable')); const result: PreflightResult = { ok: messages.length === 0, @@ -1089,7 +1186,7 @@ function ensureDiskSpace(targetPath: string, requiredBytes: number, context: str } if (freeBytes < Math.max(requiredBytes, MIN_FREE_DISK_BYTES)) { - const message = `Zu wenig Speicherplatz fur ${context}: frei ${formatBytes(freeBytes)}, benoetigt ~${formatBytes(requiredBytes)}.`; + const message = tBackend('diskSpaceShortFor', { context, free: formatBytes(freeBytes), required: formatBytes(requiredBytes) }); appendDebugLog('disk-space-check-failed', { targetPath, requiredBytes, @@ -1279,9 +1376,9 @@ function classifyDownloadError(errorMessage: string): RetryErrorClass { if (text.includes('429') || text.includes('rate limit') || text.includes('too many requests')) return 'rate_limit'; if (text.includes('401') || text.includes('403') || text.includes('unauthorized') || text.includes('forbidden')) return 'auth'; if (text.includes('timed out') || text.includes('timeout') || text.includes('network') || text.includes('connection') || text.includes('dns')) return 'network'; - if (text.includes('streamlink nicht gefunden') || text.includes('ffmpeg') || text.includes('ffprobe') || text.includes('enoent')) return 'tooling'; - if (text.includes('integritaet') || text.includes('integrity') || text.includes('kein videostream')) return 'integrity'; - if (text.includes('access denied') || text.includes('permission') || text.includes('disk') || text.includes('file') || text.includes('ordner')) return 'io'; + if (text.includes('streamlink nicht gefunden') || text.includes('streamlink not found') || text.includes('streamlink is missing') || text.includes('ffmpeg') || text.includes('ffprobe') || text.includes('enoent')) return 'tooling'; + if (text.includes('integritaet') || text.includes('integrity') || text.includes('kein videostream') || text.includes('no video stream')) return 'integrity'; + if (text.includes('access denied') || text.includes('permission') || text.includes('disk') || text.includes('file') || text.includes('ordner') || text.includes('folder')) return 'io'; return 'unknown'; } @@ -1513,12 +1610,12 @@ function validateDownloadedFileIntegrity(filePath: string, expectedDurationSecon if (!probed.hasVideo) { runtimeMetrics.integrityFailures += 1; - return { success: false, error: 'Integritaetspruefung fehlgeschlagen: Kein Videostream gefunden.' }; + return { success: false, error: tBackend('integrityNoVideo') }; } if (probed.durationSeconds <= 1) { runtimeMetrics.integrityFailures += 1; - return { success: false, error: `Integritaetspruefung fehlgeschlagen: Dauer zu kurz (${probed.durationSeconds.toFixed(2)}s).` }; + return { success: false, error: tBackend('integrityTooShort', { duration: probed.durationSeconds.toFixed(2) }) }; } if (expectedDurationSeconds && expectedDurationSeconds > 4) { @@ -1527,7 +1624,7 @@ function validateDownloadedFileIntegrity(filePath: string, expectedDurationSecon runtimeMetrics.integrityFailures += 1; return { success: false, - error: `Integritaetspruefung fehlgeschlagen: ${probed.durationSeconds.toFixed(1)}s statt erwarteter ~${expectedDurationSeconds}s.` + error: tBackend('integrityDurationMismatch', { actual: probed.durationSeconds.toFixed(1), expected: String(expectedDurationSeconds) }) }; } } @@ -2466,7 +2563,7 @@ function downloadVODPart( progress: -1, // Unknown total speed: formatSpeed(speed), eta: etaStr, - status: `${formatBytes(downloadedBytes)} heruntergeladen`, + status: tBackend('statusBytesDownloaded', { bytes: formatBytes(downloadedBytes) }), currentPart: partNum, totalParts: totalParts, downloadedBytes: downloadedBytes, @@ -2513,14 +2610,14 @@ function downloadVODPart( if (currentDownloadCancelled || cancelledItemIds.has(itemId)) { cancelledItemIds.delete(itemId); appendDebugLog('download-part-cancelled', { itemId, filename }); - resolve({ success: false, error: 'Download wurde abgebrochen.' }); + resolve({ success: false, error: tBackend('downloadCancelled') }); return; } if (code === 0 && fs.existsSync(filename)) { const stats = fs.statSync(filename); if (stats.size <= MIN_FILE_BYTES) { - const tooSmall = `Datei zu klein (${stats.size} Bytes)`; + const tooSmall = tBackend('fileTooSmall', { bytes: String(stats.size) }); appendDebugLog('download-part-failed-small-file', { itemId, filename, bytes: stats.size }); resolve({ success: false, error: tooSmall }); return; @@ -2544,7 +2641,7 @@ function downloadVODPart( return; } - const genericError = lastErrorLine || `Streamlink Fehlercode ${code ?? -1}`; + const genericError = lastErrorLine || tBackend('streamlinkExitCode', { code: String(code ?? -1) }); appendDebugLog('download-part-failed', { itemId, filename, code, error: genericError }); resolve({ success: false, error: genericError }); }); @@ -2555,7 +2652,7 @@ function downloadVODPart( activeDownloads.delete(itemId); const rawError = String(err); const errorMessage = rawError.includes('ENOENT') - ? 'Streamlink nicht gefunden. Installiere Streamlink oder Python+streamlink (py -3 -m pip install streamlink).' + ? tBackend('streamlinkNotFound') : rawError; appendDebugLog('download-part-process-error', { itemId, error: errorMessage, rawError }); resolve({ success: false, error: errorMessage }); @@ -2571,7 +2668,7 @@ async function downloadVOD( if (!isLikelyVodUrl(item.url) || !vodId) { return { success: false, - error: 'Ungueltige VOD-URL' + error: tBackend('invalidVodUrl') }; } @@ -2585,7 +2682,7 @@ async function downloadVOD( progress: -1, speed: '', eta: '', - status: 'Prufe Download-Tools...', + status: tBackend('statusCheckingTools'), currentPart: 0, totalParts: 0 }); @@ -2595,7 +2692,7 @@ async function downloadVOD( if (!streamlinkReady) { return { success: false, - error: 'Streamlink fehlt und konnte nicht automatisch installiert werden. Siehe debug.log.' + error: tBackend('streamlinkAutoInstallFailed') }; } @@ -2604,7 +2701,7 @@ async function downloadVOD( progress: -1, speed: '', eta: '', - status: 'Download gestartet', + status: tBackend('statusDownloadStarted'), currentPart: 0, totalParts: 0 }); @@ -2714,7 +2811,7 @@ async function downloadVOD( return { success: downloadedFiles.length === numParts, - error: downloadedFiles.length === numParts ? undefined : 'Nicht alle Clip-Teile konnten heruntergeladen werden.', + error: downloadedFiles.length === numParts ? undefined : tBackend('notAllClipPartsDownloaded'), outputFiles: downloadedFiles.length === numParts ? [...downloadedFiles] : undefined }; } else { @@ -2787,7 +2884,7 @@ async function downloadVOD( return { success: downloadedFiles.length === numParts, - error: downloadedFiles.length === numParts ? undefined : 'Nicht alle Teile konnten heruntergeladen werden.', + error: downloadedFiles.length === numParts ? undefined : tBackend('notAllPartsDownloaded'), outputFiles: downloadedFiles.length === numParts ? [...downloadedFiles] : undefined }; } @@ -2808,12 +2905,12 @@ async function processDownloadMergeGroup( if (mg.mergePhase === 'downloading') { const streamlinkReady = await ensureStreamlinkInstalled(); if (!streamlinkReady) { - return { success: false, error: 'Streamlink fehlt.' }; + return { success: false, error: tBackend('streamlinkMissing') }; } const ffmpegReady = await ensureFfmpegInstalled(); if (!ffmpegReady) { - return { success: false, error: 'FFmpeg fehlt.' }; + return { success: false, error: tBackend('ffmpegMissing') }; } const streamer = mg.items[0].streamer.replace(/[^a-zA-Z0-9_-]/g, ''); @@ -2835,7 +2932,7 @@ async function processDownloadMergeGroup( for (let i = 0; i < mg.items.length; i++) { if (currentDownloadCancelled || cancelledItemIds.has(item.id)) { - return { success: false, error: 'Download wurde abgebrochen.' }; + return { success: false, error: tBackend('downloadCancelled') }; } // Skip already downloaded files (retry recovery) @@ -2899,7 +2996,7 @@ async function processDownloadMergeGroup( for (let i = 0; i < mg.items.length; i++) { if (!mg.downloadedFiles[i] || !fs.existsSync(mg.downloadedFiles[i])) { mg.mergePhase = 'downloading'; - return { success: false, error: `Heruntergeladene Datei ${i + 1} fehlt.` }; + return { success: false, error: tBackend('mergeGroupFileMissing', { index: i + 1 }) }; } } @@ -2934,7 +3031,7 @@ async function processDownloadMergeGroup( ); if (!mergeSuccess) { - return { success: false, error: 'FFmpeg Merge fehlgeschlagen.' }; + return { success: false, error: tBackend('ffmpegMergeFailed') }; } mg.mergedFile = mergedFilePath; @@ -2947,7 +3044,7 @@ async function processDownloadMergeGroup( emitQueueUpdated(); if (currentDownloadCancelled || cancelledItemIds.has(item.id)) { - return { success: false, error: 'Download wurde abgebrochen.' }; + return { success: false, error: tBackend('downloadCancelled') }; } const partDuration = config.part_minutes * 60; @@ -2999,7 +3096,7 @@ async function processDownloadMergeGroup( for (const partFile of splitResult.files) { try { if (fs.existsSync(partFile)) fs.unlinkSync(partFile); } catch { } } - return { success: false, error: 'FFmpeg Split fehlgeschlagen.' }; + return { success: false, error: tBackend('ffmpegSplitFailed') }; } mg.splitFiles = splitResult.files; @@ -3054,7 +3151,7 @@ async function processOneQueueItem(item: QueueItem): Promise { item.last_error = ''; try { - let finalResult: DownloadResult = { success: false, error: 'Unbekannter Fehler beim Download' }; + let finalResult: DownloadResult = { success: false, error: tBackend('unknownDownloadError') }; const maxAttempts = getRetryAttemptLimit(); for (let attempt = 1; attempt <= maxAttempts; attempt++) { @@ -3076,7 +3173,7 @@ async function processOneQueueItem(item: QueueItem): Promise { finalResult = result; if (!isDownloading || currentDownloadCancelled || cancelledItemIds.has(item.id) || pauseRequested) { - finalResult = { success: false, error: pauseRequested ? 'Download wurde pausiert.' : 'Download wurde abgebrochen.' }; + finalResult = { success: false, error: pauseRequested ? tBackend('downloadPaused') : tBackend('downloadCancelled') }; break; } @@ -3097,13 +3194,13 @@ async function processOneQueueItem(item: QueueItem): Promise { runtimeMetrics.retriesScheduled += 1; runtimeMetrics.lastRetryDelaySeconds = retryDelaySeconds; - item.last_error = `Versuch ${attempt}/${maxAttempts} fehlgeschlagen (${errorClass}): ${result.error || 'Unbekannter Fehler'}`; + item.last_error = tBackend('attemptFailed', { attempt, max: maxAttempts, errorClass, error: result.error || tBackend('unknownDownloadError') }); mainWindow?.webContents.send('download-progress', { id: item.id, progress: -1, speed: '', eta: '', - status: `Neuer Versuch in ${retryDelaySeconds}s (${errorClass})...`, + status: tBackend('retryingIn', { seconds: retryDelaySeconds, errorClass }), currentPart: item.currentPart, totalParts: item.totalParts } as DownloadProgress); @@ -3123,7 +3220,7 @@ async function processOneQueueItem(item: QueueItem): Promise { const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert'); item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error'); item.progress = finalResult.success ? 100 : item.progress; - item.last_error = finalResult.success || wasPaused ? '' : (finalResult.error || 'Unbekannter Fehler beim Download'); + item.last_error = finalResult.success || wasPaused ? '' : (finalResult.error || tBackend('unknownDownloadError')); if (finalResult.success && Array.isArray(finalResult.outputFiles) && finalResult.outputFiles.length > 0) { // Attach the produced file paths so the renderer can offer @@ -3963,10 +4060,10 @@ ipcMain.handle('download-clip', async (_, clipUrl: string) => { if (match1) clipId = match1[1]; else if (match2) clipId = match2[1]; - else return { success: false, error: 'Ungueltige Clip-URL' }; + else return { success: false, error: tBackend('invalidClipUrl') }; const clipInfo = await getClipInfo(clipId); - if (!clipInfo) return { success: false, error: 'Clip nicht gefunden' }; + if (!clipInfo) return { success: false, error: tBackend('clipNotFound') }; // Sanitize broadcaster_name for path safety — Twitch returns the display // name which can contain unicode, spaces, or punctuation that breaks @@ -3980,7 +4077,7 @@ ipcMain.handle('download-clip', async (_, clipUrl: string) => { const clipDiskCheck = ensureDiskSpace(folder, 128 * 1024 * 1024, 'Clip-Download'); if (!clipDiskCheck.success) { - return { success: false, error: clipDiskCheck.error || 'Zu wenig Speicherplatz.' }; + return { success: false, error: clipDiskCheck.error || tBackend('diskSpaceShortGeneric') }; } const rawTitle = typeof clipInfo.title === 'string' ? clipInfo.title : ''; @@ -4009,7 +4106,7 @@ ipcMain.handle('download-clip', async (_, clipUrl: string) => { if (code !== 0 || !fs.existsSync(filename)) { appendDebugLog('clip-download-failed', { clipId, code }); - resolve({ success: false, error: `Download fehlgeschlagen (Exit-Code ${code ?? -1})` }); + resolve({ success: false, error: tBackend('downloadFailedExitCode', { code: String(code ?? -1) }) }); return; } @@ -4020,7 +4117,7 @@ ipcMain.handle('download-clip', async (_, clipUrl: string) => { if (stats.size < 16 * 1024) { try { fs.unlinkSync(filename); } catch { } appendDebugLog('clip-download-too-small', { clipId, bytes: stats.size }); - resolve({ success: false, error: `Clip-Datei zu klein (${stats.size} Bytes) — Twitch hat den Stream evtl. nicht ausgeliefert.` }); + resolve({ success: false, error: tBackend('clipFileTooSmall', { bytes: String(stats.size) }) }); return; } @@ -4028,7 +4125,7 @@ ipcMain.handle('download-clip', async (_, clipUrl: string) => { if (!integrity.success) { try { fs.unlinkSync(filename); } catch { } appendDebugLog('clip-download-integrity-failed', { clipId, error: integrity.error }); - resolve({ success: false, error: integrity.error || 'Integritaetspruefung fehlgeschlagen.' }); + resolve({ success: false, error: integrity.error || tBackend('integrityFailedGeneric') }); return; } @@ -4039,7 +4136,7 @@ ipcMain.handle('download-clip', async (_, clipUrl: string) => { proc.on('error', () => { activeClipProcesses.delete(clipId); releaseClaimedFilenamesForItem(clipId); - resolve({ success: false, error: 'Streamlink nicht gefunden' }); + resolve({ success: false, error: tBackend('streamlinkNotFound') }); }); }); }); diff --git a/src/renderer.ts b/src/renderer.ts index 19766fa..d428b7e 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1019,20 +1019,10 @@ async function downloadClip(): Promise { return; } + // Backend now produces locale-aware error strings via tBackend(), + // so we no longer need a renderer-side translation table here. const backendError = (result.error || '').trim(); - let localizedError = backendError; - - if (backendError === 'Ungueltige Clip-URL') { - localizedError = currentLanguage === 'en' ? 'Invalid clip URL' : backendError; - } else if (backendError === 'Clip nicht gefunden') { - localizedError = currentLanguage === 'en' ? 'Clip not found' : backendError; - } else if (backendError === 'Streamlink nicht gefunden') { - localizedError = currentLanguage === 'en' ? 'Streamlink not found' : backendError; - } else if (backendError.startsWith('Download fehlgeschlagen')) { - localizedError = currentLanguage === 'en' ? backendError.replace('Download fehlgeschlagen', 'Download failed') : backendError; - } - - status.textContent = UI_TEXT.clips.errorPrefix + (localizedError || UI_TEXT.clips.unknownError); + status.textContent = UI_TEXT.clips.errorPrefix + (backendError || UI_TEXT.clips.unknownError); status.className = 'clip-status error'; } diff --git a/src/styles.css b/src/styles.css index 91cecf2..8d4b010 100644 --- a/src/styles.css +++ b/src/styles.css @@ -9,6 +9,10 @@ --success: #00c853; --error: #ff4444; --warning: #ffab00; + /* Soft border that adapts to theme — used by post-4.5.x UI additions + (filter input, sort select, bulk bar) so they don't visually break + in light theme. */ + --border-soft: rgba(255, 255, 255, 0.1); } * { @@ -1367,6 +1371,7 @@ body.theme-light { --success: #00c853; --error: #e41e3f; --warning: #e68a00; + --border-soft: rgba(0, 0, 0, 0.12); } /* Light theme: swap white-alpha borders/backgrounds to black-alpha */