ts-video-player 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/core/i18n.ts CHANGED
@@ -74,6 +74,31 @@ export interface TranslationStrings {
74
74
  close: string
75
75
  menu: string
76
76
  back: string
77
+ // The controls' own names and what a screen reader hears (elements/*)
78
+ seek: string
79
+ playbackRate: string
80
+ airPlayPlay: string
81
+ remotePlayback: string
82
+ videoPlayer: string
83
+ videoRole: string
84
+ playTitle: string
85
+ audio: string
86
+ defaultTrack: string
87
+ playing: string
88
+ paused: string
89
+ muted: string
90
+ volumeLevel: string
91
+ ended: string
92
+ enteredFullscreen: string
93
+ exitedFullscreen: string
94
+ errorMessage: string
95
+ timeOf: string
96
+ hourOne: string
97
+ hourOther: string
98
+ minuteOne: string
99
+ minuteOther: string
100
+ secondOne: string
101
+ secondOther: string
77
102
  }
78
103
 
79
104
  export type TranslationKey = keyof TranslationStrings
@@ -157,6 +182,30 @@ export const englishStrings: TranslationStrings = {
157
182
  close: 'Close',
158
183
  menu: 'Menu',
159
184
  back: 'Back',
185
+ seek: 'Seek',
186
+ playbackRate: 'Playback speed {rate}x',
187
+ airPlayPlay: 'Play on AirPlay',
188
+ remotePlayback: 'Play on another device',
189
+ videoPlayer: 'Video player',
190
+ videoRole: 'video player',
191
+ playTitle: 'Play {title}',
192
+ audio: 'Audio',
193
+ defaultTrack: 'Default',
194
+ playing: 'Playing',
195
+ paused: 'Paused',
196
+ muted: 'Muted',
197
+ volumeLevel: 'Volume {percent}%',
198
+ ended: 'Playback ended',
199
+ enteredFullscreen: 'Entered fullscreen',
200
+ exitedFullscreen: 'Exited fullscreen',
201
+ errorMessage: 'Error: {message}',
202
+ timeOf: '{current} of {duration}',
203
+ hourOne: '{n} hour',
204
+ hourOther: '{n} hours',
205
+ minuteOne: '{n} minute',
206
+ minuteOther: '{n} minutes',
207
+ secondOne: '{n} second',
208
+ secondOther: '{n} seconds',
160
209
  }
161
210
 
