a11y+i18n: localize modal close aria-labels + strip dead modal title fallbacks

Two related artifacts from the 4.6.31 a11y pass.

aria-label="Close" was hardcoded English on all five modal-close X
buttons — anyone running the German locale would still hear "Close
button" from their screen reader. Added a shared
.modal-close-localizable class on each X, plus a streamers.modalCloseAria
locale string ("Close dialog" / "Dialog schliessen"), plus a small
setAriaLabelAll helper in renderer-texts that resolves the class via
querySelectorAll and applies the localized label in one shot. Now all
five modals announce in the active language.

While editing the modal headers, also removed the dead "Stream events"
and "Chat replay" English fallback text from eventsViewerTitle and
chatViewerTitle. Both h2s get their textContent overwritten the
instant openEventsViewer / openChatViewer is called (with the
streamers name or a UI_TEXT fallback), so the inline English text was
never user-visible past first-paint and only mattered to a screen
reader if a user managed to focus an unopened modal. Empty <h2/> is
cheaper and removes the i18n drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-05-11 02:00:28 +02:00
parent 4956a68d9b
commit b37244cccf
4 changed files with 21 additions and 9 deletions

View File

@ -20,7 +20,7 @@
<div class="modal-overlay" id="updateModal" role="dialog" aria-modal="true" aria-labelledby="updateModalTitle" onclick="handleUpdateModalOverlayClick(event)">
<div class="modal update-modal">
<button class="modal-close" aria-label="Close" onclick="dismissUpdateModal()">x</button>
<button class="modal-close modal-close-localizable" aria-label="Close" onclick="dismissUpdateModal()">x</button>
<div class="update-modal-eyebrow" id="updateModalEyebrow">Updates</div>
<h2 id="updateModalTitle">Update verfugbar</h2>
<p class="update-modal-message" id="updateModalMessage">Version 0.0.0 ist verfugbar. Jetzt herunterladen?</p>
@ -48,7 +48,7 @@
<!-- Clip Dialog Modal -->
<div class="modal-overlay" id="clipModal" role="dialog" aria-modal="true" aria-labelledby="clipDialogTitle">
<div class="modal clip-modal">
<button class="modal-close" aria-label="Close" onclick="closeClipDialog()">x</button>
<button class="modal-close modal-close-localizable" aria-label="Close" onclick="closeClipDialog()">x</button>
<h2 class="clip-modal-title" id="clipDialogTitle">VOD zuschneiden</h2>
<div class="clip-modal-field">
@ -116,8 +116,8 @@
<!-- Events Viewer Modal -->
<div class="modal-overlay" id="eventsViewerModal" role="dialog" aria-modal="true" aria-labelledby="eventsViewerTitle">
<div class="modal" style="max-width: 700px; max-height: 80vh; display:flex; flex-direction:column;">
<button class="modal-close" aria-label="Close" onclick="closeEventsViewer()">x</button>
<h2 id="eventsViewerTitle" style="margin-top:0;">Stream events</h2>
<button class="modal-close modal-close-localizable" aria-label="Close" onclick="closeEventsViewer()">x</button>
<h2 id="eventsViewerTitle" style="margin-top:0;"></h2>
<div id="eventsViewerStatus" style="color:var(--text-secondary); font-size:12px; margin-bottom:8px;"></div>
<div id="eventsViewerList" style="flex:1; overflow-y:auto; background: var(--bg-main); border:1px solid var(--border-soft); border-radius:6px; padding:8px;"></div>
</div>
@ -126,8 +126,8 @@
<!-- Chat Replay Viewer Modal -->
<div class="modal-overlay" id="chatViewerModal" role="dialog" aria-modal="true" aria-labelledby="chatViewerTitle">
<div class="modal" style="max-width: 800px; height: 80vh; display:flex; flex-direction:column;">
<button class="modal-close" aria-label="Close" onclick="closeChatViewer()">x</button>
<h2 id="chatViewerTitle" style="margin-top:0;">Chat replay</h2>
<button class="modal-close modal-close-localizable" aria-label="Close" onclick="closeChatViewer()">x</button>
<h2 id="chatViewerTitle" style="margin-top:0;"></h2>
<div class="form-row" style="margin-bottom:8px; gap:8px; flex-wrap:wrap; align-items:center;">
<input type="text" id="chatViewerFilter" placeholder="Filter..." oninput="onChatViewerFilterChange()" style="flex:1; min-width:160px; background: var(--bg-card); border:1px solid var(--border-soft); border-radius:6px; padding:6px 10px; color:var(--text); font-size:13px;">
<span id="chatViewerStatus" style="color:var(--text-secondary); font-size:12px;"></span>
@ -139,7 +139,7 @@
<!-- Template Guide Modal -->
<div class="modal-overlay" id="templateGuideModal" role="dialog" aria-modal="true" aria-labelledby="templateGuideTitle">
<div class="modal template-guide-modal">
<button class="modal-close" aria-label="Close" onclick="closeTemplateGuide()">x</button>
<button class="modal-close modal-close-localizable" aria-label="Close" onclick="closeTemplateGuide()">x</button>
<h2 id="templateGuideTitle">Template Guide</h2>
<p id="templateGuideIntro" class="template-guide-intro">Nutze Variablen fur Dateinamen und prufe das Ergebnis als Live-Vorschau.</p>

View File

@ -351,7 +351,8 @@ const UI_TEXT_DE = {
autoVodScanEmpty: 'Keine neuen VODs gefunden.',
autoRecordScanTriggered: 'Manueller Scan: {count} Live-Aufnahme(n) gestartet.',
autoRecordScanEmpty: 'Manueller Scan: kein Streamer ist gerade live.',
liveNowTooltip: 'Aktuell live auf Twitch'
liveNowTooltip: 'Aktuell live auf Twitch',
modalCloseAria: 'Dialog schliessen'
},
vods: {
noneTitle: 'Keine VODs',

View File

@ -351,7 +351,8 @@ const UI_TEXT_EN = {
autoVodScanEmpty: 'No new VODs found.',
autoRecordScanTriggered: 'Manual scan: {count} live recording(s) started.',
autoRecordScanEmpty: 'Manual scan: no streamers currently live.',
liveNowTooltip: 'Currently live on Twitch'
liveNowTooltip: 'Currently live on Twitch',
modalCloseAria: 'Close dialog'
},
vods: {
noneTitle: 'No VODs',

View File

@ -26,6 +26,12 @@ function setText(id: string, value: string): void {
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;
@ -236,6 +242,10 @@ function applyLanguageToStaticUI(): void {
setText('autoVodMaxAgeHoursLabel', UI_TEXT.static.autoVodMaxAgeHoursLabel);
setText('btnAutoVodScanNow', UI_TEXT.static.autoVodScanNow);
setText('btnAutoRecordScanNow', UI_TEXT.static.autoRecordScanNow);
// 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);