vidply 1.0.20 → 1.0.21

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.
@@ -11,27 +11,27 @@
11
11
  "format": "esm"
12
12
  },
13
13
  "src/i18n/languages/en.js": {
14
- "bytes": 6319,
14
+ "bytes": 6566,
15
15
  "imports": [],
16
16
  "format": "esm"
17
17
  },
18
18
  "src/i18n/languages/de.js": {
19
- "bytes": 7774,
19
+ "bytes": 8063,
20
20
  "imports": [],
21
21
  "format": "esm"
22
22
  },
23
23
  "src/i18n/languages/es.js": {
24
- "bytes": 7346,
24
+ "bytes": 7619,
25
25
  "imports": [],
26
26
  "format": "esm"
27
27
  },
28
28
  "src/i18n/languages/fr.js": {
29
- "bytes": 7560,
29
+ "bytes": 7826,
30
30
  "imports": [],
31
31
  "format": "esm"
32
32
  },
33
33
  "src/i18n/languages/ja.js": {
34
- "bytes": 7972,
34
+ "bytes": 8251,
35
35
  "imports": [],
36
36
  "format": "esm"
37
37
  },
@@ -99,7 +99,7 @@
99
99
  "format": "esm"
100
100
  },
101
101
  "src/controls/ControlBar.js": {
102
- "bytes": 129888,
102
+ "bytes": 129940,
103
103
  "imports": [
104
104
  {
105
105
  "path": "src/utils/DOMUtils.js",
@@ -269,22 +269,22 @@
269
269
  "format": "esm"
270
270
  },
271
271
  "src/renderers/HTML5Renderer.js": {
272
- "bytes": 8660,
272
+ "bytes": 9169,
273
273
  "imports": [],
274
274
  "format": "esm"
275
275
  },
276
276
  "src/renderers/YouTubeRenderer.js": {
277
- "bytes": 7186,
277
+ "bytes": 7465,
278
278
  "imports": [],
279
279
  "format": "esm"
280
280
  },
281
281
  "src/renderers/VimeoRenderer.js": {
282
- "bytes": 6699,
282
+ "bytes": 6978,
283
283
  "imports": [],
284
284
  "format": "esm"
285
285
  },
286
286
  "src/renderers/HLSRenderer.js": {
287
- "bytes": 8757,
287
+ "bytes": 9492,
288
288
  "imports": [
289
289
  {
290
290
  "path": "src/renderers/HTML5Renderer.js",
@@ -295,7 +295,7 @@
295
295
  "format": "esm"
296
296
  },
297
297
  "src/core/Player.js": {
298
- "bytes": 205467,
298
+ "bytes": 208826,
299
299
  "imports": [
300
300
  {
301
301
  "path": "src/utils/EventEmitter.js",
@@ -386,7 +386,7 @@
386
386
  "format": "esm"
387
387
  },
388
388
  "src/features/PlaylistManager.js": {
389
- "bytes": 28703,
389
+ "bytes": 32072,
390
390
  "imports": [
391
391
  {
392
392
  "path": "src/utils/DOMUtils.js",
@@ -397,6 +397,11 @@
397
397
  "path": "src/icons/Icons.js",
398
398
  "kind": "import-statement",
399
399
  "original": "../icons/Icons.js"
400
+ },
401
+ {
402
+ "path": "src/i18n/i18n.js",
403
+ "kind": "import-statement",
404
+ "original": "../i18n/i18n.js"
400
405
  }
401
406
  ],
402
407
  "format": "esm"
@@ -425,7 +430,7 @@
425
430
  "entryPoint": "src/index.js",
426
431
  "inputs": {
427
432
  "src/renderers/HTML5Renderer.js": {
428
- "bytesInOutput": 4675
433
+ "bytesInOutput": 4803
429
434
  },
430
435
  "src/index.js": {
431
436
  "bytesInOutput": 1939
@@ -437,19 +442,19 @@
437
442
  "bytesInOutput": 1581
438
443
  },
439
444
  "src/i18n/languages/en.js": {
440
- "bytesInOutput": 5230
445
+ "bytesInOutput": 5426
441
446
  },
442
447
  "src/i18n/languages/de.js": {
443
- "bytesInOutput": 6298
448
+ "bytesInOutput": 6510
444
449
  },
445
450
  "src/i18n/languages/es.js": {
446
- "bytesInOutput": 6411
451
+ "bytesInOutput": 6637
447
452
  },
448
453
  "src/i18n/languages/fr.js": {
449
- "bytesInOutput": 6680
454
+ "bytesInOutput": 6895
450
455
  },
451
456
  "src/i18n/languages/ja.js": {
452
- "bytesInOutput": 11448
457
+ "bytesInOutput": 11757
453
458
  },
454
459
  "src/i18n/translations.js": {
455
460
  "bytesInOutput": 61
@@ -467,7 +472,7 @@
467
472
  "bytesInOutput": 222
468
473
  },
469
474
  "src/controls/ControlBar.js": {
470
- "bytesInOutput": 54429
475
+ "bytesInOutput": 54457
471
476
  },
472
477
  "src/utils/StorageManager.js": {
473
478
  "bytesInOutput": 1606
@@ -494,22 +499,22 @@
494
499
  "bytesInOutput": 41981
495
500
  },
496
501
  "src/core/Player.js": {
497
- "bytesInOutput": 73997
502
+ "bytesInOutput": 74975
498
503
  },
499
504
  "src/renderers/YouTubeRenderer.js": {
500
- "bytesInOutput": 4140
505
+ "bytesInOutput": 4203
501
506
  },
502
507
  "src/renderers/VimeoRenderer.js": {
503
- "bytesInOutput": 4205
508
+ "bytesInOutput": 4268
504
509
  },
505
510
  "src/renderers/HLSRenderer.js": {
506
- "bytesInOutput": 5389
511
+ "bytesInOutput": 5583
507
512
  },
508
513
  "src/features/PlaylistManager.js": {
509
- "bytesInOutput": 13260
514
+ "bytesInOutput": 14667
510
515
  }
511
516
  },
512
- "bytes": 286341
517
+ "bytes": 290360
513
518
  }
514
519
  }
515
520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidply",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "Universal, accessible video & audio player with ES6 modules",
5
5
  "type": "module",
6
6
  "main": "dist/vidply.js",
@@ -1587,7 +1587,8 @@ export class ControlBar {
1587
1587
  attributes: {
1588
1588
  'type': 'button',
1589
1589
  'aria-label': i18n.t('player.quality'),
1590
- 'aria-expanded': 'false'
1590
+ 'aria-expanded': 'false',
1591
+ 'title': i18n.t('player.quality')
1591
1592
  }
1592
1593
  });
1593
1594
 
@@ -432,6 +432,20 @@ export class Player extends EventEmitter {
432
432
 
433
433
  // Wrap original element
434
434
  this.element.parentNode.insertBefore(this.container, this.element);
435
+
436
+ // Create track artwork element for single audio files (before video wrapper)
437
+ // This shows the poster/artwork above the audio player (similar to playlists)
438
+ if (this.element.tagName === 'AUDIO' && this.options.poster) {
439
+ this.trackArtworkElement = DOMUtils.createElement('div', {
440
+ className: `${this.options.classPrefix}-track-artwork`,
441
+ attributes: {
442
+ 'aria-hidden': 'true'
443
+ }
444
+ });
445
+ this.trackArtworkElement.style.backgroundImage = `url(${this.options.poster})`;
446
+ this.container.appendChild(this.trackArtworkElement);
447
+ }
448
+
435
449
  this.container.appendChild(this.videoWrapper);
436
450
  this.videoWrapper.appendChild(this.element);
437
451
 
@@ -850,6 +864,8 @@ export class Player extends EventEmitter {
850
864
  * @param {string} config.type - Media MIME type
851
865
  * @param {string} [config.poster] - Poster image URL
852
866
  * @param {Array} [config.tracks] - Text tracks (captions, chapters, etc.)
867
+ * @param {string} [config.audioDescriptionSrc] - Audio description video URL
868
+ * @param {string} [config.signLanguageSrc] - Sign language video URL
853
869
  */
854
870
  async load(config) {
855
871
  try {
@@ -860,6 +876,10 @@ export class Player extends EventEmitter {
860
876
  this.pause();
861
877
  }
862
878
 
879
+ // Save scroll position to prevent browser from auto-scrolling when loading new media
880
+ const scrollX = window.scrollX || window.pageXOffset;
881
+ const scrollY = window.scrollY || window.pageYOffset;
882
+
863
883
  // Clear existing text tracks
864
884
  const existingTracks = this.trackElements;
865
885
  existingTracks.forEach(track => track.remove());
@@ -893,6 +913,25 @@ export class Player extends EventEmitter {
893
913
  });
894
914
  this.invalidateTrackCache();
895
915
  }
916
+
917
+ // Remember accessibility feature states before switching tracks
918
+ const wasSignLanguageEnabled = this.state.signLanguageEnabled;
919
+ const wasAudioDescriptionEnabled = this.state.audioDescriptionEnabled;
920
+
921
+ // Update sources from config FIRST (before hiding features)
922
+ this.audioDescriptionSrc = config.audioDescriptionSrc || null;
923
+ this.signLanguageSrc = config.signLanguageSrc || null;
924
+
925
+ // Update original source for toggling
926
+ this.originalSrc = config.src;
927
+
928
+ // Hide accessibility features that were enabled (must happen AFTER updating sources)
929
+ if (wasAudioDescriptionEnabled) {
930
+ this.disableAudioDescription();
931
+ }
932
+ if (wasSignLanguageEnabled) {
933
+ this.disableSignLanguage();
934
+ }
896
935
 
897
936
  // Check if we need to change renderer type
898
937
  const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
@@ -911,6 +950,9 @@ export class Player extends EventEmitter {
911
950
  this.renderer.media = this.element; // Update media reference
912
951
  this.element.load();
913
952
  }
953
+
954
+ // Restore scroll position immediately after loading to prevent auto-scroll
955
+ window.scrollTo(scrollX, scrollY);
914
956
 
915
957
  // Reinitialize caption manager to pick up new tracks
916
958
  if (this.captionManager) {
@@ -920,12 +962,12 @@ export class Player extends EventEmitter {
920
962
 
921
963
  // Reinitialize transcript manager to pick up new tracks
922
964
  if (this.transcriptManager) {
923
- const wasVisible = this.transcriptManager.isVisible;
965
+ const wasTranscriptVisible = this.transcriptManager.isVisible;
924
966
  this.transcriptManager.destroy();
925
967
  this.transcriptManager = new TranscriptManager(this);
926
968
 
927
- // Restore visibility state if transcript was open
928
- if (wasVisible) {
969
+ // Only restore transcript visibility if new track has captions
970
+ if (wasTranscriptVisible && this.controlBar && this.controlBar.hasCaptionTracks()) {
929
971
  this.transcriptManager.showTranscript();
930
972
  }
931
973
  }
@@ -934,6 +976,28 @@ export class Player extends EventEmitter {
934
976
  if (this.controlBar) {
935
977
  this.updateControlBar();
936
978
  }
979
+
980
+ // Restore scroll position after control bar update (may have caused micro-scrolls)
981
+ window.scrollTo(scrollX, scrollY);
982
+
983
+ // Restore accessibility features if they were enabled and available in new track
984
+ if (wasSignLanguageEnabled && this.signLanguageSrc) {
985
+ // Small delay to ensure player and control bar are ready
986
+ setTimeout(() => {
987
+ this.enableSignLanguage();
988
+ // Restore scroll after sign language is shown
989
+ window.scrollTo(scrollX, scrollY);
990
+ }, 150);
991
+ }
992
+
993
+ if (wasAudioDescriptionEnabled && this.audioDescriptionSrc) {
994
+ // Small delay to ensure player is ready
995
+ setTimeout(() => {
996
+ this.enableAudioDescription();
997
+ // Restore scroll after audio description is enabled
998
+ window.scrollTo(scrollX, scrollY);
999
+ }, 150);
1000
+ }
937
1001
 
938
1002
  this.emit('sourcechange', config);
939
1003
  this.log('Media loaded successfully');
@@ -965,6 +1029,7 @@ export class Player extends EventEmitter {
965
1029
  // Reattach events for the new controls
966
1030
  controlBar.attachEvents();
967
1031
  controlBar.setupAutoHide();
1032
+ controlBar.setupOverflowDetection();
968
1033
  }
969
1034
 
970
1035
  shouldChangeRenderer(src) {
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { DOMUtils } from '../utils/DOMUtils.js';
7
7
  import { createIconElement } from '../icons/Icons.js';
8
+ import { i18n } from '../i18n/i18n.js';
8
9
 
9
10
  // Static counter for unique IDs
10
11
  let playlistInstanceCounter = 0;
@@ -35,6 +36,9 @@ export class PlaylistManager {
35
36
  this.navigationFeedback = null; // Live region for keyboard navigation feedback
36
37
  this.isPanelVisible = this.options.showPanel !== false;
37
38
 
39
+ // Track change guard to prevent cascade of next() calls
40
+ this.isChangingTrack = false;
41
+
38
42
  // Bind methods
39
43
  this.handleTrackEnd = this.handleTrackEnd.bind(this);
40
44
  this.handleTrackError = this.handleTrackError.bind(this);
@@ -216,6 +220,9 @@ export class PlaylistManager {
216
220
 
217
221
  const track = this.tracks[index];
218
222
 
223
+ // Set guard flag to prevent cascade of next() calls during track change
224
+ this.isChangingTrack = true;
225
+
219
226
  // Update current index
220
227
  this.currentIndex = index;
221
228
 
@@ -224,7 +231,9 @@ export class PlaylistManager {
224
231
  src: track.src,
225
232
  type: track.type,
226
233
  poster: track.poster,
227
- tracks: track.tracks || []
234
+ tracks: track.tracks || [],
235
+ audioDescriptionSrc: track.audioDescriptionSrc || null,
236
+ signLanguageSrc: track.signLanguageSrc || null
228
237
  });
229
238
 
230
239
  // Update UI
@@ -237,6 +246,11 @@ export class PlaylistManager {
237
246
  item: track,
238
247
  total: this.tracks.length
239
248
  });
249
+
250
+ // Clear guard flag after a short delay to ensure track is loaded
251
+ setTimeout(() => {
252
+ this.isChangingTrack = false;
253
+ }, 150);
240
254
  }
241
255
 
242
256
  /**
@@ -252,6 +266,9 @@ export class PlaylistManager {
252
266
 
253
267
  const track = this.tracks[index];
254
268
 
269
+ // Set guard flag to prevent cascade of next() calls during track change
270
+ this.isChangingTrack = true;
271
+
255
272
  // Update current index
256
273
  this.currentIndex = index;
257
274
 
@@ -260,7 +277,9 @@ export class PlaylistManager {
260
277
  src: track.src,
261
278
  type: track.type,
262
279
  poster: track.poster,
263
- tracks: track.tracks || []
280
+ tracks: track.tracks || [],
281
+ audioDescriptionSrc: track.audioDescriptionSrc || null,
282
+ signLanguageSrc: track.signLanguageSrc || null
264
283
  });
265
284
 
266
285
  // Update UI
@@ -274,9 +293,13 @@ export class PlaylistManager {
274
293
  total: this.tracks.length
275
294
  });
276
295
 
277
- // Auto-play
296
+ // Auto-play and clear guard flag after playback starts
278
297
  setTimeout(() => {
279
298
  this.player.play();
299
+ // Clear guard flag after a short delay to ensure track has started
300
+ setTimeout(() => {
301
+ this.isChangingTrack = false;
302
+ }, 50);
280
303
  }, 100);
281
304
  }
282
305
 
@@ -318,6 +341,12 @@ export class PlaylistManager {
318
341
  * Handle track end
319
342
  */
320
343
  handleTrackEnd() {
344
+ // Don't auto-advance if we're already in the process of changing tracks
345
+ // This prevents a cascade of next() calls when loading a new track triggers an 'ended' event
346
+ if (this.isChangingTrack) {
347
+ return;
348
+ }
349
+
321
350
  if (this.options.autoAdvance) {
322
351
  this.next();
323
352
  }
@@ -404,6 +433,26 @@ export class PlaylistManager {
404
433
  return;
405
434
  }
406
435
 
436
+ // Create track artwork element (shows album art/poster for audio playlists)
437
+ // Only create for audio players
438
+ if (this.player.element.tagName === 'AUDIO') {
439
+ this.trackArtworkElement = DOMUtils.createElement('div', {
440
+ className: 'vidply-track-artwork',
441
+ attributes: {
442
+ 'aria-hidden': 'true'
443
+ }
444
+ });
445
+ this.trackArtworkElement.style.display = 'none';
446
+
447
+ // Insert before video wrapper
448
+ const videoWrapper = this.container.querySelector('.vidply-video-wrapper');
449
+ if (videoWrapper) {
450
+ this.container.insertBefore(this.trackArtworkElement, videoWrapper);
451
+ } else {
452
+ this.container.appendChild(this.trackArtworkElement);
453
+ }
454
+ }
455
+
407
456
  // Create track info element (shows current track)
408
457
  this.trackInfoElement = DOMUtils.createElement('div', {
409
458
  className: 'vidply-track-info',
@@ -434,7 +483,7 @@ export class PlaylistManager {
434
483
  attributes: {
435
484
  id: `${this.uniqueId}-panel`,
436
485
  role: 'region',
437
- 'aria-label': 'Media playlist',
486
+ 'aria-label': i18n.t('playlist.title'),
438
487
  'aria-labelledby': `${this.uniqueId}-heading`
439
488
  }
440
489
  });
@@ -451,20 +500,50 @@ export class PlaylistManager {
451
500
 
452
501
  const trackNumber = this.currentIndex + 1;
453
502
  const totalTracks = this.tracks.length;
454
- const trackTitle = track.title || 'Untitled';
503
+ const trackTitle = track.title || i18n.t('playlist.untitled');
455
504
  const trackArtist = track.artist || '';
456
505
 
457
506
  // Screen reader announcement
458
- const announcement = `Now playing: Track ${trackNumber} of ${totalTracks}. ${trackTitle}${trackArtist ? ' by ' + trackArtist : ''}`;
507
+ const artistPart = trackArtist ? i18n.t('playlist.by') + trackArtist : '';
508
+ const announcement = i18n.t('playlist.nowPlaying', {
509
+ current: trackNumber,
510
+ total: totalTracks,
511
+ title: trackTitle,
512
+ artist: artistPart
513
+ });
514
+
515
+ const trackOfText = i18n.t('playlist.trackOf', {
516
+ current: trackNumber,
517
+ total: totalTracks
518
+ });
459
519
 
460
520
  this.trackInfoElement.innerHTML = `
461
521
  <span class="vidply-sr-only">${DOMUtils.escapeHTML(announcement)}</span>
462
- <div class="vidply-track-number" aria-hidden="true">Track ${trackNumber} of ${totalTracks}</div>
522
+ <div class="vidply-track-number" aria-hidden="true">${DOMUtils.escapeHTML(trackOfText)}</div>
463
523
  <div class="vidply-track-title" aria-hidden="true">${DOMUtils.escapeHTML(trackTitle)}</div>
464
524
  ${trackArtist ? `<div class="vidply-track-artist" aria-hidden="true">${DOMUtils.escapeHTML(trackArtist)}</div>` : ''}
465
525
  `;
466
526
 
467
527
  this.trackInfoElement.style.display = 'block';
528
+
529
+ // Update track artwork if available (for audio playlists)
530
+ this.updateTrackArtwork(track);
531
+ }
532
+
533
+ /**
534
+ * Update track artwork display (for audio playlists)
535
+ */
536
+ updateTrackArtwork(track) {
537
+ if (!this.trackArtworkElement) return;
538
+
539
+ // If track has a poster/artwork, show it
540
+ if (track.poster) {
541
+ this.trackArtworkElement.style.backgroundImage = `url(${track.poster})`;
542
+ this.trackArtworkElement.style.display = 'block';
543
+ } else {
544
+ // No artwork available, hide the element
545
+ this.trackArtworkElement.style.display = 'none';
546
+ }
468
547
  }
469
548
 
470
549
  /**
@@ -483,7 +562,7 @@ export class PlaylistManager {
483
562
  id: `${this.uniqueId}-heading`
484
563
  }
485
564
  });
486
- header.textContent = `Playlist (${this.tracks.length})`;
565
+ header.textContent = `${i18n.t('playlist.title')} (${this.tracks.length})`;
487
566
  this.playlistPanel.appendChild(header);
488
567
 
489
568
  // Add keyboard instructions (visually hidden)
@@ -522,9 +601,12 @@ export class PlaylistManager {
522
601
  * Create playlist item element
523
602
  */
524
603
  createPlaylistItem(track, index) {
525
- const trackPosition = `Track ${index + 1} of ${this.tracks.length}`;
526
- const trackTitle = track.title || `Track ${index + 1}`;
527
- const trackArtist = track.artist ? ` by ${track.artist}` : '';
604
+ const trackPosition = i18n.t('playlist.trackOf', {
605
+ current: index + 1,
606
+ total: this.tracks.length
607
+ });
608
+ const trackTitle = track.title || i18n.t('playlist.trackUntitled', { number: index + 1 });
609
+ const trackArtist = track.artist ? i18n.t('playlist.by') + track.artist : '';
528
610
  const isActive = index === this.currentIndex;
529
611
  const statusText = isActive ? 'Currently playing' : 'Not playing';
530
612
  const actionText = isActive ? 'Press Enter to restart' : 'Press Enter to play';
@@ -734,9 +816,12 @@ export class PlaylistManager {
734
816
  if (!button) return;
735
817
 
736
818
  const track = this.tracks[index];
737
- const trackPosition = `Track ${index + 1} of ${this.tracks.length}`;
738
- const trackTitle = track.title || `Track ${index + 1}`;
739
- const trackArtist = track.artist ? ` by ${track.artist}` : '';
819
+ const trackPosition = i18n.t('playlist.trackOf', {
820
+ current: index + 1,
821
+ total: this.tracks.length
822
+ });
823
+ const trackTitle = track.title || i18n.t('playlist.trackUntitled', { number: index + 1 });
824
+ const trackArtist = track.artist ? i18n.t('playlist.by') + track.artist : '';
740
825
 
741
826
  if (index === this.currentIndex) {
742
827
  // Update list item styling
@@ -750,7 +835,7 @@ export class PlaylistManager {
750
835
  const actionText = 'Press Enter to restart';
751
836
  button.setAttribute('aria-label', `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
752
837
 
753
- // Scroll into view
838
+ // Scroll into view within playlist panel (uses 'nearest' to minimize page scroll)
754
839
  item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
755
840
  } else {
756
841
  // Update list item styling
@@ -857,6 +942,11 @@ export class PlaylistManager {
857
942
  this.trackInfoElement.innerHTML = '';
858
943
  this.trackInfoElement.style.display = 'none';
859
944
  }
945
+
946
+ if (this.trackArtworkElement) {
947
+ this.trackArtworkElement.style.backgroundImage = '';
948
+ this.trackArtworkElement.style.display = 'none';
949
+ }
860
950
  }
861
951
 
862
952
  /**
@@ -929,6 +1019,10 @@ export class PlaylistManager {
929
1019
  this.player.off('error', this.handleTrackError);
930
1020
 
931
1021
  // Remove UI
1022
+ if (this.trackArtworkElement) {
1023
+ this.trackArtworkElement.remove();
1024
+ }
1025
+
932
1026
  if (this.trackInfoElement) {
933
1027
  this.trackInfoElement.remove();
934
1028
  }
@@ -157,6 +157,14 @@ export const de = {
157
157
  minutes: '{count} Minuten',
158
158
  second: '{count} Sekunde',
159
159
  seconds: '{count} Sekunden'
160
+ },
161
+ playlist: {
162
+ title: 'Wiedergabeliste',
163
+ trackOf: 'Titel {current} von {total}',
164
+ nowPlaying: 'Läuft gerade: Titel {current} von {total}. {title}{artist}',
165
+ by: ' von ',
166
+ untitled: 'Ohne Titel',
167
+ trackUntitled: 'Titel {number}'
160
168
  }
161
169
  };
162
170
 
@@ -157,6 +157,14 @@ export const en = {
157
157
  minutes: '{count} minutes',
158
158
  second: '{count} second',
159
159
  seconds: '{count} seconds'
160
+ },
161
+ playlist: {
162
+ title: 'Playlist',
163
+ trackOf: 'Track {current} of {total}',
164
+ nowPlaying: 'Now playing: Track {current} of {total}. {title}{artist}',
165
+ by: ' by ',
166
+ untitled: 'Untitled',
167
+ trackUntitled: 'Track {number}'
160
168
  }
161
169
  };
162
170
 
@@ -157,6 +157,14 @@ export const es = {
157
157
  minutes: '{count} minutos',
158
158
  second: '{count} segundo',
159
159
  seconds: '{count} segundos'
160
+ },
161
+ playlist: {
162
+ title: 'Lista de reproducción',
163
+ trackOf: 'Pista {current} de {total}',
164
+ nowPlaying: 'Reproduciendo ahora: Pista {current} de {total}. {title}{artist}',
165
+ by: ' por ',
166
+ untitled: 'Sin título',
167
+ trackUntitled: 'Pista {number}'
160
168
  }
161
169
  };
162
170
 
@@ -157,6 +157,14 @@ export const fr = {
157
157
  minutes: '{count} minutes',
158
158
  second: '{count} seconde',
159
159
  seconds: '{count} secondes'
160
+ },
161
+ playlist: {
162
+ title: 'Liste de lecture',
163
+ trackOf: 'Piste {current} sur {total}',
164
+ nowPlaying: 'Lecture en cours : Piste {current} sur {total}. {title}{artist}',
165
+ by: ' par ',
166
+ untitled: 'Sans titre',
167
+ trackUntitled: 'Piste {number}'
160
168
  }
161
169
  };
162
170
 
@@ -157,6 +157,14 @@ export const ja = {
157
157
  minutes: '{count}分',
158
158
  second: '{count}秒',
159
159
  seconds: '{count}秒'
160
+ },
161
+ playlist: {
162
+ title: 'プレイリスト',
163
+ trackOf: 'トラック {current}/{total}',
164
+ nowPlaying: '再生中: トラック {current}/{total}. {title}{artist}',
165
+ by: ' - ',
166
+ untitled: 'タイトルなし',
167
+ trackUntitled: 'トラック {number}'
160
168
  }
161
169
  };
162
170