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);
},