diff --git a/Gruntfile.js b/Gruntfile.js index c560df78f..32238353b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -31,7 +31,9 @@ module.exports = function(grunt) { // Project configuration. grunt.initConfig({ pkg: pkg, - + lang: { + src: 'lang/*.json' + }, build: { src: 'src/js/dependencies.js', options: { @@ -331,11 +333,11 @@ module.exports = function(grunt) { // grunt.loadTasks('./docs/tasks/'); // grunt.loadTasks('../videojs-doc-generator/tasks/'); - grunt.registerTask('pretask', ['jshint', 'less', 'build', 'minify', 'usebanner']); + grunt.registerTask('pretask', ['jshint', 'less', 'lang', 'build', 'minify', 'usebanner']); // Default task. grunt.registerTask('default', ['pretask', 'dist']); // Development watch task - grunt.registerTask('dev', ['jshint', 'less', 'build', 'qunit:source']); + grunt.registerTask('dev', ['jshint', 'less', 'lang', 'build', 'qunit:source']); grunt.registerTask('test-qunit', ['pretask', 'qunit']); // The test task will run `karma:saucelabs` when running in travis, @@ -425,7 +427,26 @@ module.exports = function(grunt) { var fs = require('fs'), gzip = require('zlib').gzip; + grunt.registerMultiTask('lang', 'Building Language Support', function() { + + grunt.log.writeln('Building Language Support'); + + var langFiles = this.files; + var combined; + + // Create a combined languages file + langFiles.forEach(function(result) { + combined += grunt.file.read(result.src); + }); + + combined = combined.replace('undefined', 'vjs.options["languages"] = '); + + grunt.file.write('build/files/combined.languages.js', combined); + + }); + grunt.registerMultiTask('build', 'Building Source', function(){ + // Fix windows file path delimiter issue var i = sourceFiles.length; while (i--) { @@ -439,6 +460,10 @@ module.exports = function(grunt) { }); // Replace CDN version ref in js. Use major/minor version. combined = combined.replace(/GENERATED_CDN_VSN/g, version.majorMinor); + + // Add Combined Langauges + combined += grunt.file.read('build/files/combined.languages.js'); + grunt.file.write('build/files/combined.video.js', combined); // Copy over other files diff --git a/lang/es.json b/lang/es.json new file mode 100644 index 000000000..8dff78e48 --- /dev/null +++ b/lang/es.json @@ -0,0 +1,28 @@ +{'es': + { + 'Play': 'Juego', + 'Pause': 'Pausa', + 'Current Time': 'Tiempo Actual', + 'Duration Time': 'Tiempo de Duracion', + 'Remaining Time': 'Tiempo Restante', + 'Stream Type': 'Tipo de Transmision', + 'LIVE': 'En Vivo', + 'Loaded': 'Cargado', + 'Progress': 'Progreso', + 'Fullscreen': 'Pantalla Completa', + 'Non-Fullscreen': 'No Pantalla Completa', + 'Mute': 'Mudo', + 'Unmuted': 'Activar sonido', + 'Playback Rate': 'Reproduccion Cambio', + 'Subtitles': 'Subtitulos', + 'subtitles off': 'subtitulos fuera', + 'Captions': 'Subtitulos', + 'captions off': 'subtitulos fuera', + 'Chapters': 'Capitulos', + 'You aborted the video playback': 'Ha anulado la reproduccion de video', + 'A network error caused the video download to fail part-way.': 'Un error en la red hizo que la descarga de video falle parte del camino.', + 'The video could not be loaded, either because the server or network failed or because the format is not supported.': 'El video no se puede cargar, ya sea porque el servidor o la red fracasaron o porque el formato no es compatible.', + 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support.': 'La reproduccion de video se ha cancelado debido a un problema de corrupcion o porque el video utilizado cuenta con su navegador no soporta.', + 'No compatible source was found for this video.': 'Ninguna fuente compatible se encontro para este video.' + } +} \ No newline at end of file diff --git a/src/js/button.js b/src/js/button.js index 36d9c3ee0..c76165cae 100644 --- a/src/js/button.js +++ b/src/js/button.js @@ -45,7 +45,7 @@ vjs.Button.prototype.createEl = function(type, props){ this.controlText_ = vjs.createEl('span', { className: 'vjs-control-text', - innerHTML: this.buttonText || 'Need Text' + innerHTML: this.localize(this.buttonText) || 'Need Text' }); this.contentEl_.appendChild(this.controlText_); diff --git a/src/js/component.js b/src/js/component.js index aa255f4f9..e56f0ebe6 100644 --- a/src/js/component.js +++ b/src/js/component.js @@ -195,6 +195,15 @@ vjs.Component.prototype.createEl = function(tagName, attributes){ return vjs.createEl(tagName, attributes); }; +vjs.Component.prototype.localize = function(string){ + var lang = this.player_.language(), + languages = this.player_.languages(); + if (languages && languages[lang] && languages[lang][string]) { + return languages[lang][string]; + } + return string; +}; + /** * Get the component's DOM element * diff --git a/src/js/control-bar/fullscreen-toggle.js b/src/js/control-bar/fullscreen-toggle.js index 1fb9ce715..8fd6d51af 100644 --- a/src/js/control-bar/fullscreen-toggle.js +++ b/src/js/control-bar/fullscreen-toggle.js @@ -25,9 +25,9 @@ vjs.FullscreenToggle.prototype.buildCSSClass = function(){ vjs.FullscreenToggle.prototype.onClick = function(){ if (!this.player_.isFullscreen()) { this.player_.requestFullscreen(); - this.controlText_.innerHTML = 'Non-Fullscreen'; + this.controlText_.innerHTML = this.localize('Non-Fullscreen'); } else { this.player_.exitFullscreen(); - this.controlText_.innerHTML = 'Fullscreen'; + this.controlText_.innerHTML = this.localize('Fullscreen'); } }; diff --git a/src/js/control-bar/live-display.js b/src/js/control-bar/live-display.js index dd44922fe..0499e9fc3 100644 --- a/src/js/control-bar/live-display.js +++ b/src/js/control-bar/live-display.js @@ -18,7 +18,7 @@ vjs.LiveDisplay.prototype.createEl = function(){ this.contentEl_ = vjs.createEl('div', { className: 'vjs-live-display', - innerHTML: 'Stream Type LIVE', + innerHTML: '' + this.localize('Stream Type') + '' + this.localize('LIVE'), 'aria-live': 'off' }); diff --git a/src/js/control-bar/mute-toggle.js b/src/js/control-bar/mute-toggle.js index 07221504e..6f2b335ca 100644 --- a/src/js/control-bar/mute-toggle.js +++ b/src/js/control-bar/mute-toggle.js @@ -29,7 +29,7 @@ vjs.MuteToggle = vjs.Button.extend({ vjs.MuteToggle.prototype.createEl = function(){ return vjs.Button.prototype.createEl.call(this, 'div', { className: 'vjs-mute-control vjs-control', - innerHTML: '
Mute
' + innerHTML: '
' + this.localize('Mute') + '
' }); }; @@ -53,12 +53,12 @@ vjs.MuteToggle.prototype.update = function(){ // This causes unnecessary and confusing information for screen reader users. // This check is needed because this function gets called every time the volume level is changed. if(this.player_.muted()){ - if(this.el_.children[0].children[0].innerHTML!='Unmute'){ - this.el_.children[0].children[0].innerHTML = 'Unmute'; // change the button text to "Unmute" + if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){ + this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute" } } else { - if(this.el_.children[0].children[0].innerHTML!='Mute'){ - this.el_.children[0].children[0].innerHTML = 'Mute'; // change the button text to "Mute" + if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){ + this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute" } } diff --git a/src/js/control-bar/play-toggle.js b/src/js/control-bar/play-toggle.js index 57758cd92..2e844d30a 100644 --- a/src/js/control-bar/play-toggle.js +++ b/src/js/control-bar/play-toggle.js @@ -34,12 +34,12 @@ vjs.PlayToggle.prototype.onClick = function(){ vjs.PlayToggle.prototype.onPlay = function(){ vjs.removeClass(this.el_, 'vjs-paused'); vjs.addClass(this.el_, 'vjs-playing'); - this.el_.children[0].children[0].innerHTML = 'Pause'; // change the button text to "Pause" + this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause" }; // OnPause - Add the vjs-paused class to the element so it can change appearance vjs.PlayToggle.prototype.onPause = function(){ vjs.removeClass(this.el_, 'vjs-playing'); vjs.addClass(this.el_, 'vjs-paused'); - this.el_.children[0].children[0].innerHTML = 'Play'; // change the button text to "Play" + this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play" }; diff --git a/src/js/control-bar/playback-rate-menu-button.js b/src/js/control-bar/playback-rate-menu-button.js index 42bf2e286..c57512c4b 100644 --- a/src/js/control-bar/playback-rate-menu-button.js +++ b/src/js/control-bar/playback-rate-menu-button.js @@ -21,7 +21,7 @@ vjs.PlaybackRateMenuButton = vjs.MenuButton.extend({ vjs.PlaybackRateMenuButton.prototype.createEl = function(){ var el = vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-playback-rate vjs-menu-button vjs-control', - innerHTML: '
Playback Rate
' + innerHTML: '
' + this.localize('Playback Rate') + '
' }); this.labelEl_ = vjs.createEl('div', { diff --git a/src/js/control-bar/progress-control.js b/src/js/control-bar/progress-control.js index c28f002b1..f385e0c32 100644 --- a/src/js/control-bar/progress-control.js +++ b/src/js/control-bar/progress-control.js @@ -124,7 +124,8 @@ vjs.LoadProgressBar = vjs.Component.extend({ vjs.LoadProgressBar.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { - className: 'vjs-load-progress' + className: 'vjs-load-progress', + innerHTML: '' + this.localize('Loaded') + ': 0%' }); }; @@ -181,7 +182,7 @@ vjs.PlayProgressBar = vjs.Component.extend({ vjs.PlayProgressBar.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-play-progress', - innerHTML: 'Progress: 0%' + innerHTML: '' + this.localize('Progress') + ': 0%' }); }; diff --git a/src/js/control-bar/time-display.js b/src/js/control-bar/time-display.js index dbbb7e5c9..30db441c4 100644 --- a/src/js/control-bar/time-display.js +++ b/src/js/control-bar/time-display.js @@ -31,7 +31,7 @@ vjs.CurrentTimeDisplay.prototype.createEl = function(){ vjs.CurrentTimeDisplay.prototype.updateContent = function(){ // Allows for smooth scrubbing, when player can't keep up. var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); - this.contentEl_.innerHTML = 'Current Time ' + vjs.formatTime(time, this.player_.duration()); + this.contentEl_.innerHTML = '' + this.localize('Current Time') + ' ' + vjs.formatTime(time, this.player_.duration()); }; /** @@ -61,7 +61,7 @@ vjs.DurationDisplay.prototype.createEl = function(){ this.contentEl_ = vjs.createEl('div', { className: 'vjs-duration-display', - innerHTML: 'Duration Time ' + '0:00', // label the duration time for screen reader users + innerHTML: '' + this.localize('Duration Time') + ' ' + '0:00', // label the duration time for screen reader users 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes }); @@ -72,7 +72,7 @@ vjs.DurationDisplay.prototype.createEl = function(){ vjs.DurationDisplay.prototype.updateContent = function(){ var duration = this.player_.duration(); if (duration) { - this.contentEl_.innerHTML = 'Duration Time ' + vjs.formatTime(duration); // label the duration time for screen reader users + this.contentEl_.innerHTML = '' + this.localize('Duration Time') + ' ' + vjs.formatTime(duration); // label the duration time for screen reader users } }; @@ -121,7 +121,7 @@ vjs.RemainingTimeDisplay.prototype.createEl = function(){ this.contentEl_ = vjs.createEl('div', { className: 'vjs-remaining-time-display', - innerHTML: 'Remaining Time ' + '-0:00', // label the remaining time for screen reader users + innerHTML: '' + this.localize('Remaining Time') + ' ' + '-0:00', // label the remaining time for screen reader users 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes }); @@ -131,7 +131,7 @@ vjs.RemainingTimeDisplay.prototype.createEl = function(){ vjs.RemainingTimeDisplay.prototype.updateContent = function(){ if (this.player_.duration()) { - this.contentEl_.innerHTML = 'Remaining Time ' + '-'+ vjs.formatTime(this.player_.remainingTime()); + this.contentEl_.innerHTML = '' + this.localize('Remaining Time') + ' ' + '-'+ vjs.formatTime(this.player_.remainingTime()); } // Allows for smooth scrubbing, when player can't keep up. diff --git a/src/js/control-bar/volume-menu-button.js b/src/js/control-bar/volume-menu-button.js index 697837046..963e569e2 100644 --- a/src/js/control-bar/volume-menu-button.js +++ b/src/js/control-bar/volume-menu-button.js @@ -42,7 +42,7 @@ vjs.VolumeMenuButton.prototype.onClick = function(){ vjs.VolumeMenuButton.prototype.createEl = function(){ return vjs.Button.prototype.createEl.call(this, 'div', { className: 'vjs-volume-menu-button vjs-menu-button vjs-control', - innerHTML: '
Mute
' + innerHTML: '
' + this.localize('Mute') + '
' }); }; vjs.VolumeMenuButton.prototype.update = vjs.MuteToggle.prototype.update; diff --git a/src/js/core.js b/src/js/core.js index 0139abfe1..ab0763408 100644 --- a/src/js/core.js +++ b/src/js/core.js @@ -103,6 +103,11 @@ vjs.options = { 'errorDisplay': {} }, + 'language': document.getElementsByTagName('html')[0].getAttribute('lang') || navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language || 'en', + + // locales and their language translations + 'languages': {}, + // Default message to show when a video cannot be played. 'notSupportedMessage': 'No compatible source was found for this video.' }; diff --git a/src/js/error-display.js b/src/js/error-display.js index 98803b8ef..dee629ae8 100644 --- a/src/js/error-display.js +++ b/src/js/error-display.js @@ -26,6 +26,6 @@ vjs.ErrorDisplay.prototype.createEl = function(){ vjs.ErrorDisplay.prototype.update = function(){ if (this.player().error()) { - this.contentEl_.innerHTML = this.player().error().message; + this.contentEl_.innerHTML = this.localize(this.player().error().message); } }; diff --git a/src/js/exports.js b/src/js/exports.js index 56e55c322..51ab77336 100644 --- a/src/js/exports.js +++ b/src/js/exports.js @@ -67,6 +67,7 @@ goog.exportProperty(vjs.Component.prototype, 'ready', vjs.Component.prototype.re goog.exportProperty(vjs.Component.prototype, 'addClass', vjs.Component.prototype.addClass); goog.exportProperty(vjs.Component.prototype, 'removeClass', vjs.Component.prototype.removeClass); goog.exportProperty(vjs.Component.prototype, 'buildCSSClass', vjs.Component.prototype.buildCSSClass); +goog.exportProperty(vjs.Component.prototype, 'localize', vjs.Component.prototype.localize); // Need to export ended to ensure it's not removed by CC, since it's not used internally goog.exportProperty(vjs.Player.prototype, 'ended', vjs.Player.prototype.ended); @@ -76,6 +77,8 @@ goog.exportProperty(vjs.Player.prototype, 'preload', vjs.Player.prototype.preloa goog.exportProperty(vjs.Player.prototype, 'remainingTime', vjs.Player.prototype.remainingTime); goog.exportProperty(vjs.Player.prototype, 'supportsFullScreen', vjs.Player.prototype.supportsFullScreen); goog.exportProperty(vjs.Player.prototype, 'currentType', vjs.Player.prototype.currentType); +goog.exportProperty(vjs.Player.prototype, 'language', vjs.Player.prototype.language); +goog.exportProperty(vjs.Player.prototype, 'languages', vjs.Player.prototype.languages); goog.exportSymbol('videojs.MediaLoader', vjs.MediaLoader); goog.exportSymbol('videojs.TextTrackDisplay', vjs.TextTrackDisplay); diff --git a/src/js/player.js b/src/js/player.js index 195234674..781628b77 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -45,6 +45,12 @@ vjs.Player = vjs.Component.extend({ // (tag must exist before Player) options = vjs.obj.merge(this.getTagSettings(tag), options); + // Update Current Language + this.language_ = options['language'] || vjs.options['language']; + + // Update Supported Languages + this.languages_ = options['languages'] || vjs.options['languages']; + // Cache for video property values. this.cache_ = {}; @@ -93,6 +99,41 @@ vjs.Player = vjs.Component.extend({ } }); +/** + * The players's stored language code + * + * @type {String} + * @private + */ +vjs.Player.prototype.language_; + +/** + * The player's language code + * @param {String} languageCode The locale string + * @return {String} The locale string when getting + * @return {vjs.Player} self, when setting + */ +vjs.Player.prototype.language = function (languageCode) { + if (languageCode === undefined) { + return this.language_; + } + + this.language_ = languageCode; + return this; +}; + +/** + * The players's stored language dictionary + * + * @type {Object} + * @private + */ +vjs.Player.prototype.languages_; + +vjs.Player.prototype.languages = function(){ + return this.languages_; +}; + /** * Player instance options, surfaced using vjs.options * vjs.options = vjs.Player.prototype.options_ diff --git a/test/index.html b/test/index.html index 38843c8c3..0235d2d07 100644 --- a/test/index.html +++ b/test/index.html @@ -25,6 +25,7 @@ 'test/unit/util.js', 'test/unit/events.js', 'test/unit/component.js', + 'test/unit/button.js', 'test/unit/mediafaker.js', 'test/unit/player.js', 'test/unit/core.js', diff --git a/test/unit/api.js b/test/unit/api.js index b80749467..155fde6b5 100644 --- a/test/unit/api.js +++ b/test/unit/api.js @@ -183,12 +183,8 @@ test('fullscreenToggle does not depend on minified player methods', function(){ noop = function(){}; requestFullscreen = false; exitFullscreen = false; - player = { - id: noop, - on: noop, - ready: noop, - reportUserActivity: noop - }; + + player = PlayerTest.makePlayer(); player['requestFullscreen'] = function(){ requestFullscreen = true; diff --git a/test/unit/button.js b/test/unit/button.js new file mode 100644 index 000000000..941e22945 --- /dev/null +++ b/test/unit/button.js @@ -0,0 +1,22 @@ +module('Button'); + +test('should localize its text', function(){ + expect(1); + + var player, testButton, el; + + player = PlayerTest.makePlayer({ + 'language': 'es', + 'languages': { + 'es': { + 'Play': 'Juego' + } + } + }); + + testButton = new vjs.Button(player); + testButton.buttonText = 'Play'; + el = testButton.createEl(); + + ok(el.textContent, 'Juego', 'translation was successful'); +}); diff --git a/test/unit/controls.js b/test/unit/controls.js index 7a144b892..9c99ebece 100644 --- a/test/unit/controls.js +++ b/test/unit/controls.js @@ -9,6 +9,8 @@ test('should hide volume control if it\'s not supported', function(){ id: noop, on: noop, ready: noop, + language: noop, + languages: noop, tech: { features: { 'volumeControl': false @@ -32,6 +34,8 @@ test('should test and toggle volume control on `loadstart`', function(){ listeners = []; player = { id: noop, + language: noop, + languages: noop, on: function(event, callback){ listeners.push(callback); },