diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c958366b7d36553c9fb7de9c4b5811d77fb92611..00ab686c202488a69c5951c741f2c181704def4a 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,6 +5,8 @@ These are notable changes in edx-platform.  This is a rolling list of changes,
 in roughly chronological order, most recent first.  Add your entries at or near
 the top.  Include a label indicating the component affected.
 
+Blades: Allow multiple transcripts with video. BLD-642.
+
 CMS: Add feature to allow exporting a course to a git repository by
 specifying the giturl in the course settings.
 
diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
index b194e356bde59d235aa89e5ec60f81f36158cf31..9a590572a311ec8f523beb9cb3f70301e19ece9b 100644
--- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
+++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
 
     # Check if the web object is a list type
     # If so, we use a slightly different mechanism for determining its value
-    if setting.has_class('metadata-list-enum'):
+    if setting.has_class('metadata-list-enum') or setting.has_class('metadata-dict'):
         list_value = ', '.join(ele.value for ele in setting.find_by_css('.list-settings-item'))
         assert_equal(value, list_value)
     elif setting.has_class('metadata-videolist-enum'):
diff --git a/cms/djangoapps/contentstore/features/transcripts.feature b/cms/djangoapps/contentstore/features/transcripts.feature
index 72f2beff3a53acffe2d45f5f593a51779795d91c..bd0a6efc1b7b85d12a16a0e30112ddec53de7d17 100644
--- a/cms/djangoapps/contentstore/features/transcripts.feature
+++ b/cms/djangoapps/contentstore/features/transcripts.feature
@@ -1,6 +1,6 @@
 @shard_3
-Feature: CMS.Transcripts
-  As a course author, I want to be able to create video components.
+Feature: CMS Transcripts
+  As a course author, I want to be able to create video components
 
     # For transcripts acceptance tests there are 3 available caption
     # files. They can be used to test various transcripts features. Two of
@@ -72,7 +72,7 @@ Feature: CMS.Transcripts
         And I remove "t_not_exist" transcripts id from store
         And I enter a "http://youtu.be/t_not_exist" source to field number 1
         Then I see status message "not found"
-        And I see value "" in the field "HTML5 Transcript"
+        And I see value "" in the field "Transcript (primary)"
 
         # Import: w/o local but with server subs
         And I remove "t__eq_exist" transcripts id from store
@@ -83,7 +83,7 @@ Feature: CMS.Transcripts
         Then I see status message "found"
         And I see button "upload_new_timed_transcripts"
         And I see button "download_to_edit"
-        And I see value "t__eq_exist" in the field "HTML5 Transcript"
+        And I see value "t__eq_exist" in the field "Transcript (primary)"
 
     #4
     Scenario: Youtube id only: check "Found" state
@@ -92,7 +92,7 @@ Feature: CMS.Transcripts
 
         And I enter a "http://youtu.be/t_not_exist" source to field number 1
         Then I see status message "found"
-        And I see value "t_not_exist" in the field "HTML5 Transcript"
+        And I see value "t_not_exist" in the field "Transcript (primary)"
 
     #5
     Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are equal
@@ -102,7 +102,7 @@ Feature: CMS.Transcripts
 
         And I enter a "http://youtu.be/t__eq_exist" source to field number 1
         And I see status message "found"
-        And I see value "t__eq_exist" in the field "HTML5 Transcript"
+        And I see value "t__eq_exist" in the field "Transcript (primary)"
 
     #6
     Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are not  equal
@@ -114,7 +114,7 @@ Feature: CMS.Transcripts
         And I see button "replace"
         And I click transcript button "replace"
         And I see status message "found"
-        And I see value "t_neq_exist" in the field "HTML5 Transcript"
+        And I see value "t_neq_exist" in the field "Transcript (primary)"
 
     #7
     Scenario: html5 source only: check "Not Found" state
@@ -123,7 +123,7 @@ Feature: CMS.Transcripts
 
         And I enter a "t_not_exist.mp4" source to field number 1
         Then I see status message "not found"
-        And I see value "" in the field "HTML5 Transcript"
+        And I see value "" in the field "Transcript (primary)"
 
     #8
     Scenario: html5 source only: check "Found" state
@@ -132,7 +132,7 @@ Feature: CMS.Transcripts
 
         And I enter a "t_not_exist.mp4" source to field number 1
         Then I see status message "found"
-        And I see value "t_not_exist" in the field "HTML5 Transcript"
+        And I see value "t_not_exist" in the field "Transcript (primary)"
 
     #9
     Scenario: User sets youtube_id w/o server but with local subs and one html5 link w/o subs
@@ -144,7 +144,7 @@ Feature: CMS.Transcripts
 
         And I enter a "test_video_name.mp4" source to field number 2
         Then I see status message "found"
-        And I see value "t_not_exist" in the field "HTML5 Transcript"
+        And I see value "t_not_exist" in the field "Transcript (primary)"
 
     # Disabled 1/29/14 due to flakiness observed in master
     #10
@@ -160,7 +160,7 @@ Feature: CMS.Transcripts
     #
     #    And I enter a "t_not_exist.mp4" source to field number 2
     #    Then I see status message "found"
-    #    And I see value "t__eq_exist" in the field "HTML5 Transcript"
+    #    And I see value "t__eq_exist" in the field "Transcript (primary)"
 
     #11
     Scenario: User sets youtube_id w/o local but with server subs and one html5 link w/o transcripts w/o import action, then another one html5 link w/o transcripts
@@ -338,7 +338,7 @@ Feature: CMS.Transcripts
         Then I see status message "uploaded_successfully"
         And I see button "download_to_edit"
         And I see button "upload_new_timed_transcripts"
-        And I see value "t__eq_exist" in the field "HTML5 Transcript"
+        And I see value "t__eq_exist" in the field "Transcript (primary)"
 
         And I enter a "http://youtu.be/t_not_exist" source to field number 2
         Then I see status message "found"
@@ -359,7 +359,7 @@ Feature: CMS.Transcripts
         And I see button "upload_new_timed_transcripts"
         And I upload the transcripts file "test_transcripts.srt"
         Then I see status message "uploaded_successfully"
-        And I see value "test_transcripts" in the field "HTML5 Transcript"
+        And I see value "test_transcripts" in the field "Transcript (primary)"
 
         And I enter a "t_not_exist.webm" source to field number 2
         Then I see status message "replace"
@@ -367,7 +367,7 @@ Feature: CMS.Transcripts
         And I see choose button "test_transcripts.mp4" number 1
         And I see choose button "t_not_exist.webm" number 2
         And I click transcript button "choose" number 2
-        And I see value "test_transcripts|t_not_exist" in the field "HTML5 Transcript"
+        And I see value "test_transcripts|t_not_exist" in the field "Transcript (primary)"
 
     #21
     Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o transcripts - click on use existing - >  change it to another one HTML5 source w/o transcripts - click on use existing
@@ -378,7 +378,7 @@ Feature: CMS.Transcripts
         Then I see status message "found"
         And I see button "download_to_edit"
         And I see button "upload_new_timed_transcripts"
-        And I see value "t_not_exist" in the field "HTML5 Transcript"
+        And I see value "t_not_exist" in the field "Transcript (primary)"
 
         And I save changes
         And I edit the component
@@ -387,13 +387,13 @@ Feature: CMS.Transcripts
         Then I see status message "use existing"
         And I see button "use_existing"
         And I click transcript button "use_existing"
-        And I see value "video_name_2" in the field "HTML5 Transcript"
+        And I see value "video_name_2" in the field "Transcript (primary)"
 
         And I enter a "video_name_3.mp4" source to field number 1
         Then I see status message "use existing"
         And I see button "use_existing"
         And I click transcript button "use_existing"
-        And I see value "video_name_3" in the field "HTML5 Transcript"
+        And I see value "video_name_3" in the field "Transcript (primary)"
 
     #22
     Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - click on use existing ->  change it to another one HTML5 source w/o transcripts - do not click on use existing -> change it to another one HTML5 source w/o transcripts - click on use existing
@@ -404,7 +404,7 @@ Feature: CMS.Transcripts
         Then I see status message "found"
         And I see button "download_to_edit"
         And I see button "upload_new_timed_transcripts"
-        And I see value "t_not_exist" in the field "HTML5 Transcript"
+        And I see value "t_not_exist" in the field "Transcript (primary)"
 
         And I save changes
         And I edit the component
@@ -413,7 +413,7 @@ Feature: CMS.Transcripts
         Then I see status message "use existing"
         And I see button "use_existing"
         And I click transcript button "use_existing"
-        And I see value "video_name_2" in the field "HTML5 Transcript"
+        And I see value "video_name_2" in the field "Transcript (primary)"
 
         And I enter a "video_name_3.mp4" source to field number 1
         Then I see status message "use existing"
@@ -423,7 +423,7 @@ Feature: CMS.Transcripts
         Then I see status message "use existing"
         And I see button "use_existing"
         And I click transcript button "use_existing"
-        And I see value "video_name_4" in the field "HTML5 Transcript"
+        And I see value "video_name_4" in the field "Transcript (primary)"
 
     #23
     Scenario: Work with 2 fields: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - do not click on use existing ->  add another one HTML5 source w/o transcripts - click on use existing
@@ -446,7 +446,7 @@ Feature: CMS.Transcripts
         Then I see status message "use existing"
         And I see button "use_existing"
         And I click transcript button "use_existing"
-        And I see value "video_name_2|video_name_3" in the field "HTML5 Transcript"
+        And I see value "video_name_2|video_name_3" in the field "Transcript (primary)"
 
     #24 Uploading subtitles with different file name than file
     Scenario: File name and name of subs are different
@@ -457,7 +457,7 @@ Feature: CMS.Transcripts
         And I see status message "not found"
         And I upload the transcripts file "test_transcripts.srt"
         Then I see status message "uploaded_successfully"
-        And I see value "video_name_1" in the field "HTML5 Transcript"
+        And I see value "video_name_1" in the field "Transcript (primary)"
 
         And I save changes
         Then when I view the video it does show the captions
@@ -488,14 +488,14 @@ Feature: CMS.Transcripts
         And I see status message "not found"
         And I upload the transcripts file "test_transcripts.srt"
         Then I see status message "uploaded_successfully"
-        And I see value "video_name_1|video_name_2" in the field "HTML5 Transcript"
+        And I see value "video_name_1|video_name_2" in the field "Transcript (primary)"
 
         And I clear field number 1
         Then I see status message "found"
-        And I see value "video_name_2" in the field "HTML5 Transcript"
+        And I see value "video_name_2" in the field "Transcript (primary)"
 
     #27
-    Scenario: Upload button for single youtube id.
+    Scenario: Upload button for single youtube id
         Given I have created a Video component
         And I edit the component
 
@@ -512,7 +512,7 @@ Feature: CMS.Transcripts
         Then I see status message "found"
 
     #28
-    Scenario: Upload button for youtube id with html5 ids.
+    Scenario: Upload button for youtube id with html5 ids
         Given I have created a Video component
         And I edit the component
 
@@ -528,7 +528,7 @@ Feature: CMS.Transcripts
         Then I see status message "uploaded_successfully"
         And I clear field number 1
         Then I see status message "found"
-        And I see value "video_name_1" in the field "HTML5 Transcript"
+        And I see value "video_name_1" in the field "Transcript (primary)"
 
         And I save changes
         Then when I view the video it does show the captions
@@ -544,14 +544,14 @@ Feature: CMS.Transcripts
         Then I see status message "not found"
 
         And I open tab "Advanced"
-        And I set value "t_not_exist" to the field "HTML5 Transcript"
+        And I set value "t_not_exist" to the field "Transcript (primary)"
 
         And I save changes
         Then when I view the video it does show the captions
         And I edit the component
 
         Then I see status message "found"
-        And I see value "video_name_1" in the field "HTML5 Transcript"
+        And I see value "video_name_1" in the field "Transcript (primary)"
 
     #30
     Scenario: Check non-ascii (chinise) transcripts
@@ -576,7 +576,7 @@ Feature: CMS.Transcripts
         Then I see status message "not found"
 
         And I open tab "Advanced"
-        And I set value "t_not_exist" to the field "HTML5 Transcript"
+        And I set value "t_not_exist" to the field "Transcript (primary)"
         And I open tab "Basic"
         Then I see status message "found"
 
@@ -585,18 +585,20 @@ Feature: CMS.Transcripts
         And I edit the component
 
         Then I see status message "found"
-        And I see value "video_name_1" in the field "HTML5 Transcript"
+        And I see value "video_name_1" in the field "Transcript (primary)"
 
     #32
     Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible w/o saving
-        Given I have created a Video component with subtitles "t_not_exist"
+        Given I have created a Video component
         And I edit the component
 
         And I enter a "t_not_exist.mp4" source to field number 1
-        Then I see status message "found"
+        Then I see status message "not found"
+        And I upload the transcripts file "chinese_transcripts.srt"
+        Then I see status message "uploaded_successfully"
 
         And I open tab "Advanced"
-        And I set value "" to the field "HTML5 Transcript"
+        And I set value "" to the field "Transcript (primary)"
         And I open tab "Basic"
         Then I see status message "not found"
 
@@ -605,21 +607,24 @@ Feature: CMS.Transcripts
         And I edit the component
 
         Then I see status message "not found"
-        And I see value "" in the field "HTML5 Transcript"
+        And I see value "" in the field "Transcript (primary)"
 
     #33
     Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible with saving
-        Given I have created a Video component with subtitles "t_not_exist"
+        Given I have created a Video component
         And I edit the component
 
         And I enter a "t_not_exist.mp4" source to field number 1
-        Then I see status message "found"
+        Then I see status message "not found"
+        And I upload the transcripts file "chinese_transcripts.srt"
+        Then I see status message "uploaded_successfully"
 
         And I save changes
+        Then I see "好 各位同学" text in the captions
         And I edit the component
 
         And I open tab "Advanced"
-        And I set value "" to the field "HTML5 Transcript"
+        And I set value "" to the field "Transcript (primary)"
         And I open tab "Basic"
         Then I see status message "not found"
 
@@ -628,7 +633,7 @@ Feature: CMS.Transcripts
         And I edit the component
 
         Then I see status message "not found"
-        And I see value "" in the field "HTML5 Transcript"
+        And I see value "" in the field "Transcript (primary)"
 
     #34
     Scenario: Video with existing subs - Advanced tab - change to another one subs - Basic tab - Found message - Save - see correct subs
@@ -647,7 +652,7 @@ Feature: CMS.Transcripts
         And I edit the component
 
         And I open tab "Advanced"
-        And I set value "t_not_exist" to the field "HTML5 Transcript"
+        And I set value "t_not_exist" to the field "Transcript (primary)"
         And I open tab "Basic"
         Then I see status message "found"
 
@@ -670,7 +675,7 @@ Feature: CMS.Transcripts
         And I edit the component
 
         And I open tab "Advanced"
-        And I revert the transcript field "HTML5 Transcript"
+        And I revert the transcript field "Transcript (primary)"
 
         And I save changes
         Then when I view the video it does not show the captions
@@ -686,7 +691,7 @@ Feature: CMS.Transcripts
         And I see status message "not found"
         And I upload the transcripts file "test_transcripts.srt"
         Then I see status message "uploaded_successfully"
-        And I see value "video_name_1.1.2" in the field "HTML5 Transcript"
+        And I see value "video_name_1.1.2" in the field "Transcript (primary)"
 
         And I save changes
         Then when I view the video it does show the captions
diff --git a/cms/djangoapps/contentstore/features/transcripts.py b/cms/djangoapps/contentstore/features/transcripts.py
index 695c96c8b880fc659dc5428886fe5cfb95d660e0..41903afccfa062e2c5196409c7c8ff98edbf682c 100644
--- a/cms/djangoapps/contentstore/features/transcripts.py
+++ b/cms/djangoapps/contentstore/features/transcripts.py
@@ -103,7 +103,7 @@ def i_do_not_see_error_message(_step):
 
 @step('I see error message "([^"]*)"$')
 def i_see_error_message(_step, error):
-    assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error.strip()])
+    assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error])
 
 
 @step('I do not see status message$')
@@ -114,7 +114,7 @@ def i_do_not_see_status_message(_step):
 @step('I see status message "([^"]*)"$')
 def i_see_status_message(_step, status):
     assert not world.css_visible(SELECTORS['error_bar'])
-    assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status.strip()])
+    assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status])
 
     DOWNLOAD_BUTTON = TRANSCRIPTS_BUTTONS["download_to_edit"][0]
     if world.is_css_present(DOWNLOAD_BUTTON, wait_time=1) \
diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature
index 07c298b0d214b94e3e73786a9a9ec59b4eec12ac..1c12915e91cecd024172704028acf5310d8e977e 100644
--- a/cms/djangoapps/contentstore/features/video-editor.feature
+++ b/cms/djangoapps/contentstore/features/video-editor.feature
@@ -1,6 +1,6 @@
 @shard_3
-Feature: CMS.Video Component Editor
-  As a course author, I want to be able to create video components.
+Feature: CMS Video Component Editor
+  As a course author, I want to be able to create video components
 
   Scenario: User can view Video metadata
     Given I have created a Video component
@@ -17,14 +17,14 @@ Feature: CMS.Video Component Editor
 
   # Sauce Labs cannot delete cookies
   @skip_sauce
-  Scenario: Captions are hidden when "show captions" is false
+  Scenario: Captions are hidden when "transcript display" is false
     Given I have created a Video component with subtitles
-    And I have set "show transcript" to False
+    And I have set "transcript display" to False
     Then when I view the video it does not show the captions
 
   # Sauce Labs cannot delete cookies
   @skip_sauce
-  Scenario: Captions are shown when "show captions" is true
+  Scenario: Captions are shown when "transcript display" is true
     Given I have created a Video component with subtitles
-    And I have set "show transcript" to True
+    And I have set "transcript display" to True
     Then when I view the video it does show the captions
diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py
index b8c74be80af9c9c045e08c2aac8a87ff2100ebf9..ea90993d67a7b7b2f693f8336f4180ec3d87a109 100644
--- a/cms/djangoapps/contentstore/features/video-editor.py
+++ b/cms/djangoapps/contentstore/features/video-editor.py
@@ -5,7 +5,7 @@ from lettuce import world, step
 from terrain.steps import reload_the_page
 
 
-@step('I have set "show transcript" to (.*)$')
+@step('I have set "transcript display" to (.*)$')
 def set_show_captions(step, setting):
     # Prevent cookies from overriding course settings
     world.browser.cookies.delete('hide_captions')
@@ -13,7 +13,7 @@ def set_show_captions(step, setting):
     world.css_click('a.edit-button')
     world.wait_for(lambda _driver: world.css_visible('a.save-button'))
     world.click_link_by_text('Advanced')
-    world.browser.select('Show Transcript', setting)
+    world.browser.select('Transcript Display', setting)
     world.css_click('a.save-button')
 
 
@@ -42,10 +42,11 @@ def correct_video_settings(_step):
         ['Display Name', 'Video', False],
         ['Download Transcript', '', False],
         ['End Time', '00:00:00', False],
-        ['HTML5 Transcript', '', False],
-        ['Show Transcript', 'True', False],
         ['Start Time', '00:00:00', False],
+        ['Transcript (primary)', '', False],
+        ['Transcript Display', 'True', False],
         ['Transcript Download Allowed', 'False', False],
+        ['Transcript Translations', '', False],
         ['Video Download Allowed', 'False', False],
         ['Video Sources', '', False],
         ['Youtube ID', 'OEoXaMPEzfM', False],
diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature
index 2ead8987e9633d4075635ac9ff59f70335c28432..fc70ee3c145ca609af85d1751055e5e2d447861f 100644
--- a/cms/djangoapps/contentstore/features/video.feature
+++ b/cms/djangoapps/contentstore/features/video.feature
@@ -1,6 +1,6 @@
 @shard_3
-Feature: CMS.Video Component
-  As a course author, I want to be able to view my created videos in Studio.
+Feature: CMS Video Component
+  As a course author, I want to be able to view my created videos in Studio
 
   # 1
   # Video Alpha Features will work in Firefox only when Firefox is the active window
@@ -43,38 +43,6 @@ Feature: CMS.Video Component
     Then the correct Youtube video is shown
 
   # 7
-  Scenario: Closed captions become visible when the mouse hovers over CC button
-    Given I have created a Video component with subtitles
-    And Make sure captions are closed
-    Then Captions become "invisible"
-    And I hover over button "CC"
-    Then Captions become "visible"
-    And I hover over button "volume"
-    Then Captions become "invisible"
-
-  # 8
-  # Disabled 11/26 due to flakiness in master.
-  # Enabled back on 11/29.
-  Scenario: Open captions never become invisible
-    Given I have created a Video component with subtitles
-    And Make sure captions are open
-    Then Captions are "visible"
-    And I hover over button "CC"
-    Then Captions are "visible"
-    And I hover over button "volume"
-    Then Captions are "visible"
-
-  # 9
-  # Disabled 11/26 due to flakiness in master.
-  # Enabled back on 11/29.
-  Scenario: Closed captions are invisible when mouse doesn't hover on CC button
-    Given I have created a Video component with subtitles
-    And Make sure captions are closed
-    Then Captions become "invisible"
-    And I hover over button "volume"
-    Then Captions are "invisible"
-
-  # 10
   # Disabled 11/26 due to flakiness in master.
   # Enabled back on 11/29.
   Scenario: When enter key is pressed on a caption shows an outline around it
@@ -84,7 +52,7 @@ Feature: CMS.Video Component
     Then I press "enter" button on caption line with data-index "0"
     And I see caption line with data-index "0" has class "focused"
 
-  # 11
+  # 8
   Scenario: When start end end times are specified, a range on slider is shown
     Given I have created a Video component with subtitles
     And Make sure captions are closed
diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py
index 2dc4bccfb3977d983604e496e9f90b46d189b224..b3777aa57de2092726186307319b1e2d1652953b 100644
--- a/cms/djangoapps/contentstore/features/video.py
+++ b/cms/djangoapps/contentstore/features/video.py
@@ -56,6 +56,13 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
     world.visit(video_url)
 
     world.wait_for_xmodule()
+
+    # update .sub filed with proper subs name (which mimics real Studio/XML behavior)
+    # this is needed only for that videos which are created in acceptance tests.
+    _step.given('I edit the component')
+    world.wait_for_ajax_complete()
+    _step.given('I save changes')
+
     world.disable_jquery_animations()
 
     world.wait_for_present('.is-initialized')
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index c19c4d198c2f175d2caf3e5e87b1004c286c6f00..8d44b98d95d12a4fe1caf183d423ef575e896515 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -492,15 +492,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
         self.assertContains(resp, 'i4x://edX/toy/video/video_with_end_time')
         self.assertContains(resp, 'i4x://edX/toy/poll_question/T1_changemind_poll_foo_2')
 
-    def test_video_module_caption_asset_path(self):
-        """
-        This verifies that a video caption url is as we expect it to be
-        """
-        resp = self._test_preview(Location('i4x', 'edX', 'toy', 'video', 'sample_video', None))
-        self.assertEquals(resp.status_code, 200)
-        content = json.loads(resp.content)
-        self.assertIn('data-caption-asset-path="/c4x/edX/toy/asset/subs_"', content['html'])
-
     def _test_preview(self, location):
         """ Preview test case. """
         direct_store = modulestore('direct')
diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
index a34927e3ef6001b594e51f315b49c18cedd75d32..c0c179abb863f0ae56e029bfc706d33eeb9118aa 100644
--- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
@@ -3,11 +3,13 @@ import unittest
 from uuid import uuid4
 import copy
 import textwrap
+from mock import patch, Mock
 
 from pymongo import MongoClient
 
 from django.test.utils import override_settings
 from django.conf import settings
+from django.utils import translation
 
 from nose.plugins.skip import SkipTest
 
@@ -16,7 +18,7 @@ from xmodule.contentstore.content import StaticContent
 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
 from xmodule.exceptions import NotFoundError
 from xmodule.contentstore.django import contentstore, _CONTENTSTORE
-from contentstore import transcripts_utils
+from xmodule.video_module import transcripts_utils
 
 from contentstore.tests.modulestore_config import TEST_MODULESTORE
 TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
@@ -188,20 +190,29 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
 
     def test_success_downloading_subs(self):
 
-        # Disabled 11/14/13
-        # This test is flakey because it performs an HTTP request on an external service
-        # Re-enable when `requests.get` is patched using `mock.patch`
-        raise SkipTest
-
+        response = textwrap.dedent("""<?xml version="1.0" encoding="utf-8" ?>
+                <transcript>
+                    <text start="0" dur="0.27"></text>
+                    <text start="0.27" dur="2.45">Test text 1.</text>
+                    <text start="2.72">Test text 2.</text>
+                    <text start="5.43" dur="1.73">Test text 3.</text>
+                </transcript>
+        """)
         good_youtube_subs = {
-            0.5: 'JMD_ifUUfsU',
-            1.0: 'hI10vDNYz4M',
-            2.0: 'AKqURZnYqpk'
+            0.5: 'good_id_1',
+            1.0: 'good_id_2',
+            2.0: 'good_id_3'
         }
         self.clear_subs_content(good_youtube_subs)
 
-        # Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
-        transcripts_utils.download_youtube_subs(good_youtube_subs, self.course)
+        with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get:
+            mock_get.return_value = Mock(status_code=200, text=response, content=response)
+            # Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
+            transcripts_utils.download_youtube_subs(good_youtube_subs, self.course, settings)
+
+        mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_1'})
+        mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_2'})
+        mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_3'})
 
         # Check assets status after importing subtitles.
         for subs_id in good_youtube_subs.values():
@@ -226,12 +237,10 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
         self.assertEqual(html5_ids[2], 'baz.1.4')
         self.assertEqual(html5_ids[3], 'foo')
 
-    def test_fail_downloading_subs(self):
+    @patch('xmodule.video_module.transcripts_utils.requests.get')
+    def test_fail_downloading_subs(self, mock_get):
 
-        # Disabled 11/14/13
-        # This test is flakey because it performs an HTTP request on an external service
-        # Re-enable when `requests.get` is patched using `mock.patch`
-        raise SkipTest
+        mock_get.return_value = Mock(status_code=404, text='Error 404')
 
         bad_youtube_subs = {
             0.5: 'BAD_YOUTUBE_ID1',
@@ -239,9 +248,8 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
             2.0: 'BAD_YOUTUBE_ID3'
         }
         self.clear_subs_content(bad_youtube_subs)
-
         with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
-            transcripts_utils.download_youtube_subs(bad_youtube_subs, self.course)
+            transcripts_utils.download_youtube_subs(bad_youtube_subs, self.course, settings)
 
         # Check assets status after importing subtitles.
         for subs_id in bad_youtube_subs.values():
@@ -267,7 +275,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
         self.clear_subs_content(good_youtube_subs)
 
         # Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
