diff --git a/.eslintignore b/.eslintignore
index 65588a7767..2222a3388c 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,5 @@
coverage/*
dist/
node_modules/
+src/i18n/
src/segment.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 0e96647f4e..cf41a2a1c3 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -5,7 +5,26 @@ const config = getBaseConfig('eslint');
/* Custom config manipulations */
config.rules = {
...config.rules,
- 'default-param-last': 'off',
+ '@typescript-eslint/default-param-last': 'off',
+ 'react/require-default-props': 'off',
+ 'import/no-named-as-default': 0,
};
+config.ignorePatterns = ["*.json", ".eslintrc.js", "*.config.js", "jsdom-with-global.js"];
+
+config.overrides = [
+ {
+ files: ['*.test.js', '*.test.jsx'],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ project: [
+ "./tsconfig.json",
+ "./functions/tsconfig.json",
+ ]
+ }
+ },
+];
+
+
+
module.exports = config;
diff --git a/.gitignore b/.gitignore
index 1fb5b30eaf..2f81fc6ff0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,5 @@ dist/
# edx
.env.private
+src/i18n/
+temp/
diff --git a/Makefile b/Makefile
index 13d2ef4569..98b0cc7e28 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,11 @@
+transifex_utils = ./node_modules/.bin/transifex-utils.js
+intl_imports = ./node_modules/frontend-platform-shim/node_modules/.bin/intl-imports.js
+
+i18n = ./src/i18n
+transifex_input = $(i18n)/transifex_input.json
+# This directory must match .babelrc .
+transifex_temp = ./temp/babel-plugin-react-intl
+
shell: ## run a shell on the cookie-cutter container
docker exec -it /bin/bash
@@ -30,3 +38,26 @@ restart-detached:
validate-no-uncommitted-package-lock-changes:
git diff --name-only --exit-code package-lock.json
+
+requirements:
+ npm install
+
+i18n.extract:
+ # Pulling display strings from .jsx files into .json files...
+ rm -rf $(transifex_temp)
+ npm run-script i18n_extract
+
+i18n.concat:
+ # Gathering JSON messages into one file...
+ $(transifex_utils) $(transifex_temp) $(transifex_input)
+
+extract_translations: | requirements i18n.extract i18n.concat
+
+pull_translations:
+ rm -rf src/i18n/messages
+ mkdir src/i18n/messages
+ cd src/i18n/messages \
+ && atlas pull \
+ translations/paragon/src/i18n/messages:paragon \
+ translations/frontend-app-admin-portal/src/i18n/messages:frontend-app-admin-portal
+ $(intl_imports) paragon frontend-app-admin-portal
diff --git a/__mocks__/react-instantsearch-dom.jsx b/__mocks__/react-instantsearch-dom.jsx
index 56f4d71b7c..f62f6554ca 100644
--- a/__mocks__/react-instantsearch-dom.jsx
+++ b/__mocks__/react-instantsearch-dom.jsx
@@ -7,7 +7,7 @@ const MockReactInstantSearch = jest.genMockFromModule(
'react-instantsearch-dom',
);
-// eslint-disable-next-line camelcase
+// eslint-disable-next-line @typescript-eslint/naming-convention
const advertised_course_run = {
start: '2020-09-09T04:00:00Z',
key: 'course-v1:edX+Bee101+3T2020',
diff --git a/docs/how_tos/i18n.rst b/docs/how_tos/i18n.rst
new file mode 100644
index 0000000000..8fbb1896f7
--- /dev/null
+++ b/docs/how_tos/i18n.rst
@@ -0,0 +1,5 @@
+####################
+React App i18n HOWTO
+####################
+
+This document has moved to the frontend-platform repo: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst
diff --git a/package-lock.json b/package-lock.json
index 45b79d93b6..b3c64050bf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
"dash-embedded-component": "file:packages/dash-embedded-component-2.0.2.tgz",
"file-saver": "1.3.8",
"font-awesome": "4.7.0",
+ "frontend-platform-shim": "file:packages/frontend-platform-shim",
"history": "4.10.1",
"html-react-parser": "3.0.7",
"jest-environment-jsdom": "26.6.1",
@@ -10926,6 +10927,10 @@
"node": ">= 0.6"
}
},
+ "node_modules/frontend-platform-shim": {
+ "resolved": "packages/frontend-platform-shim",
+ "link": true
+ },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -23178,6 +23183,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "packages/frontend-platform-shim": {
+ "version": "1.0.0",
+ "dependencies": {
+ "@edx/frontend-platform": "4.5.1"
+ },
+ "peerDependencies": {
+ "@edx/frontend-platform": "<4.1.0"
+ }
}
}
}
diff --git a/package.json b/package.json
index 1a4e3f9a25..3df219f765 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,9 @@
"scripts": {
"build": "fedx-scripts webpack",
"build:with-theme": "THEME=npm:@edx/brand-edx.org@latest npm run install-theme && fedx-scripts webpack",
- "lint": "fedx-scripts eslint --ext .js --ext .jsx .",
- "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
+ "check-types": "tsc --noemit",
+ "lint": "fedx-scripts eslint --ext .js --ext .jsx .; npm run check-types",
+ "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .tsx --ext .ts .",
"precommit": "npm run lint",
"prepublishOnly": "npm run build",
"install-theme": "npm install \"@edx/brand@${THEME}\" --no-save",
@@ -45,6 +46,7 @@
"dash-embedded-component": "file:packages/dash-embedded-component-2.0.2.tgz",
"file-saver": "1.3.8",
"font-awesome": "4.7.0",
+ "frontend-platform-shim": "file:packages/frontend-platform-shim",
"history": "4.10.1",
"html-react-parser": "3.0.7",
"jest-environment-jsdom": "26.6.1",
@@ -109,4 +111,4 @@
"resize-observer-polyfill": "1.5.1",
"ts-jest": "^26.5.0"
}
-}
+}
\ No newline at end of file
diff --git a/packages/frontend-platform-shim/package.json b/packages/frontend-platform-shim/package.json
new file mode 100644
index 0000000000..47dbfc8dab
--- /dev/null
+++ b/packages/frontend-platform-shim/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "frontend-platform-shim",
+ "version": "1.0.0",
+ "description": "Shim package to install the `intl-imports.js` script from frontend-platform@4.1+ until the `frontend-app-admin-portal` upgrades from frontend-platform@2.x.x . This package should be removed once the `frontend-app-admin-portal` is upgraded therefore the `peerDependencies` pin to ensure the TODO is coded into the dependency tree.",
+ "dependencies": {
+ "@edx/frontend-platform": "4.5.1"
+ },
+ "peerDependencies": {
+ "@edx/frontend-platform": "<4.1.0"
+ }
+}
diff --git a/src/components/Admin/Admin.test.jsx b/src/components/Admin/Admin.test.jsx
index 30ca049b38..4362ae6805 100644
--- a/src/components/Admin/Admin.test.jsx
+++ b/src/components/Admin/Admin.test.jsx
@@ -6,6 +6,7 @@ import { MemoryRouter, Link } from 'react-router-dom';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService';
import Admin from './index';
@@ -40,26 +41,28 @@ const store = mockStore({
const AdminWrapper = props => (
- {}}
- fetchDashboardAnalytics={() => {}}
- fetchPortalConfiguration={() => {}}
- fetchCsv={() => {}}
- searchEnrollmentsList={() => {}}
- tableData={[
- {
- course_title: 'Bears 101',
- course_start: Date.now(),
- },
- ]}
- match={{
- params: {},
- url: '/',
- }}
- {...props}
- />
+
+ {}}
+ fetchDashboardAnalytics={() => {}}
+ fetchPortalConfiguration={() => {}}
+ fetchCsv={() => {}}
+ searchEnrollmentsList={() => {}}
+ tableData={[
+ {
+ course_title: 'Bears 101',
+ course_start: Date.now(),
+ },
+ ]}
+ match={{
+ params: {},
+ url: '/',
+ }}
+ {...props}
+ />
+
);
diff --git a/src/components/Admin/AdminCards.jsx b/src/components/Admin/AdminCards.jsx
index 5d8a92eb31..d1afe4ac95 100644
--- a/src/components/Admin/AdminCards.jsx
+++ b/src/components/Admin/AdminCards.jsx
@@ -1,46 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
import NumberCard from '../NumberCard';
class AdminCards extends React.Component {
constructor(props) {
super(props);
+ const { intl } = this.props;
this.cards = {
numberOfUsers: {
ref: React.createRef(),
- description: 'total number of learners registered',
+ description: intl.formatMessage({
+ id: 'adminPortal.cards.registeredLearners',
+ defaultMessage: 'total number of learners registered',
+ }),
iconClassName: 'fa fa-users',
actions: [{
- label: 'Which learners are registered but not yet enrolled in any courses?',
+ label: intl.formatMessage({
+ id: 'adminPortal.cards.registeredUnenrolledLearners',
+ defaultMessage: 'Which learners are registered but not yet enrolled in any courses?',
+ }),
slug: 'registered-unenrolled-learners',
}],
},
enrolledLearners: {
ref: React.createRef(),
- description: 'learners enrolled in at least one course',
+ description: intl.formatMessage({
+ id: 'adminPortal.cards.enrolledOneCourse',
+ defaultMessage: 'learners enrolled in at least one course',
+ }),
iconClassName: 'fa fa-check',
actions: [{
- label: 'How many courses are learners enrolled in?',
+ label: intl.formatMessage({
+ id: 'adminPortal.cards.enrolledLearners',
+ defaultMessage: 'How many courses are learners enrolled in?',
+ }),
slug: 'enrolled-learners',
}, {
- label: 'Who is no longer enrolled in a current course?',
+ label: intl.formatMessage({
+ id: 'adminPortal.cards.enrolledLearnersInactiveCourses',
+ defaultMessage: 'Who is no longer enrolled in a current course?',
+ }),
slug: 'enrolled-learners-inactive-courses',
}],
},
activeLearners: {
ref: React.createRef(),
- description: 'active learners in the past week',
+ description: intl.formatMessage({
+ id: 'adminPortal.cards.activeLearnersPastWeek',
+ defaultMessage: 'active learners in the past week',
+ }),
iconClassName: 'fa fa-eye',
actions: [{
- label: 'Who are my top active learners?',
+ label: intl.formatMessage({
+ id: 'adminPortal.cards.learnersActiveWeek',
+ defaultMessage: 'Who are my top active learners?',
+ }),
slug: 'learners-active-week',
}, {
- label: 'Who has not been active for over a week?',
+ label: intl.formatMessage({
+ id: 'adminPortal.cards.learnersInactiveWeek',
+ defaultMessage: 'Who has not been active for over a week?',
+ }),
slug: 'learners-inactive-week',
}, {
- label: 'Who has not been active for over a month?',
+ label: intl.formatMessage({
+ id: 'adminPortal.cards.learnersInactiveMonth',
+ defaultMessage: 'Who has not been active for over a month?',
+ }),
slug: 'learners-inactive-month',
}],
},
@@ -49,10 +79,16 @@ class AdminCards extends React.Component {
description: 'course completions',
iconClassName: 'fa fa-trophy',
actions: [{
- label: 'How many courses have been completed by learners?',
+ label: intl.formatMessage({
+ id: 'adminPortal.cards.completedLearners',
+ defaultMessage: 'How many courses have been completed by learners?',
+ }),
slug: 'completed-learners',
}, {
- label: 'Who completed a course in the past week?',
+ label: intl.formatMessage({
+ id: 'adminPortal.cards.completedLearnersWeek',
+ defaultMessage: 'Who completed a course in the past week?',
+ }),
slug: 'completed-learners-week',
}],
},
@@ -110,6 +146,8 @@ AdminCards.propTypes = {
numberOfUsers: PropTypes.number.isRequired,
courseCompletions: PropTypes.number.isRequired,
enrolledLearners: PropTypes.number.isRequired,
+ // injected
+ intl: intlShape.isRequired,
};
-export default AdminCards;
+export default injectIntl(AdminCards);
diff --git a/src/components/Admin/SubscriptionDetailPage.jsx b/src/components/Admin/SubscriptionDetailPage.jsx
index dde917dae8..56552b08ec 100644
--- a/src/components/Admin/SubscriptionDetailPage.jsx
+++ b/src/components/Admin/SubscriptionDetailPage.jsx
@@ -10,7 +10,7 @@ import { useSubscriptionFromParams } from '../subscriptions/data/contextHooks';
import SubscriptionDetailsSkeleton from '../subscriptions/SubscriptionDetailsSkeleton';
import { LPR_SUBSCRIPTION_PAGE_SIZE } from '../subscriptions/data/constants';
-// eslint-disable-next-line no-unused-vars
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const SubscriptionDetailPage = ({ enterpriseSlug, match }) => {
const [subscription, loadingSubscription] = useSubscriptionFromParams({ match });
diff --git a/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx b/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx
index ff5b45bce0..a62e71dc20 100644
--- a/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx
+++ b/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx
@@ -2,7 +2,7 @@
import React from 'react';
import { Provider } from 'react-redux';
-
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import { screen, render } from '@testing-library/react';
// import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
@@ -11,13 +11,12 @@ import thunk from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';
import remindEmailTemplate from './emailTemplate';
import CodeAssignmentModal, { BaseCodeAssignmentModal } from '.';
-import { ASSIGNMENT_MODAL_FIELDS, NOTIFY_LEARNERS_CHECKBOX_TEST_ID } from './constants';
+import { getAssignmentModalFields, NOTIFY_LEARNERS_CHECKBOX_TEST_ID } from './constants';
import { displayCode, displaySelectedCodes } from '../CodeModal/codeModalHelpers';
import {
EMAIL_TEMPLATE_SOURCE_NEW_EMAIL,
} from '../../data/constants/emailTemplate';
-import { EMAIL_FORM_NAME } from '../EmailTemplateForm';
jest.mock('redux-form', () => ({
...jest.requireActual('redux-form'),
@@ -122,14 +121,20 @@ const initialState = {
},
};
+const mockIntl = {
+ formatMessage: message => message.defaultMessage,
+};
+
/* eslint-disable react/prop-types */
const CodeAssignmentModalWrapper = (props) => (
-
+
+
+
);
@@ -141,16 +146,19 @@ describe('CodeAssignmentModal component', () => {
expect(screen.getByText(initialProps.title)).toBeInTheDocument();
});
it('displays an error', () => {
- // eslint-disable-next-line global-require, no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, global-require
const { Field } = require('redux-form');
const error = 'Errors ahoy!';
const props = { ...initialProps, error: [error], submitFailed: true };
render(
-
+
+
+
,
);
@@ -167,11 +175,13 @@ describe('CodeAssignmentModal component', () => {
});
it('renders an email template form', () => {
render();
- expect(screen.getByText(EMAIL_FORM_NAME)).toBeInTheDocument();
+ expect(screen.getByText('Email Template')).toBeInTheDocument();
});
it('renders a auto-reminder checkbox', () => {
+ const formatMessageMock = message => message.defaultMessage;
+ const assignmentModalFields = getAssignmentModalFields(formatMessageMock);
render();
- expect(screen.getByText(ASSIGNMENT_MODAL_FIELDS['enable-nudge-emails'].label)).toBeInTheDocument();
+ expect(screen.getByText(assignmentModalFields['enable-nudge-emails'].label)).toBeInTheDocument();
});
it('renders notify learners toggle checkbox', () => {
render();
diff --git a/src/components/CodeAssignmentModal/constants.jsx b/src/components/CodeAssignmentModal/constants.jsx
index 8d2e36977f..995adefa65 100644
--- a/src/components/CodeAssignmentModal/constants.jsx
+++ b/src/components/CodeAssignmentModal/constants.jsx
@@ -1,27 +1,32 @@
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { MODAL_TYPES } from '../EmailTemplateForm/constants';
-import { EMAIL_TEMPLATE_FIELDS } from '../EmailTemplateForm';
+import { getTemplateEmailFields } from '../EmailTemplateForm';
import CheckboxWithTooltip from '../ReduxFormCheckbox/CheckboxWithTooltip';
+import messages from './messages';
+
export const ASSIGNMENT_ERROR_TITLES = {
[MODAL_TYPES.assign]: 'Unable to assign codes',
[MODAL_TYPES.save]: 'Unable to save template',
};
export const EMAIL_TEMPLATE_NUDGE_EMAIL_ID = 'enable-nudge-emails';
-export const ASSIGNMENT_MODAL_FIELDS = {
- ...EMAIL_TEMPLATE_FIELDS,
- [EMAIL_TEMPLATE_NUDGE_EMAIL_ID]: {
- name: EMAIL_TEMPLATE_NUDGE_EMAIL_ID,
- id: EMAIL_TEMPLATE_NUDGE_EMAIL_ID,
- component: CheckboxWithTooltip,
- className: 'auto-reminder-wrapper',
- icon: faInfoCircle,
- altText: 'More information',
- tooltipText: 'edX will remind learners to redeem their code 3, 10, and 19 days after you assign it.',
- label: 'Automate reminders',
- defaultChecked: true,
- },
+export const getAssignmentModalFields = formatMessage => {
+ const emailTemplateFields = getTemplateEmailFields(formatMessage);
+ return {
+ ...emailTemplateFields,
+ [EMAIL_TEMPLATE_NUDGE_EMAIL_ID]: {
+ name: EMAIL_TEMPLATE_NUDGE_EMAIL_ID,
+ id: EMAIL_TEMPLATE_NUDGE_EMAIL_ID,
+ component: CheckboxWithTooltip,
+ className: 'auto-reminder-wrapper',
+ icon: faInfoCircle,
+ altText: formatMessage(messages.modalAltText),
+ tooltipText: formatMessage(messages.modalTooltipText),
+ label: formatMessage(messages.modalFieldLabel),
+ defaultChecked: true,
+ },
+ };
};
export const NOTIFY_LEARNERS_CHECKBOX_TEST_ID = 'notify-learners-checkbox';
diff --git a/src/components/CodeAssignmentModal/index.jsx b/src/components/CodeAssignmentModal/index.jsx
index e594793bf5..e492fef0b2 100644
--- a/src/components/CodeAssignmentModal/index.jsx
+++ b/src/components/CodeAssignmentModal/index.jsx
@@ -4,6 +4,8 @@ import { reduxForm, SubmissionError } from 'redux-form';
import {
Button, Icon, Modal, Form,
} from '@edx/paragon';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
import isEmail from 'validator/lib/isEmail';
import BulkAssignFields from './BulkAssignFields';
@@ -27,9 +29,9 @@ import EmailTemplateForm from '../EmailTemplateForm';
import {
EMAIL_TEMPLATE_NUDGE_EMAIL_ID,
ASSIGNMENT_ERROR_TITLES,
- ASSIGNMENT_MODAL_FIELDS,
NOTIFY_LEARNERS_CHECKBOX_TEST_ID,
SUBMIT_BUTTON_TEST_ID,
+ getAssignmentModalFields,
} from './constants';
import { getErrors } from './validation';
@@ -339,6 +341,7 @@ export class BaseCodeAssignmentModal extends React.Component {
isBulkAssign,
submitFailed,
error,
+ intl: { formatMessage },
} = this.props;
const { mode, notify } = this.state;
@@ -376,7 +379,7 @@ export class BaseCodeAssignmentModal extends React.Component {
{ notify && (
)}
@@ -478,8 +481,11 @@ BaseCodeAssignmentModal.propTypes = {
unassignedCodes: PropTypes.number,
remainingUses: PropTypes.number,
}),
+
+ // injected
+ intl: intlShape.isRequired,
};
export default reduxForm({
form: 'code-assignment-modal-form',
-})(BaseCodeAssignmentModal);
+})(injectIntl(BaseCodeAssignmentModal));
diff --git a/src/components/CodeAssignmentModal/messages.js b/src/components/CodeAssignmentModal/messages.js
new file mode 100644
index 0000000000..455bf9667a
--- /dev/null
+++ b/src/components/CodeAssignmentModal/messages.js
@@ -0,0 +1,18 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ modalAltText: {
+ id: 'adminPortal.assignmentModal.altText',
+ defaultMessage: 'More information',
+ },
+ modalTooltipText: {
+ id: 'adminPortal.assignmentModal.tooltipText',
+ defaultMessage: 'edX will remind learners to redeem their code 3, 10, and 19 days after you assign it.',
+ },
+ modalFieldLabel: {
+ id: 'adminPortal.assignmentModal.modalFieldLabel',
+ defaultMessage: 'Automate reminders',
+ },
+});
+
+export default messages;
diff --git a/src/components/CodeReminderModal/CodeReminderModal.test.jsx b/src/components/CodeReminderModal/CodeReminderModal.test.jsx
index 8ae8c93505..75e250f063 100644
--- a/src/components/CodeReminderModal/CodeReminderModal.test.jsx
+++ b/src/components/CodeReminderModal/CodeReminderModal.test.jsx
@@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
import { screen, render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';
@@ -14,7 +15,6 @@ import { displayCode, displayEmail, displaySelectedCodes } from '../CodeModal/co
import {
EMAIL_TEMPLATE_SOURCE_NEW_EMAIL,
} from '../../data/constants/emailTemplate';
-import { EMAIL_FORM_NAME } from '../EmailTemplateForm';
jest.mock('redux-form', () => ({
...jest.requireActual('redux-form'),
@@ -114,14 +114,23 @@ const initialState = {
},
};
+const mockIntl = {
+ formatMessage(message) {
+ return message.defaultMessage;
+ },
+};
+
/* eslint-disable react/prop-types */
const CodeReminderModalWrapper = (props) => (
-
+
+
+
);
@@ -149,6 +158,6 @@ describe('CodeReminderModal component', () => {
});
it('renders an email template form', () => {
render();
- expect(screen.getByText(EMAIL_FORM_NAME)).toBeInTheDocument();
+ expect(screen.getByText('Email Template')).toBeInTheDocument();
});
});
diff --git a/src/components/CodeReminderModal/index.jsx b/src/components/CodeReminderModal/index.jsx
index eb3c77dcd9..fdd22cb562 100644
--- a/src/components/CodeReminderModal/index.jsx
+++ b/src/components/CodeReminderModal/index.jsx
@@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { reduxForm, SubmissionError } from 'redux-form';
import { Button, Icon, Modal } from '@edx/paragon';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
import SaveTemplateButton from '../../containers/SaveTemplateButton';
import { EMAIL_TEMPLATE_SUBJECT_KEY } from '../../data/constants/emailTemplate';
@@ -10,17 +12,10 @@ import ModalError from '../CodeModal/ModalError';
import { configuration, features } from '../../config';
import './CodeReminderModal.scss';
import CodeDetails from './CodeDetails';
-import EmailTemplateForm, { EMAIL_TEMPLATE_FIELDS } from '../EmailTemplateForm';
+import EmailTemplateForm, { getTemplateEmailFields } from '../EmailTemplateForm';
import { EMAIL_TEMPLATE_FILES_ID, MODAL_TYPES } from '../EmailTemplateForm/constants';
import { appendUserCodeDetails } from '../CodeModal';
-const REMINDER_EMAIL_TEMPLATE_FIELDS = {
- ...EMAIL_TEMPLATE_FIELDS,
- 'email-template-body': {
- ...EMAIL_TEMPLATE_FIELDS['email-template-body'],
- disabled: true,
- },
-};
const REMIND_MODE = MODAL_TYPES.remind;
const ERROR_MESSAGE_TITLES = {
@@ -169,13 +164,21 @@ export class BaseCodeReminderModal extends React.Component {
const {
data,
isBulkRemind,
+ intl: { formatMessage },
submitFailed,
error,
} = this.props;
const { mode } = this.state;
const numberOfSelectedCodes = this.getNumberOfSelectedCodes();
-
+ const emailTemplateFields = getTemplateEmailFields(formatMessage);
+ const reminderEmailTemplateFields = {
+ ...emailTemplateFields,
+ 'email-template-body': {
+ ...emailTemplateFields['email-template-body'],
+ disabled: true,
+ },
+ };
return (
<>
{submitFailed && (
@@ -193,7 +196,7 @@ export class BaseCodeReminderModal extends React.Component {
/>
>
);
@@ -282,8 +285,10 @@ BaseCodeReminderModal.propTypes = {
code: PropTypes.string,
email: PropTypes.string,
}),
+ // injected
+ intl: intlShape.isRequired,
};
export default reduxForm({
form: 'code-reminder-modal-form',
-})(BaseCodeReminderModal);
+})(injectIntl(BaseCodeReminderModal));
diff --git a/src/components/CodeRevokeModal/CodeRevokeModal.test.jsx b/src/components/CodeRevokeModal/CodeRevokeModal.test.jsx
index c0bae14188..37be085c64 100644
--- a/src/components/CodeRevokeModal/CodeRevokeModal.test.jsx
+++ b/src/components/CodeRevokeModal/CodeRevokeModal.test.jsx
@@ -2,6 +2,7 @@
import React from 'react';
import { Provider } from 'react-redux';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import { screen, render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
@@ -16,7 +17,6 @@ import { displayCode, displaySelectedCodes } from '../CodeModal/codeModalHelpers
import {
EMAIL_TEMPLATE_SOURCE_NEW_EMAIL,
} from '../../data/constants/emailTemplate';
-import { EMAIL_FORM_NAME } from '../EmailTemplateForm';
const sampleCodeData = {
code: 'test-code-1',
@@ -120,10 +120,12 @@ const initialState = {
const CodeRevokeModalWrapper = (props) => (
-
+
+
+
);
@@ -144,7 +146,7 @@ describe('CodeRevokeModal component', () => {
});
it('renders an email template form', () => {
render();
- expect(screen.getByText(EMAIL_FORM_NAME)).toBeInTheDocument();
+ expect(screen.getByText('Email Template')).toBeInTheDocument();
});
it('renders a auto-reminder checkbox', () => {
render();
diff --git a/src/components/CodeSearchResults/CodeSearchResults.test.jsx b/src/components/CodeSearchResults/CodeSearchResults.test.jsx
index be04bd8b12..a5b8e19c3e 100644
--- a/src/components/CodeSearchResults/CodeSearchResults.test.jsx
+++ b/src/components/CodeSearchResults/CodeSearchResults.test.jsx
@@ -1,10 +1,13 @@
import React from 'react';
+import PropTypes from 'prop-types';
+
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import { mount } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import CodeSearchResults from './index';
@@ -62,6 +65,24 @@ const initialStore = {
},
};
+const CodeSearchResultsWrapper = props => (
+
+
+
+
+
+
+
+);
+
+CodeSearchResultsWrapper.propTypes = {
+ store: PropTypes.shape({}),
+};
+
+CodeSearchResultsWrapper.defaultProps = {
+ store: null,
+};
+
describe('', () => {
beforeAll(() => {
const mockPromiseResolve = () => Promise.resolve({ data: {} });
@@ -72,19 +93,14 @@ describe('', () => {
describe('basic rendering', () => {
it('should render nothing visible when isOpen prop is false', () => {
- const store = getMockStore({ ...initialStore });
const tree = renderer
.create((
-
-
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
@@ -103,15 +119,12 @@ describe('', () => {
});
const tree = renderer
.create((
-
-
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
@@ -162,15 +175,12 @@ describe('', () => {
});
const tree = renderer
.create((
-
-
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
@@ -202,15 +212,12 @@ describe('', () => {
});
const tree = renderer
.create((
-
-
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
@@ -233,15 +240,12 @@ describe('', () => {
});
const tree = renderer
.create((
-
-
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
@@ -260,15 +264,12 @@ describe('', () => {
});
const tree = renderer
.create((
-
-
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
@@ -303,15 +304,12 @@ describe('', () => {
},
});
const wrapper = mount((
-
-
-
-
-
+
));
const mockPromiseResolve = () => Promise.resolve({ data: {} });
EcommerceApiService.fetchEmailTemplate.mockImplementation(mockPromiseResolve);
@@ -358,15 +356,12 @@ describe('', () => {
},
});
const wrapper = mount((
-
-
-
-
-
+
));
const mockPromiseResolve = () => Promise.resolve({ data: {} });
EcommerceApiService.fetchEmailTemplate.mockImplementation(mockPromiseResolve);
@@ -403,15 +398,12 @@ describe('', () => {
},
});
const wrapper = mount((
-
-
-
-
-
+
));
expect(wrapper.find('CodeSearchResults').state('isCodeRevokeSuccessful')).toBeFalsy();
wrapper.find('RevokeButton').simulate('click');
@@ -439,15 +431,12 @@ describe('', () => {
},
});
const wrapper = mount((
-
-
-
-
-
+
));
wrapper.find('.close-search-results-btn').first().simulate('click');
diff --git a/src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.jsx b/src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.jsx
index e29eb4dc12..9d59ca3fcf 100644
--- a/src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.jsx
+++ b/src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.jsx
@@ -25,44 +25,6 @@ const selectColumn = {
disableSortBy: true,
};
-const HighlightStepperSelectContent = ({ enterpriseId }) => {
- const { setCurrentSelectedRowIds } = useContentHighlightsContext();
- const currentSelectedRowIds = useContextSelector(
- ContentHighlightsContext,
- v => v[0].stepperModal.currentSelectedRowIds,
- );
- const searchClient = useContextSelector(
- ContentHighlightsContext,
- v => v[0].searchClient,
- );
- // TODO: replace testEnterpriseId with enterpriseId before push,
- // uncomment out import and replace with testEnterpriseId to test
- const searchFilters = `enterprise_customer_uuids:${ENABLE_TESTING(enterpriseId)}`;
-
- return (
-
-
-
-
-
-
-
- );
-};
-
-HighlightStepperSelectContent.propTypes = {
- enterpriseId: PropTypes.string.isRequired,
-};
-
const PriceTableCell = ({ row }) => {
const contentPrice = row.original.firstEnrollablePaidSeatPrice;
if (!contentPrice) {
@@ -176,4 +138,42 @@ BaseHighlightStepperSelectContentDataTable.defaultProps = {
const HighlightStepperSelectContentDataTable = connectStateResults(BaseHighlightStepperSelectContentDataTable);
+const HighlightStepperSelectContent = ({ enterpriseId }) => {
+ const { setCurrentSelectedRowIds } = useContentHighlightsContext();
+ const currentSelectedRowIds = useContextSelector(
+ ContentHighlightsContext,
+ v => v[0].stepperModal.currentSelectedRowIds,
+ );
+ const searchClient = useContextSelector(
+ ContentHighlightsContext,
+ v => v[0].searchClient,
+ );
+ // TODO: replace testEnterpriseId with enterpriseId before push,
+ // uncomment out import and replace with testEnterpriseId to test
+ const searchFilters = `enterprise_customer_uuids:${ENABLE_TESTING(enterpriseId)}`;
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+HighlightStepperSelectContent.propTypes = {
+ enterpriseId: PropTypes.string.isRequired,
+};
+
export default HighlightStepperSelectContent;
diff --git a/src/components/ContentHighlights/data/constants.js b/src/components/ContentHighlights/data/constants.js
index a0a4ebab64..978b549410 100644
--- a/src/components/ContentHighlights/data/constants.js
+++ b/src/components/ContentHighlights/data/constants.js
@@ -155,7 +155,7 @@ export const LEARNER_PORTAL_CATALOG_VISIBILITY = {
export const DEFAULT_ERROR_MESSAGE = {
EMPTY_HIGHLIGHT_SET: 'There is no highlighted content for this highlight collection.',
// eslint-disable-next-line quotes
- EMPTY_SELECTEDROWIDS: `You don't have any highlighted content selected. Go back to the previous step to select content.`,
+ EMPTY_SELECTEDROWIDS: 'You don\'t have any highlighted content selected. Go back to the previous step to select content.',
EXCEEDS_HIGHLIGHT_TITLE_LENGTH: `Titles may only be ${MAX_HIGHLIGHT_TITLE_LENGTH} characters or less`,
};
diff --git a/src/components/ContentHighlights/tests/ContentHighlightSet.test.jsx b/src/components/ContentHighlights/tests/ContentHighlightSet.test.jsx
index c1aecb2691..776d8fbfc8 100644
--- a/src/components/ContentHighlights/tests/ContentHighlightSet.test.jsx
+++ b/src/components/ContentHighlights/tests/ContentHighlightSet.test.jsx
@@ -44,8 +44,7 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
}));
-/* eslint-disable react/prop-types */
-// eslint-disable-next-line no-unused-vars
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ContentHighlightSetWrapper = (
enterpriseAppContextValue = initialEnterpriseAppContextValue,
{ children },
diff --git a/src/components/Coupon/Coupon.test.jsx b/src/components/Coupon/Coupon.test.jsx
index 37473ed9d1..3d61077d8a 100644
--- a/src/components/Coupon/Coupon.test.jsx
+++ b/src/components/Coupon/Coupon.test.jsx
@@ -5,6 +5,7 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { shallow, mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import { MULTI_USE } from '../../data/constants/coupons';
@@ -47,10 +48,12 @@ const initialCouponData = {
const CouponWrapper = props => (
-
+
+
+
);
diff --git a/src/components/CouponDetails/index.test.jsx b/src/components/CouponDetails/index.test.jsx
index b2edf62f15..d034ce266f 100644
--- a/src/components/CouponDetails/index.test.jsx
+++ b/src/components/CouponDetails/index.test.jsx
@@ -6,6 +6,8 @@ import { MemoryRouter } from 'react-router-dom';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import userEvent from '@testing-library/user-event';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+
import { renderWithRouter } from '../test/testUtils';
import CouponDetails from './index';
import { COUPON_FILTERS, DEFAULT_TABLE_COLUMNS } from './constants';
@@ -135,9 +137,11 @@ const defaultProps = {
const CouponDetailsWrapper = props => (
-
+
+
+
);
diff --git a/src/components/DownloadCsvButton/index.jsx b/src/components/DownloadCsvButton/index.jsx
index dd3cb10772..702c37e7e8 100644
--- a/src/components/DownloadCsvButton/index.jsx
+++ b/src/components/DownloadCsvButton/index.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button, Icon } from '@edx/paragon';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
export const CSV_CLICK_SEGMENT_EVENT_NAME = 'edx.ui.enterprise.admin_portal.download_csv.clicked';
@@ -20,6 +21,7 @@ class DownloadCsvButton extends React.Component {
enterpriseId,
id,
} = this.props;
+
const downloadButtonIconClasses = csvLoading ? ['fa-spinner', 'fa-spin'] : ['fa-download'];
return (
);
@@ -46,7 +48,7 @@ DownloadCsvButton.defaultProps = {
csvLoading: false,
fetchMethod: () => {},
disabled: false,
- buttonLabel: 'Download full report (CSV)',
+ buttonLabel: '',
};
DownloadCsvButton.propTypes = {
diff --git a/src/components/EmailTemplateForm/EmailTemplateForm.test.jsx b/src/components/EmailTemplateForm/EmailTemplateForm.test.jsx
index 3d4cc5a141..3b8190dd7e 100644
--- a/src/components/EmailTemplateForm/EmailTemplateForm.test.jsx
+++ b/src/components/EmailTemplateForm/EmailTemplateForm.test.jsx
@@ -1,13 +1,14 @@
import React from 'react';
import { Provider } from 'react-redux';
import { reduxForm } from 'redux-form';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import { screen, render } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import '@testing-library/jest-dom/extend-expect';
import { MemoryRouter } from 'react-router';
-import EmailTemplateForm, { EMAIL_FORM_NAME, EMAIL_TEMPLATE_FIELDS } from '.';
+import EmailTemplateForm, { getTemplateEmailFields } from '.';
import { MODAL_TYPES } from './constants';
import { TEMLATE_SOURCE_FIELDS_TEST_ID } from '../TemplateSourceFields';
import RenderField from '../RenderField';
@@ -16,6 +17,8 @@ const mockStore = configureMockStore([thunk]);
const ConnectedEmailTemplateForm = reduxForm({ form: 'test' })(EmailTemplateForm);
+const mockFormatMessage = message => message.defaultMessage;
+
const initialState = {
emailTemplate: {
emailTemplateSource: 'foo',
@@ -26,7 +29,9 @@ const initialState = {
const EmailTemplateFormWrapper = (props) => (
-
+
+
+
);
@@ -34,11 +39,12 @@ const EmailTemplateFormWrapper = (props) => (
describe('EmailTemplateForm', () => {
it('renders a form', () => {
render();
- expect(screen.getByText(EMAIL_FORM_NAME)).toBeInTheDocument();
+ expect(screen.getByText('Email Template')).toBeInTheDocument();
});
it('renders default fields', () => {
+ const emailTemplateFields = getTemplateEmailFields(mockFormatMessage);
render();
- Object.values(EMAIL_TEMPLATE_FIELDS).forEach((field) => {
+ Object.values(emailTemplateFields).forEach((field) => {
expect(screen.getByText(field.label)).toBeInTheDocument();
});
});
@@ -56,9 +62,10 @@ describe('EmailTemplateForm', () => {
type: 'text',
},
};
+ const emailTemplateFields = getTemplateEmailFields(mockFormatMessage);
render();
expect(screen.getByText(fields.foo.label)).toBeInTheDocument();
- Object.values(EMAIL_TEMPLATE_FIELDS).forEach((field) => {
+ Object.values(emailTemplateFields).forEach((field) => {
expect(screen.queryByText(field.label)).not.toBeInTheDocument();
});
});
diff --git a/src/components/EmailTemplateForm/index.jsx b/src/components/EmailTemplateForm/index.jsx
index b1c61335a9..5efe025768 100644
--- a/src/components/EmailTemplateForm/index.jsx
+++ b/src/components/EmailTemplateForm/index.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field } from 'redux-form';
+import { useIntl } from '@edx/frontend-platform/i18n';
import TemplateSourceFields from '../../containers/TemplateSourceFields';
import TextAreaAutoSize from '../TextAreaAutoSize';
import RenderField from '../RenderField';
@@ -16,12 +17,13 @@ import {
} from './constants';
import { EMAIL_TEMPLATE_FIELD_MAX_LIMIT, OFFER_ASSIGNMENT_EMAIL_SUBJECT_LIMIT } from '../../data/constants/emailTemplate';
-export const EMAIL_FORM_NAME = 'Email Template';
-export const EMAIL_TEMPLATE_FIELDS = {
+import messages from './messages';
+
+export const getTemplateEmailFields = (formatMessage) => ({
[EMAIL_TEMPLATE_SUBJECT_ID]: {
name: EMAIL_TEMPLATE_SUBJECT_ID,
component: RenderField,
- label: 'Customize email subject',
+ label: formatMessage(messages.emailCustomizeSubject),
type: 'text',
limit: OFFER_ASSIGNMENT_EMAIL_SUBJECT_LIMIT,
'data-hj-suppress': true,
@@ -29,21 +31,21 @@ export const EMAIL_TEMPLATE_FIELDS = {
[EMAIL_TEMPLATE_GREETING_ID]: {
name: EMAIL_TEMPLATE_GREETING_ID,
component: TextAreaAutoSize,
- label: 'Customize greeting',
+ label: formatMessage(messages.emailCustomizeGreeting),
limit: EMAIL_TEMPLATE_FIELD_MAX_LIMIT,
'data-hj-suppress': true,
},
[EMAIL_TEMPLATE_BODY_ID]: {
name: EMAIL_TEMPLATE_BODY_ID,
component: TextAreaAutoSize,
- label: 'Body',
+ label: formatMessage(messages.emailBody),
disabled: true,
'data-hj-suppress': true,
},
[EMAIL_TEMPLATE_CLOSING_ID]: {
name: EMAIL_TEMPLATE_CLOSING_ID,
component: TextAreaAutoSize,
- label: 'Customize closing',
+ label: formatMessage(messages.emailCustomizeClosing),
limit: EMAIL_TEMPLATE_FIELD_MAX_LIMIT,
'data-hj-suppress': true,
},
@@ -52,29 +54,36 @@ export const EMAIL_TEMPLATE_FIELDS = {
name: EMAIL_TEMPLATE_FILES_ID,
component: MultipleFileInputField,
type: 'file',
- label: 'add files',
+ label: formatMessage(messages.emailAddFiles),
value: [],
- description: "Max files size shouldn't exceed 250kb.",
+ description: formatMessage(messages.emailMaxFileSizeMessage),
},
}),
-};
+});
const EmailTemplateForm = ({
children, emailTemplateType, fields, currentEmail, disabled,
-}) => (
-
-);
+}) => {
+ const { formatMessage } = useIntl();
+ const fieldsWithDefault = fields || getTemplateEmailFields(formatMessage);
+
+ return (
+
+ );
+};
EmailTemplateForm.defaultProps = {
children: null,
- fields: EMAIL_TEMPLATE_FIELDS,
+ fields: null,
currentEmail: '',
disabled: false,
};
diff --git a/src/components/EmailTemplateForm/messages.js b/src/components/EmailTemplateForm/messages.js
new file mode 100644
index 0000000000..5aafd7b712
--- /dev/null
+++ b/src/components/EmailTemplateForm/messages.js
@@ -0,0 +1,34 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ emailFormName: {
+ id: 'adminPortal.emailTemplateForm.formName',
+ defaultMessage: 'Email Template',
+ },
+ emailCustomizeSubject: {
+ id: 'adminPortal.emailTemplateForm.customizeSubject',
+ defaultMessage: 'Customize email subject',
+ },
+ emailCustomizeGreeting: {
+ id: 'adminPortal.emailTemplateForm.customizeGreeting',
+ defaultMessage: 'Customize greeting',
+ },
+ emailBody: {
+ id: 'adminPortal.emailTemplateForm.body',
+ defaultMessage: 'Body',
+ },
+ emailCustomizeClosing: {
+ id: 'adminPortal.emailTemplateForm.customizeClosing',
+ defaultMessage: 'Customize closing',
+ },
+ emailAddFiles: {
+ id: 'adminPortal.emailTemplateForm.addFiles',
+ defaultMessage: 'add files',
+ },
+ emailMaxFileSizeMessage: {
+ id: 'adminPortal.emailTemplateForm.maxFileSizeMessage',
+ defaultMessage: "Max files size shouldn't exceed 250kb.",
+ },
+});
+
+export default messages;
diff --git a/src/components/EnrollmentsTable/EnrollmentsTable.test.jsx b/src/components/EnrollmentsTable/EnrollmentsTable.test.jsx
index 9c3e01b977..c627a2737c 100644
--- a/src/components/EnrollmentsTable/EnrollmentsTable.test.jsx
+++ b/src/components/EnrollmentsTable/EnrollmentsTable.test.jsx
@@ -4,6 +4,7 @@ import renderer from 'react-test-renderer';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import EnrollmentsTable from './index';
// import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService';
@@ -32,9 +33,11 @@ const store = mockStore({
const EnrollmentsWrapper = props => (
-
+
+
+
);
diff --git a/src/components/EnrollmentsTable/index.jsx b/src/components/EnrollmentsTable/index.jsx
index e53a63b5be..c1f0fb29b7 100644
--- a/src/components/EnrollmentsTable/index.jsx
+++ b/src/components/EnrollmentsTable/index.jsx
@@ -1,53 +1,84 @@
import React from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+
import TableContainer from '../../containers/TableContainer';
import { formatTimestamp, formatPassedTimestamp, formatPercentage } from '../../utils';
import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService';
const EnrollmentsTable = () => {
+ const intl = useIntl();
+
const enrollmentTableColumns = [
{
- label: 'Email',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.user_email',
+ defaultMessage: 'Email',
+ }),
key: 'user_email',
columnSortable: true,
},
{
- label: 'Course Title',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.courseTitle',
+ defaultMessage: 'Course Title',
+ }),
key: 'course_title',
columnSortable: true,
},
{
- label: 'Course Price',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.courseListPrice',
+ defaultMessage: 'Course Price',
+ }),
key: 'course_list_price',
columnSortable: true,
},
{
- label: 'Start Date',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.courseStartDate',
+ defaultMessage: 'Start Date',
+ }),
key: 'course_start_date',
columnSortable: true,
},
{
- label: 'End Date',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.courseEndDate',
+ defaultMessage: 'End Date',
+ }),
key: 'course_end_date',
columnSortable: true,
},
{
- label: 'Passed Date',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.passedDate',
+ defaultMessage: 'Passed Date',
+ }),
key: 'passed_date',
columnSortable: true,
},
{
- label: 'Current Grade',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.currentGrade',
+ defaultMessage: 'Current Grade',
+ }),
key: 'current_grade',
columnSortable: true,
},
{
- label: 'Progress Status',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.progressStatus',
+ defaultMessage: 'Progress Status',
+ }),
key: 'progress_status',
columnSortable: true,
},
{
- label: 'Last Activity Date',
+ label: intl.formatMessage({
+ id: 'adminPortal.enrollmentsTable.lastActivityDate',
+ defaultMessage: 'Last Activity Date',
+ }),
key: 'last_activity_date',
columnSortable: true,
},
diff --git a/src/components/EnterpriseList/EnterpriseList.test.jsx b/src/components/EnterpriseList/EnterpriseList.test.jsx
index 416435d60a..47b04098c0 100644
--- a/src/components/EnterpriseList/EnterpriseList.test.jsx
+++ b/src/components/EnterpriseList/EnterpriseList.test.jsx
@@ -4,6 +4,7 @@ import { MemoryRouter, Redirect } from 'react-router-dom';
import { mount } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import EnterpriseList, { TITLE } from './index';
import mockEnterpriseList from './EnterpriseList.mocks';
@@ -38,14 +39,16 @@ const store = mockStore({
const EnterpriseListWrapper = ({ initialEntries, ...rest }) => (
- {}}
- clearPortalConfiguration={() => {}}
- {...rest}
- />
+
+ {}}
+ clearPortalConfiguration={() => {}}
+ {...rest}
+ />
+
);
diff --git a/src/components/ErrorPage/ErrorPage.test.jsx b/src/components/ErrorPage/ErrorPage.test.jsx
index 7f5fd40fd4..d5df5e94ab 100644
--- a/src/components/ErrorPage/ErrorPage.test.jsx
+++ b/src/components/ErrorPage/ErrorPage.test.jsx
@@ -1,16 +1,23 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import ErrorPage from './index';
+const ErrorPageWrapper = (props) => (
+
+
+
+
+
+);
+
describe('', () => {
it('renders correctly', () => {
const tree = renderer
.create((
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
@@ -19,9 +26,7 @@ describe('', () => {
it('renders correctly for 404 errors', () => {
const tree = renderer
.create((
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
@@ -30,9 +35,7 @@ describe('', () => {
it('renders correctly for 403 errors', () => {
const tree = renderer
.create((
-
-
-
+
))
.toJSON();
expect(tree).toMatchSnapshot();
diff --git a/src/components/ErrorPage/index.jsx b/src/components/ErrorPage/index.jsx
index c319aef2a3..3f614fea33 100644
--- a/src/components/ErrorPage/index.jsx
+++ b/src/components/ErrorPage/index.jsx
@@ -4,6 +4,8 @@ import Helmet from 'react-helmet';
import { Alert } from '@edx/paragon';
import { Cancel as ErrorIcon } from '@edx/paragon/icons';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+
import NotFoundPage from '../NotFoundPage';
import ForbiddenPage from '../ForbiddenPage';
@@ -28,7 +30,7 @@ function renderErrorComponent(status, message) {
variant="danger"
icon={ErrorIcon}
>
- Error
+
{errorMessage}
diff --git a/src/components/Footer/index.jsx b/src/components/Footer/index.jsx
index 824efe33ce..192f8dbfd5 100644
--- a/src/components/Footer/index.jsx
+++ b/src/components/Footer/index.jsx
@@ -1,9 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
import { configuration } from '../../config';
import Img from '../Img';
+import messages from './messages';
import './Footer.scss';
class Footer extends React.Component {
@@ -39,6 +42,8 @@ class Footer extends React.Component {
render() {
const { enterpriseLogoNotFound } = this.state;
const { enterpriseLogo } = this.props;
+ const { formatMessage } = this.props.intl;
+
return (