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 */