Merge pull request #2243 from edx/anton/video-i18n
Internationalize the video player.
This commit is contained in:
@@ -11,6 +11,7 @@ from lxml import etree
|
||||
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
@@ -103,8 +104,10 @@ def get_transcripts_from_youtube(youtube_id):
|
||||
data = requests.get(youtube_api['url'], params=youtube_api['params'])
|
||||
|
||||
if data.status_code != 200 or not data.text:
|
||||
msg = "Can't receive transcripts from Youtube for {}. Status code: {}.".format(
|
||||
youtube_id, data.status_code)
|
||||
msg = _("Can't receive transcripts from Youtube for {youtube_id}. Status code: {statuc_code}.").format(
|
||||
youtube_id=youtube_id,
|
||||
statuc_code=data.status_code
|
||||
)
|
||||
raise GetTranscriptsFromYouTubeException(msg)
|
||||
|
||||
sub_starts, sub_ends, sub_texts = [], [], []
|
||||
@@ -162,7 +165,7 @@ def download_youtube_subs(youtube_subs, item):
|
||||
highest_speed_subs = subs
|
||||
|
||||
if not highest_speed:
|
||||
raise GetTranscriptsFromYouTubeException("Can't find any transcripts on the Youtube service.")
|
||||
raise GetTranscriptsFromYouTubeException(_("Can't find any transcripts on the Youtube service."))
|
||||
|
||||
# When we exit from the previous loop, `highest_speed` and `highest_speed_subs`
|
||||
# are the transcripts data for the highest speed available on the
|
||||
@@ -214,16 +217,16 @@ def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item):
|
||||
:returns: True, if all subs are generated and saved successfully.
|
||||
"""
|
||||
if subs_type != 'srt':
|
||||
raise TranscriptsGenerationException("We support only SubRip (*.srt) transcripts format.")
|
||||
raise TranscriptsGenerationException(_("We support only SubRip (*.srt) transcripts format."))
|
||||
try:
|
||||
srt_subs_obj = SubRipFile.from_string(subs_filedata)
|
||||
except Exception as e:
|
||||
raise TranscriptsGenerationException(
|
||||
"Something wrong with SubRip transcripts file during parsing. "
|
||||
"Inner message is {}".format(e.message)
|
||||
msg = _("Something wrong with SubRip transcripts file during parsing. Inner message is {error_message}").format(
|
||||
error_message=e.message
|
||||
)
|
||||
raise TranscriptsGenerationException(msg)
|
||||
if not srt_subs_obj:
|
||||
raise TranscriptsGenerationException("Something wrong with SubRip transcripts file during parsing.")
|
||||
raise TranscriptsGenerationException(_("Something wrong with SubRip transcripts file during parsing."))
|
||||
|
||||
sub_starts = []
|
||||
sub_ends = []
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.http import HttpResponse, Http404
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.exceptions import NotFoundError
|
||||
@@ -439,15 +440,15 @@ def _validate_transcripts_data(request):
|
||||
"""
|
||||
data = json.loads(request.GET.get('data', '{}'))
|
||||
if not data:
|
||||
raise TranscriptsRequestValidationException('Incoming video data is empty.')
|
||||
raise TranscriptsRequestValidationException(_('Incoming video data is empty.'))
|
||||
|
||||
try:
|
||||
item = _get_item(request, data)
|
||||
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
|
||||
raise TranscriptsRequestValidationException("Can't find item by locator.")
|
||||
raise TranscriptsRequestValidationException(_("Can't find item by locator."))
|
||||
|
||||
if item.category != 'video':
|
||||
raise TranscriptsRequestValidationException('Transcripts are supported only for "video" modules.')
|
||||
raise TranscriptsRequestValidationException(_('Transcripts are supported only for "video" modules.'))
|
||||
|
||||
# parse data form request.GET.['data']['video'] to useful format
|
||||
videos = {'youtube': '', 'html5': {}}
|
||||
|
||||
@@ -33,6 +33,7 @@ src_paths:
|
||||
|
||||
# Paths to library JavaScript files (optional)
|
||||
lib_paths:
|
||||
- common_static/js/test/i18n.js
|
||||
- common_static/coffee/src/ajax_prefix.js
|
||||
- common_static/coffee/src/logger.js
|
||||
- common_static/js/vendor/jasmine-jquery.js
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
expect(timeControl).toHaveAttrs({
|
||||
'role': 'slider',
|
||||
'title': 'video position',
|
||||
'title': 'Video position',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
state.videoProgressSlider.notifyThroughHandleEnd({end: true});
|
||||
|
||||
expect(state.videoProgressSlider.handle.attr('title'))
|
||||
.toBe('video ended');
|
||||
.toBe('Video ended');
|
||||
|
||||
expect('focus').toHaveBeenTriggeredOn(
|
||||
state.videoProgressSlider.handle
|
||||
@@ -252,7 +252,7 @@
|
||||
state.videoProgressSlider.notifyThroughHandleEnd({end: false});
|
||||
|
||||
expect(state.videoProgressSlider.handle.attr('title'))
|
||||
.toBe('video position');
|
||||
.toBe('Video position');
|
||||
|
||||
expect('focus').not.toHaveBeenTriggeredOn(
|
||||
state.videoProgressSlider.handle
|
||||
@@ -272,6 +272,30 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('getTimeDescription', function () {
|
||||
var cases = {
|
||||
'0': '0 seconds',
|
||||
'1': '1 second',
|
||||
'10': '10 seconds',
|
||||
|
||||
'60': '1 minute 0 seconds',
|
||||
'121': '2 minutes 1 second',
|
||||
|
||||
'3670': '1 hour 1 minute 10 seconds',
|
||||
'21541': '5 hours 59 minutes 1 second',
|
||||
},
|
||||
getTimeDescription;
|
||||
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
getTimeDescription = state.videoProgressSlider.getTimeDescription;
|
||||
|
||||
$.each(cases, function(input, output) {
|
||||
expect(getTimeDescription(input)).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
|
||||
.andReturn(null);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
@@ -15,14 +16,12 @@
|
||||
describe('constructor', function () {
|
||||
beforeEach(function () {
|
||||
spyOn($.fn, 'slider').andCallThrough();
|
||||
$.cookie.andReturn('75');
|
||||
state = jasmine.initializePlayer();
|
||||
});
|
||||
|
||||
it('initialize currentVolume to 100%', function () {
|
||||
// Please note that:
|
||||
// 0% -> 0
|
||||
// 100% -> 1.0
|
||||
expect(state.videoVolumeControl.currentVolume).toEqual(1);
|
||||
it('initialize currentVolume to 75%', function () {
|
||||
expect(state.videoVolumeControl.currentVolume).toEqual(75);
|
||||
});
|
||||
|
||||
it('render the volume control', function () {
|
||||
@@ -45,13 +44,13 @@
|
||||
it('add ARIA attributes to slider handle', function () {
|
||||
var sliderHandle = $('div.volume-slider>a.ui-slider-handle'),
|
||||
arr = [
|
||||
'muted', 'very low', 'low', 'average', 'loud',
|
||||
'very loud', 'maximum'
|
||||
'Muted', 'Very low', 'Low', 'Average', 'Loud',
|
||||
'Very loud', 'Maximum'
|
||||
];
|
||||
|
||||
expect(sliderHandle).toHaveAttrs({
|
||||
'role': 'slider',
|
||||
'title': 'volume',
|
||||
'title': 'Volume',
|
||||
'aria-disabled': 'false',
|
||||
'aria-valuemin': '0',
|
||||
'aria-valuemax': '100'
|
||||
@@ -86,33 +85,33 @@
|
||||
|
||||
describe('onChange', function () {
|
||||
var initialData = [{
|
||||
range: 'muted',
|
||||
range: 'Muted',
|
||||
value: 0,
|
||||
expectation: 'muted'
|
||||
expectation: 'Muted'
|
||||
}, {
|
||||
range: 'in ]0,20]',
|
||||
value: 10,
|
||||
expectation: 'very low'
|
||||
expectation: 'Very low'
|
||||
}, {
|
||||
range: 'in ]20,40]',
|
||||
value: 30,
|
||||
expectation: 'low'
|
||||
expectation: 'Low'
|
||||
}, {
|
||||
range: 'in ]40,60]',
|
||||
value: 50,
|
||||
expectation: 'average'
|
||||
expectation: 'Average'
|
||||
}, {
|
||||
range: 'in ]60,80]',
|
||||
value: 70,
|
||||
expectation: 'loud'
|
||||
expectation: 'Loud'
|
||||
}, {
|
||||
range: 'in ]80,100[',
|
||||
value: 90,
|
||||
expectation: 'very loud'
|
||||
expectation: 'Very loud'
|
||||
}, {
|
||||
range: 'maximum',
|
||||
range: 'Maximum',
|
||||
value: 100,
|
||||
expectation: 'maximum'
|
||||
expectation: 'Maximum'
|
||||
}];
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
@@ -16,19 +16,6 @@ define(
|
||||
'video/01_initialize.js',
|
||||
['video/03_video_player.js', 'video/00_cookie_storage.js'],
|
||||
function (VideoPlayer, CookieStorage) {
|
||||
// window.console.log() is expected to be available. We do not support
|
||||
// browsers which lack this functionality.
|
||||
|
||||
// The function gettext() is defined by a vendor library. If, however, it
|
||||
// is undefined, it is a simple wrapper. It is used to return a different
|
||||
// version of the string passed (translated string, etc.). In the basic
|
||||
// case, the original string is returned.
|
||||
if (typeof(window.gettext) == 'undefined') {
|
||||
window.gettext = function (s) {
|
||||
return s;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
|
||||
@@ -81,7 +81,7 @@ function () {
|
||||
// handle, behaves as a slider named 'video slider'.
|
||||
state.videoControl.sliderEl.find('.ui-slider-handle').attr({
|
||||
'role': 'slider',
|
||||
'title': gettext('video slider')
|
||||
'title': gettext('Video slider')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ function () {
|
||||
onStop: onStop,
|
||||
updatePlayTime: updatePlayTime,
|
||||
updateStartEndTimeRegion: updateStartEndTimeRegion,
|
||||
notifyThroughHandleEnd: notifyThroughHandleEnd
|
||||
notifyThroughHandleEnd: notifyThroughHandleEnd,
|
||||
getTimeDescription: getTimeDescription
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoProgressSlider, state);
|
||||
@@ -72,7 +73,7 @@ function () {
|
||||
// handle, behaves as a slider named 'video position'.
|
||||
state.videoProgressSlider.handle.attr({
|
||||
'role': 'slider',
|
||||
'title': 'video position',
|
||||
'title': gettext('Video position'),
|
||||
'aria-disabled': false,
|
||||
'aria-valuetext': getTimeDescription(state.videoProgressSlider
|
||||
.slider.slider('option', 'value'))
|
||||
@@ -152,11 +153,15 @@ function () {
|
||||
|
||||
if (!this.videoProgressSlider.sliderRange) {
|
||||
this.videoProgressSlider.sliderRange = $('<div />', {
|
||||
class: 'ui-slider-range ' +
|
||||
'ui-widget-header ' +
|
||||
'ui-corner-all ' +
|
||||
'slider-range'
|
||||
}).css(rangeParams);
|
||||
'class': 'ui-slider-range ' +
|
||||
'ui-widget-header ' +
|
||||
'ui-corner-all ' +
|
||||
'slider-range'
|
||||
})
|
||||
.css({
|
||||
left: rangeParams.left,
|
||||
width: rangeParams.width
|
||||
});
|
||||
|
||||
this.videoProgressSlider.sliderProgress
|
||||
.after(this.videoProgressSlider.sliderRange);
|
||||
@@ -245,61 +250,50 @@ function () {
|
||||
function notifyThroughHandleEnd(params) {
|
||||
if (params.end) {
|
||||
this.videoProgressSlider.handle
|
||||
.attr('title', 'video ended')
|
||||
.attr('title', gettext('Video ended'))
|
||||
.focus();
|
||||
} else {
|
||||
this.videoProgressSlider.handle.attr('title', 'video position');
|
||||
this.videoProgressSlider.handle
|
||||
.attr('title', gettext('Video position'));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a string describing the current time of video in hh:mm:ss
|
||||
// format.
|
||||
// Returns a string describing the current time of video in
|
||||
// `%d hours %d minutes %d seconds` format.
|
||||
function getTimeDescription(time) {
|
||||
var seconds = Math.floor(time),
|
||||
minutes = Math.floor(seconds / 60),
|
||||
hours = Math.floor(minutes / 60),
|
||||
hrStr, minStr, secStr;
|
||||
i18n = function (value, word) {
|
||||
var msg;
|
||||
|
||||
switch(word) {
|
||||
case 'hour':
|
||||
msg = ngettext('%(value)s hour', '%(value)s hours', value);
|
||||
break;
|
||||
case 'minute':
|
||||
msg = ngettext('%(value)s minute', '%(value)s minutes', value);
|
||||
break;
|
||||
case 'second':
|
||||
msg = ngettext('%(value)s second', '%(value)s seconds', value);
|
||||
break;
|
||||
}
|
||||
return interpolate(msg, {'value': value}, true);
|
||||
};
|
||||
|
||||
seconds = seconds % 60;
|
||||
minutes = minutes % 60;
|
||||
|
||||
hrStr = hours.toString(10);
|
||||
minStr = minutes.toString(10);
|
||||
secStr = seconds.toString(10);
|
||||
|
||||
if (hours) {
|
||||
hrStr += (hours < 2 ? ' hour ' : ' hours ');
|
||||
|
||||
if (minutes) {
|
||||
minStr += (minutes < 2 ? ' minute ' : ' minutes ');
|
||||
} else {
|
||||
minStr += ' 0 minutes ';
|
||||
}
|
||||
|
||||
if (seconds) {
|
||||
secStr += (seconds < 2 ? ' second ' : ' seconds ');
|
||||
} else {
|
||||
secStr += ' 0 seconds ';
|
||||
}
|
||||
|
||||
return hrStr + minStr + secStr;
|
||||
return i18n(hours, 'hour') + ' ' +
|
||||
i18n(minutes, 'minute') + ' ' +
|
||||
i18n(seconds, 'second');
|
||||
} else if (minutes) {
|
||||
minStr += (minutes < 2 ? ' minute ' : ' minutes ');
|
||||
|
||||
if (seconds) {
|
||||
secStr += (seconds < 2 ? ' second ' : ' seconds ');
|
||||
} else {
|
||||
secStr += ' 0 seconds ';
|
||||
}
|
||||
|
||||
return minStr + secStr;
|
||||
} else if (seconds) {
|
||||
secStr += (seconds < 2 ? ' second ' : ' seconds ');
|
||||
|
||||
return secStr;
|
||||
return i18n(minutes, 'minute') + ' ' +
|
||||
i18n(seconds, 'second');
|
||||
}
|
||||
|
||||
return '0 seconds';
|
||||
return i18n(seconds, 'second');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ function () {
|
||||
|
||||
volumeSliderHandleEl.attr({
|
||||
'role': 'slider',
|
||||
'title': 'volume',
|
||||
'title': gettext('Volume'),
|
||||
'aria-disabled': false,
|
||||
'aria-valuemin': slider.slider('option', 'min'),
|
||||
'aria-valuemax': slider.slider('option', 'max'),
|
||||
@@ -238,20 +238,27 @@ function () {
|
||||
// Returns a string describing the level of volume.
|
||||
function getVolumeDescription(vol) {
|
||||
if (vol === 0) {
|
||||
return 'muted';
|
||||
// Translators: Volume level equals 0%.
|
||||
return gettext('Muted');
|
||||
} else if (vol <= 20) {
|
||||
return 'very low';
|
||||
// Translators: Volume level in range (0,20]%
|
||||
return gettext('Very low');
|
||||
} else if (vol <= 40) {
|
||||
return 'low';
|
||||
// Translators: Volume level in range (20,40]%
|
||||
return gettext('Low');
|
||||
} else if (vol <= 60) {
|
||||
return 'average';
|
||||
// Translators: Volume level in range (40,60]%
|
||||
return gettext('Average');
|
||||
} else if (vol <= 80) {
|
||||
return 'loud';
|
||||
// Translators: Volume level in range (60,80]%
|
||||
return gettext('Loud');
|
||||
} else if (vol <= 99) {
|
||||
return 'very loud';
|
||||
// Translators: Volume level in range (80,100)%
|
||||
return gettext('Very loud');
|
||||
}
|
||||
|
||||
return 'maximum';
|
||||
// Translators: Volume level equals 100%.
|
||||
return gettext('Maximum');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ prepend_path: lms/static
|
||||
|
||||
# Paths to library JavaScript files (optional)
|
||||
lib_paths:
|
||||
- xmodule_js/common_static/js/test/i18n.js
|
||||
- xmodule_js/common_static/coffee/src/ajax_prefix.js
|
||||
- xmodule_js/common_static/coffee/src/logger.js
|
||||
- xmodule_js/common_static/js/vendor/jasmine-jquery.js
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider" title="Video position"></div>
|
||||
|
||||
<div class="slider" title="${_('Video position')}"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control" href="#" title="${_('Play')}" role="button" aria-disabled="false"></a></li>
|
||||
|
||||
Reference in New Issue
Block a user