Twitch-VOD-Manager/src/renderer-texts.ts
xRangerDE 7b0e511479 a11y: localized aria-label on the 3 filter/search inputs
The three filter / search inputs in the always-visible UI surface (VOD filter, sidebar streamer-list filter, Archive search) had only placeholder text as their accessible-name source. Placeholder text is unreliable as a screen-reader name — some implementations announce it, some skip it, and it disappears as soon as the user types so a re-focus during typing leaves the input unannounced.

Added three locale keys (DE+EN):
- vods.filterAria — "VOD-Titel filtern" / "Filter VOD titles"
- static.archiveSearchAria — "Archiv durchsuchen" / "Search archive"
- static.streamerListFilterAria — "Streamer-Liste filtern" / "Filter streamer list"

Wired through renderer-texts via the existing setAriaLabel helper (added in 4.6.91), so each input now has a proper aria-label that survives the placeholder vanishing and reads cleanly in screen-reader navigation.

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

348 lines
20 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 setAriaLabel(id: string, value: string): void {
const node = document.getElementById(id);
if (node) node.setAttribute('aria-label', 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;
setAriaLabel('archiveSearchQuery', UI_TEXT.static.archiveSearchAria);
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);
const statsIntroEl = document.getElementById('statsIntro');
if (statsIntroEl) applyHtml(statsIntroEl, UI_TEXT.static.statsIntro);
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);
setPlaceholder('clipUrl', UI_TEXT.clips.urlPlaceholder);
setPlaceholder('clipStartPart', UI_TEXT.clips.startPartPlaceholder);
setPlaceholder('cutterFilePath', UI_TEXT.cutter.filePathPlaceholder);
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);
setAriaLabel('streamerListFilter', UI_TEXT.static.streamerListFilterAria);
setTitle('btnStreamerBulkRemove', UI_TEXT.static.streamerBulkRemoveTitle);
setAriaLabel('btnStreamerBulkRemove', UI_TEXT.static.streamerBulkRemoveTitle);
setAriaLabel('btnAddStreamer', UI_TEXT.static.streamerAddAriaLabel);
setTitle('btnAddStreamer', UI_TEXT.static.streamerAddAriaLabel);
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);
document.getElementById('cutProgressGauge')?.setAttribute('aria-label', UI_TEXT.streamers.cutProgressAria);
document.getElementById('mergeProgressGauge')?.setAttribute('aria-label', UI_TEXT.streamers.mergeProgressAria);
document.getElementById('updateProgressGauge')?.setAttribute('aria-label', UI_TEXT.streamers.updateProgressAria);
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);
setAriaLabel('vodFilterInput', UI_TEXT.vods.filterAria);
setTitle('vodFilterClearBtn', UI_TEXT.vods.filterClearTitle);
setAriaLabel('vodFilterClearBtn', UI_TEXT.vods.filterClearTitle);
setPlaceholder('chatViewerFilter', UI_TEXT.queue.chatViewerFilterPlaceholder);
setAriaLabel('chatViewerFilter', UI_TEXT.queue.chatViewerFilterAria);
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;
}