162
211
  export const spanishStrings: TranslationStrings = {
@@ -209,6 +258,30 @@ export const spanishStrings: TranslationStrings = {
209
258
  close: 'Cerrar',
210
259
  menu: 'Menú',
211
260
  back: 'Volver',
261
+ seek: 'Buscar',
262
+ playbackRate: 'Velocidad de reproducción {rate}x',
263
+ airPlayPlay: 'Reproducir con AirPlay',
264
+ remotePlayback: 'Reproducir en otro dispositivo',
265
+ videoPlayer: 'Reproductor de vídeo',
266
+ videoRole: 'reproductor de vídeo',
267
+ playTitle: 'Reproducir {title}',
268
+ audio: 'Audio',
269
+ defaultTrack: 'Predeterminado',
270
+ playing: 'Reproduciendo',
271
+ paused: 'En pausa',
272
+ muted: 'Silenciado',
273
+ volumeLevel: 'Volumen {percent}%',
274
+ ended: 'Reproducción finalizada',
275
+ enteredFullscreen: 'Pantalla completa activada',
276
+ exitedFullscreen: 'Pantalla completa desactivada',
277
+ errorMessage: 'Error: {message}',
278
+ timeOf: '{current} de {duration}',
279
+ hourOne: '{n} hora',
280
+ hourOther: '{n} horas',
281
+ minuteOne: '{n} minuto',
282
+ minuteOther: '{n} minutos',
283
+ secondOne: '{n} segundo',
284
+ secondOther: '{n} segundos',
212
285
  }
213
286
 
214
287
  export const frenchStrings: TranslationStrings = {
@@ -261,6 +334,30 @@ export const frenchStrings: TranslationStrings = {
261
334
  close: 'Fermer',
262
335
  menu: 'Menu',
263
336
  back: 'Retour',
337
+ seek: 'Rechercher',
338
+ playbackRate: 'Vitesse de lecture {rate}x',
339
+ airPlayPlay: 'Lire avec AirPlay',
340
+ remotePlayback: 'Lire sur un autre appareil',
341
+ videoPlayer: 'Lecteur vidéo',
342
+ videoRole: 'lecteur vidéo',
343
+ playTitle: 'Lire {title}',
344
+ audio: 'Audio',
345
+ defaultTrack: 'Par défaut',
346
+ playing: 'Lecture',
347
+ paused: 'En pause',
348
+ muted: 'Son coupé',
349
+ volumeLevel: 'Volume {percent} %',
350
+ ended: 'Lecture terminée',
351
+ enteredFullscreen: 'Plein écran activé',
352
+ exitedFullscreen: 'Plein écran désactivé',
353
+ errorMessage: 'Erreur : {message}',
354
+ timeOf: '{current} sur {duration}',
355
+ hourOne: '{n} heure',
356
+ hourOther: '{n} heures',
357
+ minuteOne: '{n} minute',
358
+ minuteOther: '{n} minutes',
359
+ secondOne: '{n} seconde',
360
+ secondOther: '{n} secondes',
264
361
  }
265
362
 
266
363
  export const germanStrings: TranslationStrings = {
@@ -313,6 +410,30 @@ export const germanStrings: TranslationStrings = {
313
410
  close: 'Schließen',
314
411
  menu: 'Menü',
315
412
  back: 'Zurück',
413
+ seek: 'Spulen',
414
+ playbackRate: 'Wiedergabegeschwindigkeit {rate}x',
415
+ airPlayPlay: 'Über AirPlay abspielen',
416
+ remotePlayback: 'Auf einem anderen Gerät abspielen',
417
+ videoPlayer: 'Videoplayer',
418
+ videoRole: 'Videoplayer',
419
+ playTitle: '{title} abspielen',
420
+ audio: 'Audio',
421
+ defaultTrack: 'Standard',
422
+ playing: 'Wird abgespielt',
423
+ paused: 'Pausiert',
424
+ muted: 'Stumm',
425
+ volumeLevel: 'Lautstärke {percent} %',
426
+ ended: 'Wiedergabe beendet',
427
+ enteredFullscreen: 'Vollbild an',
428
+ exitedFullscreen: 'Vollbild aus',
429
+ errorMessage: 'Fehler: {message}',
430
+ timeOf: '{current} von {duration}',
431
+ hourOne: '{n} Stunde',
432
+ hourOther: '{n} Stunden',
433
+ minuteOne: '{n} Minute',
434
+ minuteOther: '{n} Minuten',
435
+ secondOne: '{n} Sekunde',
436
+ secondOther: '{n} Sekunden',
316
437
  }
317
438
 
318
439
  export const japaneseStrings: TranslationStrings = {
@@ -365,6 +486,30 @@ export const japaneseStrings: TranslationStrings = {
365
486
  close: '閉じる',
366
487
  menu: 'メニュー',
367
488
  back: '戻る',
489
+ seek: 'シーク',
490
+ playbackRate: '再生速度 {rate}x',
491
+ airPlayPlay: 'AirPlayで再生',
492
+ remotePlayback: '別のデバイスで再生',
493
+ videoPlayer: '動画プレーヤー',
494
+ videoRole: '動画プレーヤー',
495
+ playTitle: '{title}を再生',
496
+ audio: '音声',
497
+ defaultTrack: 'デフォルト',
498
+ playing: '再生中',
499
+ paused: '一時停止中',
500
+ muted: 'ミュート',
501
+ volumeLevel: '音量 {percent}%',
502
+ ended: '再生終了',
503
+ enteredFullscreen: '全画面表示',
504
+ exitedFullscreen: '全画面表示を終了',
505
+ errorMessage: 'エラー: {message}',
506
+ timeOf: '{current} / {duration}',
507
+ hourOne: '{n}時間',
508
+ hourOther: '{n}時間',
509
+ minuteOne: '{n}分',
510
+ minuteOther: '{n}分',
511
+ secondOne: '{n}秒',
512
+ secondOther: '{n}秒',
368
513
  }
369
514
 
370
515
  export const chineseStrings: TranslationStrings = {
@@ -417,6 +562,30 @@ export const chineseStrings: TranslationStrings = {
417
562
  close: '关闭',
418
563
  menu: '菜单',
419
564
  back: '返回',
565
+ seek: '跳转',
566
+ playbackRate: '播放速度 {rate}x',
567
+ airPlayPlay: '通过 AirPlay 播放',
568
+ remotePlayback: '在其他设备上播放',
569
+ videoPlayer: '视频播放器',
570
+ videoRole: '视频播放器',
571
+ playTitle: '播放{title}',
572
+ audio: '音频',
573
+ defaultTrack: '默认',
574
+ playing: '正在播放',
575
+ paused: '已暂停',
576
+ muted: '已静音',
577
+ volumeLevel: '音量 {percent}%',
578
+ ended: '播放结束',
579
+ enteredFullscreen: '已进入全屏',
580
+ exitedFullscreen: '已退出全屏',
581
+ errorMessage: '错误:{message}',
582
+ timeOf: '{current} / {duration}',
583
+ hourOne: '{n}小时',
584
+ hourOther: '{n}小时',
585
+ minuteOne: '{n}分钟',
586
+ minuteOther: '{n}分钟',
587
+ secondOne: '{n}秒',
588
+ secondOther: '{n}秒',
420
589
  }
421
590
 
422
591
  export const defaultTranslations: Record<string, TranslationStrings> = {
@@ -439,9 +608,12 @@ export class I18n {
439
608
  private interpolationPattern: RegExp
440
609
 
441
610
  constructor(config: I18nConfig = {}) {
611
+ // The translations first: detectLanguage() looks the browser's language
612
+ // up in them, and before they were set it threw for every instance made
613
+ // without a language.
614
+ this.translations = { ...defaultTranslations }
442
615
  this.language = config.language || this.detectLanguage() || 'en'
443
616
  this.fallbackLanguage = config.fallbackLanguage || 'en'
444
- this.translations = { ...defaultTranslations }
445
617
 
446
618
  // Merge custom translations
447
619
  if (config.translations) {
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { HTMLElementBase } from './base'
8
- import { resolvePlayer } from './utils'
8
+ import { label, resolvePlayer } from './utils'
9
9
 
10
10
  const AIRPLAY_ICON = 'M6.2 19h11.6L12 13.2 6.2 19zM19 3H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h2v-2H5V5h14v10h-2v2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'
11
11
 
@@ -28,7 +28,7 @@ export class MediaAirplayButton extends HTMLElementBase {
28
28
  button:hover { opacity: 0.8; }
29
29
  :host([connected]) button { color: var(--media-airplay-active-color, #38bdf8); }
30
30
  </style>
31
- <button part="button" type="button" aria-label="Play on AirPlay">
31
+ <button part="button" type="button" aria-label="${label(this, 'airPlayPlay')}">
32
32
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${AIRPLAY_ICON}"/></svg>
33
33
  </button>
34
34
  `
@@ -4,7 +4,7 @@
4
4
  * @module elements/media-fullscreen-button
5
5
  */
6
6
 
7
- import { resolvePlayer } from './utils'
7
+ import { label, resolvePlayer } from './utils'
8
8
 
9
9
  const ENTER_FS = 'M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z'
10
10
  const EXIT_FS = 'M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z'
@@ -29,7 +29,7 @@ export class MediaFullscreenButton extends HTMLElementBase {
29
29
  button:hover { opacity: 0.8; }
30
30
  button:disabled { opacity: 0.4; cursor: not-allowed; }
31
31
  </style>
32
- <button part="button" type="button" aria-label="Enter fullscreen">
32
+ <button part="button" type="button" aria-label="${label(this, 'enterFullscreen')}">
33
33
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
34
34
  <path d="${ENTER_FS}"/>
35
35
  </svg>
@@ -62,7 +62,7 @@ export class MediaFullscreenButton extends HTMLElementBase {
62
62
  btn.disabled = avail !== 'available'
63
63
 
64
64
  const fs = state.fullscreen
65
- btn.setAttribute('aria-label', fs ? 'Exit fullscreen' : 'Enter fullscreen')
65
+ btn.setAttribute('aria-label', label(this, fs ? 'exitFullscreen' : 'enterFullscreen'))
66
66
  const path = this.shadowRoot!.querySelector('path')!
67
67
  path.setAttribute('d', fs ? EXIT_FS : ENTER_FS)
68
68
  })
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { HTMLElementBase } from './base'
8
- import { resolvePlayer } from './utils'
8
+ import { label, resolvePlayer } from './utils'
9
9
 
10
10
  export class MediaLiveButton extends HTMLElementBase {
11
11
  private cleanup: (() => void) | null = null
@@ -21,7 +21,7 @@ export class MediaLiveButton extends HTMLElementBase {
21
21
  i { width: 8px; height: 8px; border-radius: 9999px; background: #ef4444; }
22
22
  :host(:not([at-live-edge])) button { opacity: 0.72; }
23
23
  </style>
24
- <button part="button" type="button" aria-label="Go to live edge"><i aria-hidden="true"></i><span>LIVE</span></button>
24
+ <button part="button" type="button" aria-label="${label(this, 'seekToLive')}"><i aria-hidden="true"></i><span>${label(this, 'live')}</span></button>
25
25
  `
26
26
  }
27
27
  queueMicrotask(() => this.attach())
@@ -4,7 +4,7 @@
4
4
  * @module elements/media-mute-button
5
5
  */
6
6
 
7
- import { resolvePlayer } from './utils'
7
+ import { label, resolvePlayer } from './utils'
8
8
 
9
9
  const ICON_MUTED = 'M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.796 8.796 0 0 0 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z'
10
10
  const ICON_LOW = 'M18.5 12A4.5 4.5 0 0 0 16 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z'
@@ -34,7 +34,7 @@ export class MediaMuteButton extends HTMLElementBase {
34
34
  }
35
35
  button:hover { opacity: 0.8; }
36
36
  </style>
37
- <button part="button" type="button" aria-label="Mute">
37
+ <button part="button" type="button" aria-label="${label(this, 'mute')}">
38
38
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
39
39
  <path d="${ICON_HIGH}"/>
40
40
  </svg>
@@ -59,7 +59,7 @@ export class MediaMuteButton extends HTMLElementBase {
59
59
 
60
60
  const unsub = player.subscribe((state: any) => {
61
61
  const icon = getVolumeIcon(state.muted, state.volume)
62
- btn.setAttribute('aria-label', state.muted ? 'Unmute' : 'Mute')
62
+ btn.setAttribute('aria-label', label(this, state.muted ? 'unmute' : 'mute'))
63
63
  const path = this.shadowRoot!.querySelector('path')!
64
64
  path.setAttribute('d', icon)
65
65
  })
@@ -4,7 +4,7 @@
4
4
  * @module elements/media-pip-button
5
5
  */
6
6
 
7
- import { resolvePlayer } from './utils'
7
+ import { label, resolvePlayer } from './utils'
8
8
 
9
9
  const PIP_ICON = 'M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z'
10
10
 
@@ -28,7 +28,7 @@ export class MediaPipButton extends HTMLElementBase {
28
28
  button:hover { opacity: 0.8; }
29
29
  button:disabled { opacity: 0.4; cursor: not-allowed; }
30
30
  </style>
31
- <button part="button" type="button" aria-label="Picture in picture">
31
+ <button part="button" type="button" aria-label="${label(this, 'enterPiP')}">
32
32
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
33
33
  <path d="${PIP_ICON}"/>
34
34
  </svg>
@@ -59,6 +59,7 @@ export class MediaPipButton extends HTMLElementBase {
59
59
  this.removeAttribute('hidden')
60
60
  }
61
61
  btn.disabled = avail !== 'available'
62
+ btn.setAttribute('aria-label', label(this, state.pictureInPicture ? 'exitPiP' : 'enterPiP'))
62
63
  })
63
64
 
64
65
  this._cleanup = unsub
@@ -4,7 +4,7 @@
4
4
  * @module elements/media-play-button
5
5
  */
6
6
 
7
- import { resolvePlayer } from './utils'
7
+ import { label, resolvePlayer } from './utils'
8
8
 
9
9
  const PLAY_ICON = 'M8 5v14l11-7z'
10
10
  const PAUSE_ICON = 'M6 19h4V5H6v14zm8-14v14h4V5h-4z'
@@ -28,7 +28,7 @@ export class MediaPlayButton extends HTMLElementBase {
28
28
  }
29
29
  button:hover { opacity: 0.8; }
30
30
  </style>
31
- <button part="button" type="button" aria-label="Play">
31
+ <button part="button" type="button" aria-label="${label(this, 'play')}">
32
32
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
33
33
  <path d="${PLAY_ICON}"/>
34
34
  </svg>
@@ -58,7 +58,7 @@ export class MediaPlayButton extends HTMLElementBase {
58
58
 
59
59
  const unsub = player.subscribe('paused', (state: any) => {
60
60
  const paused = state.paused
61
- btn.setAttribute('aria-label', paused ? 'Play' : 'Pause')
61
+ btn.setAttribute('aria-label', label(this, paused ? 'play' : 'pause'))
62
62
  btn.setAttribute('aria-pressed', String(!paused))
63
63
  const path = this.shadowRoot!.querySelector('path')!
64
64
  path.setAttribute('d', paused ? PLAY_ICON : PAUSE_ICON)
@@ -1,4 +1,4 @@
1
- import { resolvePlayer } from './utils'
1
+ import { label, resolvePlayer } from './utils'
2
2
  import type { PlayerState } from '../types'
3
3
 
4
4
  const defaultRates = [0.5, 0.75, 1, 1.25, 1.5, 2]
@@ -19,7 +19,7 @@ export class MediaPlaybackRateButton extends HTMLElementBase {
19
19
  button:hover { opacity: .8; }
20
20
  button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
21
21
  </style>
22
- <button part="button" type="button" aria-label="Playback speed"></button>
22
+ <button part="button" type="button" aria-label="${label(this, 'speed')}"></button>
23
23
  `
24
24
  }
25
25
  queueMicrotask(() => this.attach())
@@ -53,7 +53,7 @@ export class MediaPlaybackRateButton extends HTMLElementBase {
53
53
  button.addEventListener('click', this.onClick)
54
54
  this.cleanup = player.subscribe('playbackRate', (state: PlayerState) => {
55
55
  button.textContent = `${state.playbackRate}x`
56
- button.setAttribute('aria-label', `Playback speed ${state.playbackRate}x`)
56
+ button.setAttribute('aria-label', label(this, 'playbackRate', { rate: state.playbackRate }))
57
57
  })
58
58
  }
59
59
  }
@@ -6,7 +6,7 @@
6
6
  * @module elements/media-progress-bar
7
7
  */
8
8
 
9
- import { resolvePlayer, formatTime, formatTimePhrase } from './utils'
9
+ import { label, resolvePlayer, formatTime, formatTimePhrase } from './utils'
10
10
 
11
11
  import { HTMLElementBase } from './base'
12
12
 
@@ -68,7 +68,7 @@ export class MediaProgressBar extends HTMLElementBase {
68
68
  </style>
69
69
  <div class="container" part="container" role="slider"
70
70
  aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
71
- aria-label="Seek" tabindex="0">
71
+ aria-label="${label(this, 'seek')}" tabindex="0">
72
72
  <div class="tooltip" part="tooltip">0:00</div>
73
73
  <div class="track" part="track">
74
74
  <div class="waveform" part="waveform" aria-hidden="true"></div>
@@ -188,7 +188,7 @@ export class MediaProgressBar extends HTMLElementBase {
188
188
  thumb.style.left = `${percent}%`
189
189
 
190
190
  container.setAttribute('aria-valuenow', String(Math.round(percent)))
191
- container.setAttribute('aria-valuetext', `${formatTimePhrase(state.currentTime)} of ${formatTimePhrase(duration)}`)
191
+ container.setAttribute('aria-valuetext', label(this, 'timeOf', { current: formatTimePhrase(state.currentTime, this), duration: formatTimePhrase(duration, this) }))
192
192
 
193
193
  // Buffered
194
194
  const bufferedPct = (state.bufferedAmount || 0) * 100
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { HTMLElementBase } from './base'
11
- import { resolvePlayer } from './utils'
11
+ import { label, resolvePlayer } from './utils'
12
12
 
13
13
  const REMOTE_ICON = 'M1 18v3h3a3 3 0 0 0-3-3zm0-4v2a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7zm0-4v2c5 0 9 4 9 9h2c0-6.1-4.9-11-11-11zm18-7H5a2 2 0 0 0-2 2v3h2V5h14v10h-5v2h5a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'
14
14
 
@@ -33,7 +33,7 @@ export class MediaRemotePlaybackButton extends HTMLElementBase {
33
33
  button:hover { opacity: 0.8; }
34
34
  :host([connected]) button { color: var(--media-remote-active-color, #38bdf8); }
35
35
  </style>
36
- <button part="button" type="button" aria-label="Play on another device">
36
+ <button part="button" type="button" aria-label="${label(this, 'remotePlayback')}">
37
37
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${REMOTE_ICON}"/></svg>
38
38
  </button>
39
39
  `
@@ -1,4 +1,4 @@
1
- import { resolvePlayer } from './utils'
1
+ import { label, resolvePlayer } from './utils'
2
2
  import type { PlayerState } from '../types'
3
3
 
4
4
  import { HTMLElementBase } from './base'
@@ -38,9 +38,8 @@ export class MediaSeekButton extends HTMLElementBase {
38
38
  if (!player || !button) return
39
39
  const seconds = Number.parseFloat(this.getAttribute('seconds') ?? '10')
40
40
  const offset = Number.isFinite(seconds) && seconds !== 0 ? seconds : 10
41
- const label = offset > 0 ? `Forward ${offset} seconds` : `Back ${Math.abs(offset)} seconds`
42
41
  button.textContent = offset > 0 ? `+${offset}` : String(offset)
43
- button.setAttribute('aria-label', label)
42
+ button.setAttribute('aria-label', label(this, offset > 0 ? 'seekForward' : 'seekBackward', { seconds: Math.abs(offset) }))
44
43
  this.onClick = () => player.seekBy(offset)
45
44
  button.addEventListener('click', this.onClick)
46
45
  this.cleanup = player.subscribe((state: PlayerState) => {
@@ -7,7 +7,7 @@
7
7
  * @module elements/media-settings-menu
8
8
  */
9
9
 
10
- import { resolvePlayer } from './utils'
10
+ import { label as say, resolvePlayer } from './utils'
11
11
  import { computePosition } from '../core/popover'
12
12
 
13
13
  const GEAR_ICON = 'M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z'
@@ -56,7 +56,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
56
56
  .back { font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.2); }
57
57
  </style>
58
58
  <button class="trigger" part="trigger" type="button"
59
- aria-label="Settings" aria-haspopup="true" aria-expanded="false">
59
+ aria-label="${say(this, 'settings')}" aria-haspopup="true" aria-expanded="false">
60
60
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
61
61
  <path d="${GEAR_ICON}"/>
62
62
  </svg>
@@ -177,7 +177,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
177
177
  const state = player.state
178
178
 
179
179
  // Speed
180
- this.addItem(menu, `Speed: ${state.playbackRate}x`, () => {
180
+ this.addItem(menu, `${say(this, 'speed')}: ${state.playbackRate}x`, () => {
181
181
  this._currentPanel = 'speed'
182
182
  this.renderPanel()
183
183
  })
@@ -185,8 +185,8 @@ export class MediaSettingsMenu extends HTMLElementBase {
185
185
  // Quality
186
186
  if (state.qualities.length > 0) {
187
187
  const current = state.qualities.find((q: any) => q.selected)
188
- const label = state.autoQuality ? 'Auto' : (current ? `${current.height}p` : 'Auto')
189
- this.addItem(menu, `Quality: ${label}`, () => {
188
+ const label = state.autoQuality ? say(this, 'auto') : (current ? `${current.height}p` : say(this, 'auto'))
189
+ this.addItem(menu, `${say(this, 'quality')}: ${label}`, () => {
190
190
  this._currentPanel = 'quality'
191
191
  this.renderPanel()
192
192
  })
@@ -195,7 +195,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
195
195
  // Captions
196
196
  if (state.textTracks.length > 0) {
197
197
  const showing = state.textTracks.find((t: any) => t.mode === 'showing')
198
- this.addItem(menu, `Captions: ${showing?.label || 'Off'}`, () => {
198
+ this.addItem(menu, `${say(this, 'captions')}: ${showing?.label || say(this, 'captionsOff')}`, () => {
199
199
  this._currentPanel = 'captions'
200
200
  this.renderPanel()
201
201
  })
@@ -203,7 +203,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
203
203
 
204
204
  if (state.audioTracks.length > 1) {
205
205
  const selected = state.audioTracks.find((track: { selected: boolean }) => track.selected)
206
- this.addItem(menu, `Audio: ${selected?.label || 'Default'}`, () => {
206
+ this.addItem(menu, `${say(this, 'audio')}: ${selected?.label || say(this, 'defaultTrack')}`, () => {
207
207
  this._currentPanel = 'audio'
208
208
  this.renderPanel()
209
209
  })
@@ -211,7 +211,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
211
211
  }
212
212
 
213
213
  private renderSpeed(menu: Element, player: any): void {
214
- this.addItem(menu, 'Back', () => {
214
+ this.addItem(menu, say(this, 'back'), () => {
215
215
  this._currentPanel = 'main'
216
216
  this.renderPanel()
217
217
  }, false, true)
@@ -226,13 +226,13 @@ export class MediaSettingsMenu extends HTMLElementBase {
226
226
  }
227
227
 
228
228
  private renderQuality(menu: Element, player: any): void {
229
- this.addItem(menu, 'Back', () => {
229
+ this.addItem(menu, say(this, 'back'), () => {
230
230
  this._currentPanel = 'main'
231
231
  this.renderPanel()
232
232
  }, false, true)
233
233
 
234
234
  // Auto option
235
- this.addItem(menu, 'Auto', () => {
235
+ this.addItem(menu, say(this, 'auto'), () => {
236
236
  player.setQuality('auto')
237
237
  this.close()
238
238
  }, player.state.autoQuality)
@@ -246,14 +246,14 @@ export class MediaSettingsMenu extends HTMLElementBase {
246
246
  }
247
247
 
248
248
  private renderCaptions(menu: Element, player: any): void {
249
- this.addItem(menu, 'Back', () => {
249
+ this.addItem(menu, say(this, 'back'), () => {
250
250
  this._currentPanel = 'main'
251
251
  this.renderPanel()
252
252
  }, false, true)
253
253
 
254
254
  // Off option
255
255
  const anyShowing = player.state.textTracks.some((t: any) => t.mode === 'showing')
256
- this.addItem(menu, 'Off', () => {
256
+ this.addItem(menu, say(this, 'captionsOff'), () => {
257
257
  for (const t of player.state.textTracks) {
258
258
  if (t.mode === 'showing') player.setTextTrack(t.id, 'disabled')
259
259
  }
@@ -271,13 +271,13 @@ export class MediaSettingsMenu extends HTMLElementBase {
271
271
  }
272
272
 
273
273
  private renderAudio(menu: Element, player: any): void {
274
- this.addItem(menu, 'Back', () => {
274
+ this.addItem(menu, say(this, 'back'), () => {
275
275
  this._currentPanel = 'main'
276
276
  this.renderPanel()
277
277
  }, false, true)
278
278
 
279
279
  for (const track of player.state.audioTracks) {
280
- this.addItem(menu, track.label || track.language || 'Audio track', () => {
280
+ this.addItem(menu, track.label || track.language || say(this, 'audioTrack'), () => {
281
281
  player.setAudioTrack(track.id)
282
282
  this.close()
283
283
  }, track.selected)
@@ -39,7 +39,7 @@ export class MediaTimeDisplay extends HTMLElementBase {
39
39
  time { color: inherit; }
40
40
  .separator { margin: 0 2px; }
41
41
  </style>
42
- <time part="time" datetime="PT0S" aria-valuetext="0 seconds">0:00</time>
42
+ <time part="time" datetime="PT0S" aria-valuetext="${formatTimePhrase(0, this)}">0:00</time>
43
43
  `
44
44
  }
45
45
 
@@ -94,6 +94,6 @@ export class MediaTimeDisplay extends HTMLElementBase {
94
94
  }
95
95
 
96
96
  timeEl.setAttribute('datetime', toISODuration(seconds))
97
- timeEl.setAttribute('aria-valuetext', formatTimePhrase(seconds))
97
+ timeEl.setAttribute('aria-valuetext', formatTimePhrase(seconds, this))
98
98
  }
99
99
  }
@@ -6,7 +6,7 @@
6
6
  * @module elements/media-volume-slider
7
7
  */
8
8
 
9
- import { resolvePlayer } from './utils'
9
+ import { label, resolvePlayer } from './utils'
10
10
 
11
11
  import { HTMLElementBase } from './base'
12
12
 
@@ -45,7 +45,7 @@ export class MediaVolumeSlider extends HTMLElementBase {
45
45
  </style>
46
46
  <div class="container" part="container" role="slider"
47
47
  aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"
48
- aria-label="Volume" tabindex="0">
48
+ aria-label="${label(this, 'volume')}" tabindex="0">
49
49
  <div class="track" part="track">
50
50
  <div class="fill" part="fill"></div>
51
51
  <div class="thumb" part="thumb"></div>