diff --git a/cms/djangoapps/contentstore/transcripts_utils.py b/cms/djangoapps/contentstore/transcripts_utils.py index 7ff0999f64..79024bc4fa 100644 --- a/cms/djangoapps/contentstore/transcripts_utils.py +++ b/cms/djangoapps/contentstore/transcripts_utils.py @@ -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 = [] diff --git a/cms/djangoapps/contentstore/views/transcripts_ajax.py b/cms/djangoapps/contentstore/views/transcripts_ajax.py index 28a674cb89..03bc2067d1 100644 --- a/cms/djangoapps/contentstore/views/transcripts_ajax.py +++ b/cms/djangoapps/contentstore/views/transcripts_ajax.py @@ -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': {}} diff --git a/common/lib/xmodule/xmodule/js/js_test.yml b/common/lib/xmodule/xmodule/js/js_test.yml index 0a0699df9c..b0eb282f07 100644 --- a/common/lib/xmodule/xmodule/js/js_test.yml +++ b/common/lib/xmodule/xmodule/js/js_test.yml @@ -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 diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js index 20dfdebce1..1691220f51 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js @@ -38,7 +38,7 @@ expect(timeControl).toHaveAttrs({ 'role': 'slider', - 'title': 'video position', + 'title': 'Video position', 'aria-disabled': 'false' }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js index 8963adc287..d99ceee27a 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js @@ -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); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js index b07a441220..1c102d518c 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js @@ -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 () { diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index 4900d0c6d8..b088cd5a1c 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -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 * diff --git a/common/lib/xmodule/xmodule/js/src/video/04_video_control.js b/common/lib/xmodule/xmodule/js/src/video/04_video_control.js index f496ce7fd9..8c21568a0f 100644 --- a/common/lib/xmodule/xmodule/js/src/video/04_video_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/04_video_control.js @@ -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') }); } diff --git a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js index 7294a8bd4f..3016f0c8e5 100644 --- a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js +++ b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js @@ -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 = $('
', { - 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'); } }); diff --git a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js index f06588bd9e..291743d516 100644 --- a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js @@ -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'); } }); diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index a7520a8fc1..6fa88250ff 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -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 diff --git a/lms/templates/video.html b/lms/templates/video.html index 58c635c902..44657671e9 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -58,8 +58,7 @@