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 @@
-
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 = '';
+ byId('vodGrid').innerHTML = ``;
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 = '';
+ 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 = '';
+ 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();