Twitch-VOD-Manager/src/renderer-texts.ts
xRangerDE 5f514b1700 i18n: localize 3 empty-state strings (VOD grid + Merge tab)
Three empty-state texts hardcoded German in the HTML and never wired
through the locale system:

- VOD grid empty state: "Keine VODs" + "Wahle einen Streamer aus
  der Liste oder fuge einen neuen hinzu." Shown when no streamer is
  selected. English users were reading German strings here despite
  the rest of the app rendering in English.

- Merge tab empty state: "Keine Videos ausgewahlt." Shown in the
  Videos zusammenfugen tab before any files are added.

Existing locale tables already had `vods.noneTitle` /
`vods.noneText` / `merge.empty` in both EN and DE — they just
weren't being applied. Added IDs to the three elements
(vodGridEmptyTitle / vodGridEmptyText / mergeEmptyText) and
wired three setText calls in renderer-texts.applyText.

Zero new locale keys; pure plumbing fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:20:29 +02:00

326 lines
18 KiB
TypeScript

type LanguageCode = 'de' | 'en';
const UI_TEXTS = {
de: UI_TEXT_DE,
en: UI_TEXT_EN
} as const;
let currentLanguage: LanguageCode = 'en';
let UI_TEXT: (typeof UI_TEXTS)[LanguageCode] = UI_TEXTS[currentLanguage];
function getIntlLocale(): string {
return currentLanguage === 'en' ? 'en-US' : 'de-DE';
}
function formatUiDate(input: string | Date): string {
const date = input instanceof Date ? input : new Date(input);
return date.toLocaleDateString(getIntlLocale());
}
function formatUiNumber(value: number): string {
return value.toLocaleString(getIntlLocale());
}
function setText(id: string, value: string): void {
const node = document.getElementById(id);
if (node) node.textContent = value;
}
function setAriaLabelAll(selector: string, value: string): void {
document.querySelectorAll(selector).forEach((el) => {
el.setAttribute('aria-label', value);
});
}
function setPlaceholder(id: string, value: string): void {
const node = document.getElementById(id) as HTMLInputElement | null;
if (node) node.placeholder = value;
}
function setTitle(id: string, value: string): void {
const node = document.getElementById(id);
if (node) node.setAttribute('title', 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('navStatsText', UI_TEXT.static.navStats);
setText('navArchiveText', UI_TEXT.static.navArchive);
setText('archiveTitle', UI_TEXT.static.archiveTitle);
setText('archiveIntro', UI_TEXT.static.archiveIntro);
setText('btnArchiveSearch', UI_TEXT.static.archiveSearchBtn);
const archiveQueryInput = document.getElementById('archiveSearchQuery') as HTMLInputElement | null;
if (archiveQueryInput) archiveQueryInput.placeholder = UI_TEXT.static.archiveSearchPlaceholder;
const archiveTypeSelect = document.getElementById('archiveSearchType') as HTMLSelectElement | null;
if (archiveTypeSelect) {
const opts = archiveTypeSelect.options;
if (opts[0]) opts[0].text = UI_TEXT.static.archiveAllTypes;
if (opts[1]) opts[1].text = UI_TEXT.static.archiveTypeLive;
if (opts[2]) opts[2].text = UI_TEXT.static.archiveTypeVod;
}
const archiveSortSelect = document.getElementById('archiveSearchSort') as HTMLSelectElement | null;
if (archiveSortSelect) {
const opts = archiveSortSelect.options;
if (opts[0]) opts[0].text = UI_TEXT.static.archiveSortDateDesc;
if (opts[1]) opts[1].text = UI_TEXT.static.archiveSortDateAsc;
if (opts[2]) opts[2].text = UI_TEXT.static.archiveSortSizeDesc;
if (opts[3]) opts[3].text = UI_TEXT.static.archiveSortSizeAsc;
if (opts[4]) opts[4].text = UI_TEXT.static.archiveSortNameAsc;
}
setText('navSettingsText', UI_TEXT.static.navSettings);
setText('statsTitle', UI_TEXT.static.statsTitle);
setText('statsSummaryTitle', UI_TEXT.static.statsSummaryTitle);
setText('statsTopStreamersTitle', UI_TEXT.static.statsTopStreamersTitle);
setText('statsActivityTitle', UI_TEXT.static.statsActivityTitle);
setText('statsSizeBucketsTitle', UI_TEXT.static.statsSizeBucketsTitle);
setText('btnStatsRefresh', UI_TEXT.static.statsRefresh);
setText('queueTitleText', UI_TEXT.static.queueTitle);
setText('healthBadge', UI_TEXT.static.healthUnknown);
setText('btnRetryFailed', UI_TEXT.static.retryFailed);
setTitle('btnRetryFailed', UI_TEXT.static.retryFailedHint);
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('clipTemplateHelp', UI_TEXT.clips.templateHelp);
setPlaceholder('clipFilenameTemplate', UI_TEXT.clips.templatePlaceholder);
setText('clipDialogStartLabel', UI_TEXT.clips.dialogStart);
setText('clipDialogStartTimeLabel', UI_TEXT.clips.dialogStartTime);
setText('clipDialogEndLabel', UI_TEXT.clips.dialogEnd);
setText('clipDialogEndTimeLabel', UI_TEXT.clips.dialogEndTime);
setText('clipDialogDurationLabel', UI_TEXT.clips.dialogDuration);
setText('clipDialogPartLabel', UI_TEXT.clips.dialogPartLabel);
setText('clipDialogPartHint', UI_TEXT.clips.dialogPartHint);
setText('clipDialogFormatLabel', UI_TEXT.clips.dialogFormatLabel);
setText('clipDialogConfirmBtn', UI_TEXT.clips.dialogConfirm);
setText('cutterSelectTitle', UI_TEXT.static.cutterSelectTitle);
setText('cutterBrowseBtn', UI_TEXT.static.cutterBrowse);
setText('cutterInfoDurationLabel', UI_TEXT.cutter.infoDuration);
setText('cutterInfoResolutionLabel', UI_TEXT.cutter.infoResolution);
setText('cutterInfoFpsLabel', UI_TEXT.cutter.infoFps);
setText('cutterInfoSelectionLabel', UI_TEXT.cutter.infoSelection);
setText('cutterStartLabel', UI_TEXT.cutter.startLabel);
setText('cutterEndLabel', UI_TEXT.cutter.endLabel);
setText('btnCut', UI_TEXT.cutter.cut);
setText('mergeTitle', UI_TEXT.static.mergeTitle);
setText('mergeDesc', UI_TEXT.static.mergeDesc);
setText('mergeAddBtn', UI_TEXT.static.mergeAdd);
setText('btnMerge', UI_TEXT.merge.merge);
setText('designTitle', UI_TEXT.static.designTitle);
setText('themeLabel', UI_TEXT.static.themeLabel);
setText('themeLightOption', UI_TEXT.static.themeLight);
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('apiHelpIntro', UI_TEXT.static.apiHelpIntro);
setText('apiHelpLink', UI_TEXT.static.apiHelpLinkText);
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('parallelDownloadsLabel', UI_TEXT.static.parallelDownloadsLabel);
setText('parallelDownloads1', UI_TEXT.static.parallelDownloads1);
setText('parallelDownloads2', UI_TEXT.static.parallelDownloads2);
setText('performanceModeLabel', UI_TEXT.static.performanceModeLabel);
setText('performanceModeStability', UI_TEXT.static.performanceModeStability);
setText('performanceModeBalanced', UI_TEXT.static.performanceModeBalanced);
setText('performanceModeSpeed', UI_TEXT.static.performanceModeSpeed);
setText('smartSchedulerLabel', UI_TEXT.static.smartSchedulerLabel);
setTitle('smartSchedulerLabel', UI_TEXT.static.smartSchedulerHint);
setTitle('smartSchedulerToggle', UI_TEXT.static.smartSchedulerHint);
setText('duplicatePreventionLabel', UI_TEXT.static.duplicatePreventionLabel);
setText('persistQueueLabel', UI_TEXT.static.persistQueueLabel);
setText('autoResumeQueueLabel', UI_TEXT.static.autoResumeQueueLabel);
setTitle('autoResumeQueueLabel', UI_TEXT.static.autoResumeQueueHint);
setTitle('autoResumeQueueToggle', UI_TEXT.static.autoResumeQueueHint);
setText('notifyEachCompletionLabel', UI_TEXT.static.notifyEachCompletionLabel);
setTitle('notifyEachCompletionLabel', UI_TEXT.static.notifyEachCompletionHint);
setTitle('notifyEachCompletionToggle', UI_TEXT.static.notifyEachCompletionHint);
setText('streamlinkDisableAdsLabel', UI_TEXT.static.streamlinkDisableAdsLabel);
setTitle('streamlinkDisableAdsLabel', UI_TEXT.static.streamlinkDisableAdsHint);
setTitle('streamlinkDisableAdsToggle', UI_TEXT.static.streamlinkDisableAdsHint);
setText('downloadChatReplayLabel', UI_TEXT.static.downloadChatReplayLabel);
setTitle('downloadChatReplayLabel', UI_TEXT.static.downloadChatReplayHint);
setTitle('downloadChatReplayToggle', UI_TEXT.static.downloadChatReplayHint);
setText('captureLiveChatLabel', UI_TEXT.static.captureLiveChatLabel);
setTitle('captureLiveChatLabel', UI_TEXT.static.captureLiveChatHint);
setTitle('captureLiveChatToggle', UI_TEXT.static.captureLiveChatHint);
setText('logStreamEventsLabel', UI_TEXT.static.logStreamEventsLabel);
setTitle('logStreamEventsLabel', UI_TEXT.static.logStreamEventsHint);
setTitle('logStreamEventsToggle', UI_TEXT.static.logStreamEventsHint);
setText('streamlinkQualityLabel', UI_TEXT.static.streamlinkQualityLabel);
setTitle('streamlinkQualityLabel', UI_TEXT.static.streamlinkQualityHint);
setTitle('streamlinkQuality', UI_TEXT.static.streamlinkQualityHint);
setText('streamlinkQualityBest', UI_TEXT.static.streamlinkQualityBest);
setText('streamlinkQualitySource', UI_TEXT.static.streamlinkQualitySource);
setText('streamlinkQualityAudio', UI_TEXT.static.streamlinkQualityAudio);
setText('streamerSectionTitleText', UI_TEXT.static.streamerSectionTitle);
setPlaceholder('streamerListFilter', UI_TEXT.static.streamerListFilterPlaceholder);
setTitle('btnStreamerBulkRemove', UI_TEXT.static.streamerBulkRemoveTitle);
setText('metadataCacheMinutesLabel', UI_TEXT.static.metadataCacheMinutesLabel);
setText('filenameTemplatesTitle', UI_TEXT.static.filenameTemplatesTitle);
setText('vodTemplateLabel', UI_TEXT.static.vodTemplateLabel);
setText('partsTemplateLabel', UI_TEXT.static.partsTemplateLabel);
setText('defaultClipTemplateLabel', UI_TEXT.static.defaultClipTemplateLabel);
setText('filenameTemplateHint', UI_TEXT.static.filenameTemplateHint);
setText('filenameTemplateLint', UI_TEXT.static.templateLintOk);
setText('settingsTemplateGuideBtn', UI_TEXT.static.templateGuideButton);
setText('clipTemplateGuideBtn', UI_TEXT.static.templateGuideButton);
setText('clipTemplateLint', UI_TEXT.static.templateLintOk);
setText('templateGuideTitle', UI_TEXT.static.templateGuideTitle);
setText('templateGuideIntro', UI_TEXT.static.templateGuideIntro);
setText('templateGuideTemplateLabel', UI_TEXT.static.templateGuideTemplateLabel);
setText('templateGuideOutputLabel', UI_TEXT.static.templateGuideOutputLabel);
setText('templateGuideVarsTitle', UI_TEXT.static.templateGuideVarsTitle);
setText('templateGuideVarCol', UI_TEXT.static.templateGuideVarCol);
setText('templateGuideDescCol', UI_TEXT.static.templateGuideDescCol);
setText('templateGuideExampleCol', UI_TEXT.static.templateGuideExampleCol);
setText('templateGuideUseVod', UI_TEXT.static.templateGuideUseVod);
setText('templateGuideUseParts', UI_TEXT.static.templateGuideUseParts);
setText('templateGuideUseClip', UI_TEXT.static.templateGuideUseClip);
setText('templateGuideCloseBtn', UI_TEXT.static.templateGuideClose);
setPlaceholder('templateGuideInput', UI_TEXT.static.vodTemplatePlaceholder);
setPlaceholder('vodFilenameTemplate', UI_TEXT.static.vodTemplatePlaceholder);
setPlaceholder('partsFilenameTemplate', UI_TEXT.static.partsTemplatePlaceholder);
setPlaceholder('defaultClipFilenameTemplate', UI_TEXT.static.defaultClipTemplatePlaceholder);
setText('updateTitle', UI_TEXT.static.updateTitle);
setText('checkUpdateBtn', UI_TEXT.static.checkUpdates);
setText('preflightTitle', UI_TEXT.static.preflightTitle);
setText('btnPreflightRun', UI_TEXT.static.preflightRun);
setText('btnPreflightFix', UI_TEXT.static.preflightFix);
setText('preflightResult', UI_TEXT.static.preflightEmpty);
setText('debugLogTitle', UI_TEXT.static.debugLogTitle);
setText('btnRefreshLog', UI_TEXT.static.refreshLog);
setText('btnOpenDebugLogFile', UI_TEXT.static.openDebugLogFile);
setText('storageCardTitle', UI_TEXT.static.storageCardTitle);
setText('storageCardIntro', UI_TEXT.static.storageCardIntro);
setText('btnRefreshStorage', UI_TEXT.static.storageRefresh);
setText('cleanupTitle', UI_TEXT.static.cleanupTitle);
setText('cleanupIntro', UI_TEXT.static.cleanupIntro);
setText('autoCleanupEnabledLabel', UI_TEXT.static.cleanupEnabledLabel);
setText('autoCleanupDaysLabel', UI_TEXT.static.cleanupDaysLabel);
setText('autoCleanupTargetLabel', UI_TEXT.static.cleanupTargetLabel);
setText('autoCleanupTargetLive', UI_TEXT.static.cleanupTargetLive);
setText('autoCleanupTargetAll', UI_TEXT.static.cleanupTargetAll);
setText('autoCleanupActionLabel', UI_TEXT.static.cleanupActionLabel);
setText('autoCleanupActionArchive', UI_TEXT.static.cleanupActionArchive);
setText('autoCleanupActionDelete', UI_TEXT.static.cleanupActionDelete);
setText('btnCleanupDryRun', UI_TEXT.static.cleanupDryRun);
setText('btnCleanupRunNow', UI_TEXT.static.cleanupRunNow);
setText('discordCardTitle', UI_TEXT.static.discordCardTitle);
setText('discordCardIntro', UI_TEXT.static.discordCardIntro);
setText('discordWebhookUrlLabel', UI_TEXT.static.discordWebhookUrlLabel);
setText('discordNotifyLiveStartLabel', UI_TEXT.static.discordNotifyLiveStartLabel);
setText('discordNotifyLiveEndLabel', UI_TEXT.static.discordNotifyLiveEndLabel);
setText('discordNotifyVodCompleteLabel', UI_TEXT.static.discordNotifyVodCompleteLabel);
setText('autoResumeLiveRecordingLabel', UI_TEXT.static.autoResumeLiveRecordingLabel);
setText('autoMergeResumedPartsLabel', UI_TEXT.static.autoMergeResumedPartsLabel);
setText('deletePartsAfterMergeLabel', UI_TEXT.static.deletePartsAfterMergeLabel);
setText('discordNotifyVodAutoQueuedLabel', UI_TEXT.static.discordNotifyVodAutoQueuedLabel);
setText('autoVodCardTitle', UI_TEXT.static.autoVodCardTitle);
setText('autoVodCardIntro', UI_TEXT.static.autoVodCardIntro);
setText('autoVodPollMinutesLabel', UI_TEXT.static.autoVodPollMinutesLabel);
setText('autoVodMaxAgeHoursLabel', UI_TEXT.static.autoVodMaxAgeHoursLabel);
setText('btnAutoVodScanNow', UI_TEXT.static.autoVodScanNow);
setText('btnAutoRecordScanNow', UI_TEXT.static.autoRecordScanNow);
// Empty-state copy for the VODs grid (when no streamer is selected
// yet) and the Merge file list (no files added yet). Both were
// hardcoded German in the HTML — English users saw German strings.
setText('vodGridEmptyTitle', UI_TEXT.vods.noneTitle);
setText('vodGridEmptyText', UI_TEXT.vods.noneText);
setText('mergeEmptyText', UI_TEXT.merge.empty);
// Localize the modal close-button aria-label. The buttons share a
// .modal-close-localizable class so one call updates all five.
setAriaLabelAll('.modal-close-localizable', UI_TEXT.streamers.modalCloseAria);
setText('backupCardTitle', UI_TEXT.static.backupCardTitle);
setText('backupCardIntro', UI_TEXT.static.backupCardIntro);
setText('btnExportConfig', UI_TEXT.static.exportConfig);
setText('btnImportConfig', UI_TEXT.static.importConfig);
setText('btnResetDownloadedIds', UI_TEXT.static.resetDownloadedIds);
setText('vodHideDownloadedText', UI_TEXT.vods.hideDownloaded);
setTitle('vodHideDownloadedLabel', UI_TEXT.vods.hideDownloadedTitle);
setText('autoRefreshText', UI_TEXT.static.autoRefresh);
setText('runtimeMetricsTitle', UI_TEXT.static.runtimeMetricsTitle);
setText('btnRefreshMetrics', UI_TEXT.static.runtimeMetricsRefresh);
setText('btnExportMetrics', UI_TEXT.static.runtimeMetricsExport);
setText('runtimeMetricsAutoRefreshText', UI_TEXT.static.runtimeMetricsAutoRefresh);
setText('runtimeMetricsOutput', UI_TEXT.static.runtimeMetricsLoading);
setText('updateText', UI_TEXT.updates.bannerDefault);
setText('updateButton', UI_TEXT.updates.downloadNow);
setText('updateModalEyebrow', UI_TEXT.static.updateTitle);
setText('updateModalTitle', UI_TEXT.updates.modalAvailableTitle);
setText('updateModalDismissBtn', UI_TEXT.updates.modalDismiss);
setText('updateModalConfirmBtn', UI_TEXT.updates.modalDownloadConfirm);
setText('updateModalSkipBtn', UI_TEXT.updates.modalSkipVersion);
setText('updateChangelogLabel', UI_TEXT.updates.changelogLabel);
setText('updateChangelogToggle', UI_TEXT.updates.showChangelog);
setText('updateChangelogEmpty', UI_TEXT.updates.noChangelog);
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
setPlaceholder('vodFilterInput', UI_TEXT.vods.filterPlaceholder);
setTitle('vodFilterClearBtn', UI_TEXT.vods.filterClearTitle);
setText('vodSortLabel', UI_TEXT.vods.sortLabel);
if (typeof refreshVodSortSelectLabels === 'function') {
refreshVodSortSelectLabels();
}
setText('vodBulkAddBtn', UI_TEXT.vods.bulkAddToQueue);
setText('vodBulkMarkBtn', UI_TEXT.vods.bulkMarkDownloaded);
setText('vodBulkUnmarkBtn', UI_TEXT.vods.bulkUnmark);
setText('vodBulkClearBtn', UI_TEXT.vods.bulkClear);
if (typeof updateVodBulkBar === 'function') {
// Repopulate the count text in the new locale
updateVodBulkBar();
}
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);
}
const guideRefresh = (window as unknown as { refreshTemplateGuideTexts?: () => void }).refreshTemplateGuideTexts;
if (typeof guideRefresh === 'function') {
guideRefresh();
}
const updateRefresh = (window as unknown as { refreshUpdateUiTexts?: () => void }).refreshUpdateUiTexts;
if (typeof updateRefresh === 'function') {
updateRefresh();
}
}
function localizeCurrentStatusText(current: string): string {
const map: Record<string, keyof typeof UI_TEXT.status> = {
[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;
}