diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json index 3fe35b3..1cfcdce 100644 --- a/typescript-version/package-lock.json +++ b/typescript-version/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "3.8.5", + "version": "3.8.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "3.8.5", + "version": "3.8.6", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/typescript-version/package.json b/typescript-version/package.json index f8fd300..2684ed8 100644 --- a/typescript-version/package.json +++ b/typescript-version/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "3.8.5", + "version": "3.8.6", "description": "Twitch VOD Manager - Download Twitch VODs easily", "main": "dist/main.js", "author": "xRangerDE", diff --git a/typescript-version/src/index.html b/typescript-version/src/index.html index 77471f1..4b081ef 100644 --- a/typescript-version/src/index.html +++ b/typescript-version/src/index.html @@ -93,29 +93,29 @@ @@ -145,7 +145,7 @@ @@ -165,18 +165,19 @@
-

Twitch Clip-Download

+

Twitch Clip-Download

-

Info

-

- Unterstutzte Formate:
- - https://clips.twitch.tv/ClipName
- - https://www.twitch.tv/streamer/clip/ClipName

+

Info

+

+ Unterstutzte Formate: + - https://clips.twitch.tv/ClipName + - https://www.twitch.tv/streamer/clip/ClipName + Clips werden im Download-Ordner unter "Clips/StreamerName/" gespeichert.

@@ -186,10 +187,10 @@
-

Video auswahlen

+

Video auswahlen

- +
@@ -254,12 +255,12 @@
-

Videos zusammenfugen

-

+

Videos zusammenfugen

+

Wahle mehrere Videos aus um sie zu einem Video zusammenzufugen. Die Reihenfolge kann per Drag & Drop geandert werden.

- +
@@ -285,9 +286,9 @@
-

Design

+

Design

- +
+
+ + +
-

Twitch API

+

Twitch API

- +
- +
- +
-

Download-Einstellungen

+

Download-Einstellungen

- +
- +
- +
- +
-

Updates

-

Version: v3.8.5

- +

Updates

+

Version: v3.8.6

