diff --git a/src/components/course/course-header/tests/CoursePreview.test.jsx b/src/components/course/course-header/tests/CoursePreview.test.jsx index cf11e2368..9c142f957 100644 --- a/src/components/course/course-header/tests/CoursePreview.test.jsx +++ b/src/components/course/course-header/tests/CoursePreview.test.jsx @@ -11,6 +11,11 @@ const imageURL = 'https://test-domain.com/test-image/id.png'; const hlsUrl = 'https://test-domain.com/test-prefix/id.m3u8'; const ytUrl = 'https://www.youtube.com/watch?v=oHg5SJYRHA0'; +jest.mock('@edx/frontend-platform/i18n', () => ({ + ...jest.requireActual('@edx/frontend-platform/i18n'), + getLocale: () => 'en', +})); + describe('Course Preview Tests', () => { it('Renders preview image and not the video when video URL is not given.', () => { const { container, getByAltText } = renderWithRouter(); diff --git a/src/components/microlearning/styles/VideoDetailPage.scss b/src/components/microlearning/styles/VideoDetailPage.scss index 835aeadf6..08c18d917 100644 --- a/src/components/microlearning/styles/VideoDetailPage.scss +++ b/src/components/microlearning/styles/VideoDetailPage.scss @@ -29,6 +29,7 @@ .video-player-container-with-transcript { display: flex; + padding-bottom: 55px; } .video-js-wrapper { diff --git a/src/components/video/VideoJS.jsx b/src/components/video/VideoJS.jsx index 3cb25a6e8..03eab9547 100644 --- a/src/components/video/VideoJS.jsx +++ b/src/components/video/VideoJS.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import 'videojs-youtube'; import videojs from 'video.js'; import 'video.js/dist/video-js.css'; +import { getLocale } from '@edx/frontend-platform/i18n'; import { PLAYBACK_RATES } from './data/constants'; import { usePlayerOptions, useTranscripts } from './data'; @@ -14,10 +15,12 @@ require('videojs-vjstranscribe'); const VideoJS = ({ options, onReady, customOptions }) => { const videoRef = useRef(null); const playerRef = useRef(null); + const siteLanguage = getLocale(); const transcripts = useTranscripts({ player: playerRef.current, customOptions, + siteLanguage, }); const playerOptions = usePlayerOptions({ diff --git a/src/components/video/data/hooks.js b/src/components/video/data/hooks.js index cf5100a16..a54bcdf64 100644 --- a/src/components/video/data/hooks.js +++ b/src/components/video/data/hooks.js @@ -2,11 +2,12 @@ import { useEffect, useState } from 'react'; import { logError } from '@edx/frontend-platform/logging'; import { fetchAndAddTranscripts } from './service'; +import { sortTextTracks } from './utils'; -export function useTranscripts({ player, customOptions }) { +export function useTranscripts({ player, customOptions, siteLanguage }) { const shouldUseTranscripts = !!(customOptions?.showTranscripts && customOptions?.transcriptUrls); const [isLoading, setIsLoading] = useState(shouldUseTranscripts); - const [textTracks, setTextTracks] = useState([]); + const [textTracks, setTextTracks] = useState({}); const [transcriptUrl, setTranscriptUrl] = useState(null); useEffect(() => { @@ -15,12 +16,15 @@ export function useTranscripts({ player, customOptions }) { if (shouldUseTranscripts) { try { const result = await fetchAndAddTranscripts(customOptions.transcriptUrls, player); - setTextTracks(result); - // We are only catering to English transcripts for now as we don't have the option to change - // the transcript language yet. - if (result.en) { - setTranscriptUrl(result.en); - } + + // Sort the text tracks to prioritize the site language at the top of the list. + // Currently, video.js selects the top language from the list of transcripts. + const sortedResult = sortTextTracks(result, siteLanguage); + setTextTracks(sortedResult); + + // Default to site language, fallback to English + const preferredTranscript = sortedResult[siteLanguage] || sortedResult.en; + setTranscriptUrl(preferredTranscript); } catch (error) { logError(`Error fetching transcripts for player: ${error}`); } finally { @@ -29,7 +33,7 @@ export function useTranscripts({ player, customOptions }) { } }; fetchFn(); - }, [customOptions?.transcriptUrls, player, shouldUseTranscripts]); + }, [customOptions?.transcriptUrls, player, shouldUseTranscripts, siteLanguage]); return { textTracks, diff --git a/src/components/video/data/tests/hooks.test.js b/src/components/video/data/tests/hooks.test.js index 1c0b24a2f..da92e51a6 100644 --- a/src/components/video/data/tests/hooks.test.js +++ b/src/components/video/data/tests/hooks.test.js @@ -49,7 +49,7 @@ describe('useTranscripts', () => { expect(logError).toHaveBeenCalledWith(`Error fetching transcripts for player: Error: ${errorMessage}`); expect(result.current.isLoading).toBe(false); - expect(result.current.textTracks).toEqual([]); + expect(result.current.textTracks).toEqual({}); expect(result.current.transcriptUrl).toBeNull(); }); @@ -64,7 +64,7 @@ describe('useTranscripts', () => { customOptions: customOptionsWithoutTranscripts, })); - expect(result.current.textTracks).toEqual([]); + expect(result.current.textTracks).toEqual({}); expect(result.current.transcriptUrl).toBeNull(); }); }); diff --git a/src/components/video/data/tests/utils.test.js b/src/components/video/data/tests/utils.test.js index a13112890..242ddb3f7 100644 --- a/src/components/video/data/tests/utils.test.js +++ b/src/components/video/data/tests/utils.test.js @@ -1,4 +1,4 @@ -import { convertToWebVtt, createWebVttFile } from '../utils'; +import { convertToWebVtt, createWebVttFile, sortTextTracks } from '../utils'; describe('Video utils tests', () => { it('should convert transcript data to WebVTT format correctly', () => { @@ -40,4 +40,22 @@ describe('Video utils tests', () => { expect(blob.type).toBe('text/vtt'); expect(blob.size).toBe(mockWebVttContent.length); }); + it('should sort text tracks with site language first and others alphabetically', () => { + const mockTracks = { + en: 'https://test-domain.com/transcript-en.txt', + ar: 'https://test-domain.com/transcript-ar.txt', + fr: 'https://test-domain.com/transcript-fr.txt', + }; + + const siteLanguage = 'fr'; + + const expectedSortedTracks = { + fr: 'https://test-domain.com/transcript-fr.txt', + ar: 'https://test-domain.com/transcript-ar.txt', + en: 'https://test-domain.com/transcript-en.txt', + }; + + const result = sortTextTracks(mockTracks, siteLanguage); + expect(result).toEqual(expectedSortedTracks); + }); }); diff --git a/src/components/video/data/utils.js b/src/components/video/data/utils.js index d75d693e3..939348687 100644 --- a/src/components/video/data/utils.js +++ b/src/components/video/data/utils.js @@ -26,3 +26,16 @@ export const createWebVttFile = (webVttContent) => { const blob = new Blob([webVttContent], { type: 'text/vtt' }); return URL.createObjectURL(blob); }; + +export const sortTextTracks = (tracks, siteLanguage) => { + const sortedKeys = Object.keys(tracks).sort((a, b) => { + if (a === siteLanguage) { return -1; } + if (b === siteLanguage) { return 1; } + return a.localeCompare(b); + }); + + return sortedKeys.reduce((acc, key) => { + acc[key] = tracks[key]; + return acc; + }, {}); +}; diff --git a/src/components/video/tests/VideoJS.test.jsx b/src/components/video/tests/VideoJS.test.jsx index c21aaf0be..e559c2536 100644 --- a/src/components/video/tests/VideoJS.test.jsx +++ b/src/components/video/tests/VideoJS.test.jsx @@ -11,6 +11,11 @@ jest.mock('../data', () => ({ usePlayerOptions: jest.fn(), })); +jest.mock('@edx/frontend-platform/i18n', () => ({ + ...jest.requireActual('@edx/frontend-platform/i18n'), + getLocale: () => 'en', +})); + const hlsUrl = 'https://test-domain.com/test-prefix/id.m3u8'; const ytUrl = 'https://www.youtube.com/watch?v=oHg5SJYRHA0'; diff --git a/src/components/video/tests/VideoPlayer.test.jsx b/src/components/video/tests/VideoPlayer.test.jsx index 61711c610..8b1b99135 100644 --- a/src/components/video/tests/VideoPlayer.test.jsx +++ b/src/components/video/tests/VideoPlayer.test.jsx @@ -7,6 +7,11 @@ const hlsUrl = 'https://test-domain.com/test-prefix/id.m3u8'; const ytUrl = 'https://www.youtube.com/watch?v=oHg5SJYRHA0'; const mp3Url = 'https://example.com/audio.mp3'; +jest.mock('@edx/frontend-platform/i18n', () => ({ + ...jest.requireActual('@edx/frontend-platform/i18n'), + getLocale: () => 'en', +})); + describe('Video Player component', () => { it('Renders Video Player components correctly for HLS videos.', async () => { const { container } = renderWithRouter();