-        transcripts_utils.download_youtube_subs(good_youtube_subs, self.course)
+        transcripts_utils.download_youtube_subs(good_youtube_subs, self.course, settings)
 
         # Check assets status after importing subtitles.
         for subs_id in good_youtube_subs.values():
@@ -438,3 +446,43 @@ class TestGenerateSrtFromSjson(TestDownloadYoutubeSubs):
         }
         srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 1)
         self.assertFalse(srt_subs)
+
+
+class TestYoutubeTranscripts(unittest.TestCase):
+    """
+    Tests for checking right datastructure returning when using youtube api.
+    """
+    @patch('xmodule.video_module.transcripts_utils.requests.get')
+    def test_youtube_bad_status_code(self, mock_get):
+        mock_get.return_value = Mock(status_code=404, text='test')
+        youtube_id = 'bad_youtube_id'
+        with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
+            transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
+
+    @patch('xmodule.video_module.transcripts_utils.requests.get')
+    def test_youtube_empty_text(self, mock_get):
+        mock_get.return_value = Mock(status_code=200, text='')
+        youtube_id = 'bad_youtube_id'
+        with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
+            transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
+
+    def test_youtube_good_result(self):
+        response = textwrap.dedent("""<?xml version="1.0" encoding="utf-8" ?>
+                <transcript>
+                    <text start="0" dur="0.27"></text>
+                    <text start="0.27" dur="2.45">Test text 1.</text>
+                    <text start="2.72">Test text 2.</text>
+                    <text start="5.43" dur="1.73">Test text 3.</text>
+                </transcript>
+        """)
+        expected_transcripts = {
+            'start': [270, 2720, 5430],
+            'end': [2720, 2720, 7160],
+            'text': ['Test text 1.', 'Test text 2.', 'Test text 3.']
+        }
+        youtube_id = 'good_youtube_id'
+        with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get:
+            mock_get.return_value = Mock(status_code=200, text=response, content=response)
+            transcripts = transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
+        self.assertEqual(transcripts, expected_transcripts)
+        mock_get.assert_called_with('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_youtube_id'})
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 80548a27a72977dbadf2d67932b650642250b30c..4945a0e6f2bd3e92bc5d46904c8593175643263b 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -24,12 +24,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
 from xmodule.modulestore.inheritance import own_metadata
 from xmodule.modulestore.locator import BlockUsageLocator
 from xmodule.modulestore import Location
+from xmodule.video_module import manage_video_subtitles_save
 
 from util.json_request import expect_json, JsonResponse
 from util.string_utils import str_to_bool
 
-from ..transcripts_utils import manage_video_subtitles_save
-
 from ..utils import get_modulestore
 
 from .access import has_course_access
@@ -251,6 +250,8 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
         log.error("Can't find item by location.")
         return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
 
+    old_metadata = own_metadata(existing_item)
+
     if publish:
         if publish == 'make_private':
             _xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
@@ -299,7 +300,7 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
                     field.write_to(existing_item, value)
 
         if existing_item.category == 'video':
-            manage_video_subtitles_save(existing_item, existing_item, request.user)
+            manage_video_subtitles_save(existing_item, request.user, old_metadata, generate_translation=True)
 
     # commit to datastore
     store.update_item(existing_item, request.user.id)
diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py
index f92e8e7f216fc07a42117c195f23171006fd87c4..9722bbfa0664dc5aa69a85bf8e0d2e39a58d313c 100644
--- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py
+++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py
@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse
 from django.test.utils import override_settings
 from django.conf import settings
 
-from contentstore import transcripts_utils
+from xmodule.video_module import transcripts_utils
 from contentstore.tests.utils import CourseTestCase
 from cache_toolbox.core import del_cached_content
 from xmodule.modulestore.django import modulestore
diff --git a/cms/djangoapps/contentstore/views/transcripts_ajax.py b/cms/djangoapps/contentstore/views/transcripts_ajax.py
index 01535f6021538860dac82d30050af03af0dc0857..d30e2a29c4e7dfa142517e193962d1dd17c22493 100644
--- a/cms/djangoapps/contentstore/views/transcripts_ajax.py
+++ b/cms/djangoapps/contentstore/views/transcripts_ajax.py
@@ -26,12 +26,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
 from util.json_request import JsonResponse
 from xmodule.modulestore.locator import BlockUsageLocator
 