+
@@ -346,11 +354,12 @@
Nicht verbunden
- v3.8.5 + v3.8.6
+ diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts index f0cfbfd..5dc601d 100644 --- a/typescript-version/src/main.ts +++ b/typescript-version/src/main.ts @@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater'; // ========================================== // CONFIG & CONSTANTS // ========================================== -const APP_VERSION = '3.8.5'; +const APP_VERSION = '3.8.6'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; // Paths @@ -43,6 +43,7 @@ interface Config { theme: string; download_mode: 'parts' | 'full'; part_minutes: number; + language: 'de' | 'en'; } interface VOD { @@ -116,7 +117,8 @@ const defaultConfig: Config = { streamers: [], theme: 'twitch', download_mode: 'full', - part_minutes: 120 + part_minutes: 120, + language: 'de' }; function loadConfig(): Config { diff --git a/typescript-version/src/renderer-globals.d.ts b/typescript-version/src/renderer-globals.d.ts index 72e0c69..9d7996d 100644 --- a/typescript-version/src/renderer-globals.d.ts +++ b/typescript-version/src/renderer-globals.d.ts @@ -6,6 +6,7 @@ interface AppConfig { theme?: string; download_mode?: 'parts' | 'full'; part_minutes?: number; + language?: 'de' | 'en'; [key: string]: unknown; } diff --git a/typescript-version/src/renderer-queue.ts b/typescript-version/src/renderer-queue.ts index 5db0f89..dc936b4 100644 --- a/typescript-version/src/renderer-queue.ts +++ b/typescript-version/src/renderer-queue.ts @@ -20,18 +20,18 @@ async function clearCompleted(): Promise { } function getQueueStatusLabel(item: QueueItem): string { - if (item.status === 'completed') return 'Abgeschlossen'; - if (item.status === 'error') return 'Fehlgeschlagen'; - if (item.status === 'downloading') return 'Laeuft'; - return 'Wartet'; + if (item.status === 'completed') return UI_TEXT.queue.statusDone; + if (item.status === 'error') return UI_TEXT.queue.statusFailed; + if (item.status === 'downloading') return UI_TEXT.queue.statusRunning; + return UI_TEXT.queue.statusWaiting; } function getQueueProgressText(item: QueueItem): string { if (item.status === 'completed') return '100%'; - if (item.status === 'error') return 'Fehler'; - if (item.status === 'pending') return 'Bereit'; + if (item.status === 'error') return UI_TEXT.queue.progressError; + if (item.status === 'pending') return UI_TEXT.queue.progressReady; if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`; - return item.progressStatus || 'Lade...'; + return item.progressStatus || UI_TEXT.queue.progressLoading; } function getQueueMetaText(item: QueueItem): string { @@ -42,31 +42,31 @@ function getQueueMetaText(item: QueueItem): string { const parts: string[] = []; if (item.currentPart && item.totalParts) { - parts.push(`Teil ${item.currentPart}/${item.totalParts}`); + parts.push(`${UI_TEXT.queue.part} ${item.currentPart}/${item.totalParts}`); } if (item.speed) { - parts.push(`Geschwindigkeit: ${item.speed}`); + parts.push(`${UI_TEXT.queue.speed}: ${item.speed}`); } if (item.eta) { - parts.push(`Restzeit: ${item.eta}`); + parts.push(`${UI_TEXT.queue.eta}: ${item.eta}`); } if (!parts.length && item.status === 'pending') { - parts.push('Bereit zum Download'); + parts.push(UI_TEXT.queue.readyToDownload); } if (!parts.length && item.status === 'downloading') { - parts.push(item.progressStatus || 'Download gestartet'); + parts.push(item.progressStatus || UI_TEXT.queue.started); } if (!parts.length && item.status === 'completed') { - parts.push('Fertig'); + parts.push(UI_TEXT.queue.done); } if (!parts.length && item.status === 'error') { - parts.push('Download fehlgeschlagen'); + parts.push(UI_TEXT.queue.failed); } return parts.join(' | '); @@ -81,12 +81,12 @@ function renderQueue(): void { byId('queueCount').textContent = String(queue.length); if (queue.length === 0) { - list.innerHTML = '
Keine Downloads in der Warteschlange
'; + list.innerHTML = `
${UI_TEXT.queue.empty}
`; return; } list.innerHTML = queue.map((item: QueueItem) => { - const safeTitle = escapeHtml(item.title || 'Untitled'); + const safeTitle = escapeHtml(item.title || UI_TEXT.vods.untitled); const safeStatusLabel = escapeHtml(getQueueStatusLabel(item)); const safeProgressText = escapeHtml(getQueueProgressText(item)); const safeMeta = escapeHtml(getQueueMetaText(item)); @@ -126,6 +126,6 @@ async function toggleDownload(): Promise { const started = await window.api.startDownload(); if (!started) { renderQueue(); - alert('Die Warteschlange ist leer. Fuge zuerst ein VOD oder einen Clip hinzu.'); + alert(UI_TEXT.queue.emptyAlert); } } diff --git a/typescript-version/src/renderer-settings.ts b/typescript-version/src/renderer-settings.ts index c5dc1eb..3e20aec 100644 --- a/typescript-version/src/renderer-settings.ts +++ b/typescript-version/src/renderer-settings.ts @@ -2,14 +2,14 @@ async function connect(): Promise { const hasCredentials = Boolean((config.client_id ?? '').toString().trim() && (config.client_secret ?? '').toString().trim()); if (!hasCredentials) { isConnected = false; - updateStatus('Ohne Login (Public Modus)', false); + updateStatus(UI_TEXT.status.noLogin, false); return; } - updateStatus('Verbinde...', false); + updateStatus(UI_TEXT.status.connecting, false); const success = await window.api.login(); isConnected = success; - updateStatus(success ? 'Verbunden' : 'Verbindung fehlgeschlagen - Public Modus aktiv', success); + updateStatus(success ? UI_TEXT.status.connected : UI_TEXT.status.connectFailedPublic, success); } function updateStatus(text: string, connected: boolean): void { @@ -19,6 +19,22 @@ function updateStatus(text: string, connected: boolean): void { dot.classList.add(connected ? 'connected' : 'error'); } +function changeLanguage(lang: string): void { + const normalized = setLanguage(lang); + byId('languageSelect').value = normalized; + config.language = normalized; + void window.api.saveConfig({ language: normalized }); + + const currentStatus = byId('statusText').textContent?.trim() || ''; + updateStatus(localizeCurrentStatusText(currentStatus), isConnected); + + renderQueue(); + renderStreamers(); + if (!currentStreamer) { + byId('pageTitle').textContent = UI_TEXT.tabs.vods; + } +} + async function saveSettings(): Promise { const clientId = byId('clientId').value.trim(); const clientSecret = byId('clientSecret').value.trim(); diff --git a/typescript-version/src/renderer-streamers.ts b/typescript-version/src/renderer-streamers.ts index c768725..2430182 100644 --- a/typescript-version/src/renderer-streamers.ts +++ b/typescript-version/src/renderer-streamers.ts @@ -43,8 +43,8 @@ async function removeStreamer(name: string): Promise { byId('vodGrid').innerHTML = `
-

