From db07092880ae3316cb86e177aec0fbc59debf832 Mon Sep 17 00:00:00 2001 From: jacobo-dominguez-wgu Date: Mon, 12 May 2025 10:49:03 -0600 Subject: [PATCH] fix: various i18n issues * fix: adding messages for i18n issues related to placeholders * fix: adding messages for i18n issues related to import tag wizard stepper titles * fix: changing name to duplicated id on i18n message * fix: replacing hardcoded string with constants to solve i18n issue * fix: typo on title prop * fix: adding components prop name correctly * test: adding ut for select video modal * chore: adding description to placeholder, changing extension to constant file and adding uts for code coverage * chore: removing outdated comment lines --- src/accessibility-page/AccessibilityPage.jsx | 42 +++--- src/accessibility-page/constants.ts | 2 + .../components/TitleHeader/EditableHeader.jsx | 34 +++-- .../components/TitleHeader/messages.js | 5 + .../components/SelectVideoModal.jsx | 37 +++-- .../components/SelectVideoModal.test.jsx | 131 ++++++++++++++++++ .../VideoEditor/components/messages.ts | 11 ++ src/generic/configure-modal/AdvancedTab.jsx | 6 +- src/generic/configure-modal/messages.js | 5 + .../courses-tab/courses-filters/index.jsx | 6 +- .../courses-tab/courses-filters/messages.js | 11 ++ src/taxonomy/import-tags/ImportTagsWizard.jsx | 10 +- src/taxonomy/import-tags/messages.ts | 20 +++ 13 files changed, 257 insertions(+), 63 deletions(-) create mode 100644 src/accessibility-page/constants.ts create mode 100644 src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx create mode 100644 src/editors/containers/VideoEditor/components/messages.ts create mode 100644 src/studio-home/tabs-section/courses-tab/courses-filters/messages.js diff --git a/src/accessibility-page/AccessibilityPage.jsx b/src/accessibility-page/AccessibilityPage.jsx index c37cdf6ab..e7a0a2770 100644 --- a/src/accessibility-page/AccessibilityPage.jsx +++ b/src/accessibility-page/AccessibilityPage.jsx @@ -9,30 +9,30 @@ import messages from './messages'; import AccessibilityBody from './AccessibilityBody'; import AccessibilityForm from './AccessibilityForm'; +import { COMMUNITY_ACCESSIBILITY_LINK, ACCESSIBILITY_EMAIL } from './constants'; + const AccessibilityPage = ({ // injected intl, -}) => { - const communityAccessibilityLink = 'https://www.edx.org/accessibility'; - const email = 'accessibility@edx.org'; - return ( - <> - - - {intl.formatMessage(messages.pageTitle, { - siteName: process.env.SITE_NAME, - })} - - -
- - - - - - - ); -}; +}) => ( + <> + + + {intl.formatMessage(messages.pageTitle, { + siteName: process.env.SITE_NAME, + })} + + +
+ + + + + + +); AccessibilityPage.propTypes = { // injected diff --git a/src/accessibility-page/constants.ts b/src/accessibility-page/constants.ts new file mode 100644 index 000000000..84d1a444c --- /dev/null +++ b/src/accessibility-page/constants.ts @@ -0,0 +1,2 @@ +export const COMMUNITY_ACCESSIBILITY_LINK = 'https://www.edx.org/accessibility'; +export const ACCESSIBILITY_EMAIL = 'accessibility@edx.org'; diff --git a/src/editors/containers/EditorContainer/components/TitleHeader/EditableHeader.jsx b/src/editors/containers/EditorContainer/components/TitleHeader/EditableHeader.jsx index ba212f162..0483bffe5 100644 --- a/src/editors/containers/EditorContainer/components/TitleHeader/EditableHeader.jsx +++ b/src/editors/containers/EditorContainer/components/TitleHeader/EditableHeader.jsx @@ -2,8 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Form } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; import EditConfirmationButtons from './EditConfirmationButtons'; +import messages from './messages'; + const EditableHeader = ({ handleChange, updateTitle, @@ -11,20 +14,23 @@ const EditableHeader = ({ inputRef, localTitle, cancelEdit, -}) => ( - updateTitle(e)}> - } - onChange={handleChange} - onKeyDown={handleKeyDown} - placeholder="Title" - ref={inputRef} - value={localTitle} - /> - -); +}) => { + const intl = useIntl(); + return ( + updateTitle(e)}> + } + onChange={handleChange} + onKeyDown={handleKeyDown} + placeholder={intl.formatMessage(messages.editTitlePlaceholder)} + ref={inputRef} + value={localTitle} + /> + + ); +}; EditableHeader.defaultProps = { inputRef: null, }; diff --git a/src/editors/containers/EditorContainer/components/TitleHeader/messages.js b/src/editors/containers/EditorContainer/components/TitleHeader/messages.js index 54d92ac16..0ffbffa7e 100644 --- a/src/editors/containers/EditorContainer/components/TitleHeader/messages.js +++ b/src/editors/containers/EditorContainer/components/TitleHeader/messages.js @@ -17,6 +17,11 @@ const messages = defineMessages({ defaultMessage: 'Edit Title', description: 'Screen reader label title for icon button to edit the xblock title', }, + editTitlePlaceholder: { + id: 'authoring.texteditor.header.editTitleLabelPlaceholder', + defaultMessage: 'Title', + description: 'Screen reader label title for icon button to edit the xblock title', + }, cancelTitleEdit: { id: 'authoring.texteditor.header.cancelTitleEdit', defaultMessage: 'Cancel', diff --git a/src/editors/containers/VideoEditor/components/SelectVideoModal.jsx b/src/editors/containers/VideoEditor/components/SelectVideoModal.jsx index 07167d65c..bd7a7507f 100644 --- a/src/editors/containers/VideoEditor/components/SelectVideoModal.jsx +++ b/src/editors/containers/VideoEditor/components/SelectVideoModal.jsx @@ -3,36 +3,33 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { Button } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { thunkActions } from '../../../data/redux'; import BaseModal from '../../../sharedComponents/BaseModal'; -// This 'module' self-import hack enables mocking during tests. -// See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested -// should be re-thought and cleaned up to avoid this pattern. -// eslint-disable-next-line import/no-self-import -import * as module from './SelectVideoModal'; +import messages from './messages'; -export const hooks = { - videoList: ({ fetchVideos }) => { +export const useVideoList = ({ fetchVideos }) => { // eslint-disable-next-line react-hooks/rules-of-hooks - const [videos, setVideos] = React.useState(null); - // eslint-disable-next-line react-hooks/rules-of-hooks - React.useEffect(() => { - fetchVideos({ onSuccess: setVideos }); - }, []); - return videos; - }, - onSelectClick: ({ setSelection, videos }) => () => setSelection(videos[0]), + const [videos, setVideos] = React.useState(null); + // eslint-disable-next-line react-hooks/rules-of-hooks + React.useEffect(() => { + fetchVideos({ onSuccess: setVideos }); + }, []); + return videos; }; +export const useOnSelectClick = ({ setSelection, videos }) => () => setSelection(videos[0]); + export const SelectVideoModal = ({ fetchVideos, isOpen, close, setSelection, }) => { - const videos = module.hooks.videoList({ fetchVideos }); - const onSelectClick = module.hooks.onSelectClick({ + const intl = useIntl(); + const videos = useVideoList({ fetchVideos }); + const onSelectClick = useOnSelectClick({ setSelection, videos, }); @@ -41,13 +38,13 @@ export const SelectVideoModal = ({ Next} > {/* Content selection */} {videos && (videos.map( img => ( -
+
{img.externalUrl}
), ))} @@ -62,7 +59,7 @@ SelectVideoModal.propTypes = { fetchVideos: PropTypes.func.isRequired, }; -export const mapStateToProps = () => ({}); +export const mapStateToProps = null; export const mapDispatchToProps = { fetchVideos: thunkActions.app.fetchVideos, }; diff --git a/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx b/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx new file mode 100644 index 000000000..6901b878a --- /dev/null +++ b/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { + render, screen, fireEvent, waitFor, +} from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import { SelectVideoModal, useVideoList, useOnSelectClick } from './SelectVideoModal'; +import messages from './messages'; + +describe('SelectVideoModal', () => { + const mockStore = configureStore([]); + const mockFetchVideos = jest.fn(); + const mockSetSelection = jest.fn(); + const mockClose = jest.fn(); + let store; + + beforeEach(() => { + store = mockStore({}); + jest.clearAllMocks(); + }); + + it('renders the modal with the correct title', () => { + render( + + + + + , + ); + + expect(screen.getByText(messages.selectVideoModalTitle.defaultMessage)).toBeInTheDocument(); + }); + + it('calls close when the modal is closed', () => { + render( + + + + + , + ); + + fireEvent.click(screen.getByRole('button', { name: /close/i })); + expect(mockClose).toHaveBeenCalled(); + }); + + it('renders a div for each video in the videos array', () => { + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + const fetchVideos = jest.fn(({ onSuccess }) => onSuccess(videos)); + + render( + + + + + , + ); + + // Assert that a div is rendered for each video + videos.forEach((video) => { + expect(screen.getByText(video.externalUrl)).toBeInTheDocument(); + }); + }); +}); + +describe('SelectVideoModal hooks', () => { + describe('useVideoList', () => { + it('fetches videos and sets them in state', async () => { + // eslint-disable-next-line react/prop-types + const TestComponent = ({ fetchVideos }) => { + const videos = useVideoList({ fetchVideos }); + return ( +
+ {videos ? videos.map((v) => v.externalUrl).join(', ') : 'Loading'} +
+ ); + }; + + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + const fetchVideos = jest.fn(({ onSuccess }) => { + onSuccess(videos); + }); + + render(); + + await waitFor(() => expect(screen.getByTestId('videos').textContent).toBe(videos.map((v) => v.externalUrl).join(', '))); + + expect(fetchVideos).toHaveBeenCalled(); + }); + }); + + describe('useOnSelectClick', () => { + it('calls setSelection with the first video', () => { + const mockSetSelection = jest.fn(); + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + + const onSelectClick = useOnSelectClick({ + setSelection: mockSetSelection, + videos, + }); + + onSelectClick(); + expect(mockSetSelection).toHaveBeenCalledWith(videos[0]); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/messages.ts b/src/editors/containers/VideoEditor/components/messages.ts new file mode 100644 index 000000000..9777ecbf8 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/messages.ts @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + selectVideoModalTitle: { + id: 'authoring.videoEditor.selectVideoModal.title', + defaultMessage: 'Add a video', + description: 'Title for the select video modal.', + }, +}); + +export default messages; diff --git a/src/generic/configure-modal/AdvancedTab.jsx b/src/generic/configure-modal/AdvancedTab.jsx index 73b7a4801..ba639099e 100644 --- a/src/generic/configure-modal/AdvancedTab.jsx +++ b/src/generic/configure-modal/AdvancedTab.jsx @@ -5,7 +5,7 @@ import { Alert, Form, Hyperlink } from '@openedx/paragon'; import { Warning as WarningIcon, } from '@openedx/paragon/icons'; -import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import PrereqSettings from './PrereqSettings'; @@ -32,6 +32,8 @@ const AdvancedTab = ({ } = values; let examTypeValue = 'none'; + const intl = useIntl(); + if (isTimeLimited && isProctoredExam) { if (isOnboardingExam) { examTypeValue = 'onboardingExam'; @@ -183,7 +185,7 @@ const AdvancedTab = ({ diff --git a/src/generic/configure-modal/messages.js b/src/generic/configure-modal/messages.js index 2d1574b76..7a141ad96 100644 --- a/src/generic/configure-modal/messages.js +++ b/src/generic/configure-modal/messages.js @@ -239,6 +239,11 @@ const messages = defineMessages({ id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-allotted', defaultMessage: 'Time allotted (HH:MM):', }, + timeLimitPlaceholder: { + id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-placeholder', + defaultMessage: 'HH:MM', + description: 'The placeholder for the time limit input field, two digits for hours and two digits for minutes colons in between', + }, timeLimitDescription: { id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-description', defaultMessage: 'Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.', diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx b/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx index acd7a8df1..fdea9411e 100644 --- a/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { SearchField } from '@openedx/paragon'; import { debounce } from 'lodash'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { getStudioHomeCoursesParams } from '../../../data/selectors'; import { updateStudioHomeCoursesCustomParams } from '../../../data/slice'; @@ -11,6 +12,7 @@ import { LoadingSpinner } from '../../../../generic/Loading'; import CoursesTypesFilterMenu from './courses-types-filter-menu'; import CoursesOrderFilterMenu from './courses-order-filter-menu'; import './index.scss'; +import messages from './messages'; /* regex to check if a string has only whitespace example " " @@ -33,6 +35,8 @@ const CoursesFilters = ({ } = studioHomeCoursesParams; const [inputSearchValue, setInputSearchValue] = useState(''); + const intl = useIntl(); + const getFilterTypeData = (baseFilters) => ({ archivedCourses: { ...baseFilters, archivedOnly: true, activeOnly: undefined }, activeCourses: { ...baseFilters, activeOnly: true, archivedOnly: undefined }, @@ -107,7 +111,7 @@ const CoursesFilters = ({ value={cleanFilters ? '' : inputSearchValue} className="mr-4" data-testid="input-filter-courses-search" - placeholder="Search" + placeholder={intl.formatMessage(messages.coursesSearchPlaceholder)} /> {isLoading && ( diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/messages.js b/src/studio-home/tabs-section/courses-tab/courses-filters/messages.js new file mode 100644 index 000000000..d477348e7 --- /dev/null +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + + coursesSearchPlaceholder: { + id: 'course-authoring.studio-home.courses.search-placeholder', + defaultMessage: 'Search', + }, +}); + +export default messages; diff --git a/src/taxonomy/import-tags/ImportTagsWizard.jsx b/src/taxonomy/import-tags/ImportTagsWizard.jsx index 275b966ed..31e7b1b70 100644 --- a/src/taxonomy/import-tags/ImportTagsWizard.jsx +++ b/src/taxonomy/import-tags/ImportTagsWizard.jsx @@ -40,7 +40,7 @@ const ExportStep = ({ taxonomy }) => { const intl = useIntl(); return ( - +

{intl.formatMessage(messages.importWizardStepExportBody, { br: linebreak })}

@@ -97,7 +97,7 @@ const UploadStep = ({ }; return ( - +

{ reimport @@ -190,7 +190,7 @@ const PopulateStep = ({ }; return ( - + { intl.formatMessage(messages.importWizardStepPopulateTaxonomyName) } @@ -222,7 +222,7 @@ const PlanStep = ({ importPlan }) => { const intl = useIntl(); return ( - + {intl.formatMessage(messages.importWizardStepPlanBody, { br: linebreak, changeCount: importPlan?.length })}

    @@ -249,7 +249,7 @@ const ConfirmStep = ({ importPlan }) => { const intl = useIntl(); return ( - + {intl.formatMessage( messages.importWizardStepConfirmBody, diff --git a/src/taxonomy/import-tags/messages.ts b/src/taxonomy/import-tags/messages.ts index 1561c8a69..cef6e89ca 100644 --- a/src/taxonomy/import-tags/messages.ts +++ b/src/taxonomy/import-tags/messages.ts @@ -154,6 +154,26 @@ const messages = defineMessages({ id: 'course-authoring.import-tags.error-alert.title', defaultMessage: 'Import error', }, + importWizardStepperExportStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.export-step.title', + defaultMessage: 'Export', + }, + importWizardStepperUploadStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.upload-step.title', + defaultMessage: 'Upload', + }, + importWizardStepperPopulateStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.populate-step.title', + defaultMessage: 'Populate', + }, + importWizardStepperPlanStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.plan-step.title', + defaultMessage: 'Plan', + }, + importWizardStepperConfirmStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.confirm-step.title', + defaultMessage: 'Confirm', + }, }); export default messages;