-from ..transcripts_utils import (
+from xmodule.video_module.transcripts_utils import (
     generate_subs_from_source,
     generate_srt_from_sjson, remove_subs_from_store,
     download_youtube_subs, get_transcripts_from_youtube,
     copy_or_rename_transcript,
-    save_module,
     manage_video_subtitles_save,
     TranscriptsGenerationException,
     GetTranscriptsFromYouTubeException,
@@ -136,7 +135,7 @@ def upload_transcripts(request):
                 return error_response(response, "Can't find transcripts in storage for {}".format(sub_attr))
 
         item.sub = selected_name  # write one of  new subtitles names to item.sub attribute.
-        save_module(item, request.user)
+        item.save_with_metadata(request.user)
         response['subs'] = item.sub
         response['status'] = 'Success'
     else:
@@ -272,7 +271,11 @@ def check_transcripts(request):
         #check youtube local and server transcripts for equality
         if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']:
             try:
-                youtube_server_subs = get_transcripts_from_youtube(youtube_id)
+                youtube_server_subs = get_transcripts_from_youtube(
+                    youtube_id,
+                    settings,
+                    item.runtime.service(item, "i18n")
+                )
                 if json.loads(local_transcripts) == youtube_server_subs:  # check transcripts for equality
                     transcripts_presence['youtube_diff'] = False
             except GetTranscriptsFromYouTubeException:
@@ -389,7 +392,7 @@ def choose_transcripts(request):
 
     if item.sub != html5_id:  # update sub value
         item.sub = html5_id
-        save_module(item, request.user)
+        item.save_with_metadata(request.user)
     response = {'status': 'Success',  'subs': item.sub}
     return JsonResponse(response)
 
@@ -415,12 +418,12 @@ def replace_transcripts(request):
         return error_response(response, 'YouTube id {} is not presented in request data.'.format(youtube_id))
 
     try:
-        download_youtube_subs({1.0: youtube_id}, item)
+        download_youtube_subs({1.0: youtube_id}, item, settings)
     except GetTranscriptsFromYouTubeException as e:
         return error_response(response, e.message)
 
     item.sub = youtube_id
-    save_module(item, request.user)
+    item.save_with_metadata(request.user)
     response = {'status': 'Success',  'subs': item.sub}
     return JsonResponse(response)
 
@@ -519,10 +522,10 @@ def save_transcripts(request):
         for metadata_key, value in metadata.items():
             setattr(item, metadata_key, value)
 
-        save_module(item, request.user)  # item becomes updated with new values
+        item.save_with_metadata(request.user)  # item becomes updated with new values
 
         if new_sub:
-            manage_video_subtitles_save(None, item, request.user)
+            manage_video_subtitles_save(item, request.user)
         else:
             # If `new_sub` is empty, it means that user explicitly does not want to use
             # transcripts for current video ids and we remove all transcripts from storage.
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 23b7de10a5dbcf56216b493751b2610e97712b2a..8b619013279bd5e959f946cc9b31fb26660b7420 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -26,7 +26,9 @@ Longer TODO:
 
 import sys
 import lms.envs.common
-from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, enable_microsites
+from lms.envs.common import (
+    USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, enable_microsites, ALL_LANGUAGES
+)
 from path import path
 
 from lms.lib.xblock.mixin import LmsBlockMixin
diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss
index 5f4e9d1063d9992cfcc3f3ccc79800ef750f7b90..5fc6486698fe68f1056723ecae65603bcaf0dcb7 100644
--- a/common/lib/xmodule/xmodule/css/video/display.scss
+++ b/common/lib/xmodule/xmodule/css/video/display.scss
@@ -164,6 +164,37 @@ div.video {
         }
       }
 
+      %video-button {
+        @include transition(none);
+        display: block;
+        font-weight: 800;
+        line-height: 46px;
+        margin: 0;
+        padding: 0 0 0 15px;
+        text-indent: -9999px;
+        -webkit-font-smoothing: antialiased;
+        box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
+        color: #fff;
+        cursor: pointer;
+        border-width: 0 1px;
+        border-style: solid;
+        border-color: #000;
+
+        &:hover {
+          background-color: #444;
+          color: #fff;
+          text-decoration: none;
+          outline: 0;
+        }
+
+        &:active,
+        &:focus {
+          color: #fff;
+          background-color: #444;
+          text-decoration: none;
+        }
+      }
+
       div.slider {
         @include clearfix();
         @include transform(scaleY(0.5) translate3d(0, 50%, 0));
@@ -230,48 +261,33 @@ div.video {
           margin-bottom: 0;
 
           a {
-            border-bottom: none;
-            border-right: 1px solid #000;
+            @extend %video-button;
+            background-image: url('../images/vcr.png');
+            background-position: 15px 15px ;
+            background-repeat: no-repeat;
+            border-left: none;
             box-shadow: 1px 0 0 #555;
-            cursor: pointer;
-            display: block;
-            line-height: 46px;
             padding: 0 lh(.75);
-            text-indent: -9999px;
             width: 14px;
-            background: url('../images/vcr.png') 15px 15px no-repeat;
 
             &:focus {
               position: relative;
               z-index: 10000;
               outline: #fff dotted thin;
               outline-offset: -2px;
-              background: #333;
-            }
-
-            &:hover {
-              outline: 0;
             }
 
             &:empty {
               height: 46px;
-              background: url('../images/vcr.png') 15px 15px no-repeat;
+              background-position: 15px 15px;
             }
 
             &.play {
               background-position: 17px -114px;
-
-              &:hover {
-                background-color: #444;
-              }
             }
 
             &.pause {
               background-position: 16px -50px;
-
-              &:hover {
-                background-color: #444;
-              }
             }
           }
 
@@ -301,16 +317,12 @@ div.video {
           }
         }
 
-        div.speeds {
+        .menu-container {
           float: left;
           position: relative;
 
           &.open {
-            & > a {
-              background: url('../images/open-arrow.png') 10px center no-repeat;
-            }
-
-            ol.video_speeds {
+            .menu {
               display: block;
               opacity: 1;
               padding: 0;
@@ -319,89 +331,15 @@ div.video {
             }
           }
 
-          & > a {
-            @include clearfix();
-            @include transition(none);
-            background: url('../images/closed-arrow.png') 10px center no-repeat;
-            border-left: 1px solid #000;
-            border-right: 1px solid #000;
-            box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
-            color: #fff;
-            cursor: pointer;
-            display: block;
-            line-height: 46px; //height of play pause buttons
-            margin-right: 0;
-            padding-left: 15px;
-            position: relative;
-            -webkit-font-smoothing: antialiased;
-            min-width: 116px;
-
-            @media (max-width: 1024px) {
-              min-width: 0;
-              width: 86px;
-            }
-
-            h3 {
-              display: block;
-
-              @media (max-width: 1024px) {
-                display: none;
-              }
-            }
-
-            &:hover {
-              outline: 0;
-              opacity: 1;
-              background-color: #444;
-            }
-
-            &:active {
-              opacity: 1;
-              background-color: #444;
-            }
-
-            h3 {
-              color: #999;
-              float: left;
-              font-size: em(14);
-              font-weight: normal;
-              letter-spacing: 1px;
-              padding: 0 lh(.25) 0 lh(.5);
-              line-height: 46px;
-              text-transform: uppercase;
-            }
-
-            p.active {
-              float: left;
-              font-weight: bold;
-              margin-bottom: 0;
-              padding: 0 lh(.5) 0 0;
-
-              @media (max-width: 1024px) {
-                padding: 0 lh(.5) 0 lh(.5);
-              }
-
-              line-height: 46px;
-              color: #fff;
-            }
-          }
-
-          // fix for now
-          ol.video_speeds {
+          .menu {
             @include transition(none);
-            box-shadow: inset 1px 0 0 #555, 0 4px 0 #444;
+            box-shadow: inset 1px 0 0 #555, 0 1px 0 #444;
             background-color: #444;
             border: 1px solid #000;
             bottom: 46px;
             display: none;
             opacity: 0;
             position: absolute;
-            width: 131px;
-
-            @media (max-width: 1024px) {
-              width: 101px;
-            }
-
             z-index: 10;
 
             li {
@@ -415,6 +353,9 @@ div.video {
                 color: #fff;
                 display: block;
                 padding: lh(.5);
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
 
                 &:hover {
                   background-color: #666;
@@ -423,8 +364,10 @@ div.video {
                 }
               }
 
-              &.active {
-                font-weight: bold;
+              &.active{
+                a {
+                  font-weight: bold;
+                }
               }
 
               &:last-child {
@@ -436,6 +379,66 @@ div.video {
           }
         }
 
+        div.speeds {
+          &.open {
+            & > a {
+              background-image: url('../images/open-arrow.png');
+            }
+          }
+
+          .menu{
+            width: 131px;
+
+            @media (max-width: 1024px) {
+              width: 101px;
+            }
+          }
+
+          & > a {
+            @extend %video-button;
+            @include clearfix();
+            background-image: url('../images/closed-arrow.png');
+            background-position: 10px center;
+            background-repeat: no-repeat;
+            min-width: 116px;
+            text-indent: 0;
+
+            @media (max-width: 1024px) {
+              min-width: 0;
+              width: 86px;
+            }
+
+            h3 {
+              float: left;
+              font-size: em(14);
+              font-weight: normal;
+              letter-spacing: 1px;
+              padding: 0 lh(.25) 0 lh(.5);
+              line-height: 46px;
+              text-transform: uppercase;
+              color: #999;
+
+              @media (max-width: 1024px) {
+                display: none;
+              }
+            }
+
+            p.active {
+              float: left;
+              font-weight: bold;
+              margin-bottom: 0;
+              padding: 0 lh(.5) 0 0;
+
+              @media (max-width: 1024px) {
+                padding: 0 lh(.5) 0 lh(.5);
+              }
+
+              line-height: 46px;
+              color: #fff;
+            }
+          }
+        }
+
         div.volume {
           float: left;
           position: relative;
@@ -454,29 +457,14 @@ div.video {
           }
 
           & > a {
+            @extend %video-button;
             @include clearfix();
-            @include transition(none);
             background-image: url('../images/volume.png');
             background-position: 10px center;
             background-repeat: no-repeat;
-            border-right: 1px solid #000;
-            box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
-            color: #fff;
-            cursor: pointer;
-            display: block;
-            height: 46px;
-            margin-right: 0;
-            padding-left: 15px;
-            position: relative;
-            -webkit-font-smoothing: antialiased;
+            border-left: none;
             width: 30px;
-
-            &:hover, &:active {
-              background-color: #444;
-              color: #fff;
-              text-decoration: none;
-              outline: 0;
-            }
+            height: 46px;
           }
 
           .volume-slider-container {
@@ -523,49 +511,24 @@ div.video {
         }
 
         a.add-fullscreen {
-          @include transition(none);
+          @extend %video-button;
           background: url(../images/fullscreen.png) center no-repeat;
-          border-right: 1px solid #000;
-          box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
-          color: #797979;
-          display: block;
+          border-left: none;
           float: left;
-          line-height: 46px; //height of play pause buttons
-          margin-left: 0;
-          padding: 0 lh(.5);
-          text-indent: -9999px;
+          padding: 0 11px;
           width: 30px;
-
-          &:hover, &:active {
-            background-color: #444;
-            color: #fff;
-            text-decoration: none;
-            outline: 0;
-          }
         }
 
 
         a.quality_control {
-          @include transition(none);
+          @extend %video-button;
           background: url(../images/hd.png) center no-repeat;
-          border-right: 1px solid #000;
-          box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
-          color: #797979;
+          border-left: none;
           display: none;
           float: left;
-          line-height: 46px; //height of play pause buttons
-          margin-left: 0;
-          padding: 0 lh(.5);
-          text-indent: -9999px;
+          padding: 0 11px;
           width: 30px;
 
-          &:hover {
-            background-color: #444;
-            color: #fff;
-            text-decoration: none;
-            outline: 0;
-          }
-
           &.active {
             background-color: #F44;
             color: #0ff;
@@ -574,33 +537,26 @@ div.video {
           }
         }
 
+        div.lang {
+          & > a.hide-subtitles {
+            @extend %video-button;
+            @include transition(none);
+            box-shadow: inset 1px 0 0 #555;
+            background: url('../images/cc.png') center no-repeat;
+            border-left: none;
+            border-right: none;
+            padding: 0 11px;
+            width: 30px;
 
-        a.hide-subtitles {
-          @include transition(none);
-          background: url('../images/cc.png') center no-repeat;
-          float: left;
-          font-weight: 800;
-          line-height: 46px; //height of play pause buttons
-          margin-left: 0;
-          opacity: 1;
-          padding: 0 lh(.5);
-          position: relative;
-          text-indent: -9999px;
-          -webkit-font-smoothing: antialiased;
-          width: 30px;
-
-          &:hover {
-            background-color: #444;
-            color: #fff;
-            text-decoration: none;
-            outline: 0;
+            &.off {
+              opacity: 0.7;
+            }
           }
 
-          &.off {
-            opacity: 0.7;
+          .menu.langs-list {
+            right: -1px;
+            width: 150px;
           }
-
-          color: #797979;
         }
       }
     }
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html
index 89b4e360c6ec4d7f69c5de56f7856133dd6a99f0..c08b544bc1916325b030a938314f0aec3196fad6 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video.html
@@ -11,7 +11,10 @@
         data-start=""
         data-end=""
         data-saved-video-position="0"
-        data-caption-asset-path="/static/subs/"
+        data-transcript-language="en"
+        data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+        data-transcript-translation-url="/transcript/translation"
+        data-transcript-available-translations-url="/transcript/available_translations"
         data-autoplay="False"
         data-yt-test-timeout="1500"
         data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
@@ -51,7 +54,9 @@
                   </div>
                   <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
                   <a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
-                  <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
+                  <div class="lang menu-container">
+                    <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
+                  </div>
                 </div>
               </div>
             </section>
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html
index 982aca02328b191c270d3b0309c49e97b36f0285..7a19c951a6e57d01df620be4aac1de8989fde325 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html
@@ -10,7 +10,10 @@
         data-start=""
         data-end=""
         data-saved-video-position="0"
-        data-caption-asset-path="/static/subs/"
+        data-transcript-language="en"
+        data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+        data-transcript-translation-url="/transcript/translation"
+        data-transcript-available-translations-url="/transcript/available_translations"
         data-sub="Z5KLxerq05Y"
         data-mp4-source="xmodule/include/fixtures/test.mp4"
         data-webm-source="xmodule/include/fixtures/test.webm"
@@ -54,7 +57,9 @@
                   </div>
                   <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
                   <a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
-                  <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
+                  <div class="lang menu-container">
+                    <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
+                  </div>
                 </div>
               </div>
             </section>
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
index c3c61ba3982e69f18e34edbe7d44514363f081e8..c330a0fb8f69db3e79cefb6da7de97e02ffa11b9 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
@@ -10,7 +10,10 @@
         data-start=""
         data-end=""
         data-saved-video-position="0"
-        data-caption-asset-path="/static/subs/"
+        data-transcript-language="en"
+        data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+        data-transcript-translation-url="/transcript/translation"
+        data-transcript-available-translations-url="/transcript/available_translations"
         data-sub="Z5KLxerq05Y"
         data-mp4-source="xmodule/include/fixtures/test.mp4"
         data-webm-source="xmodule/include/fixtures/test.webm"
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
index ee98a3992e0946647431a2812992aa203d6808c0..1a28833f5624c7624388a4d0633e49b3ca6e24c5 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
@@ -11,7 +11,10 @@
         data-start=""
         data-end=""
         data-saved-video-position="0"
-        data-caption-asset-path="/static/subs/"
+        data-transcript-language="en"
+        data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+        data-transcript-translation-url="/transcript/translation"
+        data-transcript-available-translations-url="/transcript/available_translations"
         data-autoplay="False"
         data-yt-test-timeout="1500"
         data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
index 33d304688b8531f1b96c0e3bb9d2c0fcc00262ae..df512bd7ab6e11a8651111eea263a3a1a92be6f3 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
@@ -11,7 +11,10 @@
         data-start=""
         data-end=""
         data-saved-video-position="0"
-        data-caption-asset-path="/static/subs/"
+        data-transcript-language="en"
+        data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+        data-transcript-translation-url="/transcript/translation"
+        data-transcript-available-translations-url="/transcript/available_translations"
         data-autoplay="False"
         data-yt-test-timeout="1500"
         data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
@@ -51,7 +54,9 @@
                   </div>
                   <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
                   <a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
-                  <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
+                  <div class="lang menu-container">
+                    <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
+                  </div>
                 </div>
               </div>
             </section>
@@ -73,9 +78,13 @@
         class="video"
         data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
         data-show-captions="true"
+        data-speed="1.0"
         data-start=""
         data-end=""
-        data-caption-asset-path="/static/subs/"
+        data-transcript-language="en"
+        data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+        data-transcript-translation-url="/transcript/translation"
+        data-transcript-available-translations-url="/transcript/available_translations"
         data-autoplay="False"
         data-yt-test-timeout="1500"
         data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
@@ -112,7 +121,9 @@
                   </div>
                   <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
                   <a href="#" class="quality_control" title="HD">HD</a>
-                  <a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
+                  <div class="lang menu-container">
+                    <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
+                  </div>
                 </div>
               </div>
             </section>
@@ -132,9 +143,13 @@
         class="video"
         data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
         data-show-captions="true"
+        data-speed="1.0"
         data-start=""
         data-end=""
-        data-caption-asset-path="/static/subs/"
+        data-transcript-language="en"
+        data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+        data-transcript-translation-url="/transcript/translation"
+        data-transcript-available-translations-url="/transcript/available_translations"
         data-autoplay="False"
         data-yt-test-timeout="1500"
         data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
@@ -171,7 +186,9 @@
                   </div>
                   <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
                   <a href="#" class="quality_control" title="HD">HD</a>
-                  <a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
+                  <div class="lang menu-container">
+                    <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
+                  </div>
                 </div>
               </div>
             </section>
diff --git a/common/lib/xmodule/xmodule/js/spec/helper.js b/common/lib/xmodule/xmodule/js/spec/helper.js
index 59fe9af25e704971d55943051b1727338b7b9b1f..9847f4f27b0e43d4a21975024ee4ecfa4eacf93e 100644
--- a/common/lib/xmodule/xmodule/js/spec/helper.js
+++ b/common/lib/xmodule/xmodule/js/spec/helper.js
@@ -1,6 +1,4 @@
 (function ($, undefined) {
-    var oldAjaxWithPrefix = $.ajaxWithPrefix;
-
     // Stub YouTube API.
     window.YT = {
         Player: function () {
@@ -63,42 +61,6 @@
         ]
     };
 
-    // For our purposes, we need to make sure that the function
-    // $.ajaxWithPrefix does not fail when during tests a captions file is
-    // requested. It is originally defined in file:
-    //
-    //     common/static/coffee/src/ajax_prefix.js
-    //
-    // We will replace it with a function that does:
-    //
-    //     1.) Return a hard coded captions object if the file name contains
-    //         'Z5KLxerq05Y'.
-    //     2.) Behaves the same a as the original function in all other cases.
-    $.ajaxWithPrefix = function (url, settings) {
-        var data, success;
-
-        if (!settings) {
-            settings = url;
-            url = settings.url;
-            success = settings.success;
-            data = settings.data;
-        }
-
-        if (
-            url.match(/Z5KLxerq05Y/g) ||
-            url.match(/7tqY6eQzVhE/g) ||
-            url.match(/cogebirgzzM/g)
-        ) {
-            if ($.isFunction(success)) {
-                return success(jasmine.stubbedCaption);
-            } else if ($.isFunction(data)) {
-                return data(jasmine.stubbedCaption);
-            }
-        } else {
-            return oldAjaxWithPrefix.apply(this, arguments);
-        }
-    };
-
     // Time waitsFor() should wait for before failing a test.
     window.WAIT_TIMEOUT = 5000;
 
@@ -145,13 +107,16 @@
     jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50'];
 
     jasmine.stubRequests = function () {
-        return spyOn($, 'ajax').andCallFake(function (settings) {
-            var match, status, callCallback;
+        var spy = $.ajax;
 
-            if (
-                match = settings.url
-                    .match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/)
-            ) {
+        if (!($.ajax.isSpy)) {
+            spy = spyOn($, 'ajax');
+        }
+        return spy.andCallFake(function (settings) {
+            var match = settings.url
+                    .match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/),
+                status, callCallback;
+            if (match) {
                 status = match[1].split('_');
                 if (status && status[0] === 'status') {
                     callCallback = function (callback) {
@@ -177,11 +142,10 @@
                         }
                     };
                 }
-            } else if (
-                match = settings.url
-                    .match(/static(\/.*)?\/subs\/(.+)\.srt\.sjson/)
-            ) {
+            } else if (settings.url == '/transcript/translation') {
                 return settings.success(jasmine.stubbedCaption);
+            } else if (settings.url == '/transcript/available_translations') {
+                return settings.success(['uk', 'de']);
             } else if (settings.url.match(/.+\/problem_get$/)) {
                 return settings.success({
                     html: readFixtures('problem_content.html')
@@ -265,6 +229,7 @@
                 .data(params);
         }
 
+        jasmine.stubRequests();
         state = new Video('#example');
 
         state.resizer = (function () {
diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
index 995c89627ef528a4e092d9d8d626000fc983751a..b0cf54bb97470f27fb0b16c74034142230b9b673 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
@@ -181,31 +181,6 @@
             });
         });
 
-        describe('youtubeId', function () {
-            beforeEach(function () {
-                loadFixtures('video.html');
-                $.cookie.andReturn('1.0');
-                state = new Video('#example');
-            });
-
-            describe('with speed', function () {
-                it('return the video id for given speed', function () {
-                    expect(state.youtubeId('0.50'))
-                        .toEqual('7tqY6eQzVhE');
-                    expect(state.youtubeId('1.0'))
-                        .toEqual('cogebirgzzM');
-                    expect(state.youtubeId('1.50'))
-                        .toEqual('abcdefghijkl');
-                });
-            });
-
-            describe('without speed', function () {
-                it('return the video id for current speed', function () {
-                    expect(state.youtubeId()).toEqual('abcdefghijkl');
-                });
-            });
-        });
-
         describe('YouTube video in FireFox will cue first', function () {
             var oldUserAgent;
 
@@ -368,84 +343,6 @@
             });
         });
 
-        describe('setSpeed', function () {
-
-            describe('YT', function () {
-                beforeEach(function () {
-                    loadFixtures('video.html');
-                    state = new Video('#example');
-                });
-
-                it('check mapping', function () {
-                    var map = {
-                        '0.75': '0.50',
-                        '1.25': '1.50'
-                    };
-
-                    $.each(map, function(key, expected) {
-                        state.setSpeed(key, true);
-                        expect(state.speed).toBe(expected);
-                    });
-                });
-            });
-            describe('HTML5', function () {
-                beforeEach(function () {
-                    loadFixtures('video_html5.html');
-                    state = new Video('#example');
-                });
-
-                describe('when new speed is available', function () {
-                    beforeEach(function () {
-                        state.setSpeed('0.75', true);
-                    });
-
-                    it('set new speed', function () {
-                        expect(state.speed).toEqual('0.75');
-                    });
-
-                    it('save setting for new speed', function () {
-
-                        expect(state.storage.getItem('general_speed')).toBe('0.75');
-                        expect(state.storage.getItem('speed', true)).toBe('0.75');
-                    });
-                });
-
-                describe('when new speed is not available', function () {
-                    beforeEach(function () {
-                        state.setSpeed('1.75');
-                    });
-
-                    it('set speed to 1.0x', function () {
-                        expect(state.speed).toEqual('1.0');
-                    });
-                });
-
-                it('check mapping', function () {
-                    var map = {
-                        '0.25': '0.75',
-                        '0.50': '0.75',
-                        '2.0': '1.50'
-                    };
-
-                    $.each(map, function(key, expected) {
-                        state.setSpeed(key, true);
-                        expect(state.speed).toBe(expected);
-                    });
-                });
-            });
-        });
-
-        describe('getDuration', function () {
-            beforeEach(function () {
-                loadFixtures('video.html');
-                state = new Video('#example');
-            });
-
-            it('return duration for current video', function () {
-                expect(state.getDuration()).toEqual(400);
-            });
-        });
-
         describe('log', function () {
             beforeEach(function () {
                 loadFixtures('video_html5.html');
diff --git a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js
index 6177d4303c02c4aade0822170141f1890b52cc88..020de1f38440b6eea54ec81fd49f07fbe5ba4646 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js
@@ -6,8 +6,14 @@ require(
 ['video/01_initialize.js'],
 function (Initialize) {
     describe('Initialize', function () {
+        var state = {};
+
+        afterEach(function () {
+            state = {};
+        });
+
         describe('saveState function', function () {
-            var state, videoPlayerCurrentTime, newCurrentTime, speed;
+            var videoPlayerCurrentTime, newCurrentTime, speed;
 
             // We make sure that `currentTime` is a float. We need to test
             // that Math.round() is called.
@@ -40,10 +46,6 @@ function (Initialize) {
                 spyOn(Time, 'formatFull').andCallThrough();
             });
 
-            afterEach(function () {
-                state = undefined;
-            });
-
             it('data is not an object, async is true', function () {
                 itSpec({
                     asyncVal: true,
@@ -161,8 +163,211 @@ function (Initialize) {
                 });
             }
         });
-    });
 
+        describe('getCurrentLanguage', function () {
+            var msg;
+
+            beforeEach(function () {
+                state.config = {};
+                state.config.transcriptLanguages = {
+                    'de': 'German',
+                    'en': 'English',
+                    'uk': 'Ukrainian',
+                };
+            });
+
+            it ('returns current language', function () {
+                var expected;
+
+                state.lang = 'de';
+                expected = Initialize.prototype.getCurrentLanguage.call(state);
+                expect(expected).toBe('de');
+            });
+
+            msg = 'returns `en`, if language isn\'t available for the video';
+            it (msg, function () {
+                var expected;
+
+                state.lang = 'zh';
+                expected = Initialize.prototype.getCurrentLanguage.call(state);
+                expect(expected).toBe('en');
+            });
+
+            msg = 'returns any available language, if current and `en` ' +
+                    'languages aren\'t available for the video';
+            it (msg, function () {
+                var expected;
+
+                state.lang = 'zh';
+                state.config.transcriptLanguages = {
+                    'de': 'German',
+                    'uk': 'Ukrainian',
+                };
+                expected = Initialize.prototype.getCurrentLanguage.call(state);
+                expect(expected).toBe('uk');
+            });
+
+            it ('returns `null`, if transcript unavailable', function () {
+                var expected;
+
+                state.lang = 'zh';
+                state.config.transcriptLanguages = {};
+                expected = Initialize.prototype.getCurrentLanguage.call(state);
+                expect(expected).toBeNull();
+            });
+        });
+
+        describe('getDuration', function () {
+            beforeEach(function () {
+                state = {
+                    speed: '1.50',
+                    metadata: {
+                        'testId': {
+                            duration: 400
+                        },
+                        'videoId': {
+                            duration: 100
+                        }
+                    },
+                    videos: {
+                        '1.0': 'testId',
+                        '1.50': 'videoId'
+                    },
+                    youtubeId: Initialize.prototype.youtubeId
+                };
+            });
+
+            it('returns duration for current video', function () {
+                var expected = Initialize.prototype.getDuration.call(state);
+
+                expect(expected).toEqual(100);
+            });
+
+            var msg = 'returns duration for the 1.0 speed as a fallback';
+            it(msg, function () {
+                var expected;
+
+                state.speed = '2.0';
+                expected = Initialize.prototype.getDuration.call(state);
+
+                expect(expected).toEqual(400);
+            });
+        });
+
+        describe('youtubeId', function () {
+            beforeEach(function () {
+                state = {
+                    speed: '1.50',
+                    videos: {
+                        '0.50': '7tqY6eQzVhE',
+                        '1.0': 'cogebirgzzM',
+                        '1.50': 'abcdefghijkl'
+                    }
+                };
+            });
+
+            describe('with speed', function () {
+                it('return the video id for given speed', function () {
+                    $.each(state.videos, function(speed, videoId) {
+                        var expected = Initialize.prototype.youtubeId.call(
+                                state, speed
+                            );
+
+                        expect(videoId).toBe(expected);
+                    });
+                });
+            });
+
+            describe('without speed', function () {
+                it('return the video id for current speed', function () {
+                    var expected = Initialize.prototype.youtubeId.call(state);
+
+                    expect(expected).toEqual('abcdefghijkl');
+                });
+            });
+
+            describe('speed is absent in the list of video speeds', function () {
+                it('return the video id for 1.0x speed', function () {
+                    var expected = Initialize.prototype.youtubeId.call(state, '0.0');
+
+                    expect(expected).toEqual('cogebirgzzM');
+                });
+            });
+        });
+
+        describe('setSpeed', function () {
+            describe('YT', function () {
+                beforeEach(function () {
+                    state = {
+                        speeds: ['0.25', '0.50', '1.0', '1.50', '2.0'],
+                        storage: jasmine.createSpyObj('storage', ['setItem'])
+                    };
+                });
+
+                it('check mapping', function () {
+                    var map = {
+                        '0.75': '0.50',
+                        '1.25': '1.50'
+                    };
+
+                    $.each(map, function(key, expected) {
+                        Initialize.prototype.setSpeed.call(state, key);
+                        expect(state.speed).toBe(expected);
+                    });
+                });
+            });
+
+            describe('HTML5', function () {
+                beforeEach(function () {
+                    state = {
+                        speeds: ['0.75', '1.0', '1.25', '1.50'],
+                        storage: jasmine.createSpyObj('storage', ['setItem'])
+                    };
+                });
+
+                describe('when new speed is available', function () {
+                    beforeEach(function () {
+                        Initialize.prototype.setSpeed.call(state, '0.75', true);
+                    });
+
+                    it('set new speed', function () {
+                        expect(state.speed).toEqual('0.75');
+                    });
+
+                    it('save setting for new speed', function () {
+                        expect(state.storage.setItem.calls[0].args)
+                            .toEqual(['speed', '0.75', true]);
+
+                        expect(state.storage.setItem.calls[1].args)
+                            .toEqual(['general_speed', '0.75']);
+                    });
+                });
+
+                describe('when new speed is not available', function () {
+                    beforeEach(function () {
+                        Initialize.prototype.setSpeed.call(state, '1.75');
+                    });
+
+                    it('set speed to 1.0x', function () {
+                        expect(state.speed).toEqual('1.0');
+                    });
+                });
+
+                it('check mapping', function () {
+                    var map = {
+                        '0.25': '0.75',
+                        '0.50': '0.75',
+                        '2.0': '1.50'
+                    };
+
+                    $.each(map, function(key, expected) {
+                        Initialize.prototype.setSpeed.call(state, key, true);
+                        expect(state.speed).toBe(expected);
+                    });
+                });
+            });
+        });
+    });
 });
 
 }(RequireJS.requirejs, RequireJS.require, RequireJS.define));
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
index 6ad591d74eada1b1c47d1184982890d045b436eb..a9e5236c93bc5d6ed023d63e23382355343d9260 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
@@ -9,6 +9,7 @@
 
             state = jasmine.initializePlayer();
             videoControl = state.videoControl;
+            $.fn.scrollTo.reset();
         });
 
         afterEach(function () {
@@ -28,12 +29,7 @@
             describe('always', function () {
                 beforeEach(function () {
                     spyOn($, 'ajaxWithPrefix').andCallThrough();
-
                     state = jasmine.initializePlayer();
-
-                    videoControl = state.videoControl;
-
-                    $.fn.scrollTo.reset();
                 });
 
                 it('create the caption element', function () {
@@ -64,8 +60,12 @@
 
                     runs(function () {
                         expect($.ajaxWithPrefix).toHaveBeenCalledWith({
-                            url: state.videoCaption.captionURL(),
+                            url: '/transcript/translation',
                             notifyOnError: false,
+                            data: {
+                                videoId: 'Z5KLxerq05Y',
+                                language: 'en'
+                            },
                             success: jasmine.any(Function),
                             error: jasmine.any(Function)
                         });
@@ -100,23 +100,98 @@
                     expect($('.subtitles')).toHandleWith(
                         'DOMMouseScroll', state.videoCaption.onMovement
                     );
+                 });
+
+                 it('bind the scroll', function () {
+                     expect($('.subtitles'))
+                         .toHandleWith('scroll', state.videoControl.showControls);
+                 });
+
+            });
+
+            describe('renderLanguageMenu', function () {
+                describe('is rendered', function () {
+                    it('if languages more than 1', function () {
+                        state = jasmine.initializePlayer();
+                        var transcripts = state.config.transcriptLanguages,
+                            langCodes = _.keys(transcripts),
+                            langLabels = _.values(transcripts);
+
+                        expect($('.langs-list')).toExist();
+                        expect($('.langs-list')).toHandle('click');
+
+
+                        $('.langs-list li').each(function(index) {
+                            var code = $(this).data('lang-code'),
+                                link = $(this).find('a'),
+                                label = link.text();
+
+                            expect(code).toBeInArray(langCodes);
+                            expect(label).toBeInArray(langLabels);
+                        });
+                    });
+
+                    it('when clicking on link with new language', function () {
+                        state = jasmine.initializePlayer();
+                        var Caption = state.videoCaption,
+                            link = $('.langs-list li[data-lang-code="de"] a');
+
+                        spyOn(Caption, 'fetchCaption');
+                        spyOn(state.storage, 'setItem');
+
+                        state.lang = 'en';
+                        link.trigger('click');
+
+                        expect(Caption.fetchCaption).toHaveBeenCalled();
+                        expect(state.lang).toBe('de');
+                        expect(state.storage.setItem)
+                            .toHaveBeenCalledWith('language', 'de');
+                        expect($('.langs-list li.active').length).toBe(1);
+                    });
+
+                    it('when clicking on link with current language', function () {
+                        state = jasmine.initializePlayer();
+                        var Caption = state.videoCaption,
+                            link = $('.langs-list li[data-lang-code="en"] a');
+
+                        spyOn(Caption, 'fetchCaption');
+                        spyOn(state.storage, 'setItem');
+
+                        state.lang = 'en';
+                        link.trigger('click');
+
+                        expect(Caption.fetchCaption).not.toHaveBeenCalled();
+                        expect(state.lang).toBe('en');
+                        expect(state.storage.setItem)
+                            .not.toHaveBeenCalledWith('language', 'en');
+                        expect($('.langs-list li.active').length).toBe(1);
+                    });
+
+                    it('open the language toggle on hover', function () {
+                        state = jasmine.initializePlayer();
+                        $('.lang').mouseenter();
+                        expect($('.lang')).toHaveClass('open');
+                        $('.lang').mouseleave();
+                        expect($('.lang')).not.toHaveClass('open');
+                    });
                 });
 
-                it('bind the scroll', function () {
-                    expect($('.subtitles'))
-                        .toHandleWith('scroll', state.videoCaption.autoShowCaptions);
-                    expect($('.subtitles'))
-                        .toHandleWith('scroll', videoControl.showControls);
+                describe('is not rendered', function () {
+                    it('if just 1 language', function () {
+                        state = jasmine.initializePlayer(null, {
+                            'transcriptLanguages': {"en": "English"}
+                        });
+
+                        expect($('.langs-list')).not.toExist();
+                        expect($('.lang')).not.toHandle('mouseenter');
+                        expect($('.lang')).not.toHandle('mouseleave');
+                    });
                 });
             });
 
             describe('when on a non touch-based device', function () {
                 beforeEach(function () {
                     state = jasmine.initializePlayer();
-
-                    videoControl = state.videoControl;
-
-                    $.fn.scrollTo.reset();
                 });
 
                 it('render the caption', function () {
@@ -142,35 +217,46 @@
                         .toBe(true);
                 });
 
+
                 it('bind all the caption link', function () {
+                    var handlerList = ['captionMouseOverOut', 'captionClick',
+                        'captionMouseDown', 'captionFocus', 'captionBlur',
+                        'captionKeyDown'
+                    ];
+
+                    $.each(handlerList, function(index, handler) {
+                        spyOn(state.videoCaption, handler);
+                    });
                     $('.subtitles li[data-index]').each(
                         function (index, link) {
 
-                        expect($(link)).toHandleWith(
-                            'mouseover', state.videoCaption.captionMouseOverOut
-                        );
-                        expect($(link)).toHandleWith(
-                            'mouseout', state.videoCaption.captionMouseOverOut
-                        );
-                        expect($(link)).toHandleWith(
-                            'mousedown', state.videoCaption.captionMouseDown
-                        );
-                        expect($(link)).toHandleWith(
-                            'click', state.videoCaption.captionClick
-                        );
-                        expect($(link)).toHandleWith(
-                            'focus', state.videoCaption.captionFocus
-                        );
-                        expect($(link)).toHandleWith(
-                            'blur', state.videoCaption.captionBlur
-                        );
-                        expect($(link)).toHandleWith(
-                            'keydown', state.videoCaption.captionKeyDown
-                        );
+
+                        $(link).trigger('mouseover');
+                        expect(state.videoCaption.captionMouseOverOut).toHaveBeenCalled();
+
+                        state.videoCaption.captionMouseOverOut.reset();
+                        $(link).trigger('mouseout');
+                        expect(state.videoCaption.captionMouseOverOut).toHaveBeenCalled();
+
+                        $(this).click();
+                        expect(state.videoCaption.captionClick).toHaveBeenCalled();
+
+                        $(this).trigger('mousedown');
+                        expect(state.videoCaption.captionMouseDown).toHaveBeenCalled();
+
+                        $(this).trigger('focus');
+                        expect(state.videoCaption.captionFocus).toHaveBeenCalled();
+
+                        $(this).trigger('blur');
+                        expect(state.videoCaption.captionBlur).toHaveBeenCalled();
+
+                        $(this).trigger('keydown');
+                        expect(state.videoCaption.captionKeyDown).toHaveBeenCalled();
                     });
                 });
 
                 it('set rendered to true', function () {
+                    state = jasmine.initializePlayer();
                     expect(state.videoCaption.rendered).toBeTruthy();
                 });
             });
@@ -180,9 +266,6 @@
                     window.onTouchBasedDevice.andReturn(['iPad']);
 
                     state = jasmine.initializePlayer();
-
-                    videoControl = state.videoControl;
-
                     $.fn.scrollTo.reset();
                 });
 
@@ -200,12 +283,9 @@
 
             describe('when no captions file was specified', function () {
                 beforeEach(function () {
-                    loadFixtures('video_all.html');
-
-                    // Unspecify the captions file.
-                    $('#example').find('#video_id').data('sub', '');
-
-                    state = new Video('#example');
+                    state = jasmine.initializePlayer('video_all.html', {
+                        'sub': ''
+                    });
                 });
 
                 it('captions panel is not shown', function () {
@@ -218,6 +298,7 @@
             beforeEach(function () {
                 jasmine.Clock.useMock();
                 spyOn(window, 'clearTimeout');
+                state = jasmine.initializePlayer();
             });
 
             describe('when cursor is outside of the caption box', function () {
@@ -313,8 +394,254 @@
             });
         });
 
+        it('reRenderCaption', function () {
+            var Caption = state.videoCaption,
+                li;
+
+            Caption.captions = ['test'];
+            Caption.start = [500];
+
+            spyOn(Caption, 'addPaddings');
+
+            Caption.reRenderCaption();
+            li = $('ol.subtitles li');
+
+            expect(Caption.addPaddings).toHaveBeenCalled();
+            expect(li.length).toBe(1);
+            expect(li).toHaveData('start', '500');
+        });
+
+        describe('fetchCaption', function () {
+            var Caption, msg;
+
+            beforeEach(function () {
+                state = jasmine.initializePlayer();
+                Caption = state.videoCaption;
+                spyOn($, 'ajaxWithPrefix').andCallThrough();
+                spyOn(Caption, 'reRenderCaption');
+                spyOn(Caption, 'renderCaption');
+                spyOn(Caption, 'bindHandlers');
+                spyOn(Caption, 'updatePlayTime');
+                spyOn(Caption, 'hideCaptions');
+                spyOn(state, 'youtubeId').andReturn('Z5KLxerq05Y');
+            });
+
+            it('do not fetch captions, if 1.0 speed is absent', function () {
+                state.youtubeId.andReturn(void(0));
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).not.toHaveBeenCalled();
+                expect(Caption.hideCaptions).not.toHaveBeenCalled();
+            });
+
+            it('show caption on language change', function () {
+                Caption.loaded = true;
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.hideCaptions).toHaveBeenCalledWith(false);
+            });
+
+            msg = 'use cookie to show/hide captions if they have not been ' +
+                    'loaded yet';
+            it(msg, function () {
+                Caption.loaded = false;
+                state.hide_captions = false;
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.hideCaptions).toHaveBeenCalledWith(false, false);
+
+                Caption.loaded = false;
+                Caption.hideCaptions.reset();
+                state.hide_captions = true;
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.hideCaptions).toHaveBeenCalledWith(true, false);
+            });
+
+            it('on success: on touch devices', function () {
+                state.isTouch = true;
+                Caption.loaded = false;
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.bindHandlers).toHaveBeenCalled();
+                expect(Caption.renderCaption).not.toHaveBeenCalled();
+                expect(Caption.updatePlayTime).not.toHaveBeenCalled();
+                expect(Caption.reRenderCaption).not.toHaveBeenCalled();
+                expect(Caption.loaded).toBeTruthy();
+            });
+
+            msg = 'on success: change language on touch devices when ' +
+                 'captions have not been rendered yet';
+            it(msg, function () {
+                state.isTouch = true;
+                Caption.loaded = true;
+                Caption.rendered = false;
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.bindHandlers).not.toHaveBeenCalled();
+                expect(Caption.renderCaption).not.toHaveBeenCalled();
+                expect(Caption.updatePlayTime).not.toHaveBeenCalled();
+                expect(Caption.reRenderCaption).not.toHaveBeenCalled();
+                expect(Caption.loaded).toBeTruthy();
+            });
+
+            it('on success: re-render on touch devices', function () {
+                state.isTouch = true;
+                Caption.loaded = true;
+                Caption.rendered = true;
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.bindHandlers).not.toHaveBeenCalled();
+                expect(Caption.renderCaption).not.toHaveBeenCalled();
+                expect(Caption.updatePlayTime).toHaveBeenCalled();
+                expect(Caption.reRenderCaption).toHaveBeenCalled();
+                expect(Caption.loaded).toBeTruthy();
+            });
+
+            it('on success: rendered correct', function () {
+                Caption.loaded = false;
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.bindHandlers).toHaveBeenCalled();
+                expect(Caption.renderCaption).toHaveBeenCalled();
+                expect(Caption.updatePlayTime).not.toHaveBeenCalled();
+                expect(Caption.reRenderCaption).not.toHaveBeenCalled();
+                expect(Caption.loaded).toBeTruthy();
+            });
+
+            it('on success: re-rendered correct', function () {
+                Caption.loaded = true;
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.bindHandlers).not.toHaveBeenCalled();
+                expect(Caption.renderCaption).not.toHaveBeenCalled();
+                expect(Caption.updatePlayTime).toHaveBeenCalled();
+                expect(Caption.reRenderCaption).toHaveBeenCalled();
+                expect(Caption.loaded).toBeTruthy();
+            });
+
+            msg = 'on error: captions are hidden if there are no transcripts';
+            it(msg, function () {
+                spyOn(Caption, 'fetchAvailableTranslations');
+                $.ajax.andCallFake(function (settings) {
+                    settings.error([]);
+                });
+
+                state.config.transcriptLanguages = {};
+
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.fetchAvailableTranslations).not.toHaveBeenCalled();
+                expect(Caption.hideCaptions.mostRecentCall.args)
+                    .toEqual([true, false]);
+                expect(Caption.hideSubtitlesEl).toBeHidden();
+            });
+
+            msg = 'on error: fetch available translations if there are ' +
+                    'additional transcripts';
+            xit(msg, function () {
+                $.ajax
+                    .andCallFake(function (settings) {
+                        settings.error([]);
+                    });
+
+                state.config.transcriptLanguages = {
+                    'en': 'English',
+                    'uk': 'Ukrainian',
+                };
+
+                spyOn(Caption, 'fetchAvailableTranslations');
+                Caption.fetchCaption();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.fetchAvailableTranslations).toHaveBeenCalled();
+                expect(Caption.hideCaptions).not.toHaveBeenCalled();
+            });
+        });
+
+        describe('fetchAvailableTranslations', function () {
+            var Caption, msg;
+
+            beforeEach(function () {
+                state = jasmine.initializePlayer();
+                Caption = state.videoCaption;
+                spyOn($, 'ajaxWithPrefix').andCallThrough();
+                spyOn(Caption, 'hideCaptions');
+                spyOn(Caption, 'fetchCaption');
+                spyOn(Caption, 'renderLanguageMenu');
+            });
+
+            it('request created with correct parameters', function () {
+                Caption.fetchAvailableTranslations();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalledWith({
+                    url: '/transcript/available_translations',
+                    notifyOnError: false,
+                    success: jasmine.any(Function),
+                    error: jasmine.any(Function)
+                });
+            });
+
+            msg = 'on succes: language menu is rendered if translations available';
+            it(msg, function () {
+                state.config.transcriptLanguages = {
+                    'en': 'English',
+                    'uk': 'Ukrainian',
+                    'de': 'German'
+                };
+                Caption.fetchAvailableTranslations();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.fetchCaption).toHaveBeenCalled();
+                expect(state.config.transcriptLanguages).toEqual({
+                    'uk': 'Ukrainian',
+                    'de': 'German'
+                });
+                expect(Caption.renderLanguageMenu).toHaveBeenCalledWith({
+                    'uk': 'Ukrainian',
+                    'de': 'German'
+                });
+            });
+
+            msg = 'on succes: language menu isn\'t rendered if translations unavailable';
+            it(msg, function () {
+                state.config.transcriptLanguages = {
+                    'en': 'English',
+                    'ru': 'Russian'
+                };
+                Caption.fetchAvailableTranslations();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.fetchCaption).not.toHaveBeenCalled();
+                expect(state.config.transcriptLanguages).toEqual({});
+                expect(Caption.renderLanguageMenu).not.toHaveBeenCalled();
+            });
+
+            msg = 'on error: captions are hidden if there are no transcript';
+            it(msg, function () {
+                $.ajax.andCallFake(function (settings) {
+                    settings.error();
+                });
+                Caption.fetchAvailableTranslations();
+
+                expect($.ajaxWithPrefix).toHaveBeenCalled();
+                expect(Caption.hideCaptions).toHaveBeenCalledWith(true, false);
+                expect(Caption.hideSubtitlesEl).toBeHidden();
+            });
+        });
+
         describe('search', function () {
             it('return a correct caption index', function () {
+                state = jasmine.initializePlayer();
                 expect(state.videoCaption.search(0)).toEqual(-1);
                 expect(state.videoCaption.search(3120)).toEqual(1);
                 expect(state.videoCaption.search(6270)).toEqual(2);
@@ -328,13 +655,7 @@
             describe('when the caption was not rendered', function () {
                 beforeEach(function () {
                     window.onTouchBasedDevice.andReturn(['iPad']);
-
                     state = jasmine.initializePlayer();
-
-                    videoControl = state.videoControl;
-
-                    $.fn.scrollTo.reset();
-
                     state.videoCaption.play();
                 });
 
@@ -359,34 +680,6 @@
                     expect($('.subtitles li:last')).toBe('.spacing');
                 });
 
-                it('bind all the caption link', function () {
-                    $('.subtitles li[data-index]').each(
-                        function (index, link) {
-
-                        expect($(link)).toHandleWith(
-                            'mouseover', state.videoCaption.captionMouseOverOut
-                        );
-                        expect($(link)).toHandleWith(
-                            'mouseout', state.videoCaption.captionMouseOverOut
-                        );
-                        expect($(link)).toHandleWith(
-                            'mousedown', state.videoCaption.captionMouseDown
-                        );
-                        expect($(link)).toHandleWith(
-                            'click', state.videoCaption.captionClick
-                        );
-                        expect($(link)).toHandleWith(
-                            'focus', state.videoCaption.captionFocus
-                        );
-                        expect($(link)).toHandleWith(
-                            'blur', state.videoCaption.captionBlur
-                        );
-                        expect($(link)).toHandleWith(
-                            'keydown', state.videoCaption.captionKeyDown
-                        );
-                    });
-                });
-
                 it('set rendered to true', function () {
                     expect(state.videoCaption.rendered).toBeTruthy();
                 });
@@ -399,6 +692,7 @@
 
         describe('pause', function () {
             beforeEach(function () {
+                state = jasmine.initializePlayer();
                 state.videoCaption.playing = true;
                 state.videoCaption.pause();
             });
@@ -409,6 +703,10 @@
         });
 
         describe('updatePlayTime', function () {
+            beforeEach(function () {
+                state = jasmine.initializePlayer();
+            });
+
             describe('when the video speed is 1.0x', function () {
                 beforeEach(function () {
                     state.videoSpeedControl.currentSpeed = '1.0';
@@ -475,11 +773,7 @@
         describe('resize', function () {
             beforeEach(function () {
                 state = jasmine.initializePlayer();
-
                 videoControl = state.videoControl;
-
-                $.fn.scrollTo.reset();
-
                 $('.subtitles li[data-index=1]').addClass('current');
                 state.videoCaption.resize();
             });
@@ -542,10 +836,6 @@
         xdescribe('scrollCaption', function () {
             beforeEach(function () {
                 state = jasmine.initializePlayer();
-
-                videoControl = state.videoControl;
-
-                $.fn.scrollTo.reset();
             });
 
             describe('when frozen', function () {
@@ -590,6 +880,10 @@
 
         // Disabled 10/9/13 due to flakiness in master
         xdescribe('seekPlayer', function () {
+            beforeEach(function () {
+                state = jasmine.initializePlayer();
+            });
+
             describe('when the video speed is 1.0x', function () {
                 beforeEach(function () {
                     state.videoSpeedControl.currentSpeed = '1.0';
@@ -603,12 +897,6 @@
 
             describe('when the video speed is not 1.0x', function () {
                 beforeEach(function () {
-                    state = jasmine.initializePlayer();
-
-                    videoControl = state.videoControl;
-
-                    $.fn.scrollTo.reset();
-
                     state.videoSpeedControl.currentSpeed = '0.75';
                     $('.subtitles li[data-start="14910"]').trigger('click');
                 });
@@ -622,12 +910,6 @@
                 function () {
 
                 beforeEach(function () {
-                    state = jasmine.initializePlayer();
-
-                    videoControl = state.videoControl;
-
-                    $.fn.scrollTo.reset();
-
                     state.videoSpeedControl.currentSpeed = '0.75';
                     state.currentPlayerMode = 'flash';
                     $('.subtitles li[data-start="14910"]').trigger('click');
@@ -642,11 +924,6 @@
         describe('toggle', function () {
             beforeEach(function () {
                 state = jasmine.initializePlayer();
-
-                videoControl = state.videoControl;
-
-                $.fn.scrollTo.reset();
-
                 spyOn(state.videoPlayer, 'log');
                 $('.subtitles li[data-index=1]').addClass('current');
             });
@@ -722,10 +999,6 @@
         describe('caption accessibility', function () {
             beforeEach(function () {
                 state = jasmine.initializePlayer();
-
-                videoControl = state.videoControl;
-
-                $.fn.scrollTo.reset();
             });
 
             describe('when getting focus through TAB key', function () {
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
index 3dfba3008d7613450dde471d41698b8583202886..ec4f62f7e2650fd5021d91ab72d85855cd5e1b4d 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
@@ -1,4 +1,10 @@
-(function (undefined) {
+(function (requirejs, require, define, undefined) {
+
+'use strict';
+
+require(
+['video/03_video_player.js'],
+function (VideoPlayer) {
     describe('VideoPlayer', function () {
         var state, oldOTBD;
 
@@ -11,7 +17,9 @@
         afterEach(function () {
             $('source').remove();
             window.onTouchBasedDevice = oldOTBD;
-            state.storage.clear();
+            if (state.storage) {
+                state.storage.clear();
+            }
         });
 
         describe('constructor', function () {
@@ -39,8 +47,8 @@
                     expect(state.videoCaption).toBeDefined();
                     expect(state.youtubeId('1.0')).toEqual('Z5KLxerq05Y');
                     expect(state.speed).toEqual('1.50');
-                    expect(state.config.captionAssetPath)
-                        .toEqual('/static/subs/');
+                    expect(state.config.transcriptTranslationUrl)
+                        .toEqual('/transcript/translation');
                 });
 
                 it('create video speed control', function () {
@@ -307,7 +315,7 @@
                 });
 
                 waitsFor(function () {
-                    duration = state.videoPlayer.duration();
+                    var duration = state.videoPlayer.duration();
 
                     return duration > 0 && state.videoPlayer.isPlaying();
                 }, 'video begins playing', WAIT_TIMEOUT);
@@ -379,85 +387,33 @@
             });
         });
 
-        describe('onSpeedChange', function () {
+        describe('when the video is not playing', function () {
             beforeEach(function () {
                 state = jasmine.initializePlayer();
 
-                state.videoEl = $('video, iframe');
-
                 spyOn(state.videoPlayer, 'updatePlayTime').andCallThrough();
                 spyOn(state, 'setSpeed').andCallThrough();
                 spyOn(state.videoPlayer, 'log').andCallThrough();
                 spyOn(state.videoPlayer.player, 'setPlaybackRate').andCallThrough();
+                spyOn(state.videoPlayer, 'setPlaybackRate').andCallThrough();
             });
 
-            describe('always', function () {
-                beforeEach(function () {
-
-                    state.videoPlayer.currentTime = 60;
-                    state.videoPlayer.onSpeedChange('0.75', false);
-                });
-
-                it('check if speed_change_video is logged', function () {
-                    expect(state.videoPlayer.log).toHaveBeenCalledWith(
-                        'speed_change_video',
-                        {
-                            current_time: state.videoPlayer.currentTime,
-                            old_speed: '1.50',
-                            new_speed: '0.75'
-                        }
-                    );
-                });
-
-                it('convert the current time to the new speed', function () {
-                    expect(state.videoPlayer.currentTime).toEqual(60);
-                });
-
-                it('set video speed to the new speed', function () {
-                    expect(state.setSpeed).toHaveBeenCalledWith('0.75', true);
-                });
-            });
-
-            describe('when the video is playing', function () {
-                beforeEach(function () {
-                    state.videoPlayer.currentTime = 60;
-                    state.videoPlayer.play();
-                    state.videoPlayer.onSpeedChange('0.75', false);
-                });
-
-                it('trigger updatePlayTime event', function () {
-                    expect(state.videoPlayer.player.setPlaybackRate)
-                        .toHaveBeenCalledWith('0.75');
-                });
+            it('video has a correct speed', function () {
+                state.speed = '2.0';
+                state.videoPlayer.onPlay();
+                expect(state.videoPlayer.setPlaybackRate)
+                    .toHaveBeenCalledWith('2.0');
+                state.videoPlayer.onPlay();
+                expect(state.videoPlayer.setPlaybackRate.calls.length)
+                    .toEqual(1);
             });
 
-            describe('when the video is not playing', function () {
-                beforeEach(function () {
-                    state.videoPlayer.onSpeedChange('0.75', false);
-                });
-
-                it('trigger updatePlayTime event', function () {
-                    expect(state.videoPlayer.player.setPlaybackRate)
-                        .toHaveBeenCalledWith('0.75');
-                });
-
-                it('video has a correct speed', function () {
-                    spyOn(state.videoPlayer, 'onSpeedChange');
-                    state.speed = '2.0';
-                    state.videoPlayer.onPlay();
-                    expect(state.videoPlayer.onSpeedChange)
-                        .toHaveBeenCalledWith('2.0');
-                    state.videoPlayer.onPlay();
-                    expect(state.videoPlayer.onSpeedChange.calls.length).toEqual(1);
-                });
-
-                it('video has a correct volume', function () {
-                    spyOn(state.videoPlayer.player, 'setVolume');
-                    state.currentVolume = '0.26';
-                    state.videoPlayer.onPlay();
-                    expect(state.videoPlayer.player.setVolume)
-                        .toHaveBeenCalledWith('0.26');
-                });
+            it('video has a correct volume', function () {
+                spyOn(state.videoPlayer.player, 'setVolume');
+                state.currentVolume = '0.26';
+                state.videoPlayer.onPlay();
+                expect(state.videoPlayer.player.setVolume)
+                    .toHaveBeenCalledWith('0.26');
             });
         });
 
@@ -789,7 +745,7 @@
 
                     state.el.addClass('video-fullscreen');
                     state.videoControl.fullScreenState = true;
-                    isFullScreen = true;
+                    state.videoControl.isFullScreen = true;
                     state.videoControl.fullScreenEl.attr('title', 'Exit-fullscreen');
 
                     state.videoControl.toggleFullScreen(jQuery.Event('click'));
@@ -931,20 +887,6 @@
             });
         });
 
-        describe('playback rate', function () {
-            beforeEach(function () {
-                state = jasmine.initializePlayer();
-
-                state.videoEl = $('video, iframe');
-
-                state.videoPlayer.player.setPlaybackRate(1.5);
-            });
-
-            it('set the player playback rate', function () {
-                expect(state.videoPlayer.player.video.playbackRate).toEqual(1.5);
-            });
-        });
-
         describe('volume', function () {
             beforeEach(function () {
                 state = jasmine.initializePlayer();
@@ -1023,7 +965,7 @@
                     });
 
                     waitsFor(function () {
-                        duration = state.videoPlayer.duration();
+                        var duration = state.videoPlayer.duration();
 
                         return duration > 0 && state.videoPlayer.isPlaying();
                     },'Video does not play.' , WAIT_TIMEOUT);
@@ -1034,6 +976,108 @@
                 });
             });
         });
+
+        describe('onSpeedChange', function () {
+            beforeEach(function () {
+                state = {
+                    el: $(document),
+                    speed: '1.50',
+                    setSpeed: jasmine.createSpy(),
+                    saveState: jasmine.createSpy(),
+                    videoPlayer: {
+                        currentTime: 60,
+                        log: jasmine.createSpy(),
+                        updatePlayTime: jasmine.createSpy(),
+                        setPlaybackRate: jasmine.createSpy(),
+                        player: jasmine.createSpyObj('player', ['setPlaybackRate'])
+                    }
+                };
+            });
+
+            describe('always', function () {
+                it('check if speed_change_video is logged', function () {
+                    VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
+                    expect(state.videoPlayer.log).toHaveBeenCalledWith(
+                        'speed_change_video',
+                        {
+                            current_time: state.videoPlayer.currentTime,
+                            old_speed: '1.50',
+                            new_speed: '0.75'
+                        }
+                    );
+                });
+
+                it('convert the current time to the new speed', function () {
+                    state.currentPlayerMode = 'flash';
+                    VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
+                    expect(state.videoPlayer.currentTime).toBe('120.000');
+                });
+
+                it('set video speed to the new speed', function () {
+                    VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
+                    expect(state.setSpeed).toHaveBeenCalledWith('0.75', true);
+                    expect(state.saveState).toHaveBeenCalledWith(true, {
+                        speed: '0.75'
+                    });
+                    expect(state.videoPlayer.setPlaybackRate)
+                        .toHaveBeenCalledWith('0.75');
+                });
+            });
+        });
+
+        describe('setPlaybackRate', function () {
+            beforeEach(function () {
+                state = {
+                    youtubeId: jasmine.createSpy().andReturn('videoId'),
+                    videoPlayer: {
+                        currentTime: 60,
+                        isPlaying: jasmine.createSpy(),
+                        updatePlayTime: jasmine.createSpy(),
+                        setPlaybackRate: jasmine.createSpy(),
+                        player: jasmine.createSpyObj('player', [
+                            'setPlaybackRate', 'loadVideoById', 'cueVideoById'
+                        ])
+                    }
+                };
+            });
+
+            it('in Flash mode and video is playing', function () {
+                state.currentPlayerMode = 'flash';
+                state.videoPlayer.isPlaying.andReturn(true);
+                VideoPlayer.prototype.setPlaybackRate.call(state, '0.75');
+                expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
+                expect(state.videoPlayer.player.loadVideoById)
+                    .toHaveBeenCalledWith('videoId', 60);
+            });
+
+            it('in Flash mode and video not started', function () {
+                state.currentPlayerMode = 'flash';
+                state.videoPlayer.isPlaying.andReturn(false);
+                VideoPlayer.prototype.setPlaybackRate.call(state, '0.75');
+                expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
+                expect(state.videoPlayer.player.cueVideoById)
+                    .toHaveBeenCalledWith('videoId', 60);
+            });
+
+            it('in HTML5 mode', function () {
+                state.currentPlayerMode = 'html5';
+                VideoPlayer.prototype.setPlaybackRate.call(state, '0.75');
+                expect(state.videoPlayer.player.setPlaybackRate).toHaveBeenCalledWith('0.75');
+            });
+
+            it('Youtube video in FF, with new speed equal 1.0', function () {
+                state.currentPlayerMode = 'html5';
+                state.videoType = 'youtube';
+                state.browserIsFirefox = true;
+
+                state.videoPlayer.isPlaying.andReturn(false);
+                VideoPlayer.prototype.setPlaybackRate.call(state, '1.0');
+                expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
+                expect(state.videoPlayer.player.cueVideoById)
+                    .toHaveBeenCalledWith('videoId', 60);
+            });
+        });
     });
+});
 
-}).call(this);
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
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 8ab4e0b7857f4cee81a88642668ad363895bbec8..7cea30aa9e5f327786b10952d2d4183da9b3f1b1 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -57,9 +57,11 @@ function (VideoPlayer, VideoStorage) {
                     });
             });
     },
+
     methodsDict = {
         bindTo: bindTo,
         fetchMetadata: fetchMetadata,
+        getCurrentLanguage: getCurrentLanguage,
         getDuration: getDuration,
         getVideoMetadata: getVideoMetadata,
         initialize: initialize,
@@ -305,6 +307,11 @@ function (VideoPlayer, VideoStorage) {
                             value ||
                             '1.0';
                      },
+                    'transcriptLanguage': function (value) {
+                        return storage.getItem('language') ||
+                            value ||
+                            'en';
+                     },
                     'ytTestTimeout': function (value) {
                         value = parseInt(value, 10);
 
@@ -432,6 +439,7 @@ function (VideoPlayer, VideoStorage) {
             this.config.endTime = null;
         }
 
+        this.lang = this.config.transcriptLanguage;
         this.speed = Number(
             this.config.speed || this.config.generalSpeed
         ).toFixed(2).replace(/\.00$/, '.0');
@@ -631,17 +639,16 @@ function (VideoPlayer, VideoStorage) {
 
     function setSpeed(newSpeed, updateStorage) {
         // Possible speeds for each player type.
-        // flash =          [0.75, 1, 1.25, 1.5]
-        // html5 =          [0.75, 1, 1.25, 1.5]
-        // youtube html5 =  [0.25, 0.5, 1, 1.5, 2]
+        // HTML5 =          [0.75, 1, 1.25, 1.5]
+        // Youtube Flash =  [0.75, 1, 1.25, 1.5]
+        // Youtube HTML5 =  [0.25, 0.5, 1, 1.5, 2]
         var map = {
-                '0.25': '0.75',
-                '0.50': '0.75',
-                '0.75': '0.50',
-                '1.25': '1.50',
-                '2.0': '1.50'
-            },
-            useSession = true;
+                '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
+                '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
+                '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
+                '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
+                '2.0': '1.50'   // Youtube HTML5 -> HTML5 or Youtube Flash
+            };
 
         if (_.contains(this.speeds, newSpeed)) {
             this.speed = newSpeed;
@@ -712,6 +719,24 @@ function (VideoPlayer, VideoStorage) {
         }
     }
 
+    function getCurrentLanguage() {
+        var keys = _.keys(this.config.transcriptLanguages);
+
+        if (keys.length) {
+            if (!_.contains(keys, this.lang)) {
+                if (_.contains(keys, 'en')) {
+                    this.lang = 'en';
+                } else {
+                    this.lang = keys.pop();
+                }
+            }
+        } else {
+            return null;
+        }
+
+        return this.lang;
+    }
+
     /*
      * The trigger() function will assume that the @objChain is a complete
      * chain with a method (function) at the end. It will call this function.
diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js
index bd80c92aeaa904a279dd04029b345c7b508e507c..b225d0783ef356d0d2d1ce49918b1263c2141c25 100644
--- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js
+++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js
@@ -5,30 +5,16 @@ define(
 'video/03_video_player.js',
 ['video/02_html5_video.js', 'video/00_resizer.js'],
 function (HTML5Video, Resizer) {
-     var dfd = $.Deferred();
-
-    // VideoPlayer() function - what this module "exports".
-    return function (state) {
-
-        state.videoPlayer = {};
-
-        _makeFunctionsPublic(state);
-        _initialize(state);
-        // No callbacks to DOM events (click, mousemove, etc.).
-
-        return dfd.promise();
-    };
-
-    // ***************************************************************
-    // Private functions start here.
-    // ***************************************************************
-
-    // function _makeFunctionsPublic(state)
-    //
-    //     Functions which will be accessible via 'state' object. When called,
-    //     these functions will get the 'state' object as a context.
-    function _makeFunctionsPublic(state) {
-        var methodsDict = {
+    var dfd = $.Deferred(),
+        VideoPlayer = function (state) {
+            state.videoPlayer = {};
+            _makeFunctionsPublic(state);
+            _initialize(state);
+            // No callbacks to DOM events (click, mousemove, etc.).
+
+            return dfd.promise();
+        },
+        methodsDict = {
             duration: duration,
             handlePlaybackQualityChange: handlePlaybackQualityChange,
             isPlaying: isPlaying,
@@ -46,10 +32,25 @@ function (HTML5Video, Resizer) {
             onVolumeChange: onVolumeChange,
             pause: pause,
             play: play,
+            setPlaybackRate: setPlaybackRate,
             update: update,
             updatePlayTime: updatePlayTime
         };
 
+    VideoPlayer.prototype = methodsDict;
+
+    // VideoPlayer() function - what this module "exports".
+    return VideoPlayer;
+
+    // ***************************************************************
+    // Private functions start here.
+    // ***************************************************************
+
+    // function _makeFunctionsPublic(state)
+    //
+    //     Functions which will be accessible via 'state' object. When called,
+    //     these functions will get the 'state' object as a context.
+    function _makeFunctionsPublic(state) {
         state.bindTo(methodsDict, state.videoPlayer, state);
     }
 
@@ -70,7 +71,7 @@ function (HTML5Video, Resizer) {
             $(window).on('unload', state.saveState);
 
             if (state.currentPlayerMode !== 'flash') {
-                state.videoPlayer.onSpeedChange(state.speed);
+                state.videoPlayer.setPlaybackRate(state.speed);
             }
             state.videoPlayer.player.setVolume(state.currentVolume);
         });
@@ -325,33 +326,10 @@ function (HTML5Video, Resizer) {
         }
     }
 
-    function onSpeedChange(newSpeed) {
+    function setPlaybackRate(newSpeed) {
         var time = this.videoPlayer.currentTime,
             methodName, youtubeId;
 
-        if (this.currentPlayerMode === 'flash') {
-            this.videoPlayer.currentTime = Time.convert(
-                time,
-                parseFloat(this.speed),
-                newSpeed
-            );
-        }
-
-        newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0');
-
-        if (this.speed != newSpeed) {
-            this.videoPlayer.log(
-                'speed_change_video',
-                {
-                    current_time: time,
-                    old_speed: this.speed,
-                    new_speed: newSpeed
-                }
-            );
-        }
-
-        this.setSpeed(newSpeed, true);
-
         if (
             this.currentPlayerMode === 'html5' &&
             !(
@@ -377,7 +355,33 @@ function (HTML5Video, Resizer) {
             this.videoPlayer.player[methodName](youtubeId, time);
             this.videoPlayer.updatePlayTime(time);
         }
+    }
+
+    function onSpeedChange(newSpeed) {
+        var time = this.videoPlayer.currentTime,
+            isFlash = this.currentPlayerMode === 'flash';
 
+        if (isFlash) {
+            this.videoPlayer.currentTime = Time.convert(
+                time,
+                parseFloat(this.speed),
+                newSpeed
+            );
+        }
+
+        newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0');
+
+        this.videoPlayer.log(
+            'speed_change_video',
+            {
+                current_time: time,
+                old_speed: this.speed,
+                new_speed: newSpeed
+            }
+        );
+
+        this.setSpeed(newSpeed, true);
+        this.videoPlayer.setPlaybackRate(newSpeed);
         this.el.trigger('speedchange', arguments);
 
         this.saveState(true, { speed: newSpeed });
diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
index edea7bcdba081699bd2c7c7ea93ebdbc50bb4538..06ff56e180dc8ec18a7bf05e74a2538d096c4175 100644
--- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
+++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
@@ -43,8 +43,7 @@ function () {
     //     these functions will get the 'state' object as a context.
     function _makeFunctionsPublic(state) {
         var methodsDict = {
-            autoHideCaptions: autoHideCaptions,
-            autoShowCaptions: autoShowCaptions,
+            addPaddings: addPaddings,
             bindHandlers: bindHandlers,
             bottomSpacingHeight: bottomSpacingHeight,
             calculateOffset: calculateOffset,
@@ -55,8 +54,8 @@ function () {
             captionKeyDown: captionKeyDown,
             captionMouseDown: captionMouseDown,
             captionMouseOverOut: captionMouseOverOut,
-            captionURL: captionURL,
             fetchCaption: fetchCaption,
+            fetchAvailableTranslations: fetchAvailableTranslations,
             hideCaptions: hideCaptions,
             onMouseEnter: onMouseEnter,
             onMouseLeave: onMouseLeave,
@@ -65,6 +64,8 @@ function () {
             play: play,
             renderCaption: renderCaption,
             renderElements: renderElements,
+            renderLanguageMenu: renderLanguageMenu,
+            reRenderCaption: reRenderCaption,
             resize: resize,
             scrollCaption: scrollCaption,
             search: search,
@@ -105,14 +106,24 @@ function () {
      *         and the CC button will be hidden.
      */
     function renderElements() {
-        this.videoCaption.loaded = false;
+        var Caption = this.videoCaption,
+            languages = this.config.transcriptLanguages;
 
-        this.videoCaption.subtitlesEl = this.el.find('ol.subtitles');
-        this.videoCaption.hideSubtitlesEl = this.el.find('a.hide-subtitles');
+        Caption.loaded = false;
+        Caption.subtitlesEl = this.el.find('ol.subtitles');
+        Caption.container = this.el.find('.lang');
+        Caption.hideSubtitlesEl = this.el.find('a.hide-subtitles');
 
-        if (!this.videoCaption.fetchCaption()) {
-            this.videoCaption.hideCaptions(true);
-            this.videoCaption.hideSubtitlesEl.hide();
+        if (_.keys(languages).length) {
+            Caption.renderLanguageMenu(languages);
+
+            if (!Caption.fetchCaption()) {
+                Caption.hideCaptions(true);
+                Caption.hideSubtitlesEl.hide();
+            }
+        } else {
+            Caption.hideCaptions(true, false);
+            Caption.hideSubtitlesEl.hide();
         }
     }
 
@@ -121,64 +132,77 @@ function () {
     //     Bind any necessary function callbacks to DOM events (click,
     //     mousemove, etc.).
     function bindHandlers() {
-        $(window).bind('resize', this.videoCaption.resize);
-        this.videoCaption.hideSubtitlesEl.on(
-            'click', this.videoCaption.toggle
-        );
+        var self = this,
+            Caption = this.videoCaption;
 
-        this.videoCaption.subtitlesEl
-            .on(
-                'mouseenter',
-                this.videoCaption.onMouseEnter
-            ).on(
-                'mouseleave',
-                this.videoCaption.onMouseLeave
-            ).on(
-                'mousemove',
-                this.videoCaption.onMovement
-            ).on(
-                'mousewheel',
-                this.videoCaption.onMovement
-            ).on(
-                'DOMMouseScroll',
-                this.videoCaption.onMovement
-            );
+        $(window).bind('resize', Caption.resize);
+        Caption.hideSubtitlesEl.on({
+            'click': Caption.toggle
+        });
 
-        if ((this.videoType === 'html5') && (this.config.autohideHtml5)) {
-            this.el.on({
-                mousemove: this.videoCaption.autoShowCaptions,
-                keydown: this.videoCaption.autoShowCaptions
+        Caption.subtitlesEl.on({
+            mouseenter: Caption.onMouseEnter,
+            mouseleave: Caption.onMouseLeave,
+            mousemove: Caption.onMovement,
+            mousewheel: Caption.onMovement,
+            DOMMouseScroll: Caption.onMovement
+        });
+
+        if (Caption.showLanguageMenu) {
+            Caption.container.on({
+                mouseenter: onContainerMouseEnter,
+                mouseleave: onContainerMouseLeave
             });
+        }
 
-            // Moving slider on subtitles is not a mouse move, but captions and
-            // controls should be shown.
-            this.videoCaption.subtitlesEl
-                .on(
-                    'scroll', this.videoCaption.autoShowCaptions
-                )
-                .on(
-                    'scroll', this.videoControl.showControls
-                );
-        } else if (!this.config.autohideHtml5) {
-            this.videoCaption.subtitlesEl.on({
-                keydown: this.videoCaption.autoShowCaptions,
-                focus: this.videoCaption.autoShowCaptions,
+        this.el.on('speedchange', function () {
+            if (self.currentPlayerMode === 'flash') {
+                Caption.fetchCaption();
+            }
+        });
+
+        if ((this.videoType === 'html5') && (this.config.autohideHtml5)) {
+            Caption.subtitlesEl.on('scroll', this.videoControl.showControls);
+        }
+    }
 
-                // Moving slider on subtitles is not a mouse move, but captions
-                // should not be auto-hidden.
-                scroll: this.videoCaption.autoShowCaptions,
+    function onContainerMouseEnter(event) {
+        event.preventDefault();
 
-                mouseout: this.videoCaption.autoHideCaptions,
-                blur: this.videoCaption.autoHideCaptions
-            });
+        $(event.currentTarget).addClass('open');
+    }
 
-            this.videoCaption.hideSubtitlesEl.on({
-                mousemove: this.videoCaption.autoShowCaptions,
+    function onContainerMouseLeave(event) {
+        event.preventDefault();
 
-                mouseout: this.videoCaption.autoHideCaptions,
-                blur: this.videoCaption.autoHideCaptions
-            });
+        $(event.currentTarget).removeClass('open');
+    }
+
+    function onMouseEnter() {
+        if (this.videoCaption.frozen) {
+            clearTimeout(this.videoCaption.frozen);
         }
+
+        this.videoCaption.frozen = setTimeout(
+            this.videoCaption.onMouseLeave,
+            this.config.captionsFreezeTime
+        );
+    }
+
+    function onMouseLeave() {
+        if (this.videoCaption.frozen) {
+            clearTimeout(this.videoCaption.frozen);
+        }
+
+        this.videoCaption.frozen = null;
+
+        if (this.videoCaption.playing) {
+            this.videoCaption.scrollCaption();
+        }
+    }
+
+    function onMovement() {
+        this.videoCaption.onMouseEnter();
     }
 
     /**
@@ -201,8 +225,8 @@ function () {
      *         specified.
      */
     function fetchCaption() {
-        var _this = this;
-
+        var self = this,
+            Caption = self.videoCaption;
         // Check whether the captions file was specified. This is the point
         // where we either stop with the caption panel (so that a white empty
         // panel to the right of the video will not be shown), or carry on
@@ -211,30 +235,50 @@ function () {
             return false;
         }
 
-        this.videoCaption.hideCaptions(this.hide_captions);
+        if (Caption.loaded) {
+            Caption.hideCaptions(false);
+        } else {
+            Caption.hideCaptions(this.hide_captions, false);
+        }
+
+        if (Caption.fetchXHR && Caption.fetchXHR.abort) {
+            Caption.fetchXHR.abort();
+        }
 
         // Fetch the captions file. If no file was specified, or if an error
         // occurred, then we hide the captions panel, and the "CC" button
-        $.ajaxWithPrefix({
-            url: _this.videoCaption.captionURL(),
+        Caption.fetchXHR = $.ajaxWithPrefix({
+            url: self.config.transcriptTranslationUrl,
             notifyOnError: false,
+            data: {
+                videoId: this.youtubeId(),
+                language: this.getCurrentLanguage()
+            },
             success: function (captions) {
-                _this.videoCaption.captions = captions.text;
-                _this.videoCaption.start = captions.start;
-                _this.videoCaption.loaded = true;
-
-                if (_this.isTouch) {
-                    _this.videoCaption.subtitlesEl.find('li').html(
-                        gettext(
-                            'Caption will be displayed when ' +
-                            'you start playing the video.'
-                        )
-                    );
+                Caption.captions = captions.text;
+                Caption.start = captions.start;
+
+                if (Caption.loaded) {
+                    if (Caption.rendered) {
+                        Caption.reRenderCaption();
+                        Caption.updatePlayTime(self.videoPlayer.currentTime);
+                    }
                 } else {
-                    _this.videoCaption.renderCaption();
+                    if (self.isTouch) {
+                        Caption.subtitlesEl.find('li').html(
+                            gettext(
+                                'Caption will be displayed when ' +
+                                'you start playing the video.'
+                            )
+                        );
+                    } else {
+                        Caption.renderCaption();
+                    }
+
+                    Caption.bindHandlers();
                 }
 
-                _this.videoCaption.bindHandlers();
+                Caption.loaded = true;
             },
             error: function (jqXHR, textStatus, errorThrown) {
                 console.log('[Video info]: ERROR while fetching captions.');
@@ -242,70 +286,47 @@ function () {
                     '[Video info]: STATUS:', textStatus +
                     ', MESSAGE:', '' + errorThrown
                 );
-
-                _this.videoCaption.hideCaptions(true, false);
-                _this.videoCaption.hideSubtitlesEl.hide();
+                // If initial list of languages has more than 1 item, check
+                // for availability other transcripts.
+                if (_.keys(self.config.transcriptLanguages).length > 1) {
+                    Caption.fetchAvailableTranslations();
+                } else {
+                    Caption.hideCaptions(true, false);
+                    Caption.hideSubtitlesEl.hide();
+                }
             }
         });
 
         return true;
     }
 
-    function captionURL() {
-        return '' + this.config.captionAssetPath +
-            this.youtubeId('1.0') + '.srt.sjson';
-    }
-
-    function autoShowCaptions(event) {
-        if (!this.captionsShowLock) {
-            if (!this.captionsHidden) {
-                return;
-            }
-
-            this.captionsShowLock = true;
-
-            if (this.captionState === 'invisible') {
-                this.videoCaption.subtitlesEl.show();
-                this.captionState = 'visible';
-            } else if (this.captionState === 'hiding') {
-                this.videoCaption.subtitlesEl
-                    .stop(true, false).css('opacity', 1).show();
-                this.captionState = 'visible';
-            } else if (this.captionState === 'visible') {
-                clearTimeout(this.captionHideTimeout);
-            }
-
-            if (this.config.autohideHtml5) {
-                this.captionHideTimeout = setTimeout(
-                    this.videoCaption.autoHideCaptions,
-                    this.videoCaption.fadeOutTimeout
-                );
-            }
-
-            this.captionsShowLock = false;
-        }
-    }
-
-    function autoHideCaptions() {
-        var _this;
-
-        this.captionHideTimeout = null;
-
-        if (!this.captionsHidden) {
-            return;
-        }
+    function fetchAvailableTranslations() {
+        var self = this,
+            Caption = this.videoCaption;
 
-        this.captionState = 'hiding';
-
-        _this = this;
-
-        this.videoCaption.subtitlesEl
-            .fadeOut(
-                this.videoCaption.fadeOutTimeout,
-                function () {
-                    _this.captionState = 'invisible';
+        return $.ajaxWithPrefix({
+            url: self.config.transcriptAvailableTranslationsUrl,
+            notifyOnError: false,
+            success: function (response) {
+                var currentLanguages = self.config.transcriptLanguages,
+                    newLanguages = _.pick(currentLanguages, response);
+
+                // Update property with available currently translations.
+                self.config.transcriptLanguages = newLanguages;
+                // Remove an old language menu.
+                Caption.container.find('.langs-list').remove();
+
+                if (_.keys(newLanguages).length) {
+                    // And try again to fetch transcript.
+                    Caption.fetchCaption();
+                    Caption.renderLanguageMenu(newLanguages);
                 }
-            );
+            },
+            error: function (jqXHR, textStatus, errorThrown) {
+                Caption.hideCaptions(true, false);
+                Caption.hideSubtitlesEl.hide();
+            }
+        });
     }
 
     function resize() {
@@ -320,100 +341,136 @@ function () {
         this.videoCaption.setSubtitlesHeight();
     }
 
-    function onMouseEnter() {
-        if (this.videoCaption.frozen) {
-            clearTimeout(this.videoCaption.frozen);
-        }
-
-        this.videoCaption.frozen = setTimeout(
-            this.videoCaption.onMouseLeave,
-            this.config.captionsFreezeTime
-        );
-    }
+    function renderLanguageMenu(languages) {
+        var self = this,
+            menu = $('<ol class="langs-list menu">'),
+            currentLang = this.getCurrentLanguage();
 
-    function onMouseLeave() {
-        if (this.videoCaption.frozen) {
-            clearTimeout(this.videoCaption.frozen);
+        if (_.keys(languages).length < 2) {
+            return false;
         }
 
-        this.videoCaption.frozen = null;
+        this.videoCaption.showLanguageMenu = true;
 
-        if (this.videoCaption.playing) {
-            this.videoCaption.scrollCaption();
-        }
-    }
+        $.each(languages, function(code, label) {
+            var li = $('<li data-lang-code="' + code + '" />'),
+                link = $('<a href="javascript:void(0);">' + label + '</a>');
 
-    function onMovement() {
-        if (!this.config.autohideHtml5) {
-            this.videoCaption.autoShowCaptions();
-        }
+            if (currentLang === code) {
+                li.addClass('active');
+            }
 
-        this.videoCaption.onMouseEnter();
-    }
+            li.append(link);
+            menu.append(li);
+        });
 
-    function renderCaption() {
-        var container = $('<ol>'),
-            _this = this,
-            autohideHtml5 = this.config.autohideHtml5;
+        this.videoCaption.container.append(menu);
 
-        this.container.after(this.videoCaption.subtitlesEl);
-        this.el.find('.video-controls .secondary-controls')
-            .append(this.videoCaption.hideSubtitlesEl);
+        menu.on('click', 'a', function (e) {
+            var el = $(e.currentTarget).parent(),
+                Caption = self.videoCaption,
+                langCode = el.data('lang-code');
 
-        this.videoCaption.setSubtitlesHeight();
+            if (self.lang !== langCode) {
+                self.lang = langCode;
+                self.storage.setItem('language', langCode);
+                el  .addClass('active')
+                    .siblings('li')
+                    .removeClass('active');
 
-        if ((this.videoType === 'html5' && autohideHtml5) || !autohideHtml5) {
-            this.videoCaption.fadeOutTimeout = this.config.fadeOutTimeout;
-            this.videoCaption.subtitlesEl.addClass('html5');
-        }
+                Caption.fetchCaption();
+            }
+        });
+    }
 
-        $.each(this.videoCaption.captions, function(index, text) {
+    function buildCaptions (container, captions, start) {
+        var fragment = document.createDocumentFragment();
+
+        $.each(captions, function(index, text) {
             var liEl = $('<li>');
 
             liEl.html(text);
 
             liEl.attr({
                 'data-index': index,
-                'data-start': _this.videoCaption.start[index],
+                'data-start': start[index],
                 'tabindex': 0
             });
 
-            container.append(liEl);
+            fragment.appendChild(liEl[0]);
         });
 
-        this.videoCaption.subtitlesEl
-            .html(container.html())
-            .find('li[data-index]')
-            .on({
-                mouseover:  this.videoCaption.captionMouseOverOut,
-                mouseout:   this.videoCaption.captionMouseOverOut,
-                mousedown:  this.videoCaption.captionMouseDown,
-                click:      this.videoCaption.captionClick,
-                focus:      this.videoCaption.captionFocus,
-                blur:       this.videoCaption.captionBlur,
-                keydown:    this.videoCaption.captionKeyDown
-            });
+        container.append([fragment]);
+    }
+
+    function renderCaption() {
+        var Caption = this.videoCaption,
+            events = ['mouseover', 'mouseout', 'mousedown', 'click', 'focus',
+                'blur', 'keydown'].join(' ');
+
+        Caption.setSubtitlesHeight();
+
+        buildCaptions(Caption.subtitlesEl, Caption.captions, Caption.start);
+
+        Caption.subtitlesEl.on(events, 'li[data-index]', function (event) {
+            switch (event.type) {
+                case 'mouseover':
+                case 'mouseout':
+                    Caption.captionMouseOverOut(event);
+                    break;
+                case 'mousedown':
+                    Caption.captionMouseDown(event);
+                    break;
+                case 'click':
+                    Caption.captionClick(event);
+                    break;
+                case 'focusin':
+                    Caption.captionFocus(event);
+                    break;
+                case 'focusout':
+                    Caption.captionBlur(event);
+                    break;
+                case 'keydown':
+                    Caption.captionKeyDown(event);
+                    break;
+            }
+        });
 
         // Enables or disables automatic scrolling of the captions when the
         // video is playing. This feature has to be disabled when tabbing
         // through them as it interferes with that action. Initially, have this
         // flag enabled as we assume mouse use. Then, if the first caption
         // (through forward tabbing) or the last caption (through backwards
-        // tabbing) gets the focus, disable that feature. Renable it if tabbing
+        // tabbing) gets the focus, disable that feature. Re-enable it if tabbing
         // then cycles out of the the captions.
-        this.videoCaption.autoScrolling = true;
+        Caption.autoScrolling = true;
         // Keeps track of where the focus is situated in the array of captions.
         // Used to implement the automatic scrolling behavior and decide if the
         // outline around a caption has to be hidden or shown on a mouseenter
         // or mouseleave. Initially, no caption has the focus, set the
         // index to -1.
-        this.videoCaption.currentCaptionIndex = -1;
+        Caption.currentCaptionIndex = -1;
         // Used to track if the focus is coming from a click or tabbing. This
         // has to be known to decide if, when a caption gets the focus, an
         // outline has to be drawn (tabbing) or not (mouse click).
-        this.videoCaption.isMouseFocus = false;
+        Caption.isMouseFocus = false;
+        Caption.addPaddings();
+        Caption.rendered = true;
+    }
 
-        // Set top and bottom spacing heigh and make sure they are taken out of
+    function reRenderCaption() {
+        var Caption = this.videoCaption;
+
+        Caption.currentIndex = null;
+        Caption.rendered = false;
+        Caption.subtitlesEl.empty();
+        buildCaptions(Caption.subtitlesEl, Caption.captions, Caption.start);
+        Caption.addPaddings();
+        Caption.rendered = true;
+    }
+
+    function addPaddings() {
+        // Set top and bottom spacing height and make sure they are taken out of
         // the tabbing order.
         this.videoCaption.subtitlesEl
             .prepend(
@@ -426,8 +483,6 @@ function () {
                     .height(this.videoCaption.bottomSpacingHeight())
                     .attr('tabindex', -1)
             );
-
-        this.videoCaption.rendered = true;
     }
 
     // On mouseOver, hide the outline of a caption that has been tabbed to.
@@ -487,6 +542,7 @@ function () {
     function captionBlur(event) {
         var caption = $(event.target),
             captionIndex = parseInt(caption.attr('data-index'), 10);
+
         caption.removeClass('focused');
         // If we are on first or last index, we have to turn automatic scroll
         // on again when losing focus. There is no way to know in what
@@ -494,8 +550,7 @@ function () {
         // tabbing back out of the captions or on the last element and tabbing
         // forward out of the captions.
         if (captionIndex === 0 ||
-            captionIndex === this.videoCaption.captions.length-1) {
-            this.videoCaption.autoHideCaptions();
+            captionIndex === this.videoCaption.captions.length - 1) {
 
             this.videoCaption.autoScrolling = true;
         }
@@ -661,25 +716,9 @@ function () {
         event.preventDefault();
 
         if (this.el.hasClass('closed')) {
-            this.videoCaption.autoShowCaptions();
             this.videoCaption.hideCaptions(false);
         } else {
             this.videoCaption.hideCaptions(true);
-
-            // In the case when captions are not auto-hidden based on mouse
-            // movement anywhere on the video, we must hide them explicitly
-            // after the "CC" button has been clicked (to hide captions).
-            //
-            // Otherwise, in order for the captions to disappear again, the
-            // user must move the mouse button over the "CC" button, or over
-            // the captions themselves. In this case, an "autoShow" will be
-            // triggered, and after a timeout, an "autoHide".
-            if (!this.config.autohideHtml5) {
-                this.captionHideTimeout = setTimeout(
-                    this.videoCaption.autoHideCaptions(),
-                    0
-                );
-            }
         }
     }
 
@@ -751,31 +790,23 @@ function () {
 
     function setSubtitlesHeight() {
         var height = 0;
-        if (
-            ((this.videoType === 'html5') && (this.config.autohideHtml5)) ||
-            (!this.config.autohideHtml5)
-        ){
-            // on page load captionHidden = undefined
-            if  (
-                (
-                    this.captionsHidden === undefined &&
-                    this.hide_captions === true
-                ) ||
-                (this.captionsHidden === true)
-            ) {
-                // In case of html5 autoshowing subtitles, we adjust height of
-                // subs, by height of scrollbar.
-                height = this.videoControl.el.height() +
-                    0.5 * this.videoControl.sliderEl.height();
-                // Height of videoControl does not contain height of slider.
-                // css is set to absolute, to avoid yanking when slider
-                // autochanges its height.
-            }
+        // on page load captionHidden = undefined
+        if  ((this.captionsHidden === undefined && this.hide_captions) ||
+            this.captionsHidden === true
+        ) {
+            // In case of html5 autoshowing subtitles, we adjust height of
+            // subs, by height of scrollbar.
+            height = this.videoControl.el.height() +
+                0.5 * this.videoControl.sliderEl.height();
+            // Height of videoControl does not contain height of slider.
+            // css is set to absolute, to avoid yanking when slider
+            // autochanges its height.
         }
+
         this.videoCaption.subtitlesEl.css({
             maxHeight: this.videoCaption.captionHeight() - height
         });
-     }
+    }
 });
 
 }(RequireJS.requirejs, RequireJS.require, RequireJS.define));
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 4dce45af91c8c93548bb5e14f84e4c778d0ca46c..7d677928402e7c3d622eb14dec659b5f2df223bc 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -113,7 +113,7 @@ class ModelsTest(unittest.TestCase):
 
     def test_load_class(self):
         vc = XModuleDescriptor.load_class('video')
-        vc_str = "<class 'xmodule.video_module.VideoDescriptor'>"
+        vc_str = "<class 'xmodule.video_module.video_module.VideoDescriptor'>"
         self.assertEqual(str(vc), vc_str)
 
 
diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py
index 0ab8c0ced7d86d5ecb794d7eb723ed02c7e23101..be17e900fccc53eb719aa1bcdc9ae2d32c1a1516 100644
--- a/common/lib/xmodule/xmodule/tests/test_video.py
+++ b/common/lib/xmodule/xmodule/tests/test_video.py
@@ -20,7 +20,7 @@ from mock import Mock
 from . import LogicTest
 from lxml import etree
 from xmodule.modulestore import Location
-from xmodule.video_module import VideoDescriptor, _create_youtube_string
+from xmodule.video_module import VideoDescriptor, create_youtube_string
 from .test_import import DummySystem
 from xblock.field_data import DictFieldData
 from xblock.fields import ScopeIds
@@ -150,7 +150,7 @@ class VideoDescriptorTest(unittest.TestCase):
         descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
         descriptor.youtube_id_1_5 = 'rABDYkeK0x8'
         expected = "0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"
-        self.assertEqual(_create_youtube_string(descriptor), expected)
+        self.assertEqual(create_youtube_string(descriptor), expected)
 
     def test_create_youtube_string_missing(self):
         """
@@ -165,7 +165,7 @@ class VideoDescriptorTest(unittest.TestCase):
         descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
         descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
         expected = "0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
-        self.assertEqual(_create_youtube_string(descriptor), expected)
+        self.assertEqual(create_youtube_string(descriptor), expected)
 
 
 class VideoDescriptorImportTestCase(unittest.TestCase):
@@ -193,6 +193,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
               <source src="http://www.example.com/source.mp4"/>
               <source src="http://www.example.com/source.ogg"/>
               <track src="http://www.example.com/track"/>
+              <transcript language="ua" src="ukrainian_translation.srt" />
+              <transcript language="ge" src="german_translation.srt" />
             </video>
         '''
         location = Location(["i4x", "edX", "video", "default",
@@ -215,7 +217,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
             'track': 'http://www.example.com/track',
             'download_track': True,
             'html5_sources': ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'],
-            'data': ''
+            'data': '',
+            'transcripts': {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}
         })
 
     def test_from_xml(self):
@@ -230,6 +233,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
                    end_time="00:01:00">
               <source src="http://www.example.com/source.mp4"/>
               <track src="http://www.example.com/track"/>
+              <transcript language="ua" src="ukrainian_translation.srt" />
+              <transcript language="ge" src="german_translation.srt" />
             </video>
         '''
         output = VideoDescriptor.from_xml(xml_data, module_system, Mock())
@@ -245,7 +250,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
             'download_track': False,
             'download_video': False,
             'html5_sources': ['http://www.example.com/source.mp4'],
-            'data': ''
+            'data': '',
+            'transcripts': {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'},
         })
 
     def test_from_xml_missing_attributes(self):
@@ -304,7 +310,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
             'download_track': True,
             'download_video': True,
             'html5_sources': ['http://www.example.com/source.mp4'],
-            'data': ''
+            'data': '',
+            'transcripts': {},
         })
 
     def test_from_xml_no_attributes(self):
@@ -326,7 +333,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
             'download_track': False,
             'download_video': False,
             'html5_sources': [],
-            'data': ''
+            'data': '',
+            'transcripts': {},
         })
 
     def test_from_xml_double_quotes(self):
@@ -508,6 +516,7 @@ class VideoExportTestCase(unittest.TestCase):
         desc.download_track = True
         desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
         desc.download_video = True
+        desc.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}
 
         xml = desc.definition_to_xml(None)  # We don't use the `resource_fs` parameter
         expected = etree.fromstring('''\
@@ -515,9 +524,10 @@ class VideoExportTestCase(unittest.TestCase):
            <source src="http://www.example.com/source.mp4"/>
            <source src="http://www.example.com/source.ogg"/>
            <track src="http://www.example.com/track"/>
+           <transcript language="ge" src="german_translation.srt" />
+           <transcript language="ua" src="ukrainian_translation.srt" />
          </video>
         ''')
-
         self.assertXmlEqual(expected, xml)
 
     def test_export_to_xml_empty_end_time(self):
diff --git a/common/lib/xmodule/xmodule/video_module/__init__.py b/common/lib/xmodule/xmodule/video_module/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3fba59844aeb7208cc06394ef5653f74dfa45bd8
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/__init__.py
@@ -0,0 +1,10 @@
+"""
+Container for video module and it's utils.
+"""
+
+# Disable wildcard-import warnings.
+# pylint: disable=W0401
+
+from .transcripts_utils import *
+from .video_utils import *
+from .video_module import *
diff --git a/cms/djangoapps/contentstore/transcripts_utils.py b/common/lib/xmodule/xmodule/video_module/transcripts_utils.py
similarity index 61%
rename from cms/djangoapps/contentstore/transcripts_utils.py
rename to common/lib/xmodule/xmodule/video_module/transcripts_utils.py
index c80d027e881011948232bcf23d9c3f659b51323c..b416012d1c98c0723693545c22d960d112a1116f 100644
--- a/cms/djangoapps/contentstore/transcripts_utils.py
+++ b/common/lib/xmodule/xmodule/video_module/transcripts_utils.py
@@ -2,6 +2,7 @@
 Utility functions for transcripts.
 ++++++++++++++++++++++++++++++++++
 """
+import os
 import copy
 import json
 import requests
@@ -9,29 +10,27 @@ import logging
 from pysrt import SubRipTime, SubRipItem, SubRipFile
 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
 from xmodule.contentstore.django import contentstore
-from xmodule.modulestore import Location
 
-from .utils import get_modulestore
 
 log = logging.getLogger(__name__)
 
 
-class TranscriptsGenerationException(Exception):
+class TranscriptException(Exception):  # pylint disable=C0111
+    pass
+
+
+class TranscriptsGenerationException(Exception):  # pylint disable=C0111
     pass
 
 
-class GetTranscriptsFromYouTubeException(Exception):
+class GetTranscriptsFromYouTubeException(Exception):  # pylint disable=C0111
     pass
 
 
-class TranscriptsRequestValidationException(Exception):
+class TranscriptsRequestValidationException(Exception):  # pylint disable=C0111
     pass
 
 
@@ -42,7 +41,7 @@ def generate_subs(speed, source_speed, source_subs):
     Args:
     `speed`: float, for this speed subtitles will be generated,
     `source_speed`: float, speed of source_subs
-    `soource_subs`: dict, existing subtitles for speed `source_speed`.
+    `source_subs`: dict, existing subtitles for speed `source_speed`.
 
     Returns:
     `subs`: dict, actual subtitles.
@@ -64,30 +63,27 @@ def generate_subs(speed, source_speed, source_subs):
     return subs
 
 
-def save_subs_to_store(subs, subs_id, item):
+def save_subs_to_store(subs, subs_id, item, language='en'):
     """
     Save transcripts into `StaticContent`.
 
     Args:
     `subs_id`: str, subtitles id
     `item`: video module instance
+    `language`: two chars str ('uk'), language of translation of transcripts
 
     Returns: location of saved subtitles.
     """
     filedata = json.dumps(subs, indent=2)
     mime_type = 'application/json'
-    filename = 'subs_{0}.srt.sjson'.format(subs_id)
-
-    content_location = StaticContent.compute_location(
-        item.location.org, item.location.course, filename
-    )
+    filename = subs_filename(subs_id, language)
+    content_location = asset_location(item.location, filename)
     content = StaticContent(content_location, filename, mime_type, filedata)
     contentstore().save(content)
-    del_cached_content(content_location)
     return content_location
 
 
-def get_transcripts_from_youtube(youtube_id):
+def get_transcripts_from_youtube(youtube_id, settings, i18n):
     """
     Gets transcripts from youtube for youtube_id.
 
@@ -96,6 +92,8 @@ def get_transcripts_from_youtube(youtube_id):
 
     Returns (status, transcripts): bool, dict.
     """
+    _ = i18n.ugettext
+
     utf8_parser = etree.XMLParser(encoding='utf-8')
 
     youtube_api = copy.deepcopy(settings.YOUTUBE_API)
@@ -127,7 +125,7 @@ def get_transcripts_from_youtube(youtube_id):
     return {'start': sub_starts, 'end': sub_ends, 'text': sub_texts}
 
 
-def download_youtube_subs(youtube_subs, item):
+def download_youtube_subs(youtube_subs, item, settings):
     """
     Download transcripts from Youtube and save them to assets.
 
@@ -138,6 +136,9 @@ def download_youtube_subs(youtube_subs, item):
     Returns: None, if transcripts were successfully downloaded and saved.
     Otherwise raises GetTranscriptsFromYouTubeException.
     """
+    i18n = item.runtime.service(item, "i18n")
+    _ = i18n.ugettext
+
     highest_speed = highest_speed_subs = None
     missed_speeds = []
     # Iterate from lowest to highest speed and try to do download transcripts
@@ -146,7 +147,7 @@ def download_youtube_subs(youtube_subs, item):
         if not youtube_id:
             continue
         try:
-            subs = get_transcripts_from_youtube(youtube_id)
+            subs = get_transcripts_from_youtube(youtube_id, settings, i18n)
             if not subs:  # if empty subs are returned
                 raise GetTranscriptsFromYouTubeException
         except GetTranscriptsFromYouTubeException:
@@ -187,24 +188,19 @@ def download_youtube_subs(youtube_subs, item):
         )
 
 
-def remove_subs_from_store(subs_id, item):
+def remove_subs_from_store(subs_id, item, lang='en'):
     """
     Remove from store, if transcripts content exists.
     """
-    filename = 'subs_{0}.srt.sjson'.format(subs_id)
-    content_location = StaticContent.compute_location(
-        item.location.org, item.location.course, filename
-    )
     try:
-        content = contentstore().find(content_location)
+        content = asset(item.location, subs_id, lang)
         contentstore().delete(content.get_id())
-        del_cached_content(content.location)
         log.info("Removed subs %s from store", subs_id)
     except NotFoundError:
         pass
 
 
-def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item):
+def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item, language='en'):
     """Generate transcripts from source files (like SubRip format, etc.)
     and save them to assets for `item` module.
     We expect, that speed of source subs equal to 1
@@ -213,15 +209,17 @@ def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item):
     :param subs_type: type of source subs: "srt", ...
     :param subs_filedata:unicode, content of source subs.
     :param item: module object.
+    :param language: str, language of translation of transcripts
     :returns: True, if all subs are generated and saved successfully.
     """
+    _ = item.runtime.service(item, "i18n").ugettext
     if subs_type != 'srt':
         raise TranscriptsGenerationException(_("We support only SubRip (*.srt) transcripts format."))
     try:
         srt_subs_obj = SubRipFile.from_string(subs_filedata)
-    except Exception as e:
+    except Exception as ex:
         msg = _("Something wrong with SubRip transcripts file during parsing. Inner message is {error_message}").format(
-            error_message=e.message
+            error_message=ex.message
         )
         raise TranscriptsGenerationException(msg)
     if not srt_subs_obj:
@@ -245,7 +243,8 @@ def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item):
         save_subs_to_store(
             generate_subs(speed, 1, subs),
             subs_id,
-            item
+            item,
+            language
         )
 
     return subs
@@ -279,15 +278,6 @@ def generate_srt_from_sjson(sjson_subs, speed):
     return output
 
 
-def save_module(item, user):
-    """
-    Proceed with additional save operations.
-    """
-    item.save()
-    store = get_modulestore(Location(item.id))
-    store.update_item(item, user.id if user else None)
-
-
 def copy_or_rename_transcript(new_name, old_name, item, delete_old=False, user=None):
     """
     Renames `old_name` transcript file in storage to `new_name`.
@@ -302,7 +292,7 @@ def copy_or_rename_transcript(new_name, old_name, item, delete_old=False, user=N
     transcripts = contentstore().find(content_location).data
     save_subs_to_store(json.loads(transcripts), new_name, item)
     item.sub = new_name
-    save_module(item, user)
+    item.save_with_metadata(user)
     if delete_old:
         remove_subs_from_store(old_name, item)
 
@@ -316,7 +306,7 @@ def get_html5_ids(html5_sources):
     return html5_ids
 
 
-def manage_video_subtitles_save(old_item, new_item, user):
+def manage_video_subtitles_save(item, user, old_metadata=None, generate_translation=False):
     """
     Does some specific things, that can be done only on save.
 
@@ -324,6 +314,12 @@ def manage_video_subtitles_save(old_item, new_item, user):
 
     If value of `sub` field of `new_item` is cleared, transcripts should be removed.
 
+    `item` is video module instance with updated values of fields,
+    but actually have not been saved to store yet.
+
+    `old_metadata` contains old values of XFields.
+
+    # 1.
     If value of `sub` field of `new_item` is different from values of video fields of `new_item`,
     and `new_item.sub` file is present, then code in this function creates copies of
     `new_item.sub` file with new names. That names are equal to values of video fields of `new_item`
@@ -331,23 +327,28 @@ def manage_video_subtitles_save(old_item, new_item, user):
     This whole action ensures that after user changes video fields, proper `sub` files, corresponding
     to new values of video fields, will be presented in system.
 
-    old_item is not used here, but is added for future changes.
+    # 2. Generate transcripts translation only  when user clicks `save` button, not while switching tabs.
+    a) delete sjson translation for those languages, which were removed from `item.transcripts`.
+        Note: we are not deleting old SRT files to give user more flexibility.
+    b) For all SRT files in`item.transcripts` regenerate new SJSON files.
+        (To avoid confusing situation if you attempt to correct a translation by uploading
+        a new version of the SRT file with same name).
     """
 
     # 1.
-    html5_ids = get_html5_ids(new_item.html5_sources)
-    possible_video_id_list = [new_item.youtube_id_1_0] + html5_ids
-    sub_name = new_item.sub
+    html5_ids = get_html5_ids(item.html5_sources)
+    possible_video_id_list = [item.youtube_id_1_0] + html5_ids
+    sub_name = item.sub
     for video_id in possible_video_id_list:
         if not video_id:
             continue
         if not sub_name:
-            remove_subs_from_store(video_id, new_item)
+            remove_subs_from_store(video_id, item)
             continue
         # copy_or_rename_transcript changes item.sub of module
         try:
             # updates item.sub with `video_id`, if it is successful.
-            copy_or_rename_transcript(video_id, sub_name, new_item, user=user)
+            copy_or_rename_transcript(video_id, sub_name, item, user=user)
         except NotFoundError:
             # subtitles file `sub_name` is not presented in the system. Nothing to copy or rename.
             log.debug(
@@ -355,3 +356,121 @@ def manage_video_subtitles_save(old_item, new_item, user):
                 "original file does not exist.",
                 sub_name, video_id
             )
+
+    # 2.
+    if generate_translation:
+        old_langs = set(old_metadata.get('transcripts', {})) if old_metadata else set()
+        new_langs = set(item.transcripts)
+
+        for lang in old_langs.difference(new_langs):  # 2a
+            for video_id in possible_video_id_list:
+                if video_id:
+                    remove_subs_from_store(video_id, item, lang)
+
+        reraised_message = ''
+        for lang in new_langs:  # 2b
+            try:
+                generate_sjson_for_all_speeds(
+                    item,
+                    item.transcripts[lang],
+                    {speed: subs_id for subs_id, speed in youtube_speed_dict(item).iteritems()},
+                    lang,
+                )
+            except TranscriptException as ex:
+                item.transcripts.pop(lang)  # remove key from transcripts because proper srt file does not exist in assets.
+                reraised_message += ' ' + ex.message
+        if reraised_message:
+            item.save_with_metadata(user)
+            raise TranscriptException(reraised_message)
+
+
+def youtube_speed_dict(item):
+    """
+    Returns {speed: youtube_ids, ...} dict for existing youtube_ids
+    """
+    yt_ids = [item.youtube_id_0_75, item.youtube_id_1_0, item.youtube_id_1_25, item.youtube_id_1_5]
+    yt_speeds = [0.75, 1.00, 1.25, 1.50]
+    youtube_ids = {p[0]: p[1] for p in zip(yt_ids, yt_speeds) if p[0]}
+    return youtube_ids
+
+
+def subs_filename(subs_id, lang='en'):
+    """
+    Generate proper filename for storage.
+    """
+    if lang == 'en':
+        return 'subs_{0}.srt.sjson'.format(subs_id)
+    else:
+        return '{0}_subs_{1}.srt.sjson'.format(lang, subs_id)
+
+
+def asset_location(location, filename):
+    """
+    Return asset location.
+
+    `location` is module location.
+    """
+    return StaticContent.compute_location(
+        location.org, location.course, filename
+    )
+
+
+def asset(location, subs_id, lang='en', filename=None):
+    """
+    Get asset from contentstore, asset location is built from subs_id and lang.
+
+    `location` is module location.
+    """
+    return contentstore().find(
+        asset_location(
+            location,
+            subs_filename(subs_id, lang) if not filename else filename
+        )
+    )
+
+
+def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
+    """
+    Generates sjson from srt for given lang.
+
+    `item` is module object.
+    """
+    try:
+        srt_transcripts = contentstore().find(asset_location(item.location, user_filename))
+    except NotFoundError as ex:
+        raise TranscriptException("{}: Can't find uploaded transcripts: {}".format(ex.message, user_filename))
+
+    if not lang:
+        lang = item.transcript_language
+
+    generate_subs_from_source(
+        result_subs_dict,
+        os.path.splitext(user_filename)[1][1:],
+        srt_transcripts.data.decode('utf8'),
+        item,
+        lang
+    )
+
+
+def get_or_create_sjson(item):
+    """
+    Get sjson if already exists, otherwise generate it.
+
+    Generate sjson with subs_id name, from user uploaded srt.
+    Subs_id is extracted from srt filename, which was set by user.
+
+    Raises:
+        TranscriptException: when srt subtitles do not exist,
+        and exceptions from generate_subs_from_source.
+
+    `item` is module object.
+    """
+    user_filename = item.transcripts[item.transcript_language]
+    user_subs_id = os.path.splitext(user_filename)[0]
+    source_subs_id, result_subs_dict = user_subs_id, {1.0: user_subs_id}
+    try:
+        sjson_transcript = asset(item.location, source_subs_id, item.transcript_language).data
+    except (NotFoundError):  # generating sjson from srt
+        generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, item.transcript_language)
+    sjson_transcript = asset(item.location, source_subs_id, item.transcript_language).data
+    return sjson_transcript
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py
similarity index 67%
rename from common/lib/xmodule/xmodule/video_module.py
rename to common/lib/xmodule/xmodule/video_module/video_module.py
index 77204d9fbf5695db167a8d1cfb2f282a00eff155..d840601545072dd617e8ed8fdcdd94f0aa964df2 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module/video_module.py
@@ -10,15 +10,17 @@ in-browser HTML5 video method (when in HTML5 mode).
 in XML.
 """
 
+import os
 import json
 import logging
+from operator import itemgetter
 
 from lxml import etree
 from pkg_resources import resource_string
 import datetime
 import copy
 from webob import Response
-from pysrt import SubRipTime, SubRipItem
+from collections import OrderedDict
 
 from django.conf import settings
 
@@ -26,12 +28,19 @@ from xmodule.x_module import XModule, module_attr
 from xmodule.editing_module import TabsEditingDescriptor
 from xmodule.raw_module import EmptyDataRawDescriptor
 from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field
-from xmodule.contentstore.django import contentstore
-from xmodule.contentstore.content import StaticContent
 from xmodule.exceptions import NotFoundError
 from xblock.core import XBlock
-from xblock.fields import Scope, String, Float, Boolean, List, ScopeIds
+from xblock.fields import Scope, String, Float, Boolean, List, Dict, ScopeIds
 from xmodule.fields import RelativeTime
+from .transcripts_utils import (
+    generate_srt_from_sjson,
+    asset,
+    get_or_create_sjson,
+    TranscriptException,
+    generate_sjson_for_all_speeds,
+    youtube_speed_dict
+)
+from .video_utils import create_youtube_string
 
 from xmodule.modulestore.inheritance import InheritanceKeyValueStore
 from xblock.runtime import KvsFieldData
@@ -51,12 +60,6 @@ class VideoFields(object):
         scope=Scope.user_state,
         default=datetime.timedelta(seconds=0)
     )
-    show_captions = Boolean(
-        help="This controls whether or not captions are shown by default.",
-        display_name="Show Transcript",
-        scope=Scope.settings,
-        default=True
-    )
     # TODO: This should be moved to Scope.content, but this will
     # require data migration to support the old video module.
     youtube_id_1_0 = String(
@@ -130,10 +133,29 @@ class VideoFields(object):
     )
     sub = String(
         help="The name of the timed transcript track (for non-Youtube videos).",
-        display_name="HTML5 Transcript",
+        display_name="Transcript (primary)",
         scope=Scope.settings,
         default=""
     )
+    show_captions = Boolean(
+        help="This controls whether or not captions are shown by default.",
+        display_name="Transcript Display",
+        scope=Scope.settings,
+        default=True
+    )
+    # Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
+    transcripts = Dict(
+        help="Add additional transcripts in other languages",
+        display_name="Transcript Translations",
+        scope=Scope.settings,
+        default={}
+    )
+    transcript_language = String(
+        help="Preferred language for transcript",
+        display_name="Preferred language for transcript",
+        scope=Scope.preferences,
+        default="en"
+    )
     speed = Float(
         help="The last speed that was explicitly set by user for the video.",
         scope=Scope.user_state,
@@ -163,30 +185,31 @@ class VideoModule(VideoFields, XModule):
 
     # To make sure that js files are called in proper order we use numerical
     # index. We do that to avoid issues that occurs in tests.
+    module = __name__.replace('.video_module', '', 2)
     js = {
         'js': [
-            resource_string(__name__, 'js/src/video/00_video_storage.js'),
-            resource_string(__name__, 'js/src/video/00_resizer.js'),
-            resource_string(__name__, 'js/src/video/01_initialize.js'),
-            resource_string(__name__, 'js/src/video/025_focus_grabber.js'),
-            resource_string(__name__, 'js/src/video/02_html5_video.js'),
-            resource_string(__name__, 'js/src/video/03_video_player.js'),
-            resource_string(__name__, 'js/src/video/04_video_control.js'),
-            resource_string(__name__, 'js/src/video/05_video_quality_control.js'),
-            resource_string(__name__, 'js/src/video/06_video_progress_slider.js'),
-            resource_string(__name__, 'js/src/video/07_video_volume_control.js'),
-            resource_string(__name__, 'js/src/video/08_video_speed_control.js'),
-            resource_string(__name__, 'js/src/video/09_video_caption.js'),
-            resource_string(__name__, 'js/src/video/10_main.js')
+            resource_string(module, 'js/src/video/00_video_storage.js'),
+            resource_string(module, 'js/src/video/00_resizer.js'),
+            resource_string(module, 'js/src/video/01_initialize.js'),
+            resource_string(module, 'js/src/video/025_focus_grabber.js'),
+            resource_string(module, 'js/src/video/02_html5_video.js'),
+            resource_string(module, 'js/src/video/03_video_player.js'),
+            resource_string(module, 'js/src/video/04_video_control.js'),
+            resource_string(module, 'js/src/video/05_video_quality_control.js'),
+            resource_string(module, 'js/src/video/06_video_progress_slider.js'),
+            resource_string(module, 'js/src/video/07_video_volume_control.js'),
+            resource_string(module, 'js/src/video/08_video_speed_control.js'),
+            resource_string(module, 'js/src/video/09_video_caption.js'),
+            resource_string(module, 'js/src/video/10_main.js')
         ]
     }
-    css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
+    css = {'scss': [resource_string(module, 'css/video/display.scss')]}
     js_module_name = "Video"
 
     def handle_ajax(self, dispatch, data):
-        accepted_keys = ['speed', 'saved_video_position']
-
+        accepted_keys = ['speed', 'saved_video_position', 'transcript_language']
         if dispatch == 'save_user_state':
+
             for key in data:
                 if hasattr(self, key) and key in accepted_keys:
                     if key == 'saved_video_position':
@@ -206,7 +229,6 @@ class VideoModule(VideoFields, XModule):
 
     def get_html(self):
         track_url = None
-        caption_asset_path = "/static/subs/"
 
         get_ext = lambda filename: filename.rpartition('.')[-1]
         sources = {get_ext(src): src for src in self.html5_sources}
@@ -221,7 +243,26 @@ class VideoModule(VideoFields, XModule):
             if self.track:
                 track_url = self.track
             elif self.sub:
-                track_url = self.runtime.handler_url(self, 'download_transcript')
+                track_url = self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/download'
+
+        if self.transcript_language in self.transcripts:
+            transcript_language = self.transcript_language
+        elif self.sub:
+            transcript_language = 'en'
+        elif self.transcripts:
+            transcript_language = self.transcripts.keys()[0]
+        else:
+            # this for the case, when for currently selected video,
+            # there are no translations and English subtitles are not set by instructor.
+            transcript_language = 'null'
+
+        all_languages = {i[0]: i[1] for i in settings.ALL_LANGUAGES}
+        languages = {lang: all_languages[lang] for lang in self.transcripts}
+        if self.sub:
+            languages.update({'en': 'English'})
+
+        # OrderedDict for easy testing of rendered context in tests
+        transcript_languages = OrderedDict(sorted(languages.items(), key=itemgetter(1)))
 
         return self.system.render_template('video.html', {
             'ajax_url': self.system.ajax_url + '/save_user_state',
@@ -230,7 +271,6 @@ class VideoModule(VideoFields, XModule):
             # isn't on the filesystem
             'data_dir': getattr(self, 'data_dir', None),
             'display_name': self.display_name_with_default,
-            'caption_asset_path': caption_asset_path,
             'end': self.end_time.total_seconds(),
             'id': self.location.html_id(),
             'show_captions': json.dumps(self.show_captions),
@@ -241,68 +281,164 @@ class VideoModule(VideoFields, XModule):
             'start': self.start_time.total_seconds(),
             'sub': self.sub,
             'track': track_url,
-            'youtube_streams': _create_youtube_string(self),
+            'youtube_streams': create_youtube_string(self),
             # TODO: Later on the value 1500 should be taken from some global
             # configuration setting field.
             'yt_test_timeout': 1500,
             'yt_test_url': settings.YOUTUBE_TEST_URL,
+            'transcript_language': transcript_language,
+            'transcript_languages': json.dumps(transcript_languages),
+            'transcript_translation_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/translation',
+            'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/available_translations',
         })
 
-    def get_transcript(self, subs_id):
-        '''
+    def get_transcript(self):
+        """
         Returns transcript in *.srt format.
 
-        Args:
-            `subs_id`: str, subtitles id
-
         Raises:
             - NotFoundError if cannot find transcript file in storage.
             - ValueError if transcript file is empty or incorrect JSON.
             - KeyError if transcript file has incorrect format.
-        '''
-
-        filename = 'subs_{0}.srt.sjson'.format(subs_id)
-        content_location = StaticContent.compute_location(
-            self.location.org, self.location.course, filename
-        )
-        sjson_transcripts = contentstore().find(content_location)
-        str_subs = _generate_srt_from_sjson(json.loads(sjson_transcripts.data), speed=1.0)
+        """
+        lang = self.transcript_language
+        subs_id = self.sub if lang == 'en' else self.youtube_id_1_0
+        data = asset(self.location, subs_id, lang).data
+        str_subs = generate_srt_from_sjson(json.loads(data), speed=1.0)
         if not str_subs:
             log.debug('generate_srt_from_sjson produces no subtitles')
             raise ValueError
 
         return str_subs
 
-
     @XBlock.handler
-    def download_transcript(self, __, ___):
+    def transcript(self, request, dispatch):
         """
-        This is called to get transcript file without timecodes to student.
+        Entry point for transcript handlers.
+
+        Request GET should contains 2-char language code for `download`
+        and additionally `videoId` for `translation`.
+
+        Dispatches:
+        `download`: returns SRT file.
+        `translation`: returns jsoned translation text.
+        `available_translations`: returns list of languages, for which SRT files exist. For 'en' check if SJSON exists.
         """
-        try:
-            subs = self.get_transcript(self.sub)
-        except (NotFoundError):
-            log.debug("Can't find content in storage for %s transcript", self.sub)
-            return Response(status=404)
-        except (ValueError, KeyError):
-            log.debug("Invalid transcript JSON.")
-            return Response(status=400)
-
-        response = Response(
-            subs,
-            headerlist=[
-                ('Content-Disposition', 'attachment; filename="{0}.srt"'.format(self.sub)),
-            ])
-        response.content_type="application/x-subrip"
+        if dispatch == 'translation':
+            if 'language' not in request.GET or 'videoId' not in request.GET:
+                log.info("Invalid /transcript GET parameters.")
+                return Response(status=400)
+
+            lang = request.GET.get('language')
+            if lang not in ['en'] + self.transcripts.keys():
+                log.info("Video: transcript facilities are not available for given language.")
+                return Response(status=404)
+            if lang != self.transcript_language:
+                self.transcript_language = lang
+
+            try:
+                transcript = self.translation(request.GET.get('videoId'))
+            except TranscriptException as ex:
+                log.info(ex.message)
+                response = Response(status=404)
+            else:
+                response = Response(transcript)
+                response.content_type = 'application/json'
+
+        elif dispatch == 'download':
+            try:
+                subs = self.get_transcript()
+            except (NotFoundError, ValueError, KeyError):
+                log.debug("Video@download exception")
+                response = Response(status=404)
+            else:
+                response = Response(
+                    subs,
+                    headerlist=[
+                        ('Content-Disposition', 'attachment; filename="{0}.srt"'.format(self.transcript_language)),
+                    ]
+                )
+                response.content_type = "application/x-subrip"
+
+        elif dispatch == 'available_translations':
+            available_translations = []
+            if self.sub:  # check if sjson exists for 'en'.
+                try:
+                    asset(self.location, self.sub, 'en')
+                except NotFoundError:
+                    passs
+                else:
+                    available_translations = ['en']
+            for lang in self.transcripts:
+                try:
+                   asset(self.location, None, None, self.transcripts[lang])
+                except NotFoundError:
+                    continue
+                available_translations.append(lang)
+            if available_translations:
+                response = Response(json.dumps(available_translations))
+                response.content_type = 'application/json'
+            else:
+                response = Response(status=404)
+        else:  # unknown dispatch
+            log.debug("Dispatch is not allowed")
+            response = Response(status=404)
 
         return response
 
+    def translation(self, subs_id):
+        """
+        This is called to get transcript file for specific language.
+
+        subs_id: str: must be on of: self.sub or one of youtube_ids.
+
+        Logic flow:
+
+        If english -> give back `sub` subtitles:
+            Return what we have in contentstore for given subs_id,
+            We should not regenerate needed transcripts, if, for example, they present for youtube 1.0 speed,
+            and we need for other speeds. Such generation should be done in transcripts workflow.
+        If non-english:
+            a) extract subs_id from srt file name
+            if non-youtube:
+                b) try to find sjson by subs_id and return if sucessful
+                c) otherwise generate sjson from srt and return it.
+            if youtube:
+                b) try to find sjson by subs_id and return if sucessful
+                c) generate sjson from srt for all youtube speeds
+
+        Filenames naming:
+            en: subs_videoid.srt.sjson
+            non_en: uk_subs_videoid.srt.sjson
+        """
+        if self.transcript_language == 'en':
+            return asset(self.location, subs_id).data
+
+        if not self.youtube_id_1_0:  # Non-youtube (HTML5) case:
+            return get_or_create_sjson(self)
+
+        # Youtube case:
+        youtube_ids = youtube_speed_dict(self)
+        assert subs_id in youtube_ids
+
+        try:
+            sjson_transcript = asset(self.location, subs_id, self.transcript_language).data
+        except (NotFoundError):
+            log.info("Can't find content in storage for %s transcript: generating.", subs_id)
+            generate_sjson_for_all_speeds(
+                self,
+                self.transcripts[self.transcript_language],
+                {speed: subs_id for subs_id, speed in youtube_ids.iteritems()},
+                self.transcript_language
+            )
+        sjson_transcript = asset(self.location, subs_id, self.transcript_language).data
+        return sjson_transcript
 
 
 class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor):
     """Descriptor for `VideoModule`."""
     module_class = VideoModule
-    download_transcript = module_attr('download_transcript')
+    transcript = module_attr('transcript')
 
     tabs = [
         {
@@ -317,7 +453,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
     ]
 
     def __init__(self, *args, **kwargs):
-        '''
+        """
         Mostly handles backward compatibility issues.
 
         `source` is deprecated field.
@@ -327,7 +463,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
         b) If `source` is cleared it is not shown anymore.
         c) If `source` exists and `source` in `html5_sources`, do not show `source`
             field. `download_video` field has value True.
-        '''
+        """
         super(VideoDescriptor, self).__init__(*args, **kwargs)
         # For backwards compatibility -- if we've got XML data, parse
         # it out and set the metadata fields
@@ -358,6 +494,13 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
         if not download_track['explicitly_set'] and self.track:
             self.download_track = True
 
+    def save_with_metadata(self, user):
+        """
+        Save module with updated metadata to database."
+        """
+        self.save()
+        self.runtime.modulestore.update_item(self, user.id if user else None)
+
     @property
     def editable_metadata_fields(self):
         editable_fields = super(VideoDescriptor, self).editable_metadata_fields
@@ -408,7 +551,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
         Returns an xml string representing this module.
         """
         xml = etree.Element('video')
-        youtube_string = _create_youtube_string(self)
+        youtube_string = create_youtube_string(self)
         # Mild workaround to ensure that tests pass -- if a field
         # is set to its default value, we don't need to write it out.
         if youtube_string and youtube_string != '1.00:OEoXaMPEzfM':
@@ -440,11 +583,18 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
             ele.set('src', self.track)
             xml.append(ele)
 
+        # sorting for easy testing of resulting xml
+        for transcript_language in sorted(self.transcripts.keys()):
+            ele = etree.Element('transcript')
+            ele.set('language', transcript_language)
+            ele.set('src', self.transcripts[transcript_language])
+            xml.append(ele)
+
         return xml
 
     def get_context(self):
         """
-        Extend context by data for transcripts basic tab.
+        Extend context by data for transcript basic tab.
         """
         _context = super(VideoDescriptor, self).get_context()
 
@@ -503,7 +653,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
                 youtube_id = deserialize_field(cls.youtube_id_1_0, pieces[1])
                 ret[speed] = youtube_id
             except (ValueError, IndexError):
-                log.warning('Invalid YouTube ID: %s' % video)
+                log.warning('Invalid YouTube ID: %s', video)
         return ret
 
     @classmethod
@@ -527,7 +677,6 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
             'from': 'start_time',
             'to': 'end_time'
         }
-
         sources = xml.findall('source')
         if sources:
             field_data['html5_sources'] = [ele.get('src') for ele in sources]
@@ -536,6 +685,10 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
         if track is not None:
             field_data['track'] = track.get('src')
 
+        transcripts = xml.findall('transcript')
+        if transcripts:
+            field_data['transcripts'] = {tr.get('language'): tr.get('src') for tr in transcripts}
+
         for attr, value in xml.items():
             if attr in compat_keys:
                 attr = compat_keys[attr]
@@ -572,80 +725,3 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
             field_data['download_track'] = True
 
         return field_data
-
-
-def _create_youtube_string(module):
-    """
-    Create a string of Youtube IDs from `module`'s metadata
-    attributes. Only writes a speed if an ID is present in the
-    module.  Necessary for backwards compatibility with XML-based
-    courses.
-    """
-    youtube_ids = [
-        module.youtube_id_0_75,
-        module.youtube_id_1_0,
-        module.youtube_id_1_25,
-        module.youtube_id_1_5
-    ]
-    youtube_speeds = ['0.75', '1.00', '1.25', '1.50']
-    return ','.join([':'.join(pair)
-                     for pair
-                     in zip(youtube_speeds, youtube_ids)
-                     if pair[1]])
-
-
-def _generate_subs(speed, source_speed, source_subs):
-    """
-    Generate transcripts from one speed to another speed.
-
-    Args:
-    `speed`: float, for this speed subtitles will be generated,
-    `source_speed`: float, speed of source_subs
-    `soource_subs`: dict, existing subtitles for speed `source_speed`.
-
-    Returns:
-    `subs`: dict, actual subtitles.
-    """
-    if speed == source_speed:
-        return source_subs
-
-    coefficient = 1.0 * speed / source_speed
-    subs = {
-        'start': [
-            int(round(timestamp * coefficient)) for
-            timestamp in source_subs['start']
-        ],
-        'end': [
-            int(round(timestamp * coefficient)) for
-            timestamp in source_subs['end']
-        ],
-        'text': source_subs['text']}
-    return subs
-
-
-def _generate_srt_from_sjson(sjson_subs, speed):
-    """Generate transcripts with speed = 1.0 from sjson to SubRip (*.srt).
-
-    :param sjson_subs: "sjson" subs.
-    :param speed: speed of `sjson_subs`.
-    :returns: "srt" subs.
-    """
-
-    output = ''
-
-    equal_len = len(sjson_subs['start']) == len(sjson_subs['end']) == len(sjson_subs['text'])
-    if not equal_len:
-        return output
-
-    sjson_speed_1 = _generate_subs(speed, 1, sjson_subs)
-
-    for i in range(len(sjson_speed_1['start'])):
-        item = SubRipItem(
-            index=i,
-            start=SubRipTime(milliseconds=sjson_speed_1['start'][i]),
-            end=SubRipTime(milliseconds=sjson_speed_1['end'][i]),
-            text=sjson_speed_1['text'][i]
-        )
-        output += (unicode(item))
-        output += '\n'
-    return output
diff --git a/common/lib/xmodule/xmodule/video_module/video_utils.py b/common/lib/xmodule/xmodule/video_module/video_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf28865164b7d2122240e5759051f9b0e4515c36
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/video_utils.py
@@ -0,0 +1,25 @@
+"""
+Module containts utils specific for video_module but not for transcripts.
+"""
+
+
+def create_youtube_string(module):
+    """
+    Create a string of Youtube IDs from `module`'s metadata
+    attributes. Only writes a speed if an ID is present in the
+    module.  Necessary for backwards compatibility with XML-based
+    courses.
+    """
+    youtube_ids = [
+        module.youtube_id_0_75,
+        module.youtube_id_1_0,
+        module.youtube_id_1_25,
+        module.youtube_id_1_5
+    ]
+    youtube_speeds = ['0.75', '1.00', '1.25', '1.50']
+    return ','.join([
+        ':'.join(pair)
+        for pair
+        in zip(youtube_speeds, youtube_ids)
+        if pair[1]
+    ])
diff --git a/common/test/data/uploads/zh_subs_OEoXaMPEzfM.srt.sjson b/common/test/data/uploads/zh_subs_OEoXaMPEzfM.srt.sjson
new file mode 100644
index 0000000000000000000000000000000000000000..73558337fa39795ee387229a0e1e704b9dc52441
--- /dev/null
+++ b/common/test/data/uploads/zh_subs_OEoXaMPEzfM.srt.sjson
@@ -0,0 +1,17 @@
+{
+  "start": [
+    270,
+    2720,
+    5430
+  ],
+  "end": [
+    2720,
+    5430,
+    7160
+  ],
+  "text": [
+    "好 各位同学",
+    "我们今天要讲的题目是",
+    "从算筹到ENIAC"
+  ]
+}
diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature
index 57aacfbf47aa01881d54fd3df6bb7fe7766f109f..6ea5c5c9b2cac0d1f6eb9fbd07ee3a947ca8dbea 100644
--- a/lms/djangoapps/courseware/features/video.feature
+++ b/lms/djangoapps/courseware/features/video.feature
@@ -1,6 +1,6 @@
 @shard_2
-Feature: LMS.Video component
-  As a student, I want to view course videos in LMS.
+Feature: LMS Video component
+  As a student, I want to view course videos in LMS
 
   # 0
   Scenario: Video component stores position correctly when page is reloaded
@@ -58,7 +58,7 @@ Feature: LMS.Video component
     And error message has correct text
 
   # 8
-  Scenario: Video component stores speed correctly when each video is in separate sequence.
+  Scenario: Video component stores speed correctly when each video is in separate sequence
     Given I am registered for the course "test_course"
     And it has a video "A" in "Youtube" mode in position "1" of sequential
     And a video "B" in "Youtube" mode in position "2" of sequential
@@ -78,3 +78,15 @@ Feature: LMS.Video component
     Then video "B" should start playing at speed "0.50"
     When I open video "C"
     Then video "C" should start playing at speed "1.0"
+
+  # 9
+  Scenario: Language menu in Video component works correctly
+    Given the course has a Video component in Youtube mode:
+      | transcripts           | sub         |
+      | {"zh": "OEoXaMPEzfM"} | OEoXaMPEzfM |
+    And I make sure captions are closed
+    And I see video menu "language" with correct items
+    And I select language with code "zh"
+    Then I see "好 各位同学" text in the captions
+    And I select language with code "en"
+    And I see "Hi, welcome to Edx." text in the captions
diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py
index 7a710cbd74c061f0d685ad69d2563c9575ae28ad..986d811684191c3e359702eec77d123d1f4f918a 100644
--- a/lms/djangoapps/courseware/features/video.py
+++ b/lms/djangoapps/courseware/features/video.py
@@ -1,8 +1,19 @@
+# -*- coding: utf-8 -*-
 #pylint: disable=C0111
 
 from lettuce import world, step
+import json
 from common import i_am_registered_for_the_course, section_location, visit_scenario_item
 from django.utils.translation import ugettext as _
+from django.conf import settings
+from cache_toolbox.core import del_cached_content
+from xmodule.contentstore.content import StaticContent
+import os
+from functools import partial
+from xmodule.contentstore.django import contentstore
+TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
+LANGUAGES = settings.ALL_LANGUAGES
+
 
 ############### ACTIONS ####################
 
@@ -14,16 +25,23 @@ HTML5_SOURCES = [
 HTML5_SOURCES_INCORRECT = [
     'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99',
 ]
-
 VIDEO_BUTTONS = {
     'CC': '.hide-subtitles',
     'volume': '.volume',
     'play': '.video_control.play',
     'pause': '.video_control.pause',
 }
+VIDEO_MENUS = {
+    'language': '.lang .menu',
+    'speed': '.speed .menu',
+}
 
-# We should wait 300 ms for event handler invocation + 200ms for safety.
-DELAY = 0.5
+VIDEO_BUTTONS = {
+    'CC': '.hide-subtitles',
+    'volume': '.volume',
+    'play': '.video_control.play',
+    'pause': '.video_control.pause',
+}
 
 coursenum = 'test_course'
 sequence = {}
@@ -33,20 +51,20 @@ def does_not_autoplay(_step, video_type):
     assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False')
 
 
-@step('the course has a Video component in (.*) mode$')
+@step('the course has a Video component in (.*) mode(?:\:)?$')
 def view_video(_step, player_mode):
 
     i_am_registered_for_the_course(_step, coursenum)
 
     # Make sure we have a video
-    add_video_to_course(coursenum, player_mode.lower())
+    add_video_to_course(coursenum, player_mode.lower(), _step.hashes)
     visit_scenario_item('SECTION')
 
 
-@step('a video "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential$')
+@step('a video "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential(?:\:)?$')
 def add_video(_step, player_id, player_mode, position):
     sequence[player_id] = position
-    add_video_to_course(coursenum, player_mode.lower(), display_name=player_id)
+    add_video_to_course(coursenum, player_mode.lower(), _step.hashes, display_name=player_id)
 
 
 @step('I open the section with videos$')
@@ -70,49 +88,55 @@ def check_video_speed(_step, player_id, speed):
     speed_css = '.speeds p.active'
     assert world.css_has_text(speed_css, '{0}x'.format(speed))
 
-def add_video_to_course(course, player_mode, display_name='Video'):
+
+def add_video_to_course(course, player_mode, hashes, display_name='Video'):
     category = 'video'
 
     kwargs = {
         'parent_location': section_location(course),
         'category': category,
-        'display_name': display_name
+        'display_name': display_name,
+        'metadata': {},
     }
 
     if player_mode == 'html5':
-        kwargs.update({
-            'metadata': {
-                'youtube_id_1_0': '',
-                'youtube_id_0_75': '',
-                'youtube_id_1_25': '',
-                'youtube_id_1_5': '',
-                'html5_sources': HTML5_SOURCES
-            }
+        kwargs['metadata'].update({
+            'youtube_id_1_0': '',
+            'youtube_id_0_75': '',
+            'youtube_id_1_25': '',
+            'youtube_id_1_5': '',
+            'html5_sources': HTML5_SOURCES
         })
     if player_mode == 'youtube_html5':
-        kwargs.update({
-            'metadata': {
-                'html5_sources': HTML5_SOURCES
-            }
+        kwargs['metadata'].update({
+            'html5_sources': HTML5_SOURCES
         })
     if player_mode == 'youtube_html5_unsupported_video':
-        kwargs.update({
-            'metadata': {
-                'html5_sources': HTML5_SOURCES_INCORRECT
-            }
+        kwargs['metadata'].update({
+            'html5_sources': HTML5_SOURCES_INCORRECT
         })
     if player_mode == 'html5_unsupported_video':
-        kwargs.update({
-            'metadata': {
-                'youtube_id_1_0': '',
-                'youtube_id_0_75': '',
-                'youtube_id_1_25': '',
-                'youtube_id_1_5': '',
-                'html5_sources': HTML5_SOURCES_INCORRECT
-            }
+        kwargs['metadata'].update({
+            'youtube_id_1_0': '',
+            'youtube_id_0_75': '',
+            'youtube_id_1_25': '',
+            'youtube_id_1_5': '',
+            'html5_sources': HTML5_SOURCES_INCORRECT
         })
 
-    world.ItemFactory.create(**kwargs)
+    if hashes:
+        kwargs['metadata'].update(hashes[0])
+
+    if 'transcripts' in kwargs['metadata']:
+        kwargs['metadata']['transcripts'] = json.loads(kwargs['metadata']['transcripts'])
+
+        if 'sub' in kwargs['metadata']:
+            _upload_file(kwargs['metadata']['sub'], 'en', world.scenario_dict['COURSE'].location)
+
+        for lang, videoId in kwargs['metadata']['transcripts'].items():
+            _upload_file(videoId, lang, world.scenario_dict['COURSE'].location)
+
+    world.scenario_dict['VIDEO'] = world.ItemFactory.create(**kwargs)
 
 
 @step('youtube server is up and response time is (.*) seconds$')
@@ -152,6 +176,92 @@ def error_message_has_correct_text(_step):
     assert world.css_has_text(selector, text)
 
 
+@step('I make sure captions are (.+)$')
+def set_captions_visibility_state(_step, captions_state):
+    SELECTOR = '.closed .subtitles'
+    if world.is_css_not_present(SELECTOR):
+        if captions_state == 'closed':
+            world.css_find('.hide-subtitles').click()
+    else:
+        if captions_state != 'closed':
+            world.css_find('.hide-subtitles').click()
+
+
+@step('I see video menu "([^"]*)" with correct items$')
+def i_see_menu(_step, menu):
+    _open_menu(menu)
+    menu_items = world.css_find(VIDEO_MENUS[menu] + ' li')
+    Video = world.scenario_dict['VIDEO']
+    transcripts = dict(Video.transcripts)
+    if Video.sub:
+        transcripts.update({
+            'en': Video.sub
+        })
+
+    languages = {i[0]: i[1] for i in LANGUAGES}
+    transcripts = {k: languages[k] for k in transcripts}
+
+    for code, label in transcripts.items():
+        assert any([i.text == label for i in menu_items])
+        assert any([i['data-lang-code'] == code for i in menu_items])
+
+
+@step('I see "([^"]*)" text in the captions$')
+def check_text_in_the_captions(_step, text):
+    assert world.browser.is_text_present(text.strip())
+
+
+@step('I select language with code "([^"]*)"$')
+def select_language(_step, code):
+    _open_menu("language")
+    selector = VIDEO_MENUS["language"] + ' li[data-lang-code={code}]'.format(
+        code=code
+    )
+    item = world.css_find(selector)
+
+    item.click()
+
+    assert world.css_has_class(selector, 'active')
+    assert len(world.css_find(VIDEO_MENUS["language"] + ' li.active')) == 1
+    assert world.css_visible('.subtitles')
+    world.wait_for_ajax_complete()
+
+
+@step('I click on video button "([^"]*)"$')
+def click_button(_step, button):
+    world.css_find(VIDEO_BUTTONS[button]).click()
+
+
+def _upload_file(videoId, lang, location):
+    if lang == 'en':
+        filename = 'subs_{0}.srt.sjson'.format(videoId)
+    else:
+        filename = '{0}_subs_{1}.srt.sjson'.format(lang, videoId)
+
+    path = os.path.join(TEST_ROOT, 'uploads/', filename)
+    f = open(os.path.abspath(path))
+    mime_type = "application/json"
+
+    content_location = StaticContent.compute_location(
+        location.org, location.course, filename
+    )
+
+    sc_partial = partial(StaticContent, content_location, filename, mime_type)
+    content = sc_partial(f.read())
+
+    (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
+        content,
+        tempfile_path=None
+    )
+    del_cached_content(thumbnail_location)
+
+    if thumbnail_content is not None:
+        content.thumbnail_location = thumbnail_location
+
+    contentstore().save(content)
+    del_cached_content(content.location)
+
+
 def _navigate_to_an_item_in_a_sequence(number):
     sequence_css = 'a[data-element="{0}"]'.format(number)
     world.css_click(sequence_css)
@@ -165,7 +275,6 @@ def _change_video_speed(speed):
 
 @step('I click video button "([^"]*)"$')
 def click_button_video(_step, button_type):
-    world.wait(DELAY)
     world.wait_for_ajax_complete()
     button = button_type.strip()
     world.css_click(VIDEO_BUTTONS[button])
@@ -184,3 +293,9 @@ def seek_video_to_n_seconds(_step, seconds):
     time = float(seconds.strip())
     jsCode = "$('.video').data('video-player-state').videoPlayer.onSlideSeek({{time: {0:f}}})".format(time)
     world.browser.execute_script(jsCode)
+
+
+def _open_menu(menu):
+    world.browser.execute_script("$('{selector}').parent().addClass('open')".format(
+        selector=VIDEO_MENUS[menu]
+    ))
diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py
index fcd92515ef5679e196a946a9362d85d742897d20..6271024483389222b9ac744a438ad710e9aee728 100644
--- a/lms/djangoapps/courseware/tests/__init__.py
+++ b/lms/djangoapps/courseware/tests/__init__.py
@@ -90,9 +90,11 @@ class BaseTestXmodule(ModuleStoreTestCase):
         self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_data)
 
         self.item_descriptor.xmodule_runtime = self.new_module_runtime()
-        self.item_module = self.item_descriptor
 
-        self.item_url = Location(self.item_module.location).url()
+        #self.item_module = self.item_descriptor.xmodule_runtime.xmodule_instance
+        #self.item_module is None at this time
+
+        self.item_url = Location(self.item_descriptor.location).url()
 
     def setup_course(self):
         self.course = CourseFactory.create(data=self.COURSE_DATA)
@@ -130,7 +132,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
         self.assertTrue(all(self.login_statuses))
 
     def setUp(self):
-        self.setup_course();
+        self.setup_course()
         self.initialize_module(metadata=self.METADATA, data=self.DATA)
 
     def get_url(self, dispatch):
diff --git a/lms/djangoapps/courseware/tests/test_lti_integration.py b/lms/djangoapps/courseware/tests/test_lti_integration.py
index adc89fbbbd11bad54a1b08d71414d83c9473ad34..e98b4b5ec95d266c60c04a99c9574a567beb226b 100644
--- a/lms/djangoapps/courseware/tests/test_lti_integration.py
+++ b/lms/djangoapps/courseware/tests/test_lti_integration.py
@@ -27,8 +27,8 @@ class TestLTI(BaseTestXmodule):
         mocked_signature_after_sign = u'my_signature%3D'
         mocked_decoded_signature = u'my_signature='
 
-        lti_id = self.item_module.lti_id
-        module_id = unicode(urllib.quote(self.item_module.id))
+        lti_id = self.item_descriptor.lti_id
+        module_id = unicode(urllib.quote(self.item_descriptor.id))
         user_id = unicode(self.item_descriptor.xmodule_runtime.anonymous_student_id)
 
         sourcedId = "{id}:{resource_link}:{user_id}".format(
@@ -38,9 +38,9 @@ class TestLTI(BaseTestXmodule):
         )
 
         lis_outcome_service_url = 'https://{host}{path}'.format(
-                host=self.item_descriptor.xmodule_runtime.hostname,
-                path=self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'grade_handler', thirdparty=True).rstrip('/?')
-            )
+            host=self.item_descriptor.xmodule_runtime.hostname,
+            path=self.item_descriptor.xmodule_runtime.handler_url(self.item_descriptor, 'grade_handler', thirdparty=True).rstrip('/?')
+        )
         self.correct_headers = {
             u'user_id': user_id,
             u'oauth_callback': u'about:blank',
@@ -63,13 +63,13 @@ class TestLTI(BaseTestXmodule):
         saved_sign = oauthlib.oauth1.Client.sign
 
         self.expected_context = {
-            'display_name': self.item_module.display_name,
+            'display_name': self.item_descriptor.display_name,
             'input_fields': self.correct_headers,
-            'element_class': self.item_module.category,
-            'element_id': self.item_module.location.html_id(),
+            'element_class': self.item_descriptor.category,
+            'element_id': self.item_descriptor.location.html_id(),
             'launch_url': 'http://www.example.com',  # default value
             'open_in_a_new_page': True,
-            'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'preview_handler').rstrip('/?'),
+            'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_descriptor, 'preview_handler').rstrip('/?'),
         }
 
         def mocked_sign(self, *args, **kwargs):
@@ -92,11 +92,11 @@ class TestLTI(BaseTestXmodule):
         self.addCleanup(patcher.stop)
 
     def test_lti_constructor(self):
-        generated_content = self.item_module.render('student_view').content
+        generated_content = self.item_descriptor.render('student_view').content
         expected_content =  self.runtime.render_template('lti.html', self.expected_context)
         self.assertEqual(generated_content, expected_content)
 
     def test_lti_preview_handler(self):
-        generated_content = self.item_module.preview_handler(None, None).body
+        generated_content = self.item_descriptor.preview_handler(None, None).body
         expected_content = self.runtime.render_template('lti_form.html', self.expected_context)
         self.assertEqual(generated_content, expected_content)
diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py
new file mode 100644
index 0000000000000000000000000000000000000000..731f11b2d17cf23c5ef6e43d7656b42ab7cb8b03
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/test_video_handlers.py
@@ -0,0 +1,351 @@
+# -*- coding: utf-8 -*-
+"""Video xmodule tests in mongo."""
+
+from mock import patch
+import os
+import tempfile
+import textwrap
+import json
+from datetime import timedelta
+from webob import Request
+
+from xmodule.contentstore.content import StaticContent
+from xmodule.modulestore import Location
+from xmodule.contentstore.django import contentstore
+from . import BaseTestXmodule
+from .test_video_xml import SOURCE_XML
+from cache_toolbox.core import del_cached_content
+from xmodule.exceptions import NotFoundError
+
+
+def _create_srt_file(content=None):
+    """
+    Create srt file in filesystem.
+    """
+    content = content or textwrap.dedent("""
+        0
+        00:00:00,12 --> 00:00:00,100
+        Привіт, edX вітає вас.
+    """)
+    srt_file = tempfile.NamedTemporaryFile(suffix=".srt")
+    srt_file.content_type = 'application/x-subrip'
+    srt_file.write(content)
+    srt_file.seek(0)
+    return srt_file
+
+
+def _clear_assets(location):
+    """
+    Clear all assets for location.
+    """
+    store = contentstore()
+
+    content_location = StaticContent.compute_location(
+        location.org, location.course, location.name
+    )
+
+    assets, __ = store.get_all_content_for_course(content_location)
+    for asset in assets:
+        asset_location = Location(asset["_id"])
+        del_cached_content(asset_location)
+        id = StaticContent.get_id_from_location(asset_location)
+        store.delete(id)
+
+
+def _get_subs_id(filename):
+    basename = os.path.splitext(os.path.basename(filename))[0]
+    return basename.replace('subs_', '').replace('.srt', '')
+
+
+def _create_file(content=''):
+    """
+    Create temporary subs_somevalue.srt.sjson file.
+    """
+    sjson_file = tempfile.NamedTemporaryFile(prefix="subs_", suffix=".srt.sjson")
+    sjson_file.content_type = 'application/json'
+    sjson_file.write(textwrap.dedent(content))
+    sjson_file.seek(0)
+    return sjson_file
+
+
+def _upload_sjson_file(subs_file, location, default_filename='subs_{}.srt.sjson'):
+    filename = default_filename.format(_get_subs_id(subs_file.name))
+    _upload_file(subs_file, location, filename)
+
+
+def _upload_file(subs_file, location, filename):
+    mime_type = subs_file.content_type
+    content_location = StaticContent.compute_location(
+        location.org, location.course, filename
+    )
+    content = StaticContent(content_location, filename, mime_type, subs_file.read())
+    contentstore().save(content)
+    del_cached_content(content.location)
+
+
+class TestVideo(BaseTestXmodule):
+    """Integration tests: web client + mongo."""
+    CATEGORY = "video"
+    DATA = SOURCE_XML
+    METADATA = {}
+
+    def test_handle_ajax_wrong_dispatch(self):
+        responses = {
+            user.username: self.clients[user.username].post(
+                self.get_url('whatever'),
+                {},
+                HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+            for user in self.users
+        }
+
+        self.assertEqual(
+            set([
+                response.status_code
+                for _, response in responses.items()
+                ]).pop(),
+            404)
+
+    def test_handle_ajax(self):
+
+        data = [
+            {'speed': 2.0},
+            {'saved_video_position': "00:00:10"},
+            {'transcript_language': json.dumps('uk')},
+        ]
+        for sample in data:
+            response = self.clients[self.users[0].username].post(
+                self.get_url('save_user_state'),
+                sample,
+                HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+            self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(self.item_descriptor.speed, None)
+        self.item_descriptor.handle_ajax('save_user_state', {'speed': json.dumps(2.0)})
+        self.assertEqual(self.item_descriptor.speed, 2.0)
+        self.assertEqual(self.item_descriptor.global_speed, 2.0)
+
+        self.assertEqual(self.item_descriptor.saved_video_position, timedelta(0))
+        self.item_descriptor.handle_ajax('save_user_state', {'saved_video_position': "00:00:10"})
+        self.assertEqual(self.item_descriptor.saved_video_position, timedelta(0, 10))
+
+        self.assertEqual(self.item_descriptor.transcript_language, 'en')
+        self.item_descriptor.handle_ajax('save_user_state', {'transcript_language': json.dumps("uk")})
+        self.assertEqual(self.item_descriptor.transcript_language, 'uk')
+
+    def tearDown(self):
+        _clear_assets(self.item_descriptor.location)
+
+
+class TestVideoTranscriptTranslation(TestVideo):
+    """
+    Test video handlers that provide translation transcripts.
+    """
+
+    non_en_file = _create_srt_file()
+    DATA = """
+        <video show_captions="true"
+        display_name="A Name"
+        >
+            <source src="example.mp4"/>
+            <source src="example.webm"/>
+            <transcript language="uk" src="{}"/>
+        </video>
+    """.format(os.path.split(non_en_file.name)[1])
+
+    MODEL_DATA = {
+        'data': DATA
+    }
+
+    def setUp(self):
+        super(TestVideoTranscriptTranslation, self).setUp()
+        self.item_descriptor.render('student_view')
+        self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
+
+    def test_language_is_not_supported(self):
+        request = Request.blank('/download?language=ru')
+        response = self.item.transcript(request=request, dispatch='download')
+        self.assertEqual(response.status, '404 Not Found')
+
+    def test_download_transcript_not_exist(self):
+        request = Request.blank('/download?language=en')
+        response = self.item.transcript(request=request, dispatch='download')
+        self.assertEqual(response.status, '404 Not Found')
+
+    @patch('xmodule.video_module.VideoModule.get_transcript', return_value='Subs!')
+    def test_download_exist(self, __):
+        request = Request.blank('/download?language=en')
+        response = self.item.transcript(request=request, dispatch='download')
+        self.assertEqual(response.body, 'Subs!')
+
+    def test_translation_fails(self):
+        # No videoId
+        request = Request.blank('/translation?language=ru')
+        response = self.item.transcript(request=request, dispatch='translation')
+        self.assertEqual(response.status, '400 Bad Request')
+
+        # Language is not in available languages
+        request = Request.blank('/translation?language=ru&videoId=12345')
+        response = self.item.transcript(request=request, dispatch='translation')
+        self.assertEqual(response.status, '404 Not Found')
+
+    def test_translaton_en_success(self):
+        subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
+        good_sjson = _create_file(json.dumps(subs))
+        _upload_sjson_file(good_sjson, self.item_descriptor.location)
+        subs_id = _get_subs_id(good_sjson.name)
+
+        self.item.sub = subs_id
+        request = Request.blank('/translation?language=en&videoId={}'.format(subs_id))
+        response = self.item.transcript(request=request, dispatch='translation')
+        self.assertDictEqual(json.loads(response.body), subs)
+
+    def test_translaton_non_en_non_youtube_success(self):
+        subs = {
+            u'end': [100],
+            u'start': [12],
+            u'text': [
+            u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
+            ]
+        }
+        self.non_en_file.seek(0)
+        _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
+        subs_id = _get_subs_id(self.non_en_file.name)
+
+        # manually clean youtube_id_1_0, as it has default value
+        self.item.youtube_id_1_0 = ""
+        request = Request.blank('/translation?language=uk&videoId={}'.format(subs_id))
+        response = self.item.transcript(request=request, dispatch='translation')
+        self.assertDictEqual(json.loads(response.body), subs)
+
+    def test_translation_non_en_youtube(self):
+        subs = {
+            u'end': [100],
+            u'start': [12],
+            u'text': [
+            u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
+        ]}
+        self.non_en_file.seek(0)
+        _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
+        subs_id = _get_subs_id(self.non_en_file.name)
+
+        # youtube 1_0 request, will generate for all speeds for existing ids
+        self.item.youtube_id_1_0 = subs_id
+        self.item.youtube_id_0_75 = '0_75'
+        request = Request.blank('/translation?language=uk&videoId={}'.format(subs_id))
+        response = self.item.transcript(request=request, dispatch='translation')
+        self.assertDictEqual(json.loads(response.body), subs)
+
+        # 0_75 subs are exist
+        request = Request.blank('/translation?language=uk&videoId={}'.format('0_75'))
+        response = self.item.transcript(request=request, dispatch='translation')
+        calculated_0_75 = {
+            u'end': [75],
+            u'start': [9],
+            u'text': [
+            u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
+            ]
+        }
+        self.assertDictEqual(json.loads(response.body), calculated_0_75)
+        # 1_5 will be generated from 1_0
+        self.item.youtube_id_1_5 = '1_5'
+        request = Request.blank('/translation?language=uk&videoId={}'.format('1_5'))
+        response = self.item.transcript(request=request, dispatch='translation')
+        calculated_1_5 = {
+            u'end': [150],
+            u'start': [18],
+            u'text': [
+            u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
+            ]
+        }
+        self.assertDictEqual(json.loads(response.body), calculated_1_5)
+
+
+class TestVideoTranscriptsDownload(TestVideo):
+    """
+    Make sure that `get_transcript` method works correctly
+    """
+
+    DATA = """
+        <video show_captions="true"
+        display_name="A Name"
+        >
+            <source src="example.mp4"/>
+            <source src="example.webm"/>
+        </video>
+    """
+    MODEL_DATA = {
+        'data': DATA
+    }
+    METADATA = {}
+
+    def setUp(self):
+        super(TestVideoTranscriptsDownload, self).setUp()
+        self.item_descriptor.render('student_view')
+        self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
+
+    def test_good_transcript(self):
+        good_sjson = _create_file(content=textwrap.dedent("""\
+                {
+                  "start": [
+                    270,
+                    2720
+                  ],
+                  "end": [
+                    2720,
+                    5430
+                  ],
+                  "text": [
+                    "Hi, welcome to Edx.",
+                    "Let&#39;s start with what is on your screen right now."
+                  ]
+                }
+            """))
+
+        _upload_sjson_file(good_sjson, self.item.location)
+        self.item.sub = _get_subs_id(good_sjson.name)
+        text = self.item.get_transcript()
+        expected_text = textwrap.dedent("""\
+            0
+            00:00:00,270 --> 00:00:02,720
+            Hi, welcome to Edx.
+
+            1
+            00:00:02,720 --> 00:00:05,430
+            Let&#39;s start with what is on your screen right now.
+
+            """)
+
+        self.assertEqual(text, expected_text)
+
+    def test_not_found_error(self):
+        with self.assertRaises(NotFoundError):
+            self.item.get_transcript()
+
+    def test_value_error(self):
+        good_sjson = _create_file(content='bad content')
+
+        _upload_sjson_file(good_sjson, self.item.location)
+        self.item.sub = _get_subs_id(good_sjson.name)
+
+        with self.assertRaises(ValueError):
+            self.item.get_transcript()
+
+    def test_key_error(self):
+        good_sjson = _create_file(content="""
+                {
+                  "start": [
+                    270,
+                    2720
+                  ],
+                  "end": [
+                    2720,
+                    5430
+                  ]
+                }
+            """)
+
+        _upload_sjson_file(good_sjson, self.item.location)
+        self.item.sub = _get_subs_id(good_sjson.name)
+
+        with self.assertRaises(KeyError):
+            self.item.get_transcript()
diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py
index 552f591d0573b71816ac32f7697d2969340ab0e8..80849286eaa9d4b335a7dca7635ea23546b45d4f 100644
--- a/lms/djangoapps/courseware/tests/test_video_mongo.py
+++ b/lms/djangoapps/courseware/tests/test_video_mongo.py
@@ -1,46 +1,13 @@
 # -*- coding: utf-8 -*-
 """Video xmodule tests in mongo."""
-
 from mock import patch, PropertyMock
-import os
-import tempfile
-import textwrap
-from functools import partial
-
-from xmodule.contentstore.content import StaticContent
-from xmodule.modulestore import Location
-from xmodule.contentstore.django import contentstore
+import json
+
 from . import BaseTestXmodule
 from .test_video_xml import SOURCE_XML
+from .test_video_handlers import TestVideo
 from django.conf import settings
-from xmodule.video_module import _create_youtube_string
-from cache_toolbox.core import del_cached_content
-from xmodule.exceptions import NotFoundError
-
-class TestVideo(BaseTestXmodule):
-    """Integration tests: web client + mongo."""
-    CATEGORY = "video"
-    DATA = SOURCE_XML
-    METADATA = {}
-
-    def test_handle_ajax_dispatch(self):
-        responses = {
-            user.username: self.clients[user.username].post(
-                self.get_url('whatever'),
-                {},
-                HTTP_X_REQUESTED_WITH='XMLHttpRequest')
-            for user in self.users
-        }
-
-        self.assertEqual(
-            set([
-                response.status_code
-                for _, response in responses.items()
-                ]).pop(),
-            404)
-
-    def tearDown(self):
-        _clear_assets(self.item_module.location)
+from xmodule.video_module import create_youtube_string
 
 
 class TestVideoYouTube(TestVideo):
@@ -48,7 +15,7 @@ class TestVideoYouTube(TestVideo):
 
     def test_video_constructor(self):
         """Make sure that all parameters extracted correctly from xml"""
-        context = self.item_module.render('student_view').content
+        context = self.item_descriptor.render('student_view').content
 
         sources = {
             'main': u'example.mp4',
@@ -58,12 +25,12 @@ class TestVideoYouTube(TestVideo):
 
         expected_context = {
             'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
+            'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
             'data_dir': getattr(self, 'data_dir', None),
-            'caption_asset_path': '/static/subs/',
-            'show_captions': 'true',
             'display_name': u'A Name',
             'end': 3610.0,
-            'id': self.item_module.location.html_id(),
+            'id': self.item_descriptor.location.html_id(),
+            'show_captions': 'true',
             'sources': sources,
             'speed': 'null',
             'general_speed': 1.0,
@@ -71,15 +38,21 @@ class TestVideoYouTube(TestVideo):
             'saved_video_position': 0.0,
             'sub': u'a_sub_file.srt.sjson',
             'track': None,
-            'youtube_streams': _create_youtube_string(self.item_module),
-            'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
+            'youtube_streams': create_youtube_string(self.item_descriptor),
             'yt_test_timeout': 1500,
             'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
+            'transcript_language': 'en',
+            'transcript_languages': '{"en": "English", "uk": "Ukrainian"}',
+            'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
+                self.item_descriptor, 'transcript'
+            ).rstrip('/?') + '/translation',
+            'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+                self.item_descriptor, 'transcript'
+            ).rstrip('/?') + '/available_translations',
         }
-
         self.assertEqual(
             context,
-            self.item_module.xmodule_runtime.render_template('video.html', expected_context),
+            self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
         )
 
 
@@ -111,15 +84,14 @@ class TestVideoNonYouTube(TestVideo):
             u'webm': u'example.webm',
         }
 
-        context = self.item_module.render('student_view').content
+        context = self.item_descriptor.render('student_view').content
         expected_context = {
             'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
             'data_dir': getattr(self, 'data_dir', None),
-            'caption_asset_path': '/static/subs/',
             'show_captions': 'true',
             'display_name': u'A Name',
             'end': 3610.0,
-            'id': self.item_module.location.html_id(),
+            'id': self.item_descriptor.location.html_id(),
             'sources': sources,
             'speed': 'null',
             'general_speed': 1.0,
@@ -131,11 +103,19 @@ class TestVideoNonYouTube(TestVideo):
             'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
             'yt_test_timeout': 1500,
             'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
+            'transcript_language': 'en',
+            'transcript_languages': '{"en": "English"}',
+            'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
+                self.item_descriptor, 'transcript'
+            ).rstrip('/?') + '/translation',
+            'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+                self.item_descriptor, 'transcript'
+            ).rstrip('/?') + '/available_translations',
         }
 
         self.assertEqual(
             context,
-            self.item_module.xmodule_runtime.render_template('video.html', expected_context),
+            self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
         )
 
 
@@ -192,7 +172,6 @@ class TestGetHtmlMethod(BaseTestXmodule):
 
         expected_context = {
             'data_dir': getattr(self, 'data_dir', None),
-            'caption_asset_path': '/static/subs/',
             'show_captions': 'true',
             'display_name': u'A Name',
             'end': 3610.0,
@@ -222,20 +201,30 @@ class TestGetHtmlMethod(BaseTestXmodule):
             )
 
             self.initialize_module(data=DATA)
-            track_url = self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'download_transcript')
+            track_url = self.item_descriptor.xmodule_runtime.handler_url(
+                self.item_descriptor, 'transcript'
+            ).rstrip('/?') + '/download'
 
-            context = self.item_module.render('student_view').content
+            context = self.item_descriptor.render('student_view').content
 
             expected_context.update({
+                'transcript_languages': '{"en": "English"}' if self.item_descriptor.sub else '{}',
+                'transcript_language': 'en' if self.item_descriptor.sub else json.dumps(None),
+                'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
+                    self.item_descriptor, 'transcript'
+                ).rstrip('/?') + '/translation',
+                'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+                    self.item_descriptor, 'transcript'
+                ).rstrip('/?') + '/available_translations',
                 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
                 'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'],
                 'sub': data['sub'],
-                'id': self.item_module.location.html_id(),
+                'id': self.item_descriptor.location.html_id(),
             })
 
             self.assertEqual(
                 context,
-                self.item_module.xmodule_runtime.render_template('video.html', expected_context),
+                self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
             )
 
     def test_get_html_source(self):
@@ -301,7 +290,6 @@ class TestGetHtmlMethod(BaseTestXmodule):
 
         expected_context = {
             'data_dir': getattr(self, 'data_dir', None),
-            'caption_asset_path': '/static/subs/',
             'show_captions': 'true',
             'display_name': u'A Name',
             'end': 3610.0,
@@ -317,6 +305,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
             'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
             'yt_test_timeout': 1500,
             'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
+            'transcript_language': 'en',
+            'transcript_languages': '{"en": "English"}',
         }
 
         for data in cases:
@@ -326,17 +316,23 @@ class TestGetHtmlMethod(BaseTestXmodule):
                 sources=data['sources']
             )
             self.initialize_module(data=DATA)
-            context = self.item_module.render('student_view').content
+            context = self.item_descriptor.render('student_view').content
 
             expected_context.update({
+                'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
+                    self.item_descriptor, 'transcript'
+                ).rstrip('/?') + '/translation',
+                'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+                    self.item_descriptor, 'transcript'
+                ).rstrip('/?') + '/available_translations',
                 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
                 'sources': data['result'],
-                'id': self.item_module.location.html_id(),
+                'id': self.item_descriptor.location.html_id(),
             })
 
             self.assertEqual(
                 context,
-                self.item_module.xmodule_runtime.render_template('video.html', expected_context)
+                self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
             )
 
 
@@ -361,9 +357,9 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
         fields = self.item_descriptor.editable_metadata_fields
 
         self.assertIn('source', fields)
-        self.assertEqual(self.item_module.source, 'http://example.org/video.mp4')
-        self.assertTrue(self.item_module.download_video)
-        self.assertTrue(self.item_module.source_visible)
+        self.assertEqual(self.item_descriptor.source, 'http://example.org/video.mp4')
+        self.assertTrue(self.item_descriptor.download_video)
+        self.assertTrue(self.item_descriptor.source_visible)
 
     def test_source_in_html5sources(self):
         metadata = {
@@ -375,10 +371,10 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
         fields = self.item_descriptor.editable_metadata_fields
 
         self.assertNotIn('source', fields)
-        self.assertTrue(self.item_module.download_video)
-        self.assertFalse(self.item_module.source_visible)
+        self.assertTrue(self.item_descriptor.download_video)
+        self.assertFalse(self.item_descriptor.source_visible)
 
-    @patch('xmodule.x_module.XModuleDescriptor.editable_metadata_fields', new_callable=PropertyMock)
+    @patch('xmodule.video_module.VideoDescriptor.editable_metadata_fields', new_callable=PropertyMock)
     def test_download_video_is_explicitly_set(self, mock_editable_fields):
         mock_editable_fields.return_value = {
             'download_video': {
@@ -445,9 +441,9 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
         fields = self.item_descriptor.editable_metadata_fields
 
         self.assertIn('source', fields)
-        self.assertFalse(self.item_module.download_video)
-        self.assertTrue(self.item_module.source_visible)
-        self.assertTrue(self.item_module.download_track)
+        self.assertFalse(self.item_descriptor.download_video)
+        self.assertTrue(self.item_descriptor.source_visible)
+        self.assertTrue(self.item_descriptor.download_track)
 
     def test_source_is_empty(self):
         metadata = {
@@ -459,152 +455,4 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
         fields = self.item_descriptor.editable_metadata_fields
 
         self.assertNotIn('source', fields)
-        self.assertFalse(self.item_module.download_video)
-
-
-class TestVideoGetTranscriptsMethod(TestVideo):
-    """
-    Make sure that `get_transcript` method works correctly
-    """
-
-    DATA = """
-        <video show_captions="true"
-        display_name="A Name"
-        >
-            <source src="example.mp4"/>
-            <source src="example.webm"/>
-        </video>
-    """
-    MODEL_DATA = {
-        'data': DATA
-    }
-    METADATA = {}
-
-    def test_good_transcript(self):
-        self.item_module.render('student_view')
-        item = self.item_descriptor.xmodule_runtime.xmodule_instance
-
-        good_sjson = _create_file(content=textwrap.dedent("""\
-                {
-                  "start": [
-                    270,
-                    2720
-                  ],
-                  "end": [
-                    2720,
-                    5430
-                  ],
-                  "text": [
-                    "Hi, welcome to Edx.",
-                    "Let&#39;s start with what is on your screen right now."
-                  ]
-                }
-            """))
-
-        _upload_file(good_sjson, self.item_module.location)
-        subs_id = _get_subs_id(good_sjson.name)
-
-        text = item.get_transcript(subs_id)
-        expected_text = textwrap.dedent("""\
-            0
-            00:00:00,270 --> 00:00:02,720
-            Hi, welcome to Edx.
-
-            1
-            00:00:02,720 --> 00:00:05,430
-            Let&#39;s start with what is on your screen right now.
-
-            """)
-
-        self.assertEqual(text, expected_text)
-
-    def test_not_found_error(self):
-        self.item_module.render('student_view')
-        item = self.item_descriptor.xmodule_runtime.xmodule_instance
-
-        with self.assertRaises(NotFoundError):
-            item.get_transcript('wrong')
-
-    def test_value_error(self):
-        self.item_module.render('student_view')
-        item = self.item_descriptor.xmodule_runtime.xmodule_instance
-
-        good_sjson = _create_file(content='bad content')
-
-        _upload_file(good_sjson, self.item_module.location)
-        subs_id = _get_subs_id(good_sjson.name)
-
-        with self.assertRaises(ValueError):
-            item.get_transcript(subs_id)
-
-    def test_key_error(self):
-        self.item_module.render('student_view')
-        item = self.item_descriptor.xmodule_runtime.xmodule_instance
-
-        good_sjson = _create_file(content="""
-                {
-                  "start": [
-                    270,
-                    2720
-                  ],
-                  "end": [
-                    2720,
-                    5430
-                  ]
-                }
-            """)
-
-        _upload_file(good_sjson, self.item_module.location)
-        subs_id = _get_subs_id(good_sjson.name)
-
-        with self.assertRaises(KeyError):
-            item.get_transcript(subs_id)
-
-
-def _clear_assets(location):
-    store = contentstore()
-
-    content_location = StaticContent.compute_location(
-        location.org, location.course, location.name
-    )
-
-    assets, __ = store.get_all_content_for_course(content_location)
-    for asset in assets:
-        asset_location = Location(asset["_id"])
-        id = StaticContent.get_id_from_location(asset_location)
-        store.delete(id)
-
-def _get_subs_id(filename):
-        basename = os.path.splitext(os.path.basename(filename))[0]
-        return basename.replace('subs_', '').replace('.srt', '')
-
-def _create_file(content=''):
-    sjson_file = tempfile.NamedTemporaryFile(prefix="subs_", suffix=".srt.sjson")
-    sjson_file.content_type = 'application/json'
-    sjson_file.write(textwrap.dedent(content))
-    sjson_file.seek(0)
-
-    return sjson_file
-
-def _upload_file(file, location):
-    filename = 'subs_{}.srt.sjson'.format(_get_subs_id(file.name))
-    mime_type = file.content_type
-
-    content_location = StaticContent.compute_location(
-        location.org, location.course, filename
-    )
-
-    sc_partial = partial(StaticContent, content_location, filename, mime_type)
-    content = sc_partial(file.read())
-
-    (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
-        content,
-        tempfile_path=None
-    )
-    del_cached_content(thumbnail_location)
-
-    if thumbnail_content is not None:
-        content.thumbnail_location = thumbnail_location
-
-    contentstore().save(content)
-    del_cached_content(content.location)
+        self.assertFalse(self.item_descriptor.download_video)
diff --git a/lms/djangoapps/courseware/tests/test_video_xml.py b/lms/djangoapps/courseware/tests/test_video_xml.py
index 81573f6c10e9b8ec6cf086c433e5f4d54a96c3ef..cea2bc865c3edae6cf99424e74dfb56ed3a82852 100644
--- a/lms/djangoapps/courseware/tests/test_video_xml.py
+++ b/lms/djangoapps/courseware/tests/test_video_xml.py
@@ -32,6 +32,7 @@ SOURCE_XML = """
     >
         <source src="example.mp4"/>
         <source src="example.webm"/>
+        <transcript language="uk" src="ukrainian_translation.srt" />
     </video>
 """
 
diff --git a/lms/djangoapps/courseware/tests/test_word_cloud.py b/lms/djangoapps/courseware/tests/test_word_cloud.py
index 53e7c208506776d83f7248e73cbcd327c12c33d7..3d2a235e41964564f24b8c33f21f1460e73763a3 100644
--- a/lms/djangoapps/courseware/tests/test_word_cloud.py
+++ b/lms/djangoapps/courseware/tests/test_word_cloud.py
@@ -242,12 +242,12 @@ class TestWordCloud(BaseTestXmodule):
 
     def test_word_cloud_constructor(self):
         """Make sure that all parameters extracted correclty from xml"""
-        fragment = self.runtime.render(self.item_module, 'student_view')
+        fragment = self.runtime.render(self.item_descriptor, 'student_view')
 
         expected_context = {
-            'ajax_url': self.item_module.xmodule_runtime.ajax_url,
-            'element_class': self.item_module.location.category,
-            'element_id': self.item_module.location.html_id(),
+            'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url,
+            'element_class': self.item_descriptor.location.category,
+            'element_id': self.item_descriptor.location.html_id(),
             'num_inputs': 5,  # default value
             'submitted': False  # default value
         }
diff --git a/lms/envs/common.py b/lms/envs/common.py
index d8d571cf9b27eb81f2e713ad0766524c8dcfcd70..29929616e720a298cbccd5216ce13d94c4a957f0 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1287,3 +1287,193 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
 
 ##### LMS DEADLINE DISPLAY TIME_ZONE #######
 TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC'
+
+
+# Source:
+# http://loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt according to http://en.wikipedia.org/wiki/ISO_639-1
+ALL_LANGUAGES = (
+    [u"aa", u"Afar"],
+    [u"ab", u"Abkhazian"],
+    [u"af", u"Afrikaans"],
+    [u"ak", u"Akan"],
+    [u"sq", u"Albanian"],
+    [u"am", u"Amharic"],
+    [u"ar", u"Arabic"],
+    [u"an", u"Aragonese"],
+    [u"hy", u"Armenian"],
+    [u"as", u"Assamese"],
+    [u"av", u"Avaric"],
+    [u"ae", u"Avestan"],
+    [u"ay", u"Aymara"],
+    [u"az", u"Azerbaijani"],
+    [u"ba", u"Bashkir"],
+    [u"bm", u"Bambara"],
+    [u"eu", u"Basque"],
+    [u"be", u"Belarusian"],
+    [u"bn", u"Bengali"],
+    [u"bh", u"Bihari languages"],
+    [u"bi", u"Bislama"],
+    [u"bs", u"Bosnian"],
+    [u"br", u"Breton"],
+    [u"bg", u"Bulgarian"],
+    [u"my", u"Burmese"],
+    [u"ca", u"Catalan; Valencian"],
+    [u"ch", u"Chamorro"],
+    [u"ce", u"Chechen"],
+    [u"zh", u"Chinese"],
+    [u"cu", u"Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"],
+    [u"cv", u"Chuvash"],
+    [u"kw", u"Cornish"],
+    [u"co", u"Corsican"],
+    [u"cr", u"Cree"],
+    [u"cs", u"Czech"],
+    [u"da", u"Danish"],
+    [u"dv", u"Divehi; Dhivehi; Maldivian"],
+    [u"nl", u"Dutch; Flemish"],
+    [u"dz", u"Dzongkha"],
+    [u"en", u"English"],
+    [u"eo", u"Esperanto"],
+    [u"et", u"Estonian"],
+    [u"ee", u"Ewe"],
+    [u"fo", u"Faroese"],
+    [u"fj", u"Fijian"],
+    [u"fi", u"Finnish"],
+    [u"fr", u"French"],
+    [u"fy", u"Western Frisian"],
+    [u"ff", u"Fulah"],
+    [u"ka", u"Georgian"],
+    [u"de", u"German"],
+    [u"gd", u"Gaelic; Scottish Gaelic"],
+    [u"ga", u"Irish"],
+    [u"gl", u"Galician"],
+    [u"gv", u"Manx"],
+    [u"el", u"Greek, Modern (1453-)"],
+    [u"gn", u"Guarani"],
+    [u"gu", u"Gujarati"],
+    [u"ht", u"Haitian; Haitian Creole"],
+    [u"ha", u"Hausa"],
+    [u"he", u"Hebrew"],
+    [u"hz", u"Herero"],
+    [u"hi", u"Hindi"],
+    [u"ho", u"Hiri Motu"],
+    [u"hr", u"Croatian"],
+    [u"hu", u"Hungarian"],
+    [u"ig", u"Igbo"],
+    [u"is", u"Icelandic"],
+    [u"io", u"Ido"],
+    [u"ii", u"Sichuan Yi; Nuosu"],
+    [u"iu", u"Inuktitut"],
+    [u"ie", u"Interlingue; Occidental"],
+    [u"ia", u"Interlingua (International Auxiliary Language Association)"],
+    [u"id", u"Indonesian"],
+    [u"ik", u"Inupiaq"],
+    [u"it", u"Italian"],
+    [u"jv", u"Javanese"],
+    [u"ja", u"Japanese"],
+    [u"kl", u"Kalaallisut; Greenlandic"],
+    [u"kn", u"Kannada"],
+    [u"ks", u"Kashmiri"],
+    [u"kr", u"Kanuri"],
+    [u"kk", u"Kazakh"],
+    [u"km", u"Central Khmer"],
+    [u"ki", u"Kikuyu; Gikuyu"],
+    [u"rw", u"Kinyarwanda"],
+    [u"ky", u"Kirghiz; Kyrgyz"],
+    [u"kv", u"Komi"],
+    [u"kg", u"Kongo"],
+    [u"ko", u"Korean"],
+    [u"kj", u"Kuanyama; Kwanyama"],
+    [u"ku", u"Kurdish"],
+    [u"lo", u"Lao"],
+    [u"la", u"Latin"],
+    [u"lv", u"Latvian"],
+    [u"li", u"Limburgan; Limburger; Limburgish"],
+    [u"ln", u"Lingala"],
+    [u"lt", u"Lithuanian"],
+    [u"lb", u"Luxembourgish; Letzeburgesch"],
+    [u"lu", u"Luba-Katanga"],
+    [u"lg", u"Ganda"],
+    [u"mk", u"Macedonian"],
+    [u"mh", u"Marshallese"],
+    [u"ml", u"Malayalam"],
+    [u"mi", u"Maori"],
+    [u"mr", u"Marathi"],
+    [u"ms", u"Malay"],
+    [u"mg", u"Malagasy"],
+    [u"mt", u"Maltese"],
+    [u"mn", u"Mongolian"],
+    [u"na", u"Nauru"],
+    [u"nv", u"Navajo; Navaho"],
+    [u"nr", u"Ndebele, South; South Ndebele"],
+    [u"nd", u"Ndebele, North; North Ndebele"],
+    [u"ng", u"Ndonga"],
+    [u"ne", u"Nepali"],
+    [u"nn", u"Norwegian Nynorsk; Nynorsk, Norwegian"],
+    [u"nb", u"Bokmål, Norwegian; Norwegian Bokmål"],
+    [u"no", u"Norwegian"],
+    [u"ny", u"Chichewa; Chewa; Nyanja"],
+    [u"oc", u"Occitan (post 1500); Provençal"],
+    [u"oj", u"Ojibwa"],
+    [u"or", u"Oriya"],
+    [u"om", u"Oromo"],
+    [u"os", u"Ossetian; Ossetic"],
+    [u"pa", u"Panjabi; Punjabi"],
+    [u"fa", u"Persian"],
+    [u"pi", u"Pali"],
+    [u"pl", u"Polish"],
+    [u"pt", u"Portuguese"],
+    [u"ps", u"Pushto; Pashto"],
+    [u"qu", u"Quechua"],
+    [u"rm", u"Romansh"],
+    [u"ro", u"Romanian; Moldavian; Moldovan"],
+    [u"rn", u"Rundi"],
+    [u"ru", u"Russian"],
+    [u"sg", u"Sango"],
+    [u"sa", u"Sanskrit"],
+    [u"si", u"Sinhala; Sinhalese"],
+    [u"sk", u"Slovak"],
+    [u"sl", u"Slovenian"],
+    [u"se", u"Northern Sami"],
+    [u"sm", u"Samoan"],
+    [u"sn", u"Shona"],
+    [u"sd", u"Sindhi"],
+    [u"so", u"Somali"],
+    [u"st", u"Sotho, Southern"],
+    [u"es", u"Spanish; Castilian"],
+    [u"sc", u"Sardinian"],
+    [u"sr", u"Serbian"],
+    [u"ss", u"Swati"],
+    [u"su", u"Sundanese"],
+    [u"sw", u"Swahili"],
+    [u"sv", u"Swedish"],
+    [u"ty", u"Tahitian"],
+    [u"ta", u"Tamil"],
+    [u"tt", u"Tatar"],
+    [u"te", u"Telugu"],
+    [u"tg", u"Tajik"],
+    [u"tl", u"Tagalog"],
+    [u"th", u"Thai"],
+    [u"bo", u"Tibetan"],
+    [u"ti", u"Tigrinya"],
+    [u"to", u"Tonga (Tonga Islands)"],
+    [u"tn", u"Tswana"],
+    [u"ts", u"Tsonga"],
+    [u"tk", u"Turkmen"],
+    [u"tr", u"Turkish"],
+    [u"tw", u"Twi"],
+    [u"ug", u"Uighur; Uyghur"],
+    [u"uk", u"Ukrainian"],
+    [u"ur", u"Urdu"],
+    [u"uz", u"Uzbek"],
+    [u"ve", u"Venda"],
+    [u"vi", u"Vietnamese"],
+    [u"vo", u"Volapük"],
+    [u"cy", u"Welsh"],
+    [u"wa", u"Walloon"],
+    [u"wo", u"Wolof"],
+    [u"xh", u"Xhosa"],
+    [u"yi", u"Yiddish"],
+    [u"yo", u"Yoruba"],
+    [u"za", u"Zhuang; Chuang"],
+    [u"zu", u"Zulu"]
+)
diff --git a/lms/templates/video.html b/lms/templates/video.html
index 00ef4f52bb3e10c5e31590838c1ce32153d467c7..423c5af709020d64624ebd2fbe63e158441699b6 100644
--- a/lms/templates/video.html
+++ b/lms/templates/video.html
@@ -25,10 +25,13 @@
     data-saved-video-position="${saved_video_position}"
     data-start="${start}"
     data-end="${end}"
-    data-caption-asset-path="${caption_asset_path}"
+    data-transcript-language="${transcript_language}"
+    data-transcript-languages='${transcript_languages}'
     data-autoplay="${autoplay}"
     data-yt-test-timeout="${yt_test_timeout}"
     data-yt-test-url="${yt_test_url}"
+    data-transcript-translation-url="${transcript_translation_url}"
+    data-transcript-available-translations-url="${transcript_available_translations_url}"
 
     ## For now, the option "data-autohide-html5" is hard coded. This option
     ## either enables or disables autohiding of controls and captions on mouse
@@ -67,12 +70,12 @@
                       <li><div class="vidtime">0:00 / 0:00</div></li>
                   </ul>
                   <div class="secondary-controls">
-                      <div class="speeds">
+                      <div class="speeds menu-container">
                           <a href="#" title="${_('Speeds')}" role="button" aria-disabled="false">
                               <h3>${_('Speed')}</h3>
                               <p class="active"></p>
                           </a>
-                          <ol class="video_speeds" role="menu"></ol>
+                          <ol class="video_speeds menu" role="menu"></ol>
                       </div>
                       <div class="volume">
                           <a href="#" title="${_('Volume')}" role="button" aria-disabled="false"></a>
@@ -83,7 +86,10 @@
                       <a href="#" class="add-fullscreen" title="${_('Fill browser')}" role="button" aria-disabled="false">${_('Fill browser')}</a>
                       <a href="#" class="quality_control" title="${_('HD off')}" role="button" aria-disabled="false">${_('HD off')}</a>
 
-                      <a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-disabled="false">${_('Turn off captions')}</a>
+                      <div class="lang menu-container">
+                        <a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-
+                        disabled="false">${_('Turn off captions')}</a>
+                      </div>
                   </div>
               </div>
           </section>