Keine VODs

-

Wahle einen Streamer aus der Liste.

+

${UI_TEXT.vods.noneTitle}

+

${UI_TEXT.vods.noneText}

`; } @@ -59,14 +59,14 @@ async function selectStreamer(name: string): Promise { } if (!isConnected) { - updateStatus('Ohne Login (Public Modus)', false); + updateStatus(UI_TEXT.status.noLogin, false); } - byId('vodGrid').innerHTML = '

Lade VODs...

'; + byId('vodGrid').innerHTML = `

${UI_TEXT.vods.loading}

`; const userId = await window.api.getUserId(name); if (!userId) { - byId('vodGrid').innerHTML = '

Streamer nicht gefunden

'; + byId('vodGrid').innerHTML = `

${UI_TEXT.vods.notFound}

`; return; } @@ -78,7 +78,7 @@ function renderVODs(vods: VOD[] | null | undefined, streamer: string): void { const grid = byId('vodGrid'); if (!vods || vods.length === 0) { - grid.innerHTML = '

Keine VODs gefunden

Dieser Streamer hat keine VODs.

'; + grid.innerHTML = `

${UI_TEXT.vods.noResultsTitle}

${UI_TEXT.vods.noResultsText}

`; return; } @@ -86,7 +86,7 @@ function renderVODs(vods: VOD[] | null | undefined, streamer: string): void { const thumb = vod.thumbnail_url.replace('%{width}', '320').replace('%{height}', '180'); const date = new Date(vod.created_at).toLocaleDateString('de-DE'); const escapedTitle = vod.title.replace(/'/g, "\\'").replace(/\"/g, '"'); - const safeDisplayTitle = escapeHtml(vod.title || 'Unbenanntes VOD'); + const safeDisplayTitle = escapeHtml(vod.title || UI_TEXT.vods.untitled); return `
@@ -96,12 +96,12 @@ function renderVODs(vods: VOD[] | null | undefined, streamer: string): void {
${date} ${vod.duration} - ${vod.view_count.toLocaleString()} Aufrufe + ${vod.view_count.toLocaleString()} ${UI_TEXT.vods.views}
- +
`; diff --git a/typescript-version/src/renderer-texts.ts b/typescript-version/src/renderer-texts.ts new file mode 100644 index 0000000..abf472d --- /dev/null +++ b/typescript-version/src/renderer-texts.ts @@ -0,0 +1,335 @@ +type LanguageCode = 'de' | 'en'; + +const UI_TEXTS = { + de: { + appName: 'Twitch VOD Manager', + static: { + navVods: 'Twitch VODs', + navClips: 'Twitch Clips', + navCutter: 'Video schneiden', + navMerge: 'Videos zusammenfugen', + navSettings: 'Einstellungen', + queueTitle: 'Warteschlange', + clearQueue: 'Leeren', + refresh: 'Aktualisieren', + streamerPlaceholder: 'Streamer hinzufugen...', + clipsHeading: 'Twitch Clip-Download', + clipsInfoTitle: 'Info', + clipsInfoText: 'Unterstutzte Formate:\n- https://clips.twitch.tv/ClipName\n- https://www.twitch.tv/streamer/clip/ClipName\n\nClips werden im Download-Ordner unter "Clips/StreamerName/" gespeichert.', + cutterSelectTitle: 'Video auswahlen', + cutterBrowse: 'Durchsuchen', + mergeTitle: 'Videos zusammenfugen', + mergeDesc: 'Wahle mehrere Videos aus, um sie zu einem Video zusammenzufugen. Die Reihenfolge kann geandert werden.', + mergeAdd: '+ Videos hinzufugen', + designTitle: 'Design', + themeLabel: 'Theme', + languageLabel: 'Sprache', + languageDe: 'Deutsch', + languageEn: 'Englisch', + apiTitle: 'Twitch API', + clientIdLabel: 'Client ID', + clientSecretLabel: 'Client Secret', + saveSettings: 'Speichern & Verbinden', + downloadSettingsTitle: 'Download-Einstellungen', + storageLabel: 'Speicherort', + openFolder: 'Offnen', + modeLabel: 'Download-Modus', + modeFull: 'Ganzes VOD', + modeParts: 'In Teile splitten', + partMinutesLabel: 'Teil-Lange (Minuten)', + updateTitle: 'Updates', + checkUpdates: 'Nach Updates suchen', + notConnected: 'Nicht verbunden' + }, + status: { + noLogin: 'Ohne Login (Public Modus)', + connecting: 'Verbinde...', + connected: 'Verbunden', + connectFailedPublic: 'Verbindung fehlgeschlagen - Public Modus aktiv' + }, + tabs: { + vods: 'VODs', + clips: 'Clips', + cutter: 'Video schneiden', + merge: 'Videos zusammenfugen', + settings: 'Einstellungen' + }, + queue: { + empty: 'Keine Downloads in der Warteschlange', + start: 'Start', + stop: 'Stoppen', + statusDone: 'Abgeschlossen', + statusFailed: 'Fehlgeschlagen', + statusRunning: 'Laeuft', + statusWaiting: 'Wartet', + progressError: 'Fehler', + progressReady: 'Bereit', + progressLoading: 'Lade...', + readyToDownload: 'Bereit zum Download', + started: 'Download gestartet', + done: 'Fertig', + failed: 'Download fehlgeschlagen', + speed: 'Geschwindigkeit', + eta: 'Restzeit', + part: 'Teil', + emptyAlert: 'Die Warteschlange ist leer. Fuge zuerst ein VOD oder einen Clip hinzu.' + }, + vods: { + noneTitle: 'Keine VODs', + noneText: 'Wahle einen Streamer aus der Liste.', + loading: 'Lade VODs...', + notFound: 'Streamer nicht gefunden', + noResultsTitle: 'Keine VODs gefunden', + noResultsText: 'Dieser Streamer hat keine VODs.', + untitled: 'Unbenanntes VOD', + views: 'Aufrufe', + addQueue: '+ Warteschlange' + }, + clips: { + dialogTitle: 'Clip zuschneiden', + invalidDuration: 'Ungultig!', + endBeforeStart: 'Endzeit muss grosser als Startzeit sein!', + outOfRange: 'Zeit ausserhalb des VOD-Bereichs!', + enterUrl: 'Bitte URL eingeben', + loadingButton: 'Lade...', + loadingStatus: 'Download laeuft...', + downloadButton: 'Clip herunterladen', + success: 'Download erfolgreich!', + errorPrefix: 'Fehler: ', + unknownError: 'Unbekannter Fehler', + formatSimple: '(Standard)', + formatTimestamp: '(mit Zeitstempel)' + }, + cutter: { + videoInfoFailed: 'Konnte Video-Informationen nicht lesen. FFprobe installiert?', + previewLoading: 'Lade Vorschau...', + previewUnavailable: 'Vorschau nicht verfugbar', + cutting: 'Schneidet...', + cut: 'Schneiden', + cutSuccess: 'Video erfolgreich geschnitten!', + cutFailed: 'Fehler beim Schneiden des Videos.' + }, + merge: { + empty: 'Keine Videos ausgewahlt', + merging: 'Zusammenfugen...', + merge: 'Zusammenfugen', + success: 'Videos erfolgreich zusammengefugt!', + failed: 'Fehler beim Zusammenfugen der Videos.' + }, + updates: { + latest: 'Du hast die neueste Version!', + downloading: 'Wird heruntergeladen...', + available: 'verfugbar!', + downloadNow: 'Jetzt herunterladen', + downloadLabel: 'Download', + ready: 'bereit zur Installation!', + installNow: 'Jetzt installieren' + } + }, + en: { + appName: 'Twitch VOD Manager', + static: { + navVods: 'Twitch VODs', + navClips: 'Twitch Clips', + navCutter: 'Video Cutter', + navMerge: 'Merge Videos', + navSettings: 'Settings', + queueTitle: 'Queue', + clearQueue: 'Clear', + refresh: 'Refresh', + streamerPlaceholder: 'Add streamer...', + clipsHeading: 'Twitch Clip Download', + clipsInfoTitle: 'Info', + clipsInfoText: 'Supported formats:\n- https://clips.twitch.tv/ClipName\n- https://www.twitch.tv/streamer/clip/ClipName\n\nClips are saved in your download folder under "Clips/StreamerName/".', + cutterSelectTitle: 'Select video', + cutterBrowse: 'Browse', + mergeTitle: 'Merge videos', + mergeDesc: 'Select multiple videos to merge into one file. You can change the order before merging.', + mergeAdd: '+ Add videos', + designTitle: 'Design', + themeLabel: 'Theme', + languageLabel: 'Language', + languageDe: 'German', + languageEn: 'English', + apiTitle: 'Twitch API', + clientIdLabel: 'Client ID', + clientSecretLabel: 'Client Secret', + saveSettings: 'Save & Connect', + downloadSettingsTitle: 'Download Settings', + storageLabel: 'Storage Path', + openFolder: 'Open', + modeLabel: 'Download Mode', + modeFull: 'Full VOD', + modeParts: 'Split into parts', + partMinutesLabel: 'Part Length (Minutes)', + updateTitle: 'Updates', + checkUpdates: 'Check for updates', + notConnected: 'Not connected' + }, + status: { + noLogin: 'No login (public mode)', + connecting: 'Connecting...', + connected: 'Connected', + connectFailedPublic: 'Connection failed - public mode active' + }, + tabs: { + vods: 'VODs', + clips: 'Clips', + cutter: 'Video Cutter', + merge: 'Merge Videos', + settings: 'Settings' + }, + queue: { + empty: 'No downloads in queue', + start: 'Start', + stop: 'Stop', + statusDone: 'Completed', + statusFailed: 'Failed', + statusRunning: 'Running', + statusWaiting: 'Waiting', + progressError: 'Error', + progressReady: 'Ready', + progressLoading: 'Loading...', + readyToDownload: 'Ready to download', + started: 'Download started', + done: 'Done', + failed: 'Download failed', + speed: 'Speed', + eta: 'ETA', + part: 'Part', + emptyAlert: 'Queue is empty. Add a VOD or clip first.' + }, + vods: { + noneTitle: 'No VODs', + noneText: 'Select a streamer from the list.', + loading: 'Loading VODs...', + notFound: 'Streamer not found', + noResultsTitle: 'No VODs found', + noResultsText: 'This streamer has no VODs.', + untitled: 'Untitled VOD', + views: 'views', + addQueue: '+ Queue' + }, + clips: { + dialogTitle: 'Trim clip', + invalidDuration: 'Invalid!', + endBeforeStart: 'End time must be greater than start time!', + outOfRange: 'Time is outside VOD range!', + enterUrl: 'Please enter a URL', + loadingButton: 'Loading...', + loadingStatus: 'Downloading...', + downloadButton: 'Download clip', + success: 'Download successful!', + errorPrefix: 'Error: ', + unknownError: 'Unknown error', + formatSimple: '(default)', + formatTimestamp: '(with timestamp)' + }, + cutter: { + videoInfoFailed: 'Could not read video info. Is FFprobe installed?', + previewLoading: 'Loading preview...', + previewUnavailable: 'Preview unavailable', + cutting: 'Cutting...', + cut: 'Cut', + cutSuccess: 'Video cut successfully!', + cutFailed: 'Failed to cut video.' + }, + merge: { + empty: 'No videos selected', + merging: 'Merging...', + merge: 'Merge', + success: 'Videos merged successfully!', + failed: 'Failed to merge videos.' + }, + updates: { + latest: 'You are on the latest version!', + downloading: 'Downloading...', + available: 'available!', + downloadNow: 'Download now', + downloadLabel: 'Download', + ready: 'ready to install!', + installNow: 'Install now' + } + } +} as const; + +let currentLanguage: LanguageCode = 'de'; +let UI_TEXT: (typeof UI_TEXTS)[LanguageCode] = UI_TEXTS[currentLanguage]; + +function setText(id: string, value: string): void { + const node = document.getElementById(id); + if (node) node.textContent = value; +} + +function setPlaceholder(id: string, value: string): void { + const node = document.getElementById(id) as HTMLInputElement | null; + if (node) node.placeholder = value; +} + +function setLanguage(lang: string): LanguageCode { + currentLanguage = lang === 'en' ? 'en' : 'de'; + UI_TEXT = UI_TEXTS[currentLanguage]; + applyLanguageToStaticUI(); + return currentLanguage; +} + +function applyLanguageToStaticUI(): void { + setText('logoText', UI_TEXT.appName); + setText('navVodsText', UI_TEXT.static.navVods); + setText('navClipsText', UI_TEXT.static.navClips); + setText('navCutterText', UI_TEXT.static.navCutter); + setText('navMergeText', UI_TEXT.static.navMerge); + setText('navSettingsText', UI_TEXT.static.navSettings); + setText('queueTitleText', UI_TEXT.static.queueTitle); + setText('btnClear', UI_TEXT.static.clearQueue); + setText('refreshText', UI_TEXT.static.refresh); + setText('clipsHeading', UI_TEXT.static.clipsHeading); + setText('clipsInfoTitle', UI_TEXT.static.clipsInfoTitle); + setText('clipsInfoText', UI_TEXT.static.clipsInfoText); + setText('cutterSelectTitle', UI_TEXT.static.cutterSelectTitle); + setText('cutterBrowseBtn', UI_TEXT.static.cutterBrowse); + setText('mergeTitle', UI_TEXT.static.mergeTitle); + setText('mergeDesc', UI_TEXT.static.mergeDesc); + setText('mergeAddBtn', UI_TEXT.static.mergeAdd); + setText('designTitle', UI_TEXT.static.designTitle); + setText('themeLabel', UI_TEXT.static.themeLabel); + setText('languageLabel', UI_TEXT.static.languageLabel); + setText('languageDeText', UI_TEXT.static.languageDe); + setText('languageEnText', UI_TEXT.static.languageEn); + setText('apiTitle', UI_TEXT.static.apiTitle); + setText('clientIdLabel', UI_TEXT.static.clientIdLabel); + setText('clientSecretLabel', UI_TEXT.static.clientSecretLabel); + setText('saveSettingsBtn', UI_TEXT.static.saveSettings); + setText('downloadSettingsTitle', UI_TEXT.static.downloadSettingsTitle); + setText('storageLabel', UI_TEXT.static.storageLabel); + setText('openFolderBtn', UI_TEXT.static.openFolder); + setText('modeLabel', UI_TEXT.static.modeLabel); + setText('modeFullText', UI_TEXT.static.modeFull); + setText('modePartsText', UI_TEXT.static.modeParts); + setText('partMinutesLabel', UI_TEXT.static.partMinutesLabel); + setText('updateTitle', UI_TEXT.static.updateTitle); + setText('checkUpdateBtn', UI_TEXT.static.checkUpdates); + + setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder); + + const status = document.getElementById('statusText')?.textContent?.trim() || ''; + if (status === UI_TEXTS.de.static.notConnected || status === UI_TEXTS.en.static.notConnected) { + setText('statusText', UI_TEXT.static.notConnected); + } +} + +function localizeCurrentStatusText(current: string): string { + const map: Record = { + [UI_TEXTS.de.status.noLogin]: 'noLogin', + [UI_TEXTS.en.status.noLogin]: 'noLogin', + [UI_TEXTS.de.status.connecting]: 'connecting', + [UI_TEXTS.en.status.connecting]: 'connecting', + [UI_TEXTS.de.status.connected]: 'connected', + [UI_TEXTS.en.status.connected]: 'connected', + [UI_TEXTS.de.status.connectFailedPublic]: 'connectFailedPublic', + [UI_TEXTS.en.status.connectFailedPublic]: 'connectFailedPublic' + }; + + const key = map[current]; + return key ? UI_TEXT.status[key] : current; +} diff --git a/typescript-version/src/renderer-updates.ts b/typescript-version/src/renderer-updates.ts index 7c07a4d..3a77c22 100644 --- a/typescript-version/src/renderer-updates.ts +++ b/typescript-version/src/renderer-updates.ts @@ -7,7 +7,7 @@ async function checkUpdate(): Promise { setTimeout(() => { if (byId('updateBanner').style.display !== 'flex') { - alert('Du hast die neueste Version!'); + alert(UI_TEXT.updates.latest); } }, 2000); } @@ -18,7 +18,7 @@ function downloadUpdate(): void { return; } - byId('updateButton').textContent = 'Wird heruntergeladen...'; + byId('updateButton').textContent = UI_TEXT.updates.downloading; byId('updateButton').disabled = true; byId('updateProgress').style.display = 'block'; byId('updateProgressBar').classList.add('downloading'); @@ -27,8 +27,8 @@ function downloadUpdate(): void { window.api.onUpdateAvailable((info: UpdateInfo) => { byId('updateBanner').style.display = 'flex'; - byId('updateText').textContent = `Version ${info.version} verfügbar!`; - byId('updateButton').textContent = 'Jetzt herunterladen'; + byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.available}`; + byId('updateButton').textContent = UI_TEXT.updates.downloadNow; }); window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => { @@ -38,7 +38,7 @@ window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => { const mb = (progress.transferred / 1024 / 1024).toFixed(1); const totalMb = (progress.total / 1024 / 1024).toFixed(1); - byId('updateText').textContent = `Download: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`; + byId('updateText').textContent = `${UI_TEXT.updates.downloadLabel}: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`; }); window.api.onUpdateDownloaded((info: UpdateInfo) => { @@ -48,7 +48,7 @@ window.api.onUpdateDownloaded((info: UpdateInfo) => { bar.classList.remove('downloading'); bar.style.width = '100%'; - byId('updateText').textContent = `Version ${info.version} bereit zur Installation!`; - byId('updateButton').textContent = 'Jetzt installieren'; + byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.ready}`; + byId('updateButton').textContent = UI_TEXT.updates.installNow; byId('updateButton').disabled = false; }); diff --git a/typescript-version/src/renderer.ts b/typescript-version/src/renderer.ts index 69a012f..e210fdb 100644 --- a/typescript-version/src/renderer.ts +++ b/typescript-version/src/renderer.ts @@ -1,17 +1,20 @@ async function init(): Promise { config = await window.api.getConfig(); + const language = setLanguage((config.language as string) || 'de'); + config.language = language; const initialQueue = await window.api.getQueue(); queue = Array.isArray(initialQueue) ? initialQueue : []; const version = await window.api.getVersion(); byId('versionText').textContent = `v${version}`; byId('versionInfo').textContent = `Version: v${version}`; - document.title = `Twitch VOD Manager v${version}`; + document.title = `${UI_TEXT.appName} v${version}`; byId('clientId').value = config.client_id ?? ''; byId('clientSecret').value = config.client_secret ?? ''; byId('downloadPath').value = config.download_path ?? ''; byId('themeSelect').value = config.theme ?? 'twitch'; + byId('languageSelect').value = config.language ?? 'de'; byId('downloadMode').value = config.download_mode ?? 'full'; byId('partMinutes').value = String(config.part_minutes ?? 120); @@ -66,7 +69,7 @@ async function init(): Promise { if (config.client_id && config.client_secret) { await connect(); } else { - updateStatus('Ohne Login (Public Modus)', false); + updateStatus(UI_TEXT.status.noLogin, false); } if (config.streamers && config.streamers.length > 0) { @@ -111,7 +114,7 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] { function updateDownloadButtonState(): void { const btn = byId('btnStart'); - btn.textContent = downloading ? 'Stoppen' : 'Start'; + btn.textContent = downloading ? UI_TEXT.queue.stop : UI_TEXT.queue.start; btn.classList.toggle('downloading', downloading); } @@ -134,15 +137,9 @@ function showTab(tab: string): void { query(`.nav-item[data-tab="${tab}"]`).classList.add('active'); byId(tab + 'Tab').classList.add('active'); - const titles: Record = { - vods: 'VODs', - clips: 'Clips', - cutter: 'Video schneiden', - merge: 'Videos Zusammenfugen', - settings: 'Einstellungen' - }; + const titles: Record = UI_TEXT.tabs; - byId('pageTitle').textContent = currentStreamer || titles[tab] || 'Twitch VOD Manager'; + byId('pageTitle').textContent = currentStreamer || titles[tab] || UI_TEXT.appName; } function parseDurationToSeconds(durStr: string): number { @@ -185,7 +182,7 @@ function openClipDialog(url: string, title: string, date: string, streamer: stri clipDialogData = { url, title, date, streamer, duration }; clipTotalSeconds = parseDurationToSeconds(duration); - byId('clipDialogTitle').textContent = `Clip zuschneiden (${duration})`; + byId('clipDialogTitle').textContent = `${UI_TEXT.clips.dialogTitle} (${duration})`; byId('clipStartSlider').max = String(clipTotalSeconds); byId('clipEndSlider').max = String(clipTotalSeconds); byId('clipStartSlider').value = '0'; @@ -241,7 +238,7 @@ function updateClipDuration(): void { durationDisplay.textContent = formatSecondsToTime(duration); durationDisplay.style.color = '#00c853'; } else { - durationDisplay.textContent = 'Ungultig!'; + durationDisplay.textContent = UI_TEXT.clips.invalidDuration; durationDisplay.style.color = '#ff4444'; } @@ -259,8 +256,8 @@ function updateFilenameExamples(): void { const startSec = parseTimeToSeconds(byId('clipStartTime').value); const timeStr = formatSecondsToTimeDashed(startSec); - byId('formatSimple').textContent = `${dateStr}_${partNum}.mp4 (Standard)`; - byId('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 (mit Zeitstempel)`; + byId('formatSimple').textContent = `${dateStr}_${partNum}.mp4 ${UI_TEXT.clips.formatSimple}`; + byId('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 ${UI_TEXT.clips.formatTimestamp}`; } async function confirmClipDialog(): Promise { @@ -275,12 +272,12 @@ async function confirmClipDialog(): Promise { const filenameFormat = query('input[name="filenameFormat"]:checked').value as 'simple' | 'timestamp'; if (endSec <= startSec) { - alert('Endzeit muss grosser als Startzeit sein!'); + alert(UI_TEXT.clips.endBeforeStart); return; } if (startSec < 0 || endSec > clipTotalSeconds) { - alert('Zeit ausserhalb des VOD-Bereichs!'); + alert(UI_TEXT.clips.outOfRange); return; } @@ -310,28 +307,28 @@ async function downloadClip(): Promise { const btn = byId('btnClip'); if (!url) { - status.textContent = 'Bitte URL eingeben'; + status.textContent = UI_TEXT.clips.enterUrl; status.className = 'clip-status error'; return; } btn.disabled = true; - btn.textContent = 'Lade...'; - status.textContent = 'Download lauft...'; + btn.textContent = UI_TEXT.clips.loadingButton; + status.textContent = UI_TEXT.clips.loadingStatus; status.className = 'clip-status loading'; const result = await window.api.downloadClip(url); btn.disabled = false; - btn.textContent = 'Clip herunterladen'; + btn.textContent = UI_TEXT.clips.downloadButton; if (result.success) { - status.textContent = 'Download erfolgreich!'; + status.textContent = UI_TEXT.clips.success; status.className = 'clip-status success'; return; } - status.textContent = 'Fehler: ' + (result.error || 'Unbekannter Fehler'); + status.textContent = UI_TEXT.clips.errorPrefix + (result.error || UI_TEXT.clips.unknownError); status.className = 'clip-status error'; } @@ -346,7 +343,7 @@ async function selectCutterVideo(): Promise { const info = await window.api.getVideoInfo(filePath); if (!info) { - alert('Konnte Video-Informationen nicht lesen. FFprobe installiert?'); + alert(UI_TEXT.cutter.videoInfoFailed); return; } @@ -436,7 +433,7 @@ async function updatePreview(time: number): Promise { } const preview = byId('cutterPreview'); - preview.innerHTML = '

Lade Vorschau...

'; + preview.innerHTML = `

${UI_TEXT.cutter.previewLoading}

`; const frame = await window.api.extractFrame(cutterFile, time); if (frame) { @@ -444,7 +441,7 @@ async function updatePreview(time: number): Promise { return; } - preview.innerHTML = '

Vorschau nicht verfugbar

'; + preview.innerHTML = `

${UI_TEXT.cutter.previewUnavailable}

`; } async function startCutting(): Promise { @@ -454,22 +451,22 @@ async function startCutting(): Promise { isCutting = true; byId('btnCut').disabled = true; - byId('btnCut').textContent = 'Schneidet...'; + byId('btnCut').textContent = UI_TEXT.cutter.cutting; byId('cutProgress').classList.add('show'); const result = await window.api.cutVideo(cutterFile, cutterStartTime, cutterEndTime); isCutting = false; byId('btnCut').disabled = false; - byId('btnCut').textContent = 'Schneiden'; + byId('btnCut').textContent = UI_TEXT.cutter.cut; byId('cutProgress').classList.remove('show'); if (result.success) { - alert('Video erfolgreich geschnitten!\n\n' + result.outputFile); + alert(`${UI_TEXT.cutter.cutSuccess}\n\n${result.outputFile}`); return; } - alert('Fehler beim Schneiden des Videos.'); + alert(UI_TEXT.cutter.cutFailed); } async function addMergeFiles(): Promise { @@ -490,7 +487,7 @@ function renderMergeFiles(): void { list.innerHTML = `
-

Keine Videos ausgewahlt

+

${UI_TEXT.merge.empty}

`; return; @@ -541,24 +538,24 @@ async function startMerging(): Promise { isMerging = true; byId('btnMerge').disabled = true; - byId('btnMerge').textContent = 'Zusammenfugen...'; + byId('btnMerge').textContent = UI_TEXT.merge.merging; byId('mergeProgress').classList.add('show'); const result = await window.api.mergeVideos(mergeFiles, outputFile); isMerging = false; byId('btnMerge').disabled = false; - byId('btnMerge').textContent = 'Zusammenfugen'; + byId('btnMerge').textContent = UI_TEXT.merge.merge; byId('mergeProgress').classList.remove('show'); if (result.success) { - alert('Videos erfolgreich zusammengefugt!\n\n' + result.outputFile); + alert(`${UI_TEXT.merge.success}\n\n${result.outputFile}`); mergeFiles = []; renderMergeFiles(); return; } - alert('Fehler beim Zusammenfugen der Videos.'); + alert(UI_TEXT.merge.failed); } void init();