feat(radio): add tooltips + aria-labels on every player button

Radio audio-player buttons (skip-prev, play/pause FAB, skip-next, mute,
volume slider) had zero matTooltip or aria-label. The UX audit flagged
this: "Skip-prev / Skip-next — what do they skip? In radio, is the
next 'track' the next station? Next in favourites? Random? Without a
tooltip the buttons trade silently." Same critique applied to mute
and play/pause for screen-reader users.

Add hover tooltips and screen-reader labels on all 5 controls. Skip
prev/next say "Previous station" / "Next station" so the behaviour is
explicit (they walk the active filtered list). Play/Pause flips its
label with state. Mute/unmute flips when isMuted() || volume === 0.
Volume slider gets its own label so the slider thumb is announced
correctly.

i18n: 7 new AUDIO_PLAYER.* keys translated across all 17 non-English
locales by per-locale agents; all 18 locales pass coverage and
placeholder integrity checks.

52/52 ui-playback tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 4.7 committed 2026-05-19 09:03:14 +02:00
1 parent 86ec1685a5
commit 55215a5059
19 files changed
+221 -3

No files matched your search

+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "فاتح",
"SYSTEM_THEME": "النظام"
},
"AUDIO_PLAYER": {
"PLAY": "تشغيل",
"PAUSE": "إيقاف مؤقت",
"PREVIOUS_STATION": "المحطة السابقة",
"NEXT_STATION": "المحطة التالية",
"MUTE": "كتم الصوت",
"UNMUTE": "إلغاء كتم الصوت",
"VOLUME": "مستوى الصوت"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "فاتح",
"SYSTEM_THEME": "النظام"
},
"AUDIO_PLAYER": {
"PLAY": "شغل",
"PAUSE": "وقّف",
"PREVIOUS_STATION": "المحطة السابقة",
"NEXT_STATION": "المحطة التالية",
"MUTE": "كتم الصوت",
"UNMUTE": "رجّع الصوت",
"VOLUME": "الصوت"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Светлая",
"SYSTEM_THEME": "Сістэмная"
},
"AUDIO_PLAYER": {
"PLAY": "Прайграць",
"PAUSE": "Паўза",
"PREVIOUS_STATION": "Папярэдняя станцыя",
"NEXT_STATION": "Наступная станцыя",
"MUTE": "Адключыць гук",
"UNMUTE": "Уключыць гук",
"VOLUME": "Гучнасць"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Hell",
"SYSTEM_THEME": "System"
},
"AUDIO_PLAYER": {
"PLAY": "Wiedergabe",
"PAUSE": "Pause",
"PREVIOUS_STATION": "Vorheriger Sender",
"NEXT_STATION": "Nächster Sender",
"MUTE": "Stummschalten",
"UNMUTE": "Stummschaltung aufheben",
"VOLUME": "Lautstärke"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Ανοιχτό",
"SYSTEM_THEME": "Σύστημα"
},
"AUDIO_PLAYER": {
"PLAY": "Αναπαραγωγή",
"PAUSE": "Παύση",
"PREVIOUS_STATION": "Προηγούμενος σταθμός",
"NEXT_STATION": "Επόμενος σταθμός",
"MUTE": "Σίγαση",
"UNMUTE": "Κατάργηση σίγασης",
"VOLUME": "Ένταση"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Light",
"SYSTEM_THEME": "System"
},
"AUDIO_PLAYER": {
"PLAY": "Play",
"PAUSE": "Pause",
"PREVIOUS_STATION": "Previous station",
"NEXT_STATION": "Next station",
"MUTE": "Mute",
"UNMUTE": "Unmute",
"VOLUME": "Volume"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Claro",
"SYSTEM_THEME": "Sistema"
},
"AUDIO_PLAYER": {
"PLAY": "Reproducir",
"PAUSE": "Pausar",
"PREVIOUS_STATION": "Emisora anterior",
"NEXT_STATION": "Emisora siguiente",
"MUTE": "Silenciar",
"UNMUTE": "Activar sonido",
"VOLUME": "Volumen"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Clair",
"SYSTEM_THEME": "Système"
},
"AUDIO_PLAYER": {
"PLAY": "Lecture",
"PAUSE": "Pause",
"PREVIOUS_STATION": "Station précédente",
"NEXT_STATION": "Station suivante",
"MUTE": "Muet",
"UNMUTE": "Activer le son",
"VOLUME": "Volume"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Chiaro",
"SYSTEM_THEME": "Sistema"
},
"AUDIO_PLAYER": {
"PLAY": "Riproduci",
"PAUSE": "Pausa",
"PREVIOUS_STATION": "Stazione precedente",
"NEXT_STATION": "Stazione successiva",
"MUTE": "Disattiva audio",
"UNMUTE": "Attiva audio",
"VOLUME": "Volume"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "ライト",
"SYSTEM_THEME": "システム"
},
"AUDIO_PLAYER": {
"PLAY": "再生",
"PAUSE": "一時停止",
"PREVIOUS_STATION": "前の局",
"NEXT_STATION": "次の局",
"MUTE": "ミュート",
"UNMUTE": "ミュート解除",
"VOLUME": "音量"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "밝게",
"SYSTEM_THEME": "시스템"
},
"AUDIO_PLAYER": {
"PLAY": "재생",
"PAUSE": "일시정지",
"PREVIOUS_STATION": "이전 방송국",
"NEXT_STATION": "다음 방송국",
"MUTE": "음소거",
"UNMUTE": "음소거 해제",
"VOLUME": "볼륨"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Licht",
"SYSTEM_THEME": "Systeem"
},
"AUDIO_PLAYER": {
"PLAY": "Afspelen",
"PAUSE": "Pauzeren",
"PREVIOUS_STATION": "Vorige zender",
"NEXT_STATION": "Volgende zender",
"MUTE": "Dempen",
"UNMUTE": "Dempen opheffen",
"VOLUME": "Volume"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Jasny",
"SYSTEM_THEME": "Systemowy"
},
"AUDIO_PLAYER": {
"PLAY": "Odtwórz",
"PAUSE": "Wstrzymaj",
"PREVIOUS_STATION": "Poprzednia stacja",
"NEXT_STATION": "Następna stacja",
"MUTE": "Wycisz",
"UNMUTE": "Wyłącz wyciszenie",
"VOLUME": "Głośność"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Claro",
"SYSTEM_THEME": "Sistema"
},
"AUDIO_PLAYER": {
"PLAY": "Reproduzir",
"PAUSE": "Pausar",
"PREVIOUS_STATION": "Estação anterior",
"NEXT_STATION": "Próxima estação",
"MUTE": "Silenciar",
"UNMUTE": "Ativar som",
"VOLUME": "Volume"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Светлая",
"SYSTEM_THEME": "Системная"
},
"AUDIO_PLAYER": {
"PLAY": "Воспроизвести",
"PAUSE": "Пауза",
"PREVIOUS_STATION": "Предыдущая станция",
"NEXT_STATION": "Следующая станция",
"MUTE": "Отключить звук",
"UNMUTE": "Включить звук",
"VOLUME": "Громкость"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "Контейнер потока, вероятно, не поддерживается встроенным браузерным плеером",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "Açık",
"SYSTEM_THEME": "Sistem"
},
"AUDIO_PLAYER": {
"PLAY": "Oynat",
"PAUSE": "Duraklat",
"PREVIOUS_STATION": "Önceki istasyon",
"NEXT_STATION": "Sonraki istasyon",
"MUTE": "Sesi kapat",
"UNMUTE": "Sesi aç",
"VOLUME": "Ses seviyesi"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "浅色",
"SYSTEM_THEME": "跟随系统"
},
"AUDIO_PLAYER": {
"PLAY": "播放",
"PAUSE": "暂停",
"PREVIOUS_STATION": "上一个电台",
"NEXT_STATION": "下一个电台",
"MUTE": "静音",
"UNMUTE": "取消静音",
"VOLUME": "音量"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
+9
View File
@@ -379,6 +379,15 @@
"LIGHT_THEME": "淺色",
"SYSTEM_THEME": "跟隨系統"
},
"AUDIO_PLAYER": {
"PLAY": "播放",
"PAUSE": "暫停",
"PREVIOUS_STATION": "上一個電台",
"NEXT_STATION": "下一個電台",
"MUTE": "靜音",
"UNMUTE": "取消靜音",
"VOLUME": "音量"
},
"PLAYBACK_DIAGNOSTICS": {
"UNSUPPORTED_CONTAINER": {
"TITLE": "This stream container is likely unsupported by the browser player",
@@ -16,7 +16,9 @@ import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSliderModule } from '@angular/material/slider';
import { MatTooltip } from '@angular/material/tooltip';
import { Store } from '@ngrx/store';
import { TranslatePipe } from '@ngx-translate/core';
import { ChannelActions } from '@iptvnator/m3u-state';
@Component({
@@ -67,6 +69,12 @@ import { ChannelActions } from '@iptvnator/m3u-state';
<button
mat-icon-button
class="skip-btn"
[matTooltip]="
'AUDIO_PLAYER.PREVIOUS_STATION' | translate
"
[attr.aria-label]="
'AUDIO_PLAYER.PREVIOUS_STATION' | translate
"
(click)="switchChannel('previous')"
>
<mat-icon>skip_previous</mat-icon>
@@ -75,6 +83,18 @@ import { ChannelActions } from '@iptvnator/m3u-state';
<button
class="play-btn"
mat-fab
[matTooltip]="
(playState() === 'play'
? 'AUDIO_PLAYER.PAUSE'
: 'AUDIO_PLAYER.PLAY'
) | translate
"
[attr.aria-label]="
(playState() === 'play'
? 'AUDIO_PLAYER.PAUSE'
: 'AUDIO_PLAYER.PLAY'
) | translate
"
(click)="playState() === 'play' ? stop() : play()"
>
<mat-icon>{{
@@ -85,6 +105,10 @@ import { ChannelActions } from '@iptvnator/m3u-state';
<button
mat-icon-button
class="skip-btn"
[matTooltip]="'AUDIO_PLAYER.NEXT_STATION' | translate"
[attr.aria-label]="
'AUDIO_PLAYER.NEXT_STATION' | translate
"
(click)="switchChannel('next')"
>
<mat-icon>skip_next</mat-icon>
@@ -92,13 +116,38 @@ import { ChannelActions } from '@iptvnator/m3u-state';
</div>
<div class="volume-row">
<button mat-icon-button class="vol-icon" (click)="mute()">
<button
mat-icon-button
class="vol-icon"
[matTooltip]="
(isMuted() || volume() === 0
? 'AUDIO_PLAYER.UNMUTE'
: 'AUDIO_PLAYER.MUTE'
) | translate
"
[attr.aria-label]="
(isMuted() || volume() === 0
? 'AUDIO_PLAYER.UNMUTE'
: 'AUDIO_PLAYER.MUTE'
) | translate
"
(click)="mute()"
>
<mat-icon>{{ volumeIcon() }}</mat-icon>
</button>
<mat-slider class="vol-slider" min="0" max="1" step="0.05">
<mat-slider
class="vol-slider"
min="0"
max="1"
step="0.05"
[matTooltip]="'AUDIO_PLAYER.VOLUME' | translate"
>
<input
matSliderThumb
[ngModel]="volume()"
[attr.aria-label]="
'AUDIO_PLAYER.VOLUME' | translate
"
(ngModelChange)="setVolume($event)"
/>
</mat-slider>
@@ -109,7 +158,14 @@ import { ChannelActions } from '@iptvnator/m3u-state';
</div>
`,
styleUrls: ['./audio-player.component.scss'],
imports: [MatSliderModule, MatIconModule, MatButtonModule, FormsModule],
imports: [
FormsModule,
MatButtonModule,
MatIconModule,
MatSliderModule,
MatTooltip,
TranslatePipe,
],
})
export class AudioPlayerComponent {
readonly icon = input<string>('');