Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Sheehan
14c1f5b944 feat: beginnings of the recruit me some money exploits 2021-08-11 10:48:16 -04:00
101 changed files with 14932 additions and 42113 deletions

6
.env
View File

@@ -16,6 +16,7 @@ SITE_NAME=null
USER_INFO_COOKIE_NAME=null
AUTHN_MINIMAL_HEADER=true
LOGIN_ISSUE_SUPPORT_LINK=''
REGISTRATION_OPTIONAL_FIELDS=''
USER_SURVEY_COOKIE_NAME=null
COOKIE_DOMAIN=null
WELCOME_PAGE_SUPPORT_LINK=null
@@ -23,8 +24,3 @@ INFO_EMAIL=''
DISABLE_ENTERPRISE_LOGIN=''
REGISTER_CONVERSION_COOKIE_NAME=null
ENABLE_PROGRESSIVE_PROFILING=''
MARKETING_EMAILS_OPT_IN=''
ENABLE_COPPA_COMPLIANCE=''
SHOW_DYNAMIC_PROFILING_PAGE=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''

View File

@@ -17,19 +17,16 @@ MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
SITE_NAME='edX'
USER_INFO_COOKIE_NAME='edx-user-info'
AUTHN_MINIMAL_HEADER=true
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
PRIVACY_POLICY='http://localhost:18000/privacy'
REGISTRATION_OPTIONAL_FIELDS=''
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
COOKIE_DOMAIN='localhost'
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
INFO_EMAIL='info@edx.org'
DISABLE_ENTERPRISE_LOGIN=''
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
ENABLE_COPPA_COMPLIANCE=''
MARKETING_EMAILS_OPT_IN=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''

View File

@@ -15,14 +15,10 @@ MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
SITE_NAME='edX'
USER_INFO_COOKIE_NAME='edx-user-info'
LOGIN_ISSUE_SUPPORT_LINK='https://login-issue-support-url.com'
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
DISABLE_ENTERPRISE_LOGIN=''
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
MARKETING_EMAILS_OPT_IN=''
ENABLE_COPPA_COMPLIANCE=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''

View File

@@ -1,19 +0,0 @@
# Run the workflow that adds new tickets that are either:
# - labelled "DEPR"
# - title starts with "[DEPR]"
# - body starts with "Proposal Date" (this is the first template field)
# to the org-wide DEPR project board
name: Add newly created DEPR issues to the DEPR project board
on:
issues:
types: [opened]
jobs:
routeissue:
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}

View File

@@ -1,48 +0,0 @@
name: node_CI
on:
push:
branches:
- master
pull_request:
branches:
- '**'
jobs:
tests:
runs-on: ubuntu-20.04
strategy:
matrix:
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Nodejs
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- name: Install Dependencies
run: npm ci
- name: Verify No Uncommitted Package-Lock Changes
run: make validate-no-uncommitted-package-lock-changes
- name: Run i18n_extract
run: npm run i18n_extract
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Build
run: npm run build
- name: Verify Es5
run: npm run is-es5
- name: Run Code Coverage
uses: codecov/codecov-action@v2

View File

@@ -1,10 +0,0 @@
# Run commitlint on the commit messages in a pull request.
name: Lint Commit Messages
on:
- pull_request
jobs:
commitlint:
uses: edx/.github/.github/workflows/commitlint.yml@master

View File

@@ -1,13 +0,0 @@
#check package-lock file version
name: Lockfile Version check
on:
push:
branches:
- master
pull_request:
jobs:
version-check:
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master

View File

@@ -1,6 +1,7 @@
.eslintignore
.eslintrc.json
.gitignore
.travis.yml
docker-compose.yml
Dockerfile
Makefile

13
.travis.yml Executable file
View File

@@ -0,0 +1,13 @@
language: node_js
node_js: 12
install:
- npm ci
script:
- make validate-no-uncommitted-package-lock-changes
- npm run i18n_extract
- npm run lint
- npm run test
- npm run build
- npm run is-es5
after_success:
- codecov

View File

@@ -1,9 +1,8 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-authn]
[edx-platform.frontend-app-authn]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON
type = KEYVALUEJSON

View File

@@ -1,9 +1,11 @@
export TRANSIFEX_RESOURCE = frontend-app-authn
transifex_langs = "ar,fr,es_419,zh_CN,it_IT,pt_PT,de_DE,uk,ru,hi"
transifex_resource = frontend-app-authn
transifex_langs = "ar,fr,es_419,zh_CN"
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
@@ -36,17 +38,17 @@ push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
$(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --languages=$(transifex_langs)
tx pull -f --mode reviewed --language=$(transifex_langs)
# This target is used by CI.
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...
git diff --exit-code package-lock.json

View File

@@ -8,6 +8,5 @@ module.exports = createConfig('jest', {
'src/setupTest.js',
'src/i18n',
'src/index.jsx',
'MainApp.jsx',
],
});

View File

@@ -5,4 +5,5 @@ nick: Authn MFE
oeps: {}
owner: edx/vanguards
openedx-release:
maybe: true # Delete this "maybe" line when you have decided about Open edX inclusion.
ref: master

51637
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,58 +35,55 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-cookie-policy-banner": "2.1.14",
"@edx/frontend-platform": "1.15.5",
"@edx/paragon": "19.10.2",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.1.18",
"classnames": "2.3.1",
"clipboard": "2.0.10",
"core-js": "3.21.1",
"@edx/frontend-component-cookie-policy-banner": "2.1.12",
"@edx/frontend-platform": "1.12.0",
"@edx/paragon": "16.6.1",
"@fortawesome/fontawesome-svg-core": "1.2.32",
"@fortawesome/free-brands-svg-icons": "5.15.1",
"@fortawesome/free-regular-svg-icons": "5.15.1",
"@fortawesome/free-solid-svg-icons": "5.15.1",
"@fortawesome/react-fontawesome": "0.1.13",
"classnames": "2.2.6",
"core-js": "3.9.1",
"extract-react-intl-messages": "4.1.1",
"fastest-levenshtein": "1.0.12",
"form-urlencoded": "4.2.1",
"formik": "2.2.9",
"formik": "2.2.6",
"lodash.camelcase": "4.3.0",
"lodash.snakecase": "4.1.1",
"prop-types": "15.8.1",
"prop-types": "15.7.2",
"query-string": "5.1.1",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "6.1.0",
"react-loading-skeleton": "2.2.0",
"react-onclickoutside": "6.12.1",
"react-redux": "7.2.6",
"react-onclickoutside": "6.11.2",
"react-redux": "7.2.3",
"react-responsive": "8.2.0",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"redux": "4.1.2",
"@redux-devtools/extension": "3.2.2",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"redux": "4.0.5",
"redux-devtools-extension": "2.13.8",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.4",
"redux-saga": "1.1.3",
"redux-thunk": "2.4.1",
"redux-thunk": "2.3.0",
"regenerator-runtime": "0.13.9",
"reselect": "4.1.5",
"sanitize-html": "2.7.0",
"semver-regex": "3.1.4",
"universal-cookie": "4.0.4"
"reselect": "4.0.0",
"universal-cookie": "^4.0.4"
},
"devDependencies": {
"@edx/frontend-build": "9.1.4",
"@edx/reactifex": "1.0.3",
"babel-plugin-formatjs": "10.3.18",
"codecov": "3.8.2",
"@edx/frontend-build": "5.6.11",
"babel-plugin-react-intl": "8.2.25",
"codecov": "3.8.1",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.6",
"es-check": "6.2.1",
"glob": "7.2.0",
"history": "5.3.0",
"husky": "7.0.4",
"jest": "27.5.1",
"react-test-renderer": "16.14.0"
"es-check": "5.2.3",
"glob": "7.1.6",
"history": "5.0.0",
"husky": "4.3.8",
"jest": "26.6.3",
"react-test-renderer": "16.14.0",
"reactifex": "1.1.1"
}
}

View File

@@ -14,58 +14,6 @@
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
<% if (process.env.ZENDESK_KEY) { %>
<script
id="ze-snippet"
src="https://static.zdassets.com/ekr/snippet.js?key=<%= process.env.ZENDESK_KEY %>"
>
</script>
<script type="text/javascript">
window.zESettings = {
cookies: true,
webWidget: {
contactOptions: {
enabled: false,
},
chat: {
suppress: false,
},
contactForm: {
ticketForms: [
{
id: 360003368814,
subject: false,
fields: [
{
id: 'description',
prefill: {
'*': '',
},
},
],
},
],
selectTicketForm: {
'*': 'Please choose your request type:',
},
attachments: true,
},
helpCenter: {
originalArticleButton: true,
},
answerBot: {
suppress: false,
contactOnlyAfterQuery: true,
title: { '*': 'edX Support' },
avatar: {
url: '<%= process.env.ZENDESK_LOGO_URL %>',
name: { '*': 'edX Support' },
},
},
},
};
</script>
<% } %>
</head>
<body>
<div id="root"></div>

View File

@@ -1,20 +1,9 @@
{
"extends": [
"config:base",
":automergeLinters",
":automergeTesters",
":automergeMinor",
":noUnscheduledUpdates",
":semanticCommits"
"config:base"
],
"rebaseStalePrs": true,
"schedule": [
"every weekday"
],
"packageRules": [
{
"matchPackageNames": ["node", "npm"],
"enabled": false
}
]
"patch": {
"automerge": true
},
"rebaseStalePrs": true
}

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import {
@@ -14,7 +13,7 @@ import configureStore from './data/configureStore';
import { updatePathWithQueryParams } from './data/utils';
import ForgotPasswordPage from './forgot-password';
import ResetPasswordPage from './reset-password';
import WelcomePage, { ProgressiveProfiling } from './welcome';
import WelcomePage from './welcome';
import './index.scss';
registerIcons();
@@ -29,11 +28,7 @@ const MainApp = () => (
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
<Route
exact
path={WELCOME_PAGE}
component={(getConfig().SHOW_DYNAMIC_PROFILING_PAGE) ? ProgressiveProfiling : WelcomePage}
/>
<Route exact path={WELCOME_PAGE} component={WelcomePage} />
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
<Route path="*">
<Redirect to={PAGE_NOT_FOUND} />

View File

@@ -15,15 +15,6 @@ $apple-black: #000000;
$apple-focus-black: $apple-black;
$accent-a-light: #c9f2f5;
.centered-align-spinner {
left: 0;
right: 0;
bottom: 0;
top: 0;
position: absolute;
margin: auto;
}
.main-content {
@extend .pt-4;
min-width: 464px !important;
@@ -37,12 +28,8 @@ $accent-a-light: #c9f2f5;
width: 12rem;
}
.register-stateful-button-width {
min-width: 14.4rem;
}
.login-button-width {
min-width: 6rem;
width: 6rem;
}
.tpa-skeleton {
@@ -364,10 +351,6 @@ select.form-control {
height: 282px;
}
.variation1-medium-screen {
height: 300px !important;
}
.medium-screen-svg-light,
.medium-screen-svg-primary {
fill: $light-200;
@@ -438,7 +421,7 @@ select.form-control {
}
.small-screen-top-stripe {
height: 0.25rem;
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
$brand-700,
@@ -503,14 +486,6 @@ select.form-control {
height: 240px;
}
.variation1-bar-color {
stroke: $brand !important;
}
.variation2-bar-color {
stroke: $accent-a !important;
}
.medium-screen-svg-line {
padding-top: 0.5rem;
stroke: $accent-b;
@@ -526,21 +501,6 @@ select.form-control {
width: 4em;
height: 72px;
}
.dicount-heading{
margin-left: 7px;
}
.hover-text:hover {
color: $black !important;
.hover-icon {
color: $black !important;
}
}
.hover-discount-icon:hover {
color: $white !important;
}
.large-heading {
margin-left: 7px;
@@ -624,7 +584,6 @@ select.form-control {
position: absolute;
background-color: #fff;
width: 464px;
z-index: 100 !important;
}
.email-error-alert {
@@ -711,15 +670,6 @@ select.form-control {
font-weight: 400;
}
.discount-banner {
background-color: #03C7E8;
}
.dashed-border {
border-style: dashed;
border-width: thin;
padding: 0.5rem;
}
@media (min-width: 1024px) {
.mw-500 {
@@ -756,12 +706,6 @@ select.form-control {
flex-direction:column;
justify-content: center;
}
.dashed-border {
border-style: dashed;
border-width: thin;
padding: 0.25rem 0.5rem;
}
}
@media (max-width: 1199px) and (min-width: 768px) {
@@ -815,12 +759,6 @@ select.form-control {
}
}
@media (max-width: 550px) {
.variation2-text-alignment {
text-align: left;
}
}
// Smaller than Extra Small (Mobile Screens)
@media (max-width: 464px) {
.btn-social {
@@ -852,36 +790,3 @@ select.form-control {
line-height: 1.5rem;
color: $primary-700
}
.opt-checkbox {
.pgn__form-label {
font-size: 0.75rem;
line-height: 1.25rem;
}
margin-left: 3px;
}
.suggested-username {
position: relative;
margin-top: -8.7%;
margin-left: 15px;
}
.suggested-username-close-button {
right: 1rem;
position: absolute;
}
.suggested-username-with-error {
position: relative;
margin-top: -13.7%;
margin-bottom: 11%;
margin-left: 15px;
}
.scroll-suggested-username {
width: 21rem;
white-space: nowrap;
overflow-x: auto;
display: inline-flex;
}
.pgn__form-control-decorator-trailing {
right: 0 !important;
}

View File

@@ -18,7 +18,7 @@ const AuthExtraLargeLayout = (props) => {
<div className="container row p-0 m-0 large-screen-container">
<div className="col-md-9 p-0 screen-header-light">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt={getConfig().SITE_NAME} className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
<Image alt="edx" className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="min-vh-100 d-flex align-items-center">
<div>

View File

@@ -17,7 +17,7 @@ const AuthMediumLayout = (props) => {
<div className="container row p-0 mb-3 medium-container">
<div className="col-md-10 p-0 screen-header-light">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
<Image alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="d-flex align-items-center justify-content-center ml-6">
<div>

View File

@@ -17,7 +17,7 @@ const AuthSmallLayout = (props) => {
return (
<div className="small-screen-header-light">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
<Image alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className={classNames('d-flex mt-3', { 'pl-6': variant === 'sm' })}>
<div>

View File

@@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import MediaQuery from 'react-responsive';
import { breakpoints } from '@edx/paragon';
import {
ExtraSmall, Small, Medium, Large, ExtraLarge, ExtraExtraLarge,
} from '@edx/paragon';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { getLocale } from '@edx/frontend-platform/i18n';
@@ -15,66 +16,43 @@ import SmallLayout from './SmallLayout';
import AuthExtraLargeLayout from './AuthExtraLargeLayout';
import AuthMediumLayout from './AuthMediumLayout';
import AuthSmallLayout from './AuthSmallLayout';
import DiscountExperimentBanner from './DiscountBanner';
const BaseComponent = ({ children, isRegistrationPage, showWelcomeBanner }) => {
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
const [optimizelyExperimentName, setOptimizelyExperimentName] = useState('');
useEffect(() => {
const { experimentName } = window;
if (experimentName) {
setOptimizelyExperimentName(experimentName);
}
});
const BaseComponent = ({ children }) => {
const authenticatedUser = getAuthenticatedUser();
return (
<>
{isRegistrationPage && optimizelyExperimentName === 'variation2' ? <DiscountExperimentBanner /> : null}
<CookiePolicyBanner languageCode={getLocale()} />
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraLarge.maxWidth}>
<ExtraLarge>
<div className="col-md-12 extra-large-screen-top-stripe" />
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraExtraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
</ExtraLarge>
<ExtraExtraLarge>
<div className="col-md-12 extra-large-screen-top-stripe" />
</MediaQuery>
</ExtraExtraLarge>
<div className={classNames('layout', { authenticated: authenticatedUser })}>
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth}>
<ExtraSmall>
<div className="col-md-12 small-screen-top-stripe" />
{authenticatedUser ? <AuthSmallLayout variant="xs" username={authenticatedUser.username} /> : (
<SmallLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
)}
</MediaQuery>
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth}>
{authenticatedUser ? <AuthSmallLayout variant="xs" username={authenticatedUser.username} /> : <SmallLayout />}
</ExtraSmall>
<Small>
<div className="col-md-12 small-screen-top-stripe" />
{authenticatedUser ? <AuthSmallLayout username={authenticatedUser.username} /> : (
<SmallLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
)}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.medium.maxWidth}>
{authenticatedUser ? <AuthSmallLayout username={authenticatedUser.username} /> : <SmallLayout />}
</Small>
<Medium>
<div className="w-100 medium-screen-top-stripe" />
{authenticatedUser ? <AuthMediumLayout username={authenticatedUser.username} /> : (
<MediumLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
)}
</MediaQuery>
<MediaQuery minWidth={breakpoints.large.minWidth} maxWidth={breakpoints.large.maxWidth}>
{authenticatedUser ? <AuthMediumLayout username={authenticatedUser.username} /> : <MediumLayout />}
</Medium>
<Large>
<div className="w-100 large-screen-top-stripe" />
{authenticatedUser ? <AuthMediumLayout username={authenticatedUser.username} /> : (
<MediumLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
)}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraLarge.maxWidth}>
{authenticatedUser ? <AuthExtraLargeLayout username={authenticatedUser.username} /> : (
<LargeLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
)}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraExtraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
{authenticatedUser ? <AuthExtraLargeLayout variant="xxl" username={authenticatedUser.username} /> : (
<LargeLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
)}
</MediaQuery>
{authenticatedUser ? <AuthMediumLayout username={authenticatedUser.username} /> : <MediumLayout />}
</Large>
<ExtraLarge>
{authenticatedUser ? <AuthExtraLargeLayout username={authenticatedUser.username} /> : <LargeLayout />}
</ExtraLarge>
<ExtraExtraLarge>
{authenticatedUser ? <AuthExtraLargeLayout variant="xxl" username={authenticatedUser.username} /> : <LargeLayout />}
</ExtraExtraLarge>
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
{children}
@@ -84,15 +62,8 @@ const BaseComponent = ({ children, isRegistrationPage, showWelcomeBanner }) => {
);
};
BaseComponent.defaultProps = {
isRegistrationPage: false,
showWelcomeBanner: false,
};
BaseComponent.propTypes = {
children: PropTypes.node.isRequired,
isRegistrationPage: PropTypes.bool,
showWelcomeBanner: PropTypes.bool,
};
export default BaseComponent;

View File

@@ -1,71 +0,0 @@
import React, { useState } from 'react';
import ClipboardJS from 'clipboard';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { faCut } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Toast, PageBanner } from '@edx/paragon';
import messages from './messages';
const DiscountExperimentBanner = (props) => {
const { intl } = props;
const [show, setShow] = useState(true);
const [showToast, setToastShow] = useState(false);
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
const getDiscountText = () => (
<strong>
15% <FormattedMessage
id="top.discount.message.15.off"
defaultMessage="off"
description="Text used with discounts e.g. 15% off"
/>
</strong>
);
return (
<>
<Toast
onClose={() => setToastShow(false)}
show={showToast}
>
{intl.formatMessage(messages['code.copied'])}
</Toast>
<PageBanner
show={show}
dismissible
onDismiss={() => { setShow(false); }}
>
<span className="text-primary-700 small variation2-text-alignment">
<span className="mr-3">
<FormattedMessage
id="top.discount.message.body"
defaultMessage="Get {discount} your first verified certificate* with code"
description="Message body for edX discount"
values={{
discount: getDiscountText(),
}}
/>
</span>
<span className="hover-text dashed-border p-1 d-inline-flex flex-wrap align-items-center">
<span id="edx-welcome" className="font-weight-bold ">EDXWELCOME</span>
<FontAwesomeIcon
className="text-dark-200 copyIcon ml-2 hover-icon"
icon={faCut}
data-clipboard-action="copy"
data-clipboard-target="#edx-welcome"
onClick={() => setToastShow(true)}
/>
</span>
</span>
</PageBanner>
</>
);
};
DiscountExperimentBanner.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(DiscountExperimentBanner);

View File

@@ -1,18 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { Hyperlink, Image } from '@edx/paragon';
import LargeScreenLeftLayout from './LargeLeftLayout';
const LargeLayout = ({ experimentName, isRegistrationPage }) => (
const LargeLayout = () => (
<div className="container row p-0 m-0 large-screen-container">
<div className="col-md-9 p-0 screen-header-primary">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt={getConfig().SITE_NAME} className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
<Image alt="edx" className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<LargeScreenLeftLayout experimentName={experimentName} isRegistrationPage={isRegistrationPage} />
<LargeScreenLeftLayout />
</div>
<div className="col-md-3 p-0 screen-polygon">
<svg
@@ -29,14 +28,4 @@ const LargeLayout = ({ experimentName, isRegistrationPage }) => (
</div>
);
LargeLayout.defaultProps = {
experimentName: '',
isRegistrationPage: false,
};
LargeLayout.propTypes = {
experimentName: PropTypes.string,
isRegistrationPage: PropTypes.bool,
};
export default LargeLayout;

View File

@@ -1,86 +1,30 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import ClipboardJS from 'clipboard';
import { faCut } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Toast } from '@edx/paragon';
import messages from './messages';
import SideDiscountBanner from './SideDiscountBanner';
const LargeLeftLayout = (props) => {
const { intl, isRegistrationPage, experimentName } = props;
const [showToast, setToastShow] = useState(false);
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
const { intl } = props;
return (
<div className="min-vh-100 d-flex justify-content-left align-items-center">
<div className="d-flex pr-0 mt-lg-n2">
<Toast
onClose={() => setToastShow(false)}
show={showToast}
>
{intl.formatMessage(messages['code.copied'])}
</Toast>
<svg className={classNames(
'large-screen-svg-line',
{
'variation1-bar-color mt-n6 pt-0 ml-5': experimentName === 'variation1' && isRegistrationPage,
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
'ml-5': experimentName !== 'variation1' || !isRegistrationPage,
},
)}
>
<line x1="50" y1="0" x2="10" y2="215" />
</svg>
<div className={classNames({ 'pl-4': experimentName === 'variation1' && isRegistrationPage })}>
<h1 className={classNames('large-heading', { 'mb-4.5': experimentName === 'variation1' && isRegistrationPage })}>
{intl.formatMessage(messages['start.learning'])}
<span
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
>
<br />
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
{experimentName === 'variation1' && isRegistrationPage ? (
<span className="text-light-300 dicount-heading">
<span className="lead mr-3">
<SideDiscountBanner />
</span>
<span className="dashed-border d-inline-flex flex-wrap align-items-center">
<span id="edx-welcome" className="text-white edx-welcome font-weight-bold mr-1">EDXWELCOME</span>
<FontAwesomeIcon
className="text-light-700 copyIcon ml-1.5 hover-discount-icon"
icon={faCut}
data-clipboard-action="copy"
data-clipboard-target="#edx-welcome"
onClick={() => setToastShow(true)}
/>
</span>
</span>
) : null}
</div>
</div>
<div className="min-vh-100 pr-0 mt-lg-n2 d-flex align-items-center">
<svg className="large-screen-svg-line ml-5">
<line x1="50" y1="0" x2="10" y2="215" />
</svg>
<h1 className="large-heading">
{intl.formatMessage(messages['start.learning'])}
<span className="text-accent-a"><br />
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
</div>
);
};
LargeLeftLayout.propTypes = {
intl: intlShape.isRequired,
experimentName: PropTypes.string,
isRegistrationPage: PropTypes.bool,
};
LargeLeftLayout.defaultProps = {
experimentName: '',
isRegistrationPage: false,
};
export default injectIntl(LargeLeftLayout);

View File

@@ -1,81 +1,31 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { Hyperlink, Image, Toast } from '@edx/paragon';
import PropTypes from 'prop-types';
import { Hyperlink, Image } from '@edx/paragon';
import ClipboardJS from 'clipboard';
import { faCut } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import messages from './messages';
import SideDiscountBanner from './SideDiscountBanner';
const MediumLayout = (props) => {
const { intl, isRegistrationPage, experimentName } = props;
const [showToast, setToastShow] = useState(false);
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
const { intl } = props;
return (
<div className={classNames(
'container row p-0 mb-3 medium-screen-container',
{
'variation1-medium-screen': experimentName === 'variation1' && isRegistrationPage,
},
)}
>
<Toast
onClose={() => setToastShow(false)}
show={showToast}
>
{intl.formatMessage(messages['code.copied'])}
</Toast>
<div className="container row p-0 mb-3 medium-screen-container">
<div className="col-md-10 p-0 screen-header-primary">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
<Image alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="row mt-4 justify-content-center">
<svg className={classNames(
'medium-screen-svg-line pl-5',
{
'variation1-bar-color': experimentName === 'variation1' && isRegistrationPage,
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
},
)}
>
<svg className="medium-screen-svg-line pl-5">
<line x1="50" y1="0" x2="10" y2="215" />
</svg>
<div className="pb-4">
<h1 className="medium-heading">
{intl.formatMessage(messages['start.learning'])}
<span
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
>
<br />
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
{experimentName === 'variation1' && isRegistrationPage ? (
<div className="text-light-300 pl-3">
<SideDiscountBanner />
<span className="dashed-border h5 text-white">
<span id="edx-welcome" className="edx-welcome">EDXWELCOME </span>
<FontAwesomeIcon
className="text-light-700 copyIcon ml-1 hover-discount-icon"
icon={faCut}
data-clipboard-action="copy"
data-clipboard-target="#edx-welcome"
onClick={() => setToastShow(true)}
/>
</span>
</div>
) : null}
</div>
<h1 className="medium-heading pb-4">
{intl.formatMessage(messages['start.learning'])}
<span className="text-accent-a"><br />
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
</div>
<div />
</div>
<div className="col-md-2 p-0 screen-polygon">
<svg width="100%" height="100%" className="medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
@@ -90,13 +40,6 @@ const MediumLayout = (props) => {
MediumLayout.propTypes = {
intl: intlShape.isRequired,
experimentName: PropTypes.string,
isRegistrationPage: PropTypes.bool,
};
MediumLayout.defaultProps = {
experimentName: '',
isRegistrationPage: false,
};
export default injectIntl(MediumLayout);

View File

@@ -1,38 +0,0 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
export default function SideDiscountBanner() {
const getDiscountText = () => (
<span className="text-accent-a h3">
15% <FormattedMessage
id="side.discount.message.15.off"
defaultMessage="off"
description="Text used with discounts e.g. 15% off"
/>
</span>
);
const getCerificateMsg = () => (
<span className="dicount-heading">
<FormattedMessage
id="certificate.message"
defaultMessage="certificate* with code"
description="Text with certificate"
/>
</span>
);
return (
<span className="mr-1.5">
<FormattedMessage
id="side.discount.message.body"
defaultMessage="Get {discountText} your first verified {lineBreak} {certificateMsg}"
description="Message body for edX discount"
values={{
discountText: getDiscountText(),
lineBreak: <br />,
certificateMsg: getCerificateMsg(),
}}
/>
</span>
);
}

View File

@@ -1,74 +1,31 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { Hyperlink, Image, Toast } from '@edx/paragon';
import { Hyperlink, Image } from '@edx/paragon';
import PropTypes from 'prop-types';
import ClipboardJS from 'clipboard';
import { faCut } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import messages from './messages';
import SideDiscountBanner from './SideDiscountBanner';
const SmallLayout = (props) => {
const { intl, isRegistrationPage, experimentName } = props;
const [showToast, setToastShow] = useState(false);
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
const { intl } = props;
return (
<>
<div className="small-screen-header-primary">
<Toast
onClose={() => setToastShow(false)}
show={showToast}
>
{intl.formatMessage(messages['code.copied'])}
</Toast>
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
<Image alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="d-flex mt-3">
<svg className={classNames(
'small-screen-svg-line',
{
'variation1-bar-color': experimentName === 'variation1' && isRegistrationPage,
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
},
)}
>
<svg className="small-screen-svg-line">
<line x1="55" y1="0" x2="40" y2="65" />
</svg>
<div className="pb-3">
<h1 className="small-heading">
{intl.formatMessage(messages['start.learning'])}
<br />
<span
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
>
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
{(experimentName === 'variation1' && isRegistrationPage) ? (
<div className="small text-light-300 pl-2">
<SideDiscountBanner />
<span className="dashed-border h6 text-white d-inline-flex flex-wrap align-items-center">
<span id="edx-welcome" className="edx-welcome mr-1">EDXWELCOME</span>
<FontAwesomeIcon
className="text-light-700 copyIcon ml-1 hover-discount-icon"
icon={faCut}
data-clipboard-action="copy"
data-clipboard-target="#edx-welcome"
onClick={() => setToastShow(true)}
/>
</span>
</div>
) : null}
</div>
<h1 className="small-heading pb-3">
{intl.formatMessage(messages['start.learning'])}
<br />
<span className="text-accent-a">
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
</div>
</div>
</>
@@ -77,14 +34,6 @@ const SmallLayout = (props) => {
SmallLayout.propTypes = {
intl: intlShape.isRequired,
experimentName: PropTypes.string,
isRegistrationPage: PropTypes.bool,
};
SmallLayout.defaultProps = {
experimentName: '',
isRegistrationPage: false,
};
export default injectIntl(SmallLayout);

View File

@@ -11,11 +11,6 @@ const messages = defineMessages({
defaultMessage: 'with {siteName}',
description: 'Header text with site name for logistration MFE pages',
},
'code.copied': {
id: 'code.copied',
defaultMessage: 'Code copied',
description: 'part of 15% discount code copied',
},
// authenticated user base component text
'complete.your.profile.1': {
id: 'complete.your.profile.1',

View File

@@ -24,9 +24,7 @@ const FormGroup = (props) => {
<Form.Group controlId={props.name} className={props.className} isInvalid={props.errorMessage !== ''}>
<Form.Control
as={props.as}
readOnly={props.readOnly}
type={props.type}
aria-invalid={props.errorMessage !== ''}
className="form-field"
autoComplete={props.autoComplete}
name={props.name}
@@ -67,7 +65,6 @@ FormGroup.defaultProps = {
errorMessage: '',
borderClass: '',
autoComplete: null,
readOnly: false,
handleBlur: null,
handleChange: () => {},
handleFocus: null,
@@ -85,7 +82,6 @@ FormGroup.propTypes = {
errorMessage: PropTypes.string,
borderClass: PropTypes.string,
autoComplete: PropTypes.string,
readOnly: PropTypes.bool,
floatingLabel: PropTypes.string.isRequired,
handleBlur: PropTypes.func,
handleChange: PropTypes.func,

View File

@@ -70,7 +70,7 @@ const LogistrationDefaultProps = {
};
const LogistrationProps = {
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
name: PropTypes.string.isRequried,
loginUrl: PropTypes.string.isRequired,
})),
};

View File

@@ -1,11 +1,9 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { getAuthService } from '@edx/frontend-platform/auth';
import {
Tabs,
Tab,
@@ -26,13 +24,6 @@ const Logistration = (props) => {
const [institutionLogin, setInstitutionLogin] = useState(false);
const [key, setKey] = useState('');
useEffect(() => {
const authService = getAuthService();
if (authService) {
authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL);
}
});
const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
if (typeof e === 'string') {
@@ -61,7 +52,7 @@ const Logistration = (props) => {
);
return (
<BaseComponent isRegistrationPage={selectedPage === REGISTER_PAGE}>
<BaseComponent>
<div>
{institutionLogin
? (

View File

@@ -40,15 +40,15 @@ const PasswordField = (props) => {
const tooltip = (
<Tooltip id={`password-requirement-${placement}`}>
<span id="letter-check" className="d-flex position-relative align-content-start">
{LETTER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
{LETTER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['one.letter'])}
</span>
<span id="number-check" className="d-flex position-relative align-content-start">
{NUMBER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
{NUMBER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['one.number'])}
</span>
<span id="characters-check" className="d-flex position-relative align-content-start">
{props.value.length >= 8 ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
{props.value.length >= 8 ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['eight.characters'])}
</span>
</Tooltip>
@@ -63,7 +63,6 @@ const PasswordField = (props) => {
type={isPasswordHidden ? 'password' : 'text'}
name={props.name}
value={props.value}
aria-invalid={props.errorMessage !== ''}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={props.handleChange}

View File

@@ -42,7 +42,7 @@ function SocialAuthProviders(props) {
</div>
</>
)}
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
<span id="provider-name" className="mr-auto pl-2" aria-hidden="true">{provider.name}</span>
<span className="sr-only">
{referrer === LOGIN_PAGE
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })

View File

@@ -12,9 +12,9 @@ export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, thirdPartyAuthContext) => ({
export const getThirdPartyAuthContextSuccess = (thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, thirdPartyAuthContext },
payload: { thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({

View File

@@ -3,8 +3,6 @@ import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
export const defaultState = {
extendedProfile: [],
fieldDescriptions: {},
thirdPartyAuthApiStatus: null,
};
@@ -18,8 +16,6 @@ const reducer = (state = defaultState, action) => {
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
return {
...state,
extendedProfile: action.payload.fieldDescriptions.extendedProfile,
fieldDescriptions: action.payload.fieldDescriptions.fields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};

View File

@@ -18,10 +18,10 @@ import {
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const { fieldDescriptions, thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
const { thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(getThirdPartyAuthContextSuccess(
fieldDescriptions, thirdPartyAuthContext,
thirdPartyAuthContext,
));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());

View File

@@ -8,13 +8,3 @@ export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);
export const fieldDescriptionSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.fieldDescriptions,
);
export const extendedProfileSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.extendedProfile,
);

View File

@@ -18,11 +18,6 @@ export async function getThirdPartyAuthContext(urlParams) {
throw (e);
});
return {
fieldDescriptions: data.registration_fields || {},
// For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
// and deployed update it to use data.context_data
thirdPartyAuthContext: camelCaseObject(
convertKeyNames(data.context_data || data, { fullname: 'name' }),
),
thirdPartyAuthContext: camelCaseObject(convertKeyNames(data, { fullname: 'name' })),
};
}

View File

@@ -26,7 +26,7 @@ describe('fetchThirdPartyAuthContext', () => {
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data, fieldDescriptions: {} }));
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data }));
const dispatched = [];
await runSaga(
@@ -38,7 +38,7 @@ describe('fetchThirdPartyAuthContext', () => {
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextSuccess({}, data),
actions.getThirdPartyAuthContextSuccess(data),
]);
getThirdPartyAuthContext.mockClear();
});

View File

@@ -74,9 +74,9 @@ describe('PasswordField', () => {
});
passwordField.update();
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon mr-1');
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon mr-1');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon mr-1');
});
it('should update password requirement checks', async () => {

View File

@@ -32,7 +32,7 @@ exports[`SocialAuthProviders should match social auth provider with default icon
</div>
<span
aria-hidden="true"
className="notranslate mr-auto pl-2"
className="mr-auto pl-2"
id="provider-name"
>
Apple
@@ -77,7 +77,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
</div>
<span
aria-hidden="true"
className="notranslate mr-auto pl-2"
className="mr-auto pl-2"
id="provider-name"
>
Apple
@@ -110,7 +110,7 @@ Array [
</div>
<span
aria-hidden="true"
className="notranslate mr-auto pl-2"
className="mr-auto pl-2"
id="provider-name"
>
Apple
@@ -139,7 +139,7 @@ Array [
</div>
<span
aria-hidden="true"
className="notranslate mr-auto pl-2"
className="mr-auto pl-2"
id="provider-name"
>
Facebook

View File

@@ -13,7 +13,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
className="alert-message-content"
>
<p>
You have successfully signed into Google, but your Google account does not have a linked Your Platform Name Here account. To link your accounts, sign in now using your Your Platform Name Here password.
You have successfully signed into Google, but your Google account does not have a linked edX account. To link your accounts, sign in now using your edX password.
</p>
</div>
</div>
@@ -38,7 +38,7 @@ exports[`ThirdPartyAuthAlert should match register page third party auth alert m
Almost done!
</div>
<p>
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
You've successfully signed into Google! We just need a little more information before you start learning with edX.
</p>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { getConfig } from '@edx/frontend-platform';
import { applyMiddleware, createStore, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from '@redux-devtools/extension';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';

View File

@@ -19,7 +19,6 @@ export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
export const DEFAULT_STATE = 'default';
export const PENDING_STATE = 'pending';
export const COMPLETE_STATE = 'complete';
export const FAILURE_STATE = 'failure';
// Regex
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
@@ -28,8 +27,7 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
export const LETTER_REGEX = /[a-zA-Z]/;
export const NUMBER_REGEX = /\d/;
export const VALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
// Query string parameters that can be passed to LMS to manage
// things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free'];
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next'];

View File

@@ -1,24 +0,0 @@
import { useState, useEffect } from 'react';
import { breakpoints } from '@edx/paragon';
/**
* A react hook used to determine if the current window is mobile or not.
* returns true if the window is of mobile size.
* Code source: https://github.com/edx/prospectus/blob/master/src/utils/useMobileResponsive.js
*/
const useMobileResponsive = (breakpoint) => {
const [isMobileWindow, setIsMobileWindow] = useState();
const checkForMobile = () => {
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint || breakpoints.small.maxWidth}px)`).matches);
};
useEffect(() => {
checkForMobile();
window.addEventListener('resize', checkForMobile);
// return this function here to clean up the event listener
return () => window.removeEventListener('resize', checkForMobile);
}, []);
return isMobileWindow;
};
export default useMobileResponsive;

View File

@@ -1,148 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form, Icon } from '@edx/paragon';
import { ExpandMore } from '@edx/paragon/icons';
const FormFieldRenderer = (props) => {
let formField = null;
const {
errorMessage, fieldData, onChangeHandler, isRequired, value,
} = props;
const handleFocus = (e) => {
if (props.handleFocus) { props.handleFocus(e); }
};
const handleOnBlur = (e) => {
if (props.handleBlur) { props.handleBlur(e); }
};
switch (fieldData.type) {
case 'select': {
if (!fieldData.options) {
return null;
}
formField = (
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
<Form.Control
as="select"
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={fieldData.label}
onBlur={handleOnBlur}
onFocus={handleFocus}
>
<option key="default" value="">{fieldData.label}</option>
{fieldData.options.map(option => (
<option className="data-hj-suppress" key={option[0]} value={option[0]}>{option[1]}</option>
))}
</Form.Control>
{isRequired && errorMessage && (
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</Form.Group>
);
break;
}
case 'textarea': {
formField = (
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
<Form.Control
as="textarea"
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
onChange={(e) => onChangeHandler(e)}
floatingLabel={fieldData.label}
onBlur={handleOnBlur}
onFocus={handleFocus}
/>
{isRequired && errorMessage && (
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</Form.Group>
);
break;
}
case 'text': {
formField = (
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
<Form.Control
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
onChange={(e) => onChangeHandler(e)}
floatingLabel={fieldData.label}
onBlur={handleOnBlur}
onFocus={handleFocus}
/>
{isRequired && errorMessage && (
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</Form.Group>
);
break;
}
case 'checkbox': {
formField = (
<Form.Group isInvalid={isRequired && errorMessage}>
<Form.Checkbox
id={fieldData.name}
checked={!!value}
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
onChange={(e) => onChangeHandler(e)}
onBlur={handleOnBlur}
onFocus={handleFocus}
>
{fieldData.label}
</Form.Checkbox>
{isRequired && errorMessage && (
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</Form.Group>
);
break;
}
default:
break;
}
return formField;
};
FormFieldRenderer.defaultProps = {
value: '',
handleBlur: null,
handleFocus: null,
errorMessage: '',
isRequired: false,
};
FormFieldRenderer.propTypes = {
fieldData: PropTypes.shape({
type: PropTypes.string,
label: PropTypes.string,
name: PropTypes.string,
}).isRequired,
onChangeHandler: PropTypes.func.isRequired,
handleBlur: PropTypes.func,
handleFocus: PropTypes.func,
errorMessage: PropTypes.string,
isRequired: PropTypes.bool,
value: PropTypes.string,
};
export default FormFieldRenderer;

View File

@@ -1 +0,0 @@
export { default } from './FieldRenderer';

View File

@@ -1,197 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import FieldRenderer from '../FieldRenderer';
describe('FieldRendererTests', () => {
let value = '';
const changeHandler = (e) => {
if (e.target.type === 'checkbox') {
value = e.target.checked;
} else {
value = e.target.value;
}
};
beforeEach(() => {
value = '';
});
it('should render select field type', () => {
const fieldData = {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
options: [['1997', 1997], ['1998', 1998]],
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('select#yob-field');
field.simulate('change', { target: { value: 1997 } });
expect(field.type()).toEqual('select');
expect(fieldRenderer.find('label').text()).toEqual('Year of Birth');
expect(value).toEqual(1997);
});
it('should return null if no options are provided for select field', () => {
const fieldData = {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
});
it('should render textarea field', () => {
const fieldData = {
type: 'textarea',
label: 'Why do you want to join this platform?',
name: 'goals-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#goals-field').last();
field.simulate('change', { target: { value: 'These are my goals.' } });
expect(field.type()).toEqual('textarea');
expect(fieldRenderer.find('label').text()).toEqual('Why do you want to join this platform?');
expect(value).toEqual('These are my goals.');
});
it('should render an input field', () => {
const fieldData = {
type: 'text',
label: 'Company',
name: 'company-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#company-field').last();
field.simulate('change', { target: { value: 'ABC' } });
expect(field.type()).toEqual('input');
expect(fieldRenderer.find('label').text()).toEqual('Company');
expect(value).toEqual('ABC');
});
it('should render checkbox field', () => {
const fieldData = {
type: 'checkbox',
label: 'I agree that edX may send me marketing messages.',
name: 'marketing-emails-opt-in-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('input#marketing-emails-opt-in-field');
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(field.prop('type')).toEqual('checkbox');
expect(fieldRenderer.find('label').text()).toEqual('I agree that edX may send me marketing messages.');
expect(value).toEqual(true);
});
it('should return null if field type is unknown', () => {
const fieldData = {
type: 'unknown',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
});
it('should run onBlur and onFocus functions for a field if given', () => {
const fieldData = { type: 'text', label: 'Test', name: 'test-field' };
let functionValue = '';
const onBlur = (e) => {
functionValue = `${e.target.name} blurred`;
};
const onFocus = (e) => {
functionValue = `${e.target.name} focussed`;
};
const fieldRenderer = mount(
<FieldRenderer
handleFocus={onFocus}
handleBlur={onBlur}
value={value}
fieldData={fieldData}
onChangeHandler={changeHandler}
/>,
);
const field = fieldRenderer.find('#test-field').last();
field.simulate('focus');
expect(functionValue).toEqual('test-field focussed');
field.simulate('blur');
expect(functionValue).toEqual('test-field blurred');
});
it('should render error message for required text fields', () => {
const fieldData = { type: 'text', label: 'First Name', name: 'first-name-field' };
const fieldRenderer = mount(
<FieldRenderer
isRequired
fieldData={fieldData}
onChangeHandler={changeHandler}
errorMessage="Enter your first name"
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Enter your first name');
});
it('should render error message for required select fields', () => {
const fieldData = {
type: 'select', label: 'Preference', name: 'preference-field', options: [['a', 'Opt 1'], ['b', 'Opt 2']],
};
const fieldRenderer = mount(
<FieldRenderer
isRequired
fieldData={fieldData}
onChangeHandler={changeHandler}
errorMessage="Select your preference"
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Select your preference');
});
it('should render error message for required textarea fields', () => {
const fieldData = { type: 'textarea', label: 'Goals', name: 'goals-field' };
const fieldRenderer = mount(
<FieldRenderer
isRequired
fieldData={fieldData}
onChangeHandler={changeHandler}
errorMessage="Tell us your goals"
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Tell us your goals');
});
it('should render error message for required checkbox fields', () => {
const fieldData = { type: 'checkbox', label: 'Honor Code', name: 'honor-code-field' };
const fieldRenderer = mount(
<FieldRenderer
isRequired
fieldData={fieldData}
onChangeHandler={changeHandler}
errorMessage="You must agree to our Honor Code"
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('You must agree to our Honor Code');
});
});

View File

@@ -97,11 +97,11 @@ const ForgotPasswordPage = (props) => {
{ siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
<Form className="mw-xs">
<ForgotPasswordAlert email={props.email} emailError={errors.email} status={status} />
<h2 className="h4">
<h4>
{intl.formatMessage(messages['forgot.password.page.heading'])}
</h2>
</h4>
<p className="mb-4">
{intl.formatMessage(messages['forgot.password.page.instructions'])}
</p>
@@ -116,8 +116,6 @@ const ForgotPasswordPage = (props) => {
helpText={[intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
/>
<StatefulButton
id="submit-forget-password"
name="submit-forget-password"
type="submit"
variant="brand"
className="login-button-width"
@@ -131,7 +129,6 @@ const ForgotPasswordPage = (props) => {
/>
<Hyperlink
id="forgot-password"
name="forgot-password"
className="ml-4 font-weight-500 text-body"
destination={supportUrl}
onClick={e => {

View File

@@ -4,10 +4,6 @@ import caMessages from './messages/ca.json';
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import zhcnMessages from './messages/zh_CN.json';
import ititMessages from './messages/it_IT.json';
import ptptMessages from './messages/pt_PT.json';
import dedeMessages from './messages/de_DE.json';
import hiMessages from './messages/hi.json';
import heMessages from './messages/he.json';
import idMessages from './messages/id.json';
import kokrMessages from './messages/ko_kr.json';
@@ -23,9 +19,6 @@ const messages = {
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
'it-it': ititMessages,
'pt-pt': ptptMessages,
'de-de': dedeMessages,
ca: caMessages,
he: heMessages,
id: idMessages,
@@ -35,7 +28,6 @@ const messages = {
ru: ruMessages,
th: thMessages,
uk: ukMessages,
hi: hiMessages,
};
export default messages;

View File

@@ -1,15 +1,9 @@
{
"top.discount.message.15.off": "off",
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"code.copied": "Code copied",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"side.discount.message.15.off": "off",
"certificate.message": "certificate* with code",
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Check your email",
"forgot.password.confirmation.support.link": "contact technical support",
@@ -96,13 +90,9 @@
"sign.in.heading": "Sign in",
"account.activation.success.message.title": "Success! You have activated your account.",
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
"account.activation.info.message": "This account has already been activated.",
"account.already.activated.message": "This account has already been activated.",
"account.activation.error.message.title": "Your account could not be activated",
"account.activation.support.link": "contact support",
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
"account.confirmation.success.message": "Sign in to continue.",
"account.confirmation.info.message": "This email has already been confirmed.",
"account.confirmation.error.message.title": "Your email could not be confirmed",
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
"login.failure.header.title": "We couldn't sign you in.",
"contact.support.link": "contact {platformName} support",
@@ -112,40 +102,28 @@
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"create.account.button": "Create an account",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.username.field.error": "Username must be between 2 and 30 characters",
"empty.password.field.error": "Password criteria has not been met",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Enter a valid email address",
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
"username.validation.message": "Username must be between 2 and 30 characters",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-).",
"support.education.research": "Support education research by providing additional information. (Optional)",
"registration.request.failure.header": "We couldn't create your account.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
@@ -154,8 +132,6 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
@@ -172,11 +148,10 @@
"registration.field.education.levels.el": "Elementary/primary school",
"registration.field.education.levels.none": "No formal education",
"registration.field.education.levels.other": "Other education",
"registration.username.suggestion.label": "Suggested:",
"registration.username.suggestion.label": "Available:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",

View File

@@ -1,224 +0,0 @@
{
"top.discount.message.15.off": "off",
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"code.copied": "Code copied",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"side.discount.message.15.off": "off",
"certificate.message": "certificate* with code",
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Check your email",
"forgot.password.confirmation.support.link": "contact technical support",
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
"sso.sign.in.with": "Sign in with {providerName}",
"sso.create.account.using": "Create account using {providerName}",
"show.password": "Show password",
"hide.password": "Hide password",
"one.letter": "1 Letter",
"one.number": "1 Number",
"eight.characters": "8 Characters",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
"tpa.alert.heading": "Almost done!",
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": "Forgot Password | {siteName}",
"forgot.password.page.heading": "Reset password",
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
"forgot.password.page.invalid.email.message": "Enter a valid email address",
"forgot.password.page.email.field.label": "Email",
"forgot.password.page.submit.button": "Submit",
"forgot.password.error.alert.title.": "We were unable to contact you.",
"forgot.password.error.message.title": "An error occurred.",
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
"forgot.password.empty.email.field.error": "Enter your email",
"forgot.password.invalid.email": "An error occurred.",
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
"confirmation.message.title": "Check your email",
"confirmation.support.link": "contact technical support",
"need.help.sign.in.text": "Need help signing in?",
"additional.help.text": "For additional help, contact edX support at ",
"sign.in.text": "Sign in",
"extend.field.errors": "{emailError} below.",
"invalid.token.heading": "Invalid password reset link",
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
"token.validation.rate.limit.error.heading": "Too many requests",
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"token.validation.internal.sever.error.heading": "Token validation failure",
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
"login.page.title": "Login | {siteName}",
"login.user.identity.label": "Username or email",
"login.password.label": "Password",
"sign.in.button": "Sign in",
"sign.in.btn.pending.state": "Loading",
"need.help.signing.in.collapsible.menu": "Need help signing in?",
"forgot.password.link": "Forgot my password",
"forgot.password": "Forgot password",
"other.sign.in.issues": "Other sign in issues",
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Sign in with institution/campus credentials",
"institution.login.page.back.button": "Back to sign in",
"create.an.account": "Create an account",
"login.other.options.heading": "Or sign in with:",
"non.compliant.password.title": "We recently changed our password requirements",
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"first.time.here": "First time here?",
"email.help.message": "The email address you used to register with edX.",
"enterprise.login.btn.text": "Company or school credentials",
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"register.link": "Create an account",
"sign.in.heading": "Sign in",
"account.activation.success.message.title": "Success! You have activated your account.",
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
"account.activation.info.message": "This account has already been activated.",
"account.activation.error.message.title": "Your account could not be activated",
"account.activation.support.link": "contact support",
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
"account.confirmation.success.message": "Sign in to continue.",
"account.confirmation.info.message": "This email has already been confirmed.",
"account.confirmation.error.message.title": "Your email could not be confirmed",
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
"login.failure.header.title": "We couldn't sign you in.",
"contact.support.link": "contact {platformName} support",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
"login.locked.out.error.message": "To protect your account, its been temporarily locked. Try again in {lockedOutPeriod} minutes.",
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.username.field.error": "Username must be between 2 and 30 characters",
"empty.password.field.error": "Password criteria has not been met",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Enter a valid email address",
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
"username.validation.message": "Username must be between 2 and 30 characters",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
"support.education.research": "Support education research by providing additional information. (Optional)",
"registration.request.failure.header": "We couldn't create your account.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
"registration.field.gender.options.f": "Female",
"registration.field.gender.options.m": "Male",
"registration.field.gender.options.o": "Other/Prefer not to say",
"registration.field.education.levels.label": "Highest level of education completed (optional)",
"registration.field.education.levels.p": "Doctorate",
"registration.field.education.levels.m": "Master's or professional degree",
"registration.field.education.levels.b": "Bachelor's degree",
"registration.field.education.levels.a": "Associate's degree",
"registration.field.education.levels.hs": "Secondary/high school",
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
"registration.field.education.levels.el": "Elementary/primary school",
"registration.field.education.levels.none": "No formal education",
"registration.field.education.levels.other": "Other education",
"registration.username.suggestion.label": "Suggested:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Enter and confirm your new password.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"forgot.password.confirmation.sign.in.link": "sign in",
"reset.password.request.forgot.password.text": "Forgot password",
"reset.password.request.invalid.token.header": "Invalid password reset link",
"reset.password.empty.new.password.field.error": "Please enter your new password.",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"reset.password.request.server.error": "Failed to reset password",
"reset.password.token.validation.sever.error": "Token validation failure",
"reset.server.rate.limit.error": "Too many requests.",
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"progressive.profiling.page.title": "Optional Fields | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"gender.options.label": "Gender (optional)",
"gender.options.f": "Female",
"gender.options.m": "Male",
"gender.options.o": "Other/Prefer not to say",
"education.levels.label": "Highest level of education completed (optional)",
"education.levels.p": "Doctorate",
"education.levels.m": "Master's or professional degree",
"education.levels.b": "Bachelor's degree",
"education.levels.a": "Associate's degree",
"education.levels.hs": "Secondary/high school",
"education.levels.jhs": "Junior secondary/junior high/middle school",
"education.levels.el": "Elementary/primary school",
"education.levels.none": "No formal education",
"education.levels.other": "Other education",
"year.of.birth.label": "Year of birth (optional)",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"continue.to.platform": "Continue to {platformName}",
"modal.title": "Thanks for letting us know.",
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
"welcome.page.error.heading": "We couldn't update your profile",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time."
}

View File

@@ -1,15 +1,9 @@
{
"top.discount.message.15.off": "de descuento",
"top.discount.message.body": "Obtén un {discount} en tu primer certificado* con un código",
"start.learning": "Empieza a aprender",
"with.site.name": "con {siteName}",
"code.copied": "Código copiado",
"complete.your.profile.1": "Completado",
"complete.your.profile.2": "tu perfil ",
"welcome.to.platform": "¡Bienvenido a {siteName}, {username}!",
"side.discount.message.15.off": "de descuento",
"certificate.message": "Certificado* con código",
"side.discount.message.body": "Obtén {discountText} tu primer {lineBreak} {certificateMsg} verificado",
"institution.login.page.sub.heading": "Selecciona tu institución de la lista siguiente",
"forgot.password.confirmation.title": "Verifica tu correo electrónico",
"forgot.password.confirmation.support.link": "contacta con el equipo de soporte técnico",
@@ -96,13 +90,9 @@
"sign.in.heading": "Iniciar sesión",
"account.activation.success.message.title": "Ha sido un éxito. Has activado tu cuenta.",
"account.activation.success.message": "Ahora recibirás por correo electrónico actualizaciones y alertas relacionadas con los cursos en los que estás inscrito. Inicia sesión para continuar.",
"account.activation.info.message": "La cuenta ya ha sido activada.",
"account.already.activated.message": "La cuenta ya ha sido activada.",
"account.activation.error.message.title": "Tu cuenta no ha podido ser activada",
"account.activation.support.link": "contacta al equipo de soporte de edX",
"account.confirmation.success.message.title": "¡Éxito! Has confirmado tu correo electrónico.",
"account.confirmation.success.message": "Inicia sesión para continuar.",
"account.confirmation.info.message": "Este correo electrónico ya ha sido confirmado.",
"account.confirmation.error.message.title": "Tu correo electrónico no pudo ser confirmado",
"login.rate.limit.reached.message": "Demasiados intentos fallidos de inicio de sesión. Inténtelo de nuevo más tarde.",
"login.failure.header.title": "No se ha podido iniciar tu sesión.",
"contact.support.link": "entrar en contacto con el soporte de {platformName}",
@@ -112,40 +102,28 @@
"login.form.invalid.error.message": "Por favor, complete los siguientes campos.",
"login.incorrect.credentials.error.reset.link.text": "restablecer la contraseña",
"login.incorrect.credentials.error.before.account.blocked.text": "Pulse aquí para restablecerla.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, aceptas el {tosAndHonorCode} y reconoces que {platformName} y cada\n Miembro procesa tus datos personales de acuerdo con la {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Nombre completo",
"registration.email.label": "Correo electrónico",
"registration.username.label": "Nombre de usuario público",
"registration.password.label": "Contraseña",
"registration.country.label": "País/Región",
"registration.opt.in.label": "Acepto que {siteName} pueda enviarme mensajes de marketing.",
"help.text.name": "Este nombre será utilizado por los certificados que obtengas.",
"help.text.username.1": "El nombre que te identificará en tus cursos.",
"help.text.username.2": "Esto no puede modificarse posteriormente.",
"help.text.email": "Para la activación de la cuenta y las actualizaciones importantes",
"create.account.for.free.button": "Crea una cuenta gratis",
"create.account.button": "Crear una cuenta",
"create.an.account.btn.pending.state": "Cargando",
"registration.other.options.heading": "O regístrese con:",
"register.institution.login.button": "Credenciales de la institución/campus",
"register.institution.login.page.title": "Registro con credenciales de la institución/campus",
"empty.name.field.error": "Introduce tu nombre completo",
"empty.email.field.error": "Introduce tu email",
"empty.username.field.error": "El nombre de usuario debe tener entre 2 y 30 caracteres",
"empty.password.field.error": "No se han cumplido los criterios de la contraseña",
"empty.country.field.error": "Selecciona tu país o región de residencia",
"email.invalid.format.error": "Introduce una dirección de correo electrónico válida",
"email.ratelimit.less.chars.validation.message": "El correo electrónico debe tener 3 caracteres.",
"username.validation.message": "El nombre de usuario debe tener entre 2 y 30 caracteres",
"name.validation.message": "Introduce un nombre válido",
"username.format.validation.message": "Los nombres de usuario solo pueden contener letras (A-Z, a-z), números (0-9), guiones bajos (_) y guiones (-). Los nombres de usuario no pueden contener espacios.",
"username.format.validation.message": "Los nombres de usuario únicamente pueden contener las letras (A-Z, a-z), números (0-9), guión bajo (_) y guiones (-).",
"support.education.research": "Apoya la investigación sobre educación proporcionando información adicional. (Opcional)",
"registration.request.failure.header": "No pudimos crear tu cuenta.",
"registration.empty.form.submission.error": "Por favor, verifica tus respuestas y vuelve a intentarlo.",
@@ -154,8 +132,6 @@
"registration.tpa.session.expired": "Inscripción usando {provider} ha expirado.",
"terms.of.service.and.honor.code": "Condiciones de servicio y código de honor",
"privacy.policy": "Política de privacidad ",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Año de nacimiento (opcional)",
"registration.field.gender.options.label": "Género (opcional)",
"registration.goals.label": "Díganos por qué estás interesado en edX (opcional)",
@@ -172,11 +148,10 @@
"registration.field.education.levels.el": "Enseñanza primaria",
"registration.field.education.levels.none": "Ninguna educación formal",
"registration.field.education.levels.other": "Otra educación",
"registration.username.suggestion.label": "Se recomienda:",
"registration.username.suggestion.label": "Disponible:",
"registration.using.tpa.form.heading": "Termina de crear tu cuenta",
"did.you.mean.alert.text": "¿Quieres decir",
"certificate.msg": "*No se incluyen los programas de: MicroMasters en Analytics: Essential Tools and Methods de GTx, Certificación Profesional de Corporate Finance de ColumbiaX o cursos o programas ofrecidos por Wharton y NYIF en esta oferta.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, aceptas el {tosAndHonorCode} y reconoces que {platformName} y cada\n Miembro procesa tus datos personales de acuerdo con la {privacyPolicy}.",
"sign.in": "Iniciar sesión",
"reset.password.page.title": "Restablecer contraseña | {siteName}",
"reset.password": "Restablecer mi contraseña",

View File

@@ -1,224 +1,199 @@
{
"top.discount.message.15.off": "arrêt",
"top.discount.message.body": "Obtenez {discount} votre première attestation vérifiée* avec ce code",
"start.learning": "Démarrer l'apprentissage",
"with.site.name": "avec {siteName}",
"code.copied": "Code copié",
"complete.your.profile.1": "Terminé",
"complete.your.profile.2": "votre profil",
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
"side.discount.message.15.off": "arrêt",
"certificate.message": "attestation* avec code",
"side.discount.message.body": "Obtenez {discountText} votre première {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Sélectionner votre institution dans la liste ci-dessous",
"forgot.password.confirmation.title": "Vérifiez votre email",
"forgot.password.confirmation.support.link": "contacter le support technique",
"forgot.password.confirmation.info": "Si vous ne recevez pas de message de réinitialisation de mot de passe après 1 minute, vérifiez que vous avez entré la bonne adresse courriel ou vérifiez votre dossier de pourriels.",
"logistration.sign.in": "Connectez-vous",
"logistration.register": "S'inscrire",
"internal.server.error.message": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
"server.ratelimit.error.message": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps.",
"enterprisetpa.title.heading": "Souhaitez-vous vous connecter à l'aide de vos identifiants {providerName} ?",
"enterprisetpa.sso.button.title": "Connectez-vous avec {providerName}",
"enterprisetpa.login.button.text": "Montrez-moi d'autres méthodes pour me connecter ou m'inscrire",
"sso.sign.in.with": "Connectez-vous avec {providerName}",
"sso.create.account.using": "Créer un compte avec {providerName}",
"show.password": "Afficher le mot de passe",
"hide.password": "Cacher le mot de passe ",
"one.letter": "1 lettre",
"one.number": "1 nombre",
"eight.characters": "8 caractères",
"password.sr.only.helping.text": "Le mot de passe doit contenir au moins 8 caractères, au moins une lettre et au moins un chiffre",
"tpa.alert.heading": "Presque fini !",
"login.third.party.auth.account.not.linked": "Vous vous êtes connecté avec succès à {currentProvider}, mais votre compte {currentProvider} n'a pas de compte relié à {platformName}. Pour lier vos comptes, connectez-vous en utilisant votre mot de passe {platformName}.",
"register.third.party.auth.account.not.linked": "Vous vous êtes connecté avec succès à {currentProvider} ! Nous avons juste besoin d'un peu plus d'informations avant que vous commenciez à apprendre avec {platformName}.",
"error.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
"forgot.password.confirmation.message": "Nous avons envoyé un courriel à {email} avec des instructions pour réinitialiser votre mot de passe.\n Si vous ne recevez pas de message de réinitialisation de mot de passe après 1 minute, vérifiez que vous avez saisi\nl'adresse courriel correctement, ou vérifiez votre dossier de courriel indésirable. Si vous avez besoin d'aide supplémentaire, {supportLink}.",
"forgot.password.page.title": " Mot de passe oublié | {siteName}",
"forgot.password.page.heading": "Réinitialiser le mot de passe",
"forgot.password.page.instructions": "Veuillez entrer votre adresse courriel ci-dessous et nous vous enverrons un courriel avec les instructions pour réinitialiser votre mot de passe.",
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Check your email",
"forgot.password.confirmation.support.link": "contact technical support",
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
"sso.sign.in.with": "Sign in with {providerName}",
"sso.create.account.using": "Create account using {providerName}",
"show.password": "Show password",
"hide.password": "Hide password",
"one.letter": "1 Letter",
"one.number": "1 Number",
"eight.characters": "8 Characters",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
"tpa.alert.heading": "Almost done!",
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": "Forgot Password | {siteName}",
"forgot.password.page.heading": "Reset password",
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
"forgot.password.page.invalid.email.message": "Enter a valid email address",
"forgot.password.page.email.field.label": "Email",
"forgot.password.page.submit.button": "Envoyez",
"forgot.password.error.alert.title.": "Nous n'avons pas pu vous contacter.",
"forgot.password.error.message.title": "Une erreur est survenue.",
"forgot.password.page.submit.button": "Submit",
"forgot.password.error.alert.title.": "We were unable to contact you.",
"forgot.password.error.message.title": "An error occurred.",
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
"forgot.password.empty.email.field.error": "Saisissez votre courriel",
"forgot.password.invalid.email": "Une erreur est survenue.",
"forgot.password.invalid.email.message": "L'adresse email que vous avez fournie est incorrecte.",
"forgot.password.email.help.text": "L'adresse courriel que vous avez utilisée pour vous inscrire sur {platformName}",
"confirmation.message.title": "Vérifiez votre email",
"confirmation.support.link": "contacter le support technique",
"need.help.sign.in.text": "Besoin d'aide pour vous enregistrer?",
"additional.help.text": "Pour une aide supplémentaire, contactez le support edX à ",
"sign.in.text": "Connectez-vous",
"extend.field.errors": "{emailError} ci-dessous.",
"invalid.token.heading": "Lien de réinitialisation du mot de passe non valide",
"invalid.token.error.message": "Ce lien de réinitialisation de mot de passe n'est pas valide. Il a peut-être déjà été utilisé. Entrez votre courriel ci-dessous pour recevoir un nouveau lien.",
"token.validation.rate.limit.error.heading": "Trop de demandes",
"token.validation.rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps.",
"token.validation.internal.sever.error.heading": "Échec de la validation du jeton",
"token.validation.internal.sever.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
"internal.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps.",
"account.activation.error.message": "Une erreur s'est produite, veuillez {supportLink} pour résoudre ce problème.",
"login.inactive.user.error": "Pour vous connecter, vous devez activer votre compte.{lineBreak}\n {lineBreak}Nous venons d'envoyer un lien d'activation à {email}. Si vous ne recevez pas de courriel,\n vérifiez vos dossiers de spam ou {supportLink}.",
"login.incorrect.credentials.error.attempts.text.1": "Le nom d'utilisateur, le courriel ou le mot de passe que vous avez entré est incorrect. Vous avez {remainingAttempts} tentatives\n de connexion avant que votre compte soit temporairement verrouillé.",
"login.incorrect.credentials.error.attempts.text.2": "Si vous avez oublié votre mot de passe, {resetLink}",
"account.locked.out.message.2": "Par mesure de sécurité, vous pouvez {resetLink} avant de réessayer.",
"login.incorrect.credentials.error.with.reset.link": "Le nom d'utilisateur, l'adresse courriel ou le mot de passe que vous avez saisis sont incorrects. Veuillez réessayer ou {resetLink}.",
"login.page.title": "Connexion | {siteName}",
"login.user.identity.label": "Nom d'utilisateur ou courriel",
"login.password.label": "Mot de passe",
"sign.in.button": "Connectez-vous",
"sign.in.btn.pending.state": "Chargement en cours",
"need.help.signing.in.collapsible.menu": "Besoin d'aide pour vous enregistrer?",
"forgot.password.link": "J'ai oublié mon mot de passe",
"forgot.password": "Mot de passe oublié",
"other.sign.in.issues": "Autres problèmes de connexion",
"need.other.help.signing.in.collapsible.menu": "Encore besoin d'aide pour vous enregistrer?",
"institution.login.button": "Identifiants de l'établissement/du campus",
"institution.login.page.title": "Connectez vous avec les crédentiels d'institution ou de campus",
"institution.login.page.back.button": "Retour à la connexion",
"create.an.account": "Créer un compte",
"login.other.options.heading": "Ou se connecter avec :",
"non.compliant.password.title": "Nous avons récemment modifié nos exigences en matière de mot de passe",
"non.compliant.password.message": "Votre mot de passe actuel ne répond pas aux nouvelles exigences de sécurité. Nous venons d'envoyer un message de réinitialisation de mot de passe à l'adresse courriel associée à ce compte. Merci de nous aider à protéger vos données.",
"account.locked.out.message.1": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans 30 minutes.",
"first.time.here": "C'est votre première visite ?",
"email.help.message": "L'adresse électronique que vous avez utilisée pour vous inscrire à edX.",
"enterprise.login.btn.text": "Identifiants de la compagnie ou de l'école",
"email.format.validation.message": "L'adresse email que vous avez fournie est incorrecte.",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 3 caractères.",
"email.validation.message": "Entrez votre nom d'utilisateur ou votre adresse courriel",
"password.validation.message": "Les critères de mot de passe n'ont pas été remplis",
"register.link": "Créer un compte",
"sign.in.heading": "Connectez-vous",
"account.activation.success.message.title": "Succès! Vous avez activé votre compte.",
"account.activation.success.message": "Vous recevrez maintenant des mises à jour par courriel et des alertes de notre part concernant les cours auxquels vous êtes inscrit. Connectez-vous pour continuer.",
"account.activation.info.message": "Ce compte a déjà été activé.",
"account.activation.error.message.title": "Votre compte n'a pas pu être activé",
"account.activation.support.link": "contacter le support",
"account.confirmation.success.message.title": "Succès ! Vous avez confirmé votre courriel.",
"account.confirmation.success.message": "Se connecter pour continuer.",
"account.confirmation.info.message": "Ce courriel a déjà été confirmé.",
"account.confirmation.error.message.title": "Votre courriel ne peut pas être confirmé.",
"login.rate.limit.reached.message": "Trop de tentatives de connexion échouées. Réessayez plus tard.",
"login.failure.header.title": "Nous n'avons pas pu vous connecter.",
"contact.support.link": "veuillez contacter le support {platformName}",
"login.incorrect.credentials.error": "Le nom d'utilisateur, l'adresse courriel ou le mot de passe que vous avez saisis sont incorrects. Veuillez réessayer.",
"login.failed.attempt.error": "Il vous reste {remainingAttempts} tentatives de connexion supplémentaires avant que votre compte ne soit temporairement verrouillé.",
"login.locked.out.error.message": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans {lockedOutPeriod} minutes.",
"login.form.invalid.error.message": "Veuillez remplir les champs ci-dessous.",
"login.incorrect.credentials.error.reset.link.text": "réinitialiser votre mot de passe",
"login.incorrect.credentials.error.before.account.blocked.text": "cliquez ici pour le réinitialiser.",
"password.security.nudge.title": "Sécurité du mot de passe",
"password.security.block.title": "Changement de mot de passe requis",
"password.security.nudge.body": "Notre système a détecté que votre mot de passe est vulnérable. Nous vous recommandons de le modifier afin que votre compte reste sécurisé.",
"password.security.block.body": "Notre système a détecté que votre mot de passe est vulnérable. Changez votre mot de passe afin que votre compte reste sécurisé.",
"password.security.close.button": "Fermer",
"password.security.redirect.to.reset.password.button": "Réinitialiser votre mot de passe",
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque\n membre peut traiter vos données personnelles conformément à la {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "S'inscrire | {siteName}",
"registration.fullname.label": "Nom complet",
"forgot.password.empty.email.field.error": "Enter your email",
"forgot.password.invalid.email": "An error occurred.",
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
"confirmation.message.title": "Check your email",
"confirmation.support.link": "contact technical support",
"need.help.sign.in.text": "Need help signing in?",
"additional.help.text": "For additional help, contact edX support at ",
"sign.in.text": "Sign in",
"extend.field.errors": "{emailError} below.",
"invalid.token.heading": "Invalid password reset link",
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
"token.validation.rate.limit.error.heading": "Too many requests",
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"token.validation.internal.sever.error.heading": "Token validation failure",
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
"login.page.title": "Login | {siteName}",
"login.user.identity.label": "Username or email",
"login.password.label": "Password",
"sign.in.button": "Sign in",
"sign.in.btn.pending.state": "Loading",
"need.help.signing.in.collapsible.menu": "Need help signing in?",
"forgot.password.link": "Forgot my password",
"forgot.password": "Forgot password",
"other.sign.in.issues": "Other sign in issues",
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Sign in with institution/campus credentials",
"institution.login.page.back.button": "Back to sign in",
"create.an.account": "Create an account",
"login.other.options.heading": "Or sign in with:",
"non.compliant.password.title": "We recently changed our password requirements",
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"first.time.here": "First time here?",
"email.help.message": "The email address you used to register with edX.",
"enterprise.login.btn.text": "Company or school credentials",
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"register.link": "Create an account",
"sign.in.heading": "Sign in",
"account.activation.success.message.title": "Success! You have activated your account.",
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
"account.already.activated.message": "This account has already been activated.",
"account.activation.error.message.title": "Your account could not be activated",
"account.activation.support.link": "contact support",
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
"login.failure.header.title": "We couldn't sign you in.",
"contact.support.link": "contact {platformName} support",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
"login.locked.out.error.message": "To protect your account, its been temporarily locked. Try again in {lockedOutPeriod} minutes.",
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Nom d'utilisateur public",
"registration.password.label": "Mot de passe",
"registration.country.label": "Pays/Région",
"registration.opt.in.label": "{siteName} peux m'envoyer des messages de marketing.",
"help.text.name": "Ce nom sera utilisé pour toutes les attestations que vous obtiendrez.",
"help.text.username.1": "Le nom qui vous identifiera dans vos cours.",
"help.text.username.2": "Cela ne peut pas être modifié ultérieurement.",
"help.text.email": "Pour l'activation du compte et les mises à jour importantes",
"create.account.for.free.button": "Créer un compte gratuitement",
"create.an.account.btn.pending.state": "Chargement en cours",
"registration.other.options.heading": "Ou inscrivez-vous avec :",
"register.institution.login.button": "Identifiants de l'établissement/du campus",
"register.institution.login.page.title": "Inscription avec les crédentiels d'institution ou de campus",
"empty.name.field.error": "Saisissez votre nom complet",
"empty.email.field.error": "Saisissez votre courriel",
"empty.username.field.error": "Le nom d'utilisateur doit comporter entre 2 et 30 caractères",
"empty.password.field.error": "Les critères de mot de passe n'ont pas été remplis",
"empty.country.field.error": "Sélectionnez votre pays ou région de résidence",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Enter a valid email address",
"email.ratelimit.less.chars.validation.message": "Le courriel doit comporter 3 caractères.",
"username.validation.message": "Le nom d'utilisateur doit comporter entre 2 et 30 caractères",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Les noms d'utilisateur peuvent seulement contenir des lettres (A-Z, a-z), des chiffres (0-9), des tirets bas (_) et des traits d'union (-). Les noms d'utilisateur ne doivent pas contenir d'espaces.",
"support.education.research": "Soutenez la recherche en éducation en fournissant des informations additionnelles. (Optionel)",
"registration.request.failure.header": "Nous n'avons pas pu créer votre compte.",
"registration.empty.form.submission.error": "Veuillez vérifier vos réponses et réessayer.",
"registration.request.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
"registration.rate.limit.error": "Trop de tentatives d'inscriptions ont échoué. Réessayez plus tard.",
"registration.tpa.session.expired": "L'inscription avec {provider} a échouée.",
"terms.of.service.and.honor.code": "Conditions d'utilisation et Code d'honneur",
"privacy.policy": "Politique de confidentialité",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Année de naissance (facultatif)",
"registration.field.gender.options.label": "Sexe (facultatif)",
"registration.goals.label": "Dites-nous pourquoi vous êtes intéressé par edX (facultatif)",
"registration.field.gender.options.f": "Femme",
"registration.field.gender.options.m": "Homme",
"registration.field.gender.options.o": "Autre / Préfère ne pas répondre",
"registration.field.education.levels.label": "Plus haut niveau de scolarité atteint (facultatif)",
"registration.field.education.levels.p": "Doctorat",
"registration.field.education.levels.m": "Master ou diplôme professionnel",
"registration.field.education.levels.b": "Diplôme de premier cycle supérieur",
"registration.field.education.levels.a": "Grade de l'associé",
"registration.field.education.levels.hs": "Lycée / enseignement secondaire",
"registration.field.education.levels.jhs": "Collège / enseignement secondaire inférieur",
"registration.field.education.levels.el": "Enseignement primaire",
"registration.field.education.levels.none": "Sans diplôme",
"registration.field.education.levels.other": "Autre niveau d'étude",
"registration.username.suggestion.label": "Suggéré :",
"registration.using.tpa.form.heading": "Terminer la création de votre compte",
"did.you.mean.alert.text": "Vouliez-vous dire",
"certificate.msg": "*L'offre n'est pas éligible au programme GTx Analytics: Essential Tools and Methods MicroMasters, au programme de certificat professionnel en finance d'entreprise de ColumbiaX, ni aux cours ou programmes proposés par Wharton et NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Connectez-vous",
"reset.password.page.title": "Réinitialiser le mot de passe | {siteName}",
"reset.password": "Réinitialiser le mot de passe",
"reset.password.page.instructions": "Saisir et confirmer votre nouveau mot de passe.",
"new.password.label": "Nouveau mot de passe",
"confirm.password.label": "Confirmer le mot de passe",
"passwords.do.not.match": "Les mots de passe ne correspondent pas",
"confirm.your.password": "Confirmer votre mot de passe",
"forgot.password.confirmation.sign.in.link": "connexion",
"reset.password.request.forgot.password.text": "Mot de passe oublié",
"reset.password.request.invalid.token.header": "Lien de réinitialisation du mot de passe non valide",
"reset.password.empty.new.password.field.error": "Veuillez entrer votre nouveau mot de passe.",
"reset.password.failure.heading": "Nous n'avons pas pu réinitialiser votre mot de passe.",
"reset.password.form.submission.error": "Veuillez vérifier vos réponses et réessayer.",
"reset.password.request.server.error": "Échec de la réinitialisation du mot de passe",
"reset.password.token.validation.sever.error": "Échec de la validation du jeton",
"reset.server.rate.limit.error": "Trop de demandes.",
"reset.password.success.heading": "Réinitialisation du mot de passe complétée.",
"reset.password.success": "Votre mot de passe a été réinitialisé. Connectez-vous à votre compte.",
"progressive.profiling.page.title": "Champs optionnels | {siteName}",
"progressive.profiling.page.heading": "Quelques questions pour vous nous aideront à devenir plus intelligents.",
"gender.options.label": "Sexe (facultatif)",
"gender.options.f": "Femme",
"gender.options.m": "Homme",
"gender.options.o": "Autre / Préfère ne pas répondre",
"education.levels.label": "Plus haut niveau de scolarité atteint (facultatif)",
"education.levels.p": "Doctorat",
"education.levels.m": "Master ou diplôme professionnel",
"education.levels.b": "Diplôme de premier cycle supérieur",
"education.levels.a": "Grade de l'associé",
"education.levels.hs": "Lycée / enseignement secondaire",
"education.levels.jhs": "Collège / enseignement secondaire inférieur",
"education.levels.el": "Enseignement primaire",
"education.levels.none": "Sans diplôme",
"education.levels.other": "Autre niveau d'étude",
"year.of.birth.label": "Année de naissance (facultatif)",
"optional.fields.information.link": "En savoir plus sur la façon dont nous utilisons ces informations.",
"optional.fields.submit.button": "Envoyez",
"optional.fields.skip.button": "Ignorer pour l'instant",
"continue.to.platform": "Continuer vers {platformName}",
"modal.title": "Merci de nous en informer.",
"modal.description": "Vous pouvez compléter votre profil dans les paramètres à tout moment si vous changez d'avis.",
"welcome.page.error.heading": "Nous n'avons pas pu mettre à jour votre profil",
"welcome.page.error.message": "Une erreur s'est produite. Vous pouvez compléter votre profil dans les paramètres à tout moment."
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
"username.validation.message": "Username must be between 2 and 30 characters",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-).",
"support.education.research": "Support education research by providing additional information. (Optional)",
"registration.request.failure.header": "We couldn't create your account.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
"registration.field.gender.options.f": "Female",
"registration.field.gender.options.m": "Male",
"registration.field.gender.options.o": "Other/Prefer not to say",
"registration.field.education.levels.label": "Highest level of education completed (optional)",
"registration.field.education.levels.p": "Doctorate",
"registration.field.education.levels.m": "Master's or professional degree",
"registration.field.education.levels.b": "Bachelor's degree",
"registration.field.education.levels.a": "Associate's degree",
"registration.field.education.levels.hs": "Secondary/high school",
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
"registration.field.education.levels.el": "Elementary/primary school",
"registration.field.education.levels.none": "No formal education",
"registration.field.education.levels.other": "Other education",
"registration.username.suggestion.label": "Available:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Enter and confirm your new password.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"forgot.password.confirmation.sign.in.link": "sign in",
"reset.password.request.forgot.password.text": "Forgot password",
"reset.password.request.invalid.token.header": "Invalid password reset link",
"reset.password.empty.new.password.field.error": "Please enter your new password.",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"reset.password.request.server.error": "Failed to reset password",
"reset.password.token.validation.sever.error": "Token validation failure",
"reset.server.rate.limit.error": "Too many requests.",
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"progressive.profiling.page.title": "Optional Fields | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"gender.options.label": "Gender (optional)",
"gender.options.f": "Female",
"gender.options.m": "Male",
"gender.options.o": "Other/Prefer not to say",
"education.levels.label": "Highest level of education completed (optional)",
"education.levels.p": "Doctorate",
"education.levels.m": "Master's or professional degree",
"education.levels.b": "Bachelor's degree",
"education.levels.a": "Associate's degree",
"education.levels.hs": "Secondary/high school",
"education.levels.jhs": "Junior secondary/junior high/middle school",
"education.levels.el": "Elementary/primary school",
"education.levels.none": "No formal education",
"education.levels.other": "Other education",
"year.of.birth.label": "Year of birth (optional)",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"continue.to.platform": "Continue to {platformName}",
"modal.title": "Thanks for letting us know.",
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
"welcome.page.error.heading": "We couldn't update your profile",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time."
}

View File

@@ -1,224 +0,0 @@
{
"top.discount.message.15.off": "off",
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"code.copied": "Code copied",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"side.discount.message.15.off": "off",
"certificate.message": "certificate* with code",
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Check your email",
"forgot.password.confirmation.support.link": "contact technical support",
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
"sso.sign.in.with": "Sign in with {providerName}",
"sso.create.account.using": "Create account using {providerName}",
"show.password": "Show password",
"hide.password": "Hide password",
"one.letter": "1 Letter",
"one.number": "1 Number",
"eight.characters": "8 Characters",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
"tpa.alert.heading": "Almost done!",
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": "Forgot Password | {siteName}",
"forgot.password.page.heading": "Reset password",
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
"forgot.password.page.invalid.email.message": "Enter a valid email address",
"forgot.password.page.email.field.label": "Email",
"forgot.password.page.submit.button": "Submit",
"forgot.password.error.alert.title.": "We were unable to contact you.",
"forgot.password.error.message.title": "An error occurred.",
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
"forgot.password.empty.email.field.error": "Enter your email",
"forgot.password.invalid.email": "An error occurred.",
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
"confirmation.message.title": "Check your email",
"confirmation.support.link": "contact technical support",
"need.help.sign.in.text": "Need help signing in?",
"additional.help.text": "For additional help, contact edX support at ",
"sign.in.text": "Sign in",
"extend.field.errors": "{emailError} below.",
"invalid.token.heading": "Invalid password reset link",
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
"token.validation.rate.limit.error.heading": "Too many requests",
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"token.validation.internal.sever.error.heading": "Token validation failure",
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
"login.page.title": "Login | {siteName}",
"login.user.identity.label": "Username or email",
"login.password.label": "Password",
"sign.in.button": "Sign in",
"sign.in.btn.pending.state": "Loading",
"need.help.signing.in.collapsible.menu": "Need help signing in?",
"forgot.password.link": "Forgot my password",
"forgot.password": "Forgot password",
"other.sign.in.issues": "Other sign in issues",
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Sign in with institution/campus credentials",
"institution.login.page.back.button": "Back to sign in",
"create.an.account": "Create an account",
"login.other.options.heading": "Or sign in with:",
"non.compliant.password.title": "We recently changed our password requirements",
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"first.time.here": "First time here?",
"email.help.message": "The email address you used to register with edX.",
"enterprise.login.btn.text": "Company or school credentials",
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"register.link": "Create an account",
"sign.in.heading": "Sign in",
"account.activation.success.message.title": "Success! You have activated your account.",
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
"account.activation.info.message": "This account has already been activated.",
"account.activation.error.message.title": "Your account could not be activated",
"account.activation.support.link": "contact support",
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
"account.confirmation.success.message": "Sign in to continue.",
"account.confirmation.info.message": "This email has already been confirmed.",
"account.confirmation.error.message.title": "Your email could not be confirmed",
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
"login.failure.header.title": "We couldn't sign you in.",
"contact.support.link": "contact {platformName} support",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
"login.locked.out.error.message": "To protect your account, its been temporarily locked. Try again in {lockedOutPeriod} minutes.",
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.username.field.error": "Username must be between 2 and 30 characters",
"empty.password.field.error": "Password criteria has not been met",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Enter a valid email address",
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
"username.validation.message": "Username must be between 2 and 30 characters",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
"support.education.research": "Support education research by providing additional information. (Optional)",
"registration.request.failure.header": "We couldn't create your account.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
"registration.field.gender.options.f": "Female",
"registration.field.gender.options.m": "Male",
"registration.field.gender.options.o": "Other/Prefer not to say",
"registration.field.education.levels.label": "Highest level of education completed (optional)",
"registration.field.education.levels.p": "Doctorate",
"registration.field.education.levels.m": "Master's or professional degree",
"registration.field.education.levels.b": "Bachelor's degree",
"registration.field.education.levels.a": "Associate's degree",
"registration.field.education.levels.hs": "Secondary/high school",
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
"registration.field.education.levels.el": "Elementary/primary school",
"registration.field.education.levels.none": "No formal education",
"registration.field.education.levels.other": "Other education",
"registration.username.suggestion.label": "Suggested:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Enter and confirm your new password.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"forgot.password.confirmation.sign.in.link": "sign in",
"reset.password.request.forgot.password.text": "Forgot password",
"reset.password.request.invalid.token.header": "Invalid password reset link",
"reset.password.empty.new.password.field.error": "Please enter your new password.",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"reset.password.request.server.error": "Failed to reset password",
"reset.password.token.validation.sever.error": "Token validation failure",
"reset.server.rate.limit.error": "Too many requests.",
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"progressive.profiling.page.title": "Optional Fields | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"gender.options.label": "Gender (optional)",
"gender.options.f": "Female",
"gender.options.m": "Male",
"gender.options.o": "Other/Prefer not to say",
"education.levels.label": "Highest level of education completed (optional)",
"education.levels.p": "Doctorate",
"education.levels.m": "Master's or professional degree",
"education.levels.b": "Bachelor's degree",
"education.levels.a": "Associate's degree",
"education.levels.hs": "Secondary/high school",
"education.levels.jhs": "Junior secondary/junior high/middle school",
"education.levels.el": "Elementary/primary school",
"education.levels.none": "No formal education",
"education.levels.other": "Other education",
"year.of.birth.label": "Year of birth (optional)",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"continue.to.platform": "Continue to {platformName}",
"modal.title": "Thanks for letting us know.",
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
"welcome.page.error.heading": "We couldn't update your profile",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time."
}

View File

@@ -1,224 +0,0 @@
{
"top.discount.message.15.off": "off",
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"code.copied": "Code copied",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"side.discount.message.15.off": "off",
"certificate.message": "certificate* with code",
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Check your email",
"forgot.password.confirmation.support.link": "contatta il supporto tecnico",
"forgot.password.confirmation.info": "Se non ricevi un messaggio di reimpostazione della password entro 1 minuto, verifica di aver inserito l'indirizzo e-mail corretto o controlla la cartella della posta indesiderata.",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"internal.server.error.message": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
"server.ratelimit.error.message": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi.",
"enterprisetpa.title.heading": "Vuoi accedere utilizzando le credenziali {providerName}?",
"enterprisetpa.sso.button.title": "Accedi utilizzando {providerName}",
"enterprisetpa.login.button.text": "Mostrami altre modalità di accesso o registrazione",
"sso.sign.in.with": "Accedi con {providerName}",
"sso.create.account.using": "Crea un account utilizzando {providerName}",
"show.password": "Show password",
"hide.password": "Hide password",
"one.letter": "1 Letter",
"one.number": "1 Number",
"eight.characters": "8 Characters",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
"tpa.alert.heading": "Almost done!",
"login.third.party.auth.account.not.linked": "Hai correttamente effettuato l'accesso in {currentProvider}, ma il tuo account {currentProvider} non ha un account {platformName} ad esso abbinato. Per collegare i tuoi account accesi utilizzando la password {platformName}. ",
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
"error.notfound.message": "La pagina che stai cercando non è disponibile o si è verificato un errore nell'URL. Controlla l'URL e riprova. ",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": "Dimenticato la password | {siteName}",
"forgot.password.page.heading": "Reset password",
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
"forgot.password.page.invalid.email.message": "Inserisci un indirizzo email valido",
"forgot.password.page.email.field.label": "Email",
"forgot.password.page.submit.button": "Submit",
"forgot.password.error.alert.title.": "We were unable to contact you.",
"forgot.password.error.message.title": "Si è verificato un errore. ",
"forgot.password.request.in.progress.message": "La tua richiesta precedente è in corso di elaborazione, riprova tra qualche istante. ",
"forgot.password.empty.email.field.error": "Enter your email",
"forgot.password.invalid.email": "Si è verificato un errore. ",
"forgot.password.invalid.email.message": "L'indirizzo email che hai fornito non è formattato correttamente. ",
"forgot.password.email.help.text": "L'indirizzo email che hai utilizzato per registrarti con {platformName}",
"confirmation.message.title": "Check your email",
"confirmation.support.link": "contatta il supporto tecnico",
"need.help.sign.in.text": "Hai bisogno di aiuto per l'accesso? ",
"additional.help.text": "For additional help, contact edX support at ",
"sign.in.text": "Sign in",
"extend.field.errors": "{emailError} below.",
"invalid.token.heading": "Link di ripristino della password non valido",
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
"token.validation.rate.limit.error.heading": "Too many requests",
"token.validation.rate.limit.error": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi.",
"token.validation.internal.sever.error.heading": "Errore di convalida del token",
"token.validation.internal.sever.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
"internal.server.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
"rate.limit.error": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi.",
"account.activation.error.message": "Si è verificato un errore, seleziona {supportLink} per risolvere il problema. ",
"login.inactive.user.error": "Per accedere, devi attivare il tuo account.{lineBreak} {lineBreak}Abbiamo appena inviato un link di attivazione a {email}. Se non ricevi un'email, controlla la cartella della posta indesiderata oppure seleziona {supportLink}.",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
"login.page.title": "Accesso | {siteName}",
"login.user.identity.label": "Nome utente o email ",
"login.password.label": "Password",
"sign.in.button": "Accedi",
"sign.in.btn.pending.state": "Loading",
"need.help.signing.in.collapsible.menu": "Hai bisogno di aiuto per l'accesso?",
"forgot.password.link": "Ho dimenticato la mia password",
"forgot.password": "Password dimenticata",
"other.sign.in.issues": "Altri problemi legati all'accesso",
"need.other.help.signing.in.collapsible.menu": "Hai bisogno di ulteriore aiuto per l'accesso?",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Accedi con le credenziali dell'istituzione/campus",
"institution.login.page.back.button": "Torna all'accesso",
"create.an.account": "Create an account",
"login.other.options.heading": "Or sign in with:",
"non.compliant.password.title": "Abbiamo di recente modificato i requisiti per la password ",
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"first.time.here": "È la prima volta che ci visiti?",
"email.help.message": "L'indirizzo email che hai utilizzato per registrarti con edX.",
"enterprise.login.btn.text": "Company or school credentials",
"email.format.validation.message": "L'indirizzo email che hai fornito non è formattato correttamente. ",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"register.link": "Crea un account",
"sign.in.heading": "Sign in",
"account.activation.success.message.title": "Completato correttamente! Hai attivato il tuo account. ",
"account.activation.success.message": "A breve ti invieremo avvisi e aggiornamenti via email relativi al corso a cui ti sei iscritto. Accedi per proseguire.",
"account.activation.info.message": "This account has already been activated.",
"account.activation.error.message.title": "Impossibile attivare il tuo account.",
"account.activation.support.link": "contatta il supporto",
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
"account.confirmation.success.message": "Sign in to continue.",
"account.confirmation.info.message": "This email has already been confirmed.",
"account.confirmation.error.message.title": "Your email could not be confirmed",
"login.rate.limit.reached.message": "Troppi tentativi di login falliti. Riprova più tardi.",
"login.failure.header.title": "Impossibile autorizzare il tuo accesso.",
"contact.support.link": "contatta il supporto {platformName} ",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
"login.failed.attempt.error": "Hai a disposizione altri {remainingAttempts} tentativi di accesso prima che il tuo account venga temporaneamente bloccato.",
"login.locked.out.error.message": "Il tuo account è stato temporaneamente bloccato per motivi di sicurezza. Riprova tra {lockedOutPeriod} minuti.",
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Registrazione | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "Questo nome verrà utilizzato per tutti i certificati conseguiti.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Registrati con le credenziali dell'istituzione/campus",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.username.field.error": "Username must be between 2 and 30 characters",
"empty.password.field.error": "Password criteria has not been met",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Inserisci un indirizzo email valido",
"email.ratelimit.less.chars.validation.message": "Email deve avere 3 caratteri.",
"username.validation.message": "Username must be between 2 and 30 characters",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
"support.education.research": "Supportare la ricerca del livello di istruzione fornendo informazioni aggiuntive. (Facoltativo)",
"registration.request.failure.header": "Impossibile creare il tuo account.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
"registration.request.server.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
"registration.rate.limit.error": "Troppi tentativi di registrazione non riusciti. Prova di nuovo più tardi.",
"registration.tpa.session.expired": "La registrazione mediante {provider} è andata in timeout.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Anno di nascita (facoltativo)",
"registration.field.gender.options.label": "Genere (facoltativo)",
"registration.goals.label": "Dicci perché sei interessato a edX (facoltativo)",
"registration.field.gender.options.f": "Female",
"registration.field.gender.options.m": "Male",
"registration.field.gender.options.o": "Altro/Preferisco non dire",
"registration.field.education.levels.label": "Livello di istruzione più elevato raggiunto (opzionale) ",
"registration.field.education.levels.p": "Doctorate",
"registration.field.education.levels.m": "Master's or professional degree",
"registration.field.education.levels.b": "Bachelor's degree",
"registration.field.education.levels.a": "Associate's degree",
"registration.field.education.levels.hs": "Scuola Superiore/Liceo",
"registration.field.education.levels.jhs": "Scuola Media",
"registration.field.education.levels.el": "Elementary/primary school",
"registration.field.education.levels.none": "No formal education",
"registration.field.education.levels.other": "Other education",
"registration.username.suggestion.label": "Suggested:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Ripristina password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Immettere e confermare la nuova password. ",
"new.password.label": "Nuova password",
"confirm.password.label": "Conferma password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"forgot.password.confirmation.sign.in.link": "sign in",
"reset.password.request.forgot.password.text": "Dimenticato la password",
"reset.password.request.invalid.token.header": "Link di ripristino della password non valido",
"reset.password.empty.new.password.field.error": "Immetti la nuova password.",
"reset.password.failure.heading": "Impossibile ripristinare la tua password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"reset.password.request.server.error": "Ripristino della password non riuscito",
"reset.password.token.validation.sever.error": "Errore di convalida del token",
"reset.server.rate.limit.error": "Troppe richieste.",
"reset.password.success.heading": "Ripristino della password completato.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"progressive.profiling.page.title": "Optional Fields | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"gender.options.label": "Genere (facoltativo)",
"gender.options.f": "Female",
"gender.options.m": "Male",
"gender.options.o": "Altro/Preferisco non dire",
"education.levels.label": "Livello di istruzione più elevato raggiunto (opzionale) ",
"education.levels.p": "Doctorate",
"education.levels.m": "Master's or professional degree",
"education.levels.b": "Bachelor's degree",
"education.levels.a": "Associate's degree",
"education.levels.hs": "Scuola Superiore/Liceo",
"education.levels.jhs": "Scuola Media",
"education.levels.el": "Elementary/primary school",
"education.levels.none": "No formal education",
"education.levels.other": "Other education",
"year.of.birth.label": "Anno di nascita (facoltativo)",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"continue.to.platform": "Continue to {platformName}",
"modal.title": "Thanks for letting us know.",
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
"welcome.page.error.heading": "We couldn't update your profile",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time."
}

View File

@@ -1,224 +0,0 @@
{
"top.discount.message.15.off": "desligado",
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
"start.learning": "Começar a aprender",
"with.site.name": "with {siteName}",
"code.copied": "Código copiado",
"complete.your.profile.1": "Concluído",
"complete.your.profile.2": "o seu perfil",
"welcome.to.platform": "Bem vindo a {siteName}, {username}!",
"side.discount.message.15.off": "desligado",
"certificate.message": "certificate* with code",
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Verifique o seu email",
"forgot.password.confirmation.support.link": "contacto o suporte técnico",
"forgot.password.confirmation.info": "Se não receber uma mensagem para alterar a palavra-passe após 1 minuto, verifique se introduziu o endereço de correio electrónico correcto, ou verifique a sua pasta de spam.",
"logistration.sign.in": "Iniciar sessão",
"logistration.register": "Registe-se",
"internal.server.error.message": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
"server.ratelimit.error.message": "Ocorreu um erro devido a demasiados pedidos. Por favor, tente novamente após algum tempo.",
"enterprisetpa.title.heading": "Gostaria de iniciar sessão usando as suas {providerName} credenciais?",
"enterprisetpa.sso.button.title": "Inicie a sessão utilizando {providerName}",
"enterprisetpa.login.button.text": "Mostre-me outras formas de iniciar sessão ou registar-se",
"sso.sign.in.with": "Inicie sessão com {providerName}",
"sso.create.account.using": "Criar conta usando {providerName}",
"show.password": "Show password",
"hide.password": "Hide password",
"one.letter": "1 Letter",
"one.number": "1 Number",
"eight.characters": "8 Characters",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
"tpa.alert.heading": "Almost done!",
"login.third.party.auth.account.not.linked": "Iniciou sessão com sucesso em {currentProvider}, mas a sua conta {currentProvider} não está vinculada a uma conta {platformName}. Para vincular as suas contas, inicie sessão através da sua palavra-passe em {platformName}.",
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
"error.notfound.message": "A página que procura não está disponível ou há um erro no URL. Por favor, verifique o URL e tente novamente.",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": "Esqueceu a Senha | {siteName}",
"forgot.password.page.heading": "Reset password",
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
"forgot.password.page.invalid.email.message": "Introduzir um endereço de correio electrónico válido",
"forgot.password.page.email.field.label": "Email",
"forgot.password.page.submit.button": "Submeter",
"forgot.password.error.alert.title.": "We were unable to contact you.",
"forgot.password.error.message.title": "Ocorreu um erro.",
"forgot.password.request.in.progress.message": "O seu pedido anterior está a ser processado, por favor tente novamente dentro de momentos.",
"forgot.password.empty.email.field.error": "Enter your email",
"forgot.password.invalid.email": "Ocorreu um erro.",
"forgot.password.invalid.email.message": "O endereço de email fornecido não está formatado correctamente.",
"forgot.password.email.help.text": "O endereço de e-mail que usou para se registar em {platformName}",
"confirmation.message.title": "Verifique o seu email",
"confirmation.support.link": "contact technical support",
"need.help.sign.in.text": "Need help signing in?",
"additional.help.text": "For additional help, contact edX support at ",
"sign.in.text": "Sign in",
"extend.field.errors": "{emailError} below.",
"invalid.token.heading": "Invalid password reset link",
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
"token.validation.rate.limit.error.heading": "Too many requests",
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"token.validation.internal.sever.error.heading": "Token validation failure",
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"account.activation.error.message": "Alguma coisa correu mal, siga {supportLink} para resolver esta questão.",
"login.inactive.user.error": "Para iniciar sessão, precisa ativar a sua conta. {lineBreak}\n {lineBreak} Acabámos de enviar um link de ativação para {email}. Se não receber um e-mail,\n verifique as suas pastas de spam ou {supportLink}.",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
"login.page.title": "Iniciar sessão | {siteName}",
"login.user.identity.label": "Username or email",
"login.password.label": "Password",
"sign.in.button": "Iniciar Sessão",
"sign.in.btn.pending.state": "Loading",
"need.help.signing.in.collapsible.menu": "Precisa de ajuda para entrar?",
"forgot.password.link": "Esqueci-me da minha palavra-passe",
"forgot.password": "Forgot password",
"other.sign.in.issues": "Outros problemas de inicio de sessão",
"need.other.help.signing.in.collapsible.menu": "Precisa de outra ajuda para entrar?",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Inicie sessão com credenciais de instituição/campus",
"institution.login.page.back.button": "Voltar para iniciar sessão",
"create.an.account": "Criar uma conta",
"login.other.options.heading": "Or sign in with:",
"non.compliant.password.title": "Recentemente mudámos os nossos requisitos de palavra-passe",
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"first.time.here": "Está a entrar pela primeira vez?",
"email.help.message": "O endereço de e-mail que usou para se registrar no edX.",
"enterprise.login.btn.text": "Company or school credentials",
"email.format.validation.message": "O endereço de e-mail fornecido não está formatado correctamente.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"register.link": "Criar uma conta",
"sign.in.heading": "Iniciar Sessão",
"account.activation.success.message.title": "Sucesso! Você ativou a sua conta.",
"account.activation.success.message": "Receberá agora actualizações por e-mail e alertas nossos relacionados com os cursos em que está inscrito. Inicie a sessão para continuar.",
"account.activation.info.message": "This account has already been activated.",
"account.activation.error.message.title": "A sua conta não pôde ser ativada",
"account.activation.support.link": "contato de suporte",
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
"account.confirmation.success.message": "Sign in to continue.",
"account.confirmation.info.message": "This email has already been confirmed.",
"account.confirmation.error.message.title": "Your email could not be confirmed",
"login.rate.limit.reached.message": "Muitas tentativas de login sem sucesso. Tente novamente mais tarde.",
"login.failure.header.title": "O seu acesso não foi possível.",
"contact.support.link": "contactar o suporte {platformName}",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
"login.failed.attempt.error": "Tem mais {remainingAttempts} tentativas de inicio sessão antes que a sua conta seja temporariamente bloqueada.",
"login.locked.out.error.message": "Para proteger a sua conta, esta foi temporariamente bloqueada. Tente novamente em {lockedOutPeriod} minutos.",
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Registar | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Registo com credenciais da instituição/campus",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.username.field.error": "Username must be between 2 and 30 characters",
"empty.password.field.error": "Password criteria has not been met",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Enter a valid email address",
"email.ratelimit.less.chars.validation.message": "O e-mail deve ter 3 carateres.",
"username.validation.message": "Username must be between 2 and 30 characters",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
"support.education.research": "Apoie a pesquisa em educação fornecendo informações adicionais. (Opcional)",
"registration.request.failure.header": "Não foi possível criar a sua conta.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
"registration.request.server.error": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Termos de Serviço e Código de Honra",
"privacy.policy": "Política de Privacidade",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Ano de Nascimento (opcional)",
"registration.field.gender.options.label": "Género (opcional)",
"registration.goals.label": "Diga-nos porque está interessado no edX (opcional)",
"registration.field.gender.options.f": "Feminino",
"registration.field.gender.options.m": "Masculino",
"registration.field.gender.options.o": "Outros/Prefere não dizer",
"registration.field.education.levels.label": "Nível mais elevado de escolaridade concluído (opcional)",
"registration.field.education.levels.p": "Doutoramento",
"registration.field.education.levels.m": "Mestrado ou Grau Profissional",
"registration.field.education.levels.b": "Licenciatura",
"registration.field.education.levels.a": "Pós-graduação",
"registration.field.education.levels.hs": "Secundário",
"registration.field.education.levels.jhs": "2ªciclo/3ºciclo",
"registration.field.education.levels.el": "Primária",
"registration.field.education.levels.none": "Sem estudos",
"registration.field.education.levels.other": "Outra educação",
"registration.username.suggestion.label": "Suggested:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Redefinir Palavra-passe | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Insira e confirme a sua nova palavra-passe.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"forgot.password.confirmation.sign.in.link": "inicie a sessão",
"reset.password.request.forgot.password.text": "Esqueci-me da palavra-passe",
"reset.password.request.invalid.token.header": "Link para Redefinir Palavra-passe inválido",
"reset.password.empty.new.password.field.error": "Por favor, introduza a sua nova palavra-passe.",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"reset.password.request.server.error": "Falha na redefinição da palavra-passe",
"reset.password.token.validation.sever.error": "Falha na validação do Token",
"reset.server.rate.limit.error": "Too many requests.",
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"progressive.profiling.page.title": "Optional Fields | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"gender.options.label": "Gender (optional)",
"gender.options.f": "Female",
"gender.options.m": "Male",
"gender.options.o": "Other/Prefer not to say",
"education.levels.label": "Highest level of education completed (optional)",
"education.levels.p": "Doctorate",
"education.levels.m": "Master's or professional degree",
"education.levels.b": "Bachelor's degree",
"education.levels.a": "Associate's degree",
"education.levels.hs": "Secondary/high school",
"education.levels.jhs": "Junior secondary/junior high/middle school",
"education.levels.el": "Elementary/primary school",
"education.levels.none": "No formal education",
"education.levels.other": "Other education",
"year.of.birth.label": "Year of birth (optional)",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"continue.to.platform": "Continue to {platformName}",
"modal.title": "Thanks for letting us know.",
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
"welcome.page.error.heading": "We couldn't update your profile",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time."
}

View File

@@ -1,224 +1 @@
{
"top.discount.message.15.off": "off",
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"code.copied": "Code copied",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"side.discount.message.15.off": "off",
"certificate.message": "certificate* with code",
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Check your email",
"forgot.password.confirmation.support.link": "contact technical support",
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
"sso.sign.in.with": "Sign in with {providerName}",
"sso.create.account.using": "Create account using {providerName}",
"show.password": "Show password",
"hide.password": "Hide password",
"one.letter": "1 Letter",
"one.number": "1 Number",
"eight.characters": "8 Characters",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
"tpa.alert.heading": "Almost done!",
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": "Forgot Password | {siteName}",
"forgot.password.page.heading": "Reset password",
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
"forgot.password.page.invalid.email.message": "Enter a valid email address",
"forgot.password.page.email.field.label": "Email",
"forgot.password.page.submit.button": "Submit",
"forgot.password.error.alert.title.": "We were unable to contact you.",
"forgot.password.error.message.title": "An error occurred.",
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
"forgot.password.empty.email.field.error": "Enter your email",
"forgot.password.invalid.email": "An error occurred.",
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
"confirmation.message.title": "Check your email",
"confirmation.support.link": "contact technical support",
"need.help.sign.in.text": "Need help signing in?",
"additional.help.text": "For additional help, contact edX support at ",
"sign.in.text": "Sign in",
"extend.field.errors": "{emailError} below.",
"invalid.token.heading": "Invalid password reset link",
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
"token.validation.rate.limit.error.heading": "Too many requests",
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"token.validation.internal.sever.error.heading": "Token validation failure",
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
"login.page.title": "Login | {siteName}",
"login.user.identity.label": "Username or email",
"login.password.label": "Password",
"sign.in.button": "Sign in",
"sign.in.btn.pending.state": "Loading",
"need.help.signing.in.collapsible.menu": "Need help signing in?",
"forgot.password.link": "Forgot my password",
"forgot.password": "Forgot password",
"other.sign.in.issues": "Other sign in issues",
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Sign in with institution/campus credentials",
"institution.login.page.back.button": "Back to sign in",
"create.an.account": "Create an account",
"login.other.options.heading": "Or sign in with:",
"non.compliant.password.title": "We recently changed our password requirements",
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"first.time.here": "First time here?",
"email.help.message": "The email address you used to register with edX.",
"enterprise.login.btn.text": "Company or school credentials",
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"register.link": "Create an account",
"sign.in.heading": "Sign in",
"account.activation.success.message.title": "Success! You have activated your account.",
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
"account.activation.info.message": "This account has already been activated.",
"account.activation.error.message.title": "Your account could not be activated",
"account.activation.support.link": "contact support",
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
"account.confirmation.success.message": "Sign in to continue.",
"account.confirmation.info.message": "This email has already been confirmed.",
"account.confirmation.error.message.title": "Your email could not be confirmed",
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
"login.failure.header.title": "We couldn't sign you in.",
"contact.support.link": "contact {platformName} support",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
"login.locked.out.error.message": "To protect your account, its been temporarily locked. Try again in {lockedOutPeriod} minutes.",
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.username.field.error": "Username must be between 2 and 30 characters",
"empty.password.field.error": "Password criteria has not been met",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Enter a valid email address",
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
"username.validation.message": "Username must be between 2 and 30 characters",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
"support.education.research": "Support education research by providing additional information. (Optional)",
"registration.request.failure.header": "We couldn't create your account.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
"registration.field.gender.options.f": "Female",
"registration.field.gender.options.m": "Male",
"registration.field.gender.options.o": "Other/Prefer not to say",
"registration.field.education.levels.label": "Highest level of education completed (optional)",
"registration.field.education.levels.p": "Doctorate",
"registration.field.education.levels.m": "Master's or professional degree",
"registration.field.education.levels.b": "Bachelor's degree",
"registration.field.education.levels.a": "Associate's degree",
"registration.field.education.levels.hs": "Secondary/high school",
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
"registration.field.education.levels.el": "Elementary/primary school",
"registration.field.education.levels.none": "No formal education",
"registration.field.education.levels.other": "Other education",
"registration.username.suggestion.label": "Suggested:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Enter and confirm your new password.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"forgot.password.confirmation.sign.in.link": "sign in",
"reset.password.request.forgot.password.text": "Forgot password",
"reset.password.request.invalid.token.header": "Invalid password reset link",
"reset.password.empty.new.password.field.error": "Please enter your new password.",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"reset.password.request.server.error": "Failed to reset password",
"reset.password.token.validation.sever.error": "Token validation failure",
"reset.server.rate.limit.error": "Too many requests.",
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"progressive.profiling.page.title": "Optional Fields | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"gender.options.label": "Gender (optional)",
"gender.options.f": "Female",
"gender.options.m": "Male",
"gender.options.o": "Other/Prefer not to say",
"education.levels.label": "Highest level of education completed (optional)",
"education.levels.p": "Doctorate",
"education.levels.m": "Master's or professional degree",
"education.levels.b": "Bachelor's degree",
"education.levels.a": "Associate's degree",
"education.levels.hs": "Secondary/high school",
"education.levels.jhs": "Junior secondary/junior high/middle school",
"education.levels.el": "Elementary/primary school",
"education.levels.none": "No formal education",
"education.levels.other": "Other education",
"year.of.birth.label": "Year of birth (optional)",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"continue.to.platform": "Continue to {platformName}",
"modal.title": "Thanks for letting us know.",
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
"welcome.page.error.heading": "We couldn't update your profile",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time."
}
{}

View File

@@ -1,224 +1 @@
{
"top.discount.message.15.off": "off",
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"code.copied": "Code copied",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"side.discount.message.15.off": "off",
"certificate.message": "certificate* with code",
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Check your email",
"forgot.password.confirmation.support.link": "contact technical support",
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
"sso.sign.in.with": "Sign in with {providerName}",
"sso.create.account.using": "Create account using {providerName}",
"show.password": "Show password",
"hide.password": "Hide password",
"one.letter": "1 Letter",
"one.number": "1 Number",
"eight.characters": "8 Characters",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
"tpa.alert.heading": "Almost done!",
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": "Forgot Password | {siteName}",
"forgot.password.page.heading": "Reset password",
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
"forgot.password.page.invalid.email.message": "Enter a valid email address",
"forgot.password.page.email.field.label": "Email",
"forgot.password.page.submit.button": "Submit",
"forgot.password.error.alert.title.": "We were unable to contact you.",
"forgot.password.error.message.title": "An error occurred.",
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
"forgot.password.empty.email.field.error": "Enter your email",
"forgot.password.invalid.email": "An error occurred.",
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
"confirmation.message.title": "Check your email",
"confirmation.support.link": "contact technical support",
"need.help.sign.in.text": "Need help signing in?",
"additional.help.text": "For additional help, contact edX support at ",
"sign.in.text": "Sign in",
"extend.field.errors": "{emailError} below.",
"invalid.token.heading": "Invalid password reset link",
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
"token.validation.rate.limit.error.heading": "Too many requests",
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"token.validation.internal.sever.error.heading": "Token validation failure",
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
"login.page.title": "Login | {siteName}",
"login.user.identity.label": "Username or email",
"login.password.label": "Password",
"sign.in.button": "Sign in",
"sign.in.btn.pending.state": "Loading",
"need.help.signing.in.collapsible.menu": "Need help signing in?",
"forgot.password.link": "Forgot my password",
"forgot.password": "Forgot password",
"other.sign.in.issues": "Other sign in issues",
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Sign in with institution/campus credentials",
"institution.login.page.back.button": "Back to sign in",
"create.an.account": "Create an account",
"login.other.options.heading": "Or sign in with:",
"non.compliant.password.title": "We recently changed our password requirements",
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"first.time.here": "First time here?",
"email.help.message": "The email address you used to register with edX.",
"enterprise.login.btn.text": "Company or school credentials",
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"register.link": "Create an account",
"sign.in.heading": "Sign in",
"account.activation.success.message.title": "Success! You have activated your account.",
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
"account.activation.info.message": "This account has already been activated.",
"account.activation.error.message.title": "Your account could not be activated",
"account.activation.support.link": "contact support",
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
"account.confirmation.success.message": "Sign in to continue.",
"account.confirmation.info.message": "This email has already been confirmed.",
"account.confirmation.error.message.title": "Your email could not be confirmed",
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
"login.failure.header.title": "We couldn't sign you in.",
"contact.support.link": "contact {platformName} support",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
"login.locked.out.error.message": "To protect your account, its been temporarily locked. Try again in {lockedOutPeriod} minutes.",
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.username.field.error": "Username must be between 2 and 30 characters",
"empty.password.field.error": "Password criteria has not been met",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Enter a valid email address",
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
"username.validation.message": "Username must be between 2 and 30 characters",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
"support.education.research": "Support education research by providing additional information. (Optional)",
"registration.request.failure.header": "We couldn't create your account.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
"registration.field.gender.options.f": "Female",
"registration.field.gender.options.m": "Male",
"registration.field.gender.options.o": "Other/Prefer not to say",
"registration.field.education.levels.label": "Highest level of education completed (optional)",
"registration.field.education.levels.p": "Doctorate",
"registration.field.education.levels.m": "Master's or professional degree",
"registration.field.education.levels.b": "Bachelor's degree",
"registration.field.education.levels.a": "Associate's degree",
"registration.field.education.levels.hs": "Secondary/high school",
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
"registration.field.education.levels.el": "Elementary/primary school",
"registration.field.education.levels.none": "No formal education",
"registration.field.education.levels.other": "Other education",
"registration.username.suggestion.label": "Suggested:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Enter and confirm your new password.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"forgot.password.confirmation.sign.in.link": "sign in",
"reset.password.request.forgot.password.text": "Forgot password",
"reset.password.request.invalid.token.header": "Invalid password reset link",
"reset.password.empty.new.password.field.error": "Please enter your new password.",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"reset.password.request.server.error": "Failed to reset password",
"reset.password.token.validation.sever.error": "Token validation failure",
"reset.server.rate.limit.error": "Too many requests.",
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"progressive.profiling.page.title": "Optional Fields | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"gender.options.label": "Gender (optional)",
"gender.options.f": "Female",
"gender.options.m": "Male",
"gender.options.o": "Other/Prefer not to say",
"education.levels.label": "Highest level of education completed (optional)",
"education.levels.p": "Doctorate",
"education.levels.m": "Master's or professional degree",
"education.levels.b": "Bachelor's degree",
"education.levels.a": "Associate's degree",
"education.levels.hs": "Secondary/high school",
"education.levels.jhs": "Junior secondary/junior high/middle school",
"education.levels.el": "Elementary/primary school",
"education.levels.none": "No formal education",
"education.levels.other": "Other education",
"year.of.birth.label": "Year of birth (optional)",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"continue.to.platform": "Continue to {platformName}",
"modal.title": "Thanks for letting us know.",
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
"welcome.page.error.heading": "We couldn't update your profile",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time."
}
{}

View File

@@ -1,15 +1,9 @@
{
"top.discount.message.15.off": "off",
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"code.copied": "Code copied",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"side.discount.message.15.off": "off",
"certificate.message": "certificate* with code",
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"forgot.password.confirmation.title": "Check your email",
"forgot.password.confirmation.support.link": "contact technical support",
@@ -96,13 +90,9 @@
"sign.in.heading": "Sign in",
"account.activation.success.message.title": "Success! You have activated your account.",
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
"account.activation.info.message": "This account has already been activated.",
"account.already.activated.message": "This account has already been activated.",
"account.activation.error.message.title": "Your account could not be activated",
"account.activation.support.link": "contact support",
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
"account.confirmation.success.message": "Sign in to continue.",
"account.confirmation.info.message": "This email has already been confirmed.",
"account.confirmation.error.message.title": "Your email could not be confirmed",
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
"login.failure.header.title": "We couldn't sign you in.",
"contact.support.link": "contact {platformName} support",
@@ -112,40 +102,28 @@
"login.form.invalid.error.message": "Please fill in the fields below.",
"login.incorrect.credentials.error.reset.link.text": "reset your password",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"create.account.button": "Create an account",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
"register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name",
"empty.email.field.error": "Enter your email",
"empty.username.field.error": "Username must be between 2 and 30 characters",
"empty.password.field.error": "Password criteria has not been met",
"empty.country.field.error": "Select your country or region of residence",
"email.invalid.format.error": "Enter a valid email address",
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
"username.validation.message": "Username must be between 2 and 30 characters",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-).",
"support.education.research": "Support education research by providing additional information. (Optional)",
"registration.request.failure.header": "We couldn't create your account.",
"registration.empty.form.submission.error": "Please check your responses and try again.",
@@ -154,8 +132,6 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
@@ -172,11 +148,10 @@
"registration.field.education.levels.el": "Elementary/primary school",
"registration.field.education.levels.none": "No formal education",
"registration.field.education.levels.other": "Other education",
"registration.username.suggestion.label": "Suggested:",
"registration.username.suggestion.label": "Available:",
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",

View File

@@ -31,17 +31,14 @@ initialize({
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
REGISTRATION_OPTIONAL_FIELDS: process.env.REGISTRATION_OPTIONAL_FIELDS || '',
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME || null,
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
WELCOME_PAGE_SUPPORT_LINK: process.env.WELCOME_PAGE_SUPPORT_LINK || null,
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
INFO_EMAIL: process.env.INFO_EMAIL || '',
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
ENABLE_PROGRESSIVE_PROFILING: process.env.ENABLE_PROGRESSIVE_PROFILING || false,
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
ENABLE_COPPA_COMPLIANCE: process.env.ENABLE_COPPA_COMPLIANCE || '',
SHOW_DYNAMIC_PROFILING_PAGE: process.env.SHOW_DYNAMIC_PROFILING_PAGE || false,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
ENABLE_PROGRESSIVE_PROFILING: true,
});
},
},

View File

@@ -13,8 +13,6 @@ const AccountActivationMessage = (props) => {
const { intl, messageType } = props;
const variant = messageType === ACCOUNT_ACTIVATION_MESSAGE.ERROR ? 'danger' : messageType;
const activationOrVerification = getConfig().MARKETING_EMAILS_OPT_IN ? 'confirmation' : 'activation';
let activationMessage;
let heading;
@@ -25,12 +23,12 @@ const AccountActivationMessage = (props) => {
switch (messageType) {
case ACCOUNT_ACTIVATION_MESSAGE.SUCCESS: {
heading = intl.formatMessage(messages[`account.${activationOrVerification}.success.message.title`]);
activationMessage = <span>{intl.formatMessage(messages[`account.${activationOrVerification}.success.message`])}</span>;
heading = intl.formatMessage(messages['account.activation.success.message.title']);
activationMessage = intl.formatMessage(messages['account.activation.success.message']);
break;
}
case ACCOUNT_ACTIVATION_MESSAGE.INFO: {
activationMessage = intl.formatMessage(messages[`account.${activationOrVerification}.info.message`]);
activationMessage = intl.formatMessage(messages['account.already.activated.message']);
break;
}
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
@@ -40,7 +38,7 @@ const AccountActivationMessage = (props) => {
</Alert.Link>
);
heading = intl.formatMessage(messages[`account.${activationOrVerification}.error.message.title`]);
heading = intl.formatMessage(messages['account.activation.error.message.title']);
activationMessage = (
<FormattedMessage
id="account.activation.error.message"

View File

@@ -1,89 +0,0 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { Link, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
ActionRow, ModalDialog, useToggle,
} from '@edx/paragon';
import messages from './messages';
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils';
import useMobileResponsive from '../data/utils/useMobileResponsive';
const ChangePasswordPrompt = ({ intl, variant, redirectUrl }) => {
const isMobileView = useMobileResponsive();
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
const handlers = {
handleToggleOff: () => {
if (variant === 'block') {
setRedirectToResetPasswordPage(true);
} else {
window.location.href = redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
}
},
};
// eslint-disable-next-line no-unused-vars
const [isOpen, open, close] = useToggle(true, handlers);
if (redirectToResetPasswordPage) {
return <Redirect to={updatePathWithQueryParams(RESET_PAGE)} />;
}
return (
<ModalDialog
title="Password security"
isOpen={isOpen}
onClose={close}
size={isMobileView ? 'sm' : 'md'}
hasCloseButton={false}
>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages[`password.security.${variant}.title`])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{intl.formatMessage(messages[`password.security.${variant}.body`])}
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow className={classNames(
{ 'd-flex flex-column': isMobileView },
)}
>
{variant === 'nudge' ? (
<ModalDialog.CloseButton id="password-security-close" variant="tertiary">
{intl.formatMessage(messages['password.security.close.button'])}
</ModalDialog.CloseButton>
) : null}
<Link
id="password-security-reset-password"
className={classNames(
'btn btn-primary',
{ 'w-100': isMobileView },
)}
to={updatePathWithQueryParams(RESET_PAGE)}
>
{intl.formatMessage(messages['password.security.redirect.to.reset.password.button'])}
</Link>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};
ChangePasswordPrompt.defaultProps = {
variant: 'block',
redirectUrl: null,
};
ChangePasswordPrompt.propTypes = {
intl: intlShape.isRequired,
variant: PropTypes.oneOf(['nudge', 'block']),
redirectUrl: PropTypes.string,
};
export default injectIntl(ChangePasswordPrompt);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getAuthService } from '@edx/frontend-platform/auth';
import { Alert, Hyperlink } from '@edx/paragon';
import { Error } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
@@ -16,16 +15,12 @@ import {
INTERNAL_SERVER_ERROR,
INVALID_FORM,
NON_COMPLIANT_PASSWORD_EXCEPTION,
NUDGE_PASSWORD_CHANGE,
REQUIRE_PASSWORD_CHANGE,
} from './data/constants';
import messages from './messages';
import ChangePasswordPrompt from './ChangePasswordPrompt';
const LoginFailureMessage = (props) => {
const { intl } = props;
const { context, errorCode, value } = props.loginError;
const authService = getAuthService();
let errorList;
let link;
let resetLink = (
@@ -123,7 +118,7 @@ const LoginFailureMessage = (props) => {
}
case INCORRECT_EMAIL_PASSWORD:
if (context.failureCount <= 1) {
errorList = <p>{intl.formatMessage(messages['login.incorrect.credentials.error'])}</p>;
errorList = intl.formatMessage(messages['login.incorrect.credentials.error']);
} else if (context.failureCount === 2) {
errorList = (
<p>
@@ -136,19 +131,6 @@ const LoginFailureMessage = (props) => {
);
}
break;
case NUDGE_PASSWORD_CHANGE:
// Need to clear the CSRF token here to fetch a new one because token is already rotated after successful login.
if (authService) {
authService.getCsrfTokenService().clearCsrfTokenCache();
}
return (
<ChangePasswordPrompt
redirectUrl={props.loginError.redirectUrl}
variant="nudge"
/>
);
case REQUIRE_PASSWORD_CHANGE:
return <ChangePasswordPrompt />;
default:
// TODO: use errorCode instead of processing error messages on frontend
errorList = value.trim().split('\n');
@@ -183,7 +165,6 @@ const LoginFailureMessage = (props) => {
LoginFailureMessage.defaultProps = {
loginError: {
redirectUrl: null,
errorCode: null,
value: '',
},
@@ -195,7 +176,6 @@ LoginFailureMessage.propTypes = {
email: PropTypes.string,
errorCode: PropTypes.string,
value: PropTypes.string,
redirectUrl: PropTypes.string,
}),
intl: intlShape.isRequired,
};

View File

@@ -224,7 +224,7 @@ class LoginPage extends React.Component {
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
{this.props.resetPassword && !this.props.loginError ? <ResetPasswordSuccess /> : null}
<Form name="sign-in-form" id="sign-in-form">
<Form>
<FormGroup
name="emailOrUsername"
value={this.state.emailOrUsername}
@@ -243,8 +243,6 @@ class LoginPage extends React.Component {
floatingLabel={intl.formatMessage(messages['login.password.label'])}
/>
<StatefulButton
name="sign-in"
id="sign-in"
type="submit"
variant="brand"
className="login-button-width"
@@ -258,7 +256,6 @@ class LoginPage extends React.Component {
/>
<Link
id="forgot-password"
name="forgot-password"
className="btn btn-link font-weight-500 text-body"
to={updatePathWithQueryParams(RESET_PAGE)}
onClick={this.handleForgotPasswordLinkClickEvent}

View File

@@ -7,8 +7,6 @@ export const FORBIDDEN_REQUEST = 'forbidden-request';
export const FAILED_LOGIN_ATTEMPT = 'failed-login-attempt';
export const ACCOUNT_LOCKED_OUT = 'account-locked-out';
export const INCORRECT_EMAIL_PASSWORD = 'incorrect-email-or-password';
export const NUDGE_PASSWORD_CHANGE = 'nudge-password-change';
export const REQUIRE_PASSWORD_CHANGE = 'require-password-change';
// Account Activation Message
export const ACCOUNT_ACTIVATION_MESSAGE = {

View File

@@ -1,6 +1,6 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import * as QueryString from 'query-string';
import querystring from 'querystring';
// eslint-disable-next-line import/prefer-default-export
export async function loginRequest(creds) {
@@ -12,7 +12,7 @@ export async function loginRequest(creds) {
const { data } = await getAuthenticatedHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/api/user/v2/account/login_session/`,
QueryString.stringify(creds),
querystring.stringify(creds),
requestConfig,
)
.catch((e) => {

View File

@@ -20,7 +20,7 @@ const messages = defineMessages({
'sign.in.button': {
id: 'sign.in.button',
defaultMessage: 'Sign in',
description: 'Sign in button label that appears on login page',
description: 'Button label that appears on login page',
},
'sign.in.btn.pending.state': {
id: 'sign.in.btn.pending.state',
@@ -155,8 +155,8 @@ const messages = defineMessages({
defaultMessage: 'You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.',
description: 'Message show to learners when their account has been activated successfully',
},
'account.activation.info.message': {
id: 'account.activation.info.message',
'account.already.activated.message': {
id: 'account.already.activated.message',
defaultMessage: 'This account has already been activated.',
description: 'Message shown when learner account has already been activated',
},
@@ -170,27 +170,6 @@ const messages = defineMessages({
defaultMessage: 'contact support',
description: 'Link text used in account activation error message to go to learner help center',
},
// Email Confirmation Strings
'account.confirmation.success.message.title': {
id: 'account.confirmation.success.message.title',
defaultMessage: 'Success! You have confirmed your email.',
description: 'Account verification success message title',
},
'account.confirmation.success.message': {
id: 'account.confirmation.success.message',
defaultMessage: 'Sign in to continue.',
description: 'Message show to learners when their account has been activated successfully',
},
'account.confirmation.info.message': {
id: 'account.confirmation.info.message',
defaultMessage: 'This email has already been confirmed.',
description: 'Message shown when learner account has already been verified',
},
'account.confirmation.error.message.title': {
id: 'account.confirmation.error.message.title',
defaultMessage: 'Your email could not be confirmed',
description: 'Account verification error message title',
},
'internal.server.error.message': {
id: 'internal.server.error.message',
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
@@ -241,39 +220,6 @@ const messages = defineMessages({
defaultMessage: 'click here to reset it.',
description: 'Reset password link text for incorrect email or password credentials before blocking account',
},
// Vulnerable password change prompt
'password.security.nudge.title': {
id: 'password.security.nudge.title',
defaultMessage: 'Password security',
description: 'Title for prompt that nudges user to change their vulnerable password',
},
'password.security.block.title': {
id: 'password.security.block.title',
defaultMessage: 'Password change required',
description: 'Title for prompt that asks user to change their vulnerable password',
},
'password.security.nudge.body': {
id: 'password.security.nudge.body',
defaultMessage: 'Our system detected that your password is vulnerable. '
+ 'We recommend you change it so that your account stays secure.',
description: 'Message copy for prompt that nudges user to change their vulnerable password',
},
'password.security.block.body': {
id: 'password.security.block.body',
defaultMessage: 'Our system detected that your password is vulnerable. '
+ 'Change your password so that your account stays secure.',
description: 'Message copy for prompt that asks user to change their vulnerable password',
},
'password.security.close.button': {
id: 'password.security.close.button',
defaultMessage: 'Close',
description: 'Button to close popup',
},
'password.security.redirect.to.reset.password.button': {
id: 'password.security.redirect.to.reset.password.button',
defaultMessage: 'Reset your password',
description: 'Button to redirect users to Reset Password page',
},
});
export default messages;

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { mergeConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import AccountActivationMessage from '../AccountActivationMessage';
@@ -10,12 +9,6 @@ import { ACCOUNT_ACTIVATION_MESSAGE } from '../data/constants';
const IntlAccountActivationMessage = injectIntl(AccountActivationMessage);
describe('AccountActivationMessage', () => {
beforeEach(() => {
mergeConfig({
MARKETING_EMAILS_OPT_IN: '',
});
});
it('should match account already activated message', () => {
const accountActivationMessage = mount(
<IntlProvider locale="en">
@@ -62,45 +55,3 @@ describe('AccountActivationMessage', () => {
expect(accountActivationMessage).toEqual({});
});
});
describe('EmailConfirmationMessage', () => {
beforeEach(() => {
mergeConfig({
MARKETING_EMAILS_OPT_IN: 'true',
});
});
it('should match email already confirmed message', () => {
const accountVerificationMessage = mount(
<IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
</IntlProvider>,
);
const expectedMessage = 'This email has already been confirmed.';
expect(accountVerificationMessage.find('#account-activation-message').find('div').first().text()).toEqual(expectedMessage);
});
it('should match email confirmation success message', () => {
const accountVerificationMessage = mount(
<IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
</IntlProvider>,
);
const expectedMessage = 'Success! You have confirmed your email.Sign in to continue.';
expect(accountVerificationMessage.find('#account-activation-message').first().text()).toEqual(expectedMessage);
});
it('should match email confirmation error message', () => {
const accountVerificationMessage = mount(
<IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
</IntlProvider>,
);
const expectedMessage = 'Your email could not be confirmed'
+ 'Something went wrong, please contact support to resolve this issue.';
expect(accountVerificationMessage.find('#account-activation-message').first().text()).toEqual(expectedMessage);
});
});

View File

@@ -1,73 +0,0 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import { MemoryRouter, Router } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import ChangePasswordPrompt from '../ChangePasswordPrompt';
import { RESET_PAGE } from '../../data/constants';
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
const history = createMemoryHistory();
describe('ChangePasswordPromptTests', () => {
let props = {};
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: query,
})),
});
});
it('[nudge modal] should redirect to next url when user clicks close button', () => {
const dashboardUrl = getConfig().BASE_URL.concat('/dashboard');
props = {
variant: 'nudge',
redirectUrl: dashboardUrl,
};
delete window.location;
window.location = { href: getConfig().BASE_URL };
const changePasswordPrompt = mount(
<IntlProvider locale="en">
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);
changePasswordPrompt.find('button#password-security-close').simulate('click');
expect(window.location.href).toBe(dashboardUrl);
});
it('[block modal] should redirect to reset password page when user clicks outside modal', async () => {
props = {
variant: 'block',
};
const changePasswordPrompt = mount(
<IntlProvider locale="en">
<MemoryRouter>
<Router history={history}>
<IntlChangePasswordPrompt {...props} />
</Router>
</MemoryRouter>
</IntlProvider>,
);
await act(async () => {
await changePasswordPrompt.find('div.pgn__modal-backdrop').first().simulate('click');
});
changePasswordPrompt.update();
expect(history.location.pathname).toEqual(RESET_PAGE);
});
});

View File

@@ -1,9 +1,7 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import * as auth from '@edx/frontend-platform/auth';
import LoginFailureMessage from '../LoginFailure';
import {
@@ -14,27 +12,13 @@ import {
NON_COMPLIANT_PASSWORD_EXCEPTION,
FAILED_LOGIN_ATTEMPT,
INCORRECT_EMAIL_PASSWORD,
NUDGE_PASSWORD_CHANGE,
REQUIRE_PASSWORD_CHANGE,
} from '../data/constants';
jest.mock('@edx/frontend-platform/auth');
auth.getAuthService = jest.fn();
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
describe('LoginFailureMessage', () => {
let props = {};
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: query,
})),
});
});
it('should match non compliant password error message', () => {
props = {
loginError: {
@@ -237,48 +221,4 @@ describe('LoginFailureMessage', () => {
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
expect(loginFailureMessage.find('#login-failure-alert').find('a').props().href).toEqual('/reset');
});
it('should show modal that nudges users to change password', () => {
props = {
loginError: {
errorCode: NUDGE_PASSWORD_CHANGE,
},
};
const loginFailureMessage = mount(
<IntlProvider locale="en">
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);
expect(loginFailureMessage.find('.pgn__modal-title').text()).toEqual('Password security');
expect(loginFailureMessage.find('.pgn__modal-body').text()).toEqual(
'Our system detected that your password is vulnerable. '
+ 'We recommend you change it so that your account stays secure.',
);
});
it('should show modal that requires users to change password', () => {
props = {
loginError: {
errorCode: REQUIRE_PASSWORD_CHANGE,
},
};
const loginFailureMessage = mount(
<IntlProvider locale="en">
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);
expect(loginFailureMessage.find('.pgn__modal-title').text()).toEqual('Password change required');
expect(loginFailureMessage.find('.pgn__modal-body').text()).toEqual(
'Our system detected that your password is vulnerable. '
+ 'Change your password so that your account stays secure.',
);
});
});

View File

@@ -9,7 +9,6 @@ import renderer from 'react-test-renderer';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { loginRequest, loginRequestFailure, loginRequestReset } from '../data/actions';
@@ -19,11 +18,9 @@ import LoginPage from '../LoginPage';
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-platform/auth');
analytics.sendTrackEvent = jest.fn();
analytics.sendPageEvent = jest.fn();
auth.getAuthService = jest.fn();
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
const IntlLoginPage = injectIntl(LoginPage);
@@ -218,8 +215,8 @@ describe('LoginPage', () => {
it('should match account activation message', () => {
const activationMessage = 'Success! You have activated your account.'
+ 'You will now receive email updates and alerts from us related '
+ 'to the courses you are enrolled in. Sign in to continue.';
+ 'You will now receive email updates and alerts from us related '
+ 'to the courses you are enrolled in. Sign in to continue.';
delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?account_activation_status=success' };
@@ -241,9 +238,8 @@ describe('LoginPage', () => {
},
});
const expectedMessage = `${'You have successfully signed into Apple, but your Apple account does not have a '
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
getConfig().SITE_NAME } password.`;
const expectedMessage = 'You have successfully signed into Apple, but your Apple account does not have a '
+ 'linked edX account. To link your accounts, sign in now using your edX password.';
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage);
@@ -266,14 +262,14 @@ describe('LoginPage', () => {
// ******** test redirection ********
it('should redirect to url returned by login endpoint', () => {
const dashboardUrl = 'http://localhost:18000/enterprise/select/active/?success_url=/dashboard';
const dasboardUrl = 'http://localhost:18000/enterprise/select/active/?success_url=/dashboard';
store = mockStore({
...initialState,
login: {
...initialState.login,
loginResult: {
success: true,
redirectUrl: dashboardUrl,
redirectUrl: dasboardUrl,
},
},
});
@@ -281,7 +277,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(dashboardUrl);
expect(window.location.href).toBe(dasboardUrl);
});
it('should redirect to finishAuthUrl upon successful login via SSO', () => {

View File

@@ -176,12 +176,11 @@ class CountryDropdown extends React.Component {
render() {
return (
<div className="mb-4">
<div>
<FormGroup
as="input"
name={this.props.name}
readOnly={this.props.readOnly}
autoComplete="chrome-off"
autoComplete="off"
className="mb-0"
floatingLabel={this.props.floatingLabel}
trailingElement={this.state.icon}
@@ -209,7 +208,6 @@ CountryDropdown.defaultProps = {
value: null,
errorMessage: null,
errorCode: null,
readOnly: false,
};
CountryDropdown.propTypes = {
@@ -224,7 +222,6 @@ CountryDropdown.propTypes = {
errorMessage: PropTypes.string,
errorCode: PropTypes.string,
name: PropTypes.string.isRequired,
readOnly: PropTypes.bool,
};
export default onClickOutside(CountryDropdown);

View File

@@ -1,88 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, Form } from '@edx/paragon';
import messages from './messages';
const HonorCode = (props) => {
const {
intl, errorMessage, onChangeHandler, fieldType, value,
} = props;
if (fieldType === 'tos_and_honor_code') {
return (
<div id="honor-code" className="micro text-muted mt-4">
<FormattedMessage
id="register.page.terms.of.service.and.honor.code"
defaultMessage="By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each
Member process your personal data in accordance with the {privacyPolicy}."
description="Text that appears on registration form stating honor code and privacy policy"
values={{
platformName: getConfig().SITE_NAME,
tosAndHonorCode: (
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
</Hyperlink>
),
privacyPolicy: (
<Hyperlink variant="muted" destination={getConfig().PRIVACY_POLICY || '#'} target="_blank">
{intl.formatMessage(messages['privacy.policy'])}
</Hyperlink>
),
}}
/>
</div>
);
}
return (
<div id="honor-code" className="micro text-muted">
<Form.Checkbox
className="opt-checkbox mt-1"
id="honor-code"
checked={value}
name="honor_code"
value={value}
onChange={onChangeHandler}
>
<FormattedMessage
id="register.page.honor.code"
defaultMessage="I agree to the {platformName} {tosAndHonorCode}"
description="Text that appears on registration form stating honor code"
values={{
platformName: getConfig().SITE_NAME,
tosAndHonorCode: (
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
{intl.formatMessage(messages['honor.code'])}
</Hyperlink>
),
}}
/>
</Form.Checkbox>
{errorMessage && (
<Form.Control.Feedback type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</div>
);
};
HonorCode.defaultProps = {
errorMessage: '',
onChangeHandler: null,
fieldType: 'honor_code',
value: false,
};
HonorCode.propTypes = {
intl: intlShape.isRequired,
errorMessage: PropTypes.string,
onChangeHandler: PropTypes.func,
fieldType: PropTypes.string,
value: PropTypes.bool,
};
export default injectIntl(HonorCode);

View File

@@ -0,0 +1,104 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form, Icon } from '@edx/paragon';
import { ExpandMore } from '@edx/paragon/icons';
import { EDUCATION_LEVELS, GENDER_OPTIONS, YEAR_OF_BIRTH_OPTIONS } from './data/constants';
import messages from './messages';
const OptionalFields = (props) => {
const {
intl, optionalFields, onChangeHandler, values,
} = props;
const getOptions = () => ({
yearOfBirthOptions: YEAR_OF_BIRTH_OPTIONS.map(({ value, label }) => (
<option className="data-hj-suppress" key={value} value={value}>{label}</option>
)),
educationLevelOptions: EDUCATION_LEVELS.map(key => (
<option className="data-hj-suppress" key={key} value={key}>
{intl.formatMessage(messages[`registration.field.education.levels.${key || 'label'}`])}
</option>
)),
genderOptions: GENDER_OPTIONS.map(key => (
<option className="data-hj-suppress" key={key} value={key}>
{intl.formatMessage(messages[`registration.field.gender.options.${key || 'label'}`])}
</option>
)),
});
return (
<div className="mt-3">
{optionalFields.includes('gender') && (
<Form.Group controlId="gender">
<Form.Control
as="select"
name="gender"
value={values.gender}
onChange={(e) => onChangeHandler('gender', e.target.value)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={intl.formatMessage(messages['registration.field.gender.options.label'])}
>
{getOptions().genderOptions}
</Form.Control>
</Form.Group>
)}
{optionalFields.includes('yearOfBirth') && (
<Form.Group controlId="yearOfBirth">
<Form.Control
as="select"
name="yearOfBirth"
value={values.yearOfBirth}
onChange={(e) => onChangeHandler('yearOfBirth', e.target.value)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={intl.formatMessage(messages['registration.year.of.birth.label'])}
>
<option value="">{intl.formatMessage(messages['registration.year.of.birth.label'])}</option>
{getOptions().yearOfBirthOptions}
</Form.Control>
</Form.Group>
)}
{optionalFields.includes('levelOfEducation') && (
<Form.Group controlId="levelOfEducation">
<Form.Control
as="select"
name="levelOfEducation"
value={values.levelOfEducation}
onChange={(e) => onChangeHandler('levelOfEducation', e.target.value)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={intl.formatMessage(messages['registration.field.education.levels.label'])}
>
{getOptions().educationLevelOptions}
</Form.Control>
</Form.Group>
)}
{optionalFields.includes('goals') && (
<Form.Group controlId="goals">
<Form.Control
as="textarea"
name="goals"
value={values.goals}
onChange={(e) => onChangeHandler('goals', e.target.value)}
floatingLabel={intl.formatMessage(messages['registration.goals.label'])}
/>
</Form.Group>
)}
</div>
);
};
OptionalFields.propTypes = {
intl: intlShape.isRequired,
optionalFields: PropTypes.arrayOf(PropTypes.string).isRequired,
onChangeHandler: PropTypes.func.isRequired,
values: PropTypes.shape({
gender: PropTypes.string,
goals: PropTypes.string,
levelOfEducation: PropTypes.string,
yearOfBirth: PropTypes.string,
}).isRequired,
};
export default injectIntl(OptionalFields);

View File

@@ -1,68 +1,59 @@
import React from 'react';
import snakeCase from 'lodash.snakecase';
import { connect } from 'react-redux';
import Skeleton from 'react-loading-skeleton';
import { Helmet } from 'react-helmet';
import PropTypes, { string } from 'prop-types';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
injectIntl, intlShape, getCountryList, getLocale,
injectIntl, intlShape, getCountryList, getLocale, FormattedMessage,
} from '@edx/frontend-platform/i18n';
import {
Alert, Form, StatefulButton, Icon,
Alert, Form, Hyperlink, StatefulButton, Icon,
} from '@edx/paragon';
import { Error, Close } from '@edx/paragon/icons';
import FormFieldRenderer from '../field-renderer';
import {
clearUsernameSuggestions, registerNewUser, resetRegistrationForm, fetchRealtimeValidations,
} from './data/actions';
import {
FIELDS, FORM_SUBMISSION_ERROR, DEFAULT_SERVICE_PROVIDER_DOMAINS, DEFAULT_TOP_LEVEL_DOMAINS, COMMON_EMAIL_PROVIDERS,
FORM_SUBMISSION_ERROR, DEFAULT_SERVICE_PROVIDER_DOMAINS, DEFAULT_TOP_LEVEL_DOMAINS, COMMON_EMAIL_PROVIDERS,
} from './data/constants';
import {
registrationErrorSelector,
registrationRequestSelector,
validationsSelector,
usernameSuggestionsSelector,
registrationErrorSelector, registrationRequestSelector, validationsSelector, usernameSuggestionsSelector,
} from './data/selectors';
import messages from './messages';
import OptionalFields from './OptionalFields';
import RegistrationFailure from './RegistrationFailure';
import UsernameField from './UsernameField';
import HonorCode from './HonorCode';
import {
RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton,
InstitutionLogistration, FormGroup, PasswordField,
} from '../common-components';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import {
extendedProfileSelector,
fieldDescriptionSelector,
thirdPartyAuthContextSelector,
} from '../common-components/data/selectors';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import {
DEFAULT_STATE, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, LETTER_REGEX, NUMBER_REGEX, VALID_NAME_REGEX,
DEFAULT_STATE, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, LETTER_REGEX, NUMBER_REGEX,
} from '../data/constants';
import {
getTpaProvider, getTpaHint, getAllPossibleQueryParam, setSurveyCookie, setCookie,
} from '../data/utils';
import CountryDropdown from './CountryDropdown';
import { getLevenshteinSuggestion, getSuggestionForInvalidEmail } from './utils';
import TermsOfService from './TermsOfService';
class RegistrationPage extends React.Component {
constructor(props, context) {
super(props, context);
sendPageEvent('login_and_registration', 'register');
const optionalFields = getConfig().REGISTRATION_OPTIONAL_FIELDS ? getConfig().REGISTRATION_OPTIONAL_FIELDS.split(',') : [];
this.handleOnClose = this.handleOnClose.bind(this);
this.queryParams = getAllPossibleQueryParam();
// TODO: Once we have tested it and ready for openedX we can remove this flag and make the code
// permanent part of Authn and remove extra code
this.showDynamicRegistrationFields = getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS;
this.tpaHint = getTpaHint();
this.state = {
country: '',
@@ -70,7 +61,6 @@ class RegistrationPage extends React.Component {
name: '',
password: '',
username: '',
marketingOptIn: true,
errors: {
email: '',
name: '',
@@ -82,17 +72,16 @@ class RegistrationPage extends React.Component {
emailWarningSuggestion: null,
errorCode: null,
failureCount: 0,
optionalFields,
optionalFieldsState: {},
showOptionalField: false,
startTime: Date.now(),
totalRegistrationTime: 0,
optimizelyExperimentName: '',
readOnly: true,
validatePassword: false,
values: {},
optimizelyExperimentName: '', // eslint-disable-line react/no-unused-state
};
}
componentDidMount() {
const payload = { ...this.queryParams };
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'page',
@@ -100,32 +89,19 @@ class RegistrationPage extends React.Component {
isActive: true,
});
if (payload.save_for_later === 'true') {
sendTrackEvent('edx.bi.user.saveforlater.course.enroll.clicked', { category: 'save-for-later' });
}
const payload = { ...this.queryParams };
if (this.tpaHint) {
payload.tpa_hint = this.tpaHint;
}
this.props.resetRegistrationForm();
this.props.getThirdPartyAuthContext(payload);
this.getExperiments();
}
shouldComponentUpdate(nextProps) {
if (this.props.usernameSuggestions.length > 0 && this.state.username === '') {
this.setState({
username: ' ',
});
return false;
}
if (this.props.validationDecisions !== nextProps.validationDecisions) {
const state = { errors: { ...this.state.errors, ...nextProps.validationDecisions } };
let validatePassword = false;
if (state.errors.password) {
validatePassword = true;
}
if (nextProps.registrationErrorCode) {
state.errorCode = nextProps.registrationErrorCode;
}
@@ -148,7 +124,6 @@ class RegistrationPage extends React.Component {
suggestedTopLevelDomain,
suggestedSldMessage,
suggestedServiceLevelDomain,
validatePassword,
});
return false;
}
@@ -177,35 +152,40 @@ class RegistrationPage extends React.Component {
}
getExperiments = () => {
const { experimentName } = window;
const { optimizelyExperimentName } = window;
if (experimentName) {
this.setState({ optimizelyExperimentName: experimentName });
if (optimizelyExperimentName) {
// eslint-disable-next-line react/no-unused-state
this.setState({ optimizelyExperimentName });
}
};
onChangeHandler = (e) => {
const { name, value, checked } = e.target;
const { errors, values } = this.state;
if (e.target.type === 'checkbox') {
errors[name] = '';
values[name] = checked;
} else {
values[name] = value;
}
const state = { errors, values };
this.setState({ ...state });
};
getOptionalFields() {
return (
<OptionalFields
optionalFields={this.state.optionalFields}
values={this.state.optionalFieldsState}
onChangeHandler={
(fieldName, value) => {
this.setState(prevState => ({
optionalFieldsState: { ...prevState.optionalFieldsState, [fieldName]: value },
}));
}
}
/>
);
}
handleSubmit = (e) => {
e.preventDefault();
const { startTime } = this.state;
const totalRegistrationTime = (Date.now() - startTime) / 1000;
const dynamicFieldErrorMessages = {};
let payload = {
name: this.state.name,
username: this.state.username,
email: this.state.email,
country: this.state.country,
honor_code: true,
is_authn_mfe: true,
};
@@ -215,28 +195,12 @@ class RegistrationPage extends React.Component {
payload.password = this.state.password;
}
if (this.showDynamicRegistrationFields) {
payload.extendedProfile = [];
Object.keys(this.props.fieldDescriptions).forEach((fieldName) => {
if (this.props.extendedProfile.includes(fieldName)) {
payload.extendedProfile.push({ fieldName, fieldValue: this.state.values[fieldName] });
} else {
payload[fieldName] = this.state.values[fieldName];
}
dynamicFieldErrorMessages[fieldName] = this.props.fieldDescriptions[fieldName].error_message;
});
if (
this.props.fieldDescriptions[FIELDS.HONOR_CODE]
&& this.props.fieldDescriptions[FIELDS.HONOR_CODE].type === 'tos_and_honor_code'
) {
payload[FIELDS.HONOR_CODE] = true;
}
} else {
payload.country = this.state.country;
payload.honor_code = true;
}
let errors = {};
Object.keys(payload).forEach(key => {
errors = this.validateInput(key, payload[key], { ...payload, form_field_key: key });
});
if (!this.isFormValid(payload, dynamicFieldErrorMessages)) {
if (!this.isFormValid(errors)) {
this.setState(prevState => ({
errorCode: FORM_SUBMISSION_ERROR,
failureCount: prevState.failureCount + 1,
@@ -244,11 +208,15 @@ class RegistrationPage extends React.Component {
return;
}
if (getConfig().MARKETING_EMAILS_OPT_IN) {
payload.marketing_emails_opt_in = this.state.marketingOptIn;
}
// Since optional fields and query params are not validated we can add it to payload after
// required fields have been validated. This will save us unwanted calls to validateInput()
payload = { ...payload, ...this.queryParams };
this.state.optionalFields.forEach((key) => {
if (this.state.optionalFieldsState[key]) {
payload[snakeCase(key)] = this.state.optionalFieldsState[key];
}
});
payload = snakeCaseObject(payload);
payload.totalRegistrationTime = totalRegistrationTime;
this.setState({
totalRegistrationTime,
@@ -277,7 +245,12 @@ class RegistrationPage extends React.Component {
}
handleOnChange = (e) => {
if (!(e.target.name === 'username' && e.target.value.length > 30)) {
if (e.target.name === 'optionalFields') {
sendTrackEvent('edx.bi.user.register.optional_fields_selected', {});
this.setState({
showOptionalField: e.target.checked,
});
} else if (!(e.target.name === 'username' && e.target.value.length > 30)) {
this.setState({
[e.target.name]: e.target.value,
});
@@ -291,9 +264,6 @@ class RegistrationPage extends React.Component {
if (e.target.name === 'username') {
this.props.clearUsernameSuggestions();
}
if (e.target.name === 'country') {
state.readOnly = false;
}
if (e.target.name === 'passwordValidation') {
state.errors.password = '';
}
@@ -322,45 +292,15 @@ class RegistrationPage extends React.Component {
}
}
handleUsernameSuggestionClose = () => {
this.setState({
username: '',
});
this.props.clearUsernameSuggestions();
}
validateDynamicFields = (e) => {
const { errors } = this.state;
const { name, value } = e.target;
if (!value) {
errors[name] = this.props.fieldDescriptions[name].error_message;
}
this.setState({ errors });
}
isFormValid(payload, dynamicFieldError) {
const { errors } = this.state;
let isValid = true;
Object.keys(payload).forEach(key => {
if (!payload[key]) {
errors[key] = (key in dynamicFieldError) ? dynamicFieldError[key] : this.props.intl.formatMessage(messages[`empty.${key}.field.error`]);
}
// Mark form invalid, if there was already a validation error for this key or we added empty field error
if (errors[key]) {
isValid = false;
}
});
this.setState({ errors });
return isValid;
isFormValid(validations) {
const keyValidList = Object.entries(validations).map(([key]) => !validations[key]);
return keyValidList.every((current) => current === true);
}
validateInput(fieldName, value, payload) {
const { errors } = this.state;
const { intl, statusCode } = this.props;
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
const urlRegex = new RegExp(VALID_NAME_REGEX);
switch (fieldName) {
case 'email':
@@ -419,22 +359,11 @@ class RegistrationPage extends React.Component {
case 'name':
if (!value) {
errors.name = intl.formatMessage(messages['empty.name.field.error']);
} else if (value && value.match(urlRegex)) {
errors.name = intl.formatMessage(messages['name.validation.message']);
} else {
errors.name = '';
}
if (!this.state.username.trim() && value) {
// fetch username suggestions based on the full name
this.props.fetchRealtimeValidations(payload);
}
break;
case 'username':
if (value === ' ' && this.props.usernameSuggestions.length > 0) {
errors.username = '';
break;
}
if (!value || value.length <= 1 || value.length > 30) {
errors.username = intl.formatMessage(messages['username.validation.message']);
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
@@ -444,16 +373,14 @@ class RegistrationPage extends React.Component {
} else {
errors.username = '';
}
if (this.state.validatePassword) {
this.props.fetchRealtimeValidations({ ...payload, form_field_key: 'password' });
}
break;
case 'password':
errors.password = '';
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
errors.password = intl.formatMessage(messages['password.validation.message']);
} else if (payload && statusCode !== 403) {
this.props.fetchRealtimeValidations(payload);
} else {
errors.password = '';
}
break;
case 'country':
@@ -577,73 +504,6 @@ class RegistrationPage extends React.Component {
});
}
const honorCode = [];
const formFields = this.showDynamicRegistrationFields ? (
Object.keys(this.props.fieldDescriptions).map((fieldName) => {
const fieldData = this.props.fieldDescriptions[fieldName];
switch (fieldData.name) {
case FIELDS.COUNTRY:
return (
<span key={fieldData.name}>
<CountryDropdown
name="country"
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
options={getCountryList(getLocale())}
valueKey="code"
displayValueKey="name"
value={this.state.values[fieldData.name]}
handleBlur={this.handleOnBlur}
handleFocus={this.handleOnFocus}
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
handleChange={
(value) => this.setState(prevState => ({ values: { ...prevState.values, country: value } }))
}
errorCode={this.state.errorCode}
readOnly={this.state.readOnly}
/>
</span>
);
case FIELDS.HONOR_CODE:
honorCode.push(
<span key={fieldData.name}>
<HonorCode
fieldType={fieldData.type}
value={this.state.values[fieldData.name]}
onChangeHandler={this.onChangeHandler}
errorMessage={this.state.errors[fieldData.name]}
/>
</span>,
);
return null;
case FIELDS.TERMS_OF_SERVICE:
honorCode.push(
<span key={fieldData.name}>
<TermsOfService
value={this.state.values[fieldData.name]}
onChangeHandler={this.onChangeHandler}
errorMessage={this.state.errors[fieldData.name]}
/>
</span>,
);
return null;
default:
return (
<span key={fieldData.name}>
<FormFieldRenderer
fieldData={fieldData}
value={this.state.values[fieldData.name]}
onChangeHandler={this.onChangeHandler}
handleBlur={this.validateDynamicFields}
handleFocus={this.handleOnFocus}
errorMessage={this.state.errors[fieldData.name]}
isRequired
/>
</span>
);
}
})
) : null;
return (
<>
<Helmet>
@@ -675,7 +535,7 @@ class RegistrationPage extends React.Component {
<h4 className="mt-4 mb-4">{intl.formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
</>
)}
<Form id="registration-form" name="registration-form">
<Form>
<FormGroup
name="name"
value={this.state.name}
@@ -686,6 +546,18 @@ class RegistrationPage extends React.Component {
helpText={[intl.formatMessage(messages['help.text.name'])]}
floatingLabel={intl.formatMessage(messages['registration.fullname.label'])}
/>
<UsernameField
name="username"
value={this.state.username}
handleBlur={this.handleOnBlur}
handleChange={this.handleOnChange}
handleFocus={this.handleOnFocus}
errorMessage={this.state.errors.username}
helpText={[intl.formatMessage(messages['help.text.username.1']), intl.formatMessage(messages['help.text.username.2'])]}
floatingLabel={intl.formatMessage(messages['registration.username.label'])}
handleSuggestionClick={this.handleSuggestionClick}
usernameSuggestions={this.props.usernameSuggestions}
/>
<FormGroup
name="email"
value={this.state.email}
@@ -700,20 +572,6 @@ class RegistrationPage extends React.Component {
{this.renderEmailFeedback()}
</FormGroup>
<UsernameField
name="username"
value={this.state.username}
handleBlur={this.handleOnBlur}
handleChange={this.handleOnChange}
handleFocus={this.handleOnFocus}
errorMessage={this.state.errors.username}
helpText={[intl.formatMessage(messages['help.text.username.1']), intl.formatMessage(messages['help.text.username.2'])]}
floatingLabel={intl.formatMessage(messages['registration.username.label'])}
handleSuggestionClick={this.handleSuggestionClick}
usernameSuggestions={this.props.usernameSuggestions}
handleUsernameSuggestionClose={this.handleUsernameSuggestionClose}
/>
{!currentProvider && (
<PasswordField
name="password"
@@ -725,49 +583,61 @@ class RegistrationPage extends React.Component {
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
/>
)}
{!(this.showDynamicRegistrationFields)
&& (
<CountryDropdown
name="country"
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
options={getCountryList(getLocale())}
valueKey="code"
displayValueKey="name"
value={this.state.country}
handleBlur={this.handleOnBlur}
handleFocus={this.handleOnFocus}
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
handleChange={(value) => this.setState({ country: value })}
errorCode={this.state.errorCode}
readOnly={this.state.readOnly}
<CountryDropdown
name="country"
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
options={getCountryList(getLocale())}
valueKey="code"
displayValueKey="name"
value={this.state.country}
handleBlur={this.handleOnBlur}
handleFocus={this.handleOnFocus}
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
handleChange={(value) => this.setState({ country: value })}
errorCode={this.state.errorCode}
/>
<div id="honor-code" className="micro text-muted mt-4">
<FormattedMessage
id="register.page.terms.of.service.and.honor.code"
defaultMessage="By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each
Member process your personal data in accordance with the {privacyPolicy}."
description="Text that appears on registration form stating honor code and privacy policy"
values={{
platformName: getConfig().SITE_NAME,
tosAndHonorCode: (
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
</Hyperlink>
),
privacyPolicy: (
<Hyperlink variant="muted" destination={getConfig().PRIVACY_POLICY || '#'} target="_blank">
{intl.formatMessage(messages['privacy.policy'])}
</Hyperlink>
),
}}
/>
)}
{formFields}
{(getConfig().MARKETING_EMAILS_OPT_IN)
&& (
<Form.Checkbox
className="opt-checkbox"
name="marketing_emails_opt_in"
checked={this.state.marketingOptIn}
onChange={(e) => this.setState({ marketingOptIn: e.target.checked })}
>
{intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME })}
</Form.Checkbox>
)}
{!(this.showDynamicRegistrationFields) ? (
<HonorCode
fieldType="tos_and_honor_code"
/>
) : <div className="mt-4">{honorCode}</div>}
</div>
{getConfig().REGISTRATION_OPTIONAL_FIELDS ? (
<Form.Group className="mb-0 mt-2 small">
<Form.Check
id="optional-field-checkbox"
type="checkbox"
name="optionalFields"
value={this.state.showOptionalField}
onClick={this.handleOnChange}
onChange={this.handleOnChange}
label={intl.formatMessage(messages['support.education.research'])}
/>
</Form.Group>
) : null}
{ this.state.showOptionalField ? this.getOptionalFields() : null }
<StatefulButton
name="register-user"
id="register-user"
type="submit"
variant="brand"
className="register-stateful-button-width mt-4 mb-4"
className="stateful-button-width mt-4 mb-4"
state={submitState}
labels={{
default: intl.formatMessage(messages['create.account.for.free.button']),
default: intl.formatMessage(messages['create.account.button']),
pending: '',
}}
onClick={this.handleSubmit}
@@ -779,13 +649,6 @@ class RegistrationPage extends React.Component {
thirdPartyAuthApiStatus,
intl)}
</Form>
{(this.state.optimizelyExperimentName === 'variation1' || this.state.optimizelyExperimentName === 'variation2')
? (
<div id="certificate-msg" className="mt-4 mb-3 micro text-gray-500">
{intl.formatMessage(messages['certificate.msg'])}
</div>
)
: null}
</div>
</>
);
@@ -830,8 +693,6 @@ class RegistrationPage extends React.Component {
}
RegistrationPage.defaultProps = {
extendedProfile: [],
fieldDescriptions: {},
registrationResult: null,
registerNewUser: null,
registrationErrorCode: null,
@@ -851,8 +712,6 @@ RegistrationPage.defaultProps = {
};
RegistrationPage.propTypes = {
extendedProfile: PropTypes.arrayOf(PropTypes.string),
fieldDescriptions: PropTypes.shape({}),
intl: intlShape.isRequired,
getThirdPartyAuthContext: PropTypes.func.isRequired,
registerNewUser: PropTypes.func,
@@ -906,18 +765,16 @@ const mapStateToProps = state => {
validationDecisions: validationsSelector(state),
statusCode: state.register.statusCode,
usernameSuggestions: usernameSuggestionsSelector(state),
fieldDescriptions: fieldDescriptionSelector(state),
extendedProfile: extendedProfileSelector(state),
};
};
export default connect(
mapStateToProps,
{
clearUsernameSuggestions,
getThirdPartyAuthContext,
fetchRealtimeValidations,
registerNewUser,
resetRegistrationForm,
clearUsernameSuggestions,
},
)(injectIntl(RegistrationPage));

View File

@@ -1,60 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, Form } from '@edx/paragon';
import messages from './messages';
const TermsOfService = (props) => {
const {
intl, errorMessage, onChangeHandler, value,
} = props;
return (
<div id="terms-of-service" className="micro text-muted">
<Form.Checkbox
className="opt-checkbox mt-1"
id="tos"
checked={value}
name="terms_of_service"
value={value}
onChange={onChangeHandler}
>
<FormattedMessage
id="register.page.terms.of.service"
defaultMessage="I agree to the {platformName} {termsOfService}"
description="Text that appears on registration form stating terms of service.
It is a legal document that users must agree to."
values={{
platformName: getConfig().SITE_NAME,
termsOfService: (
<Hyperlink variant="muted" destination={getConfig().TOS_LINK || '#'} target="_blank">
{intl.formatMessage(messages['terms.of.service'])}
</Hyperlink>
),
}}
/>
</Form.Checkbox>
{errorMessage && (
<Form.Control.Feedback type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</div>
);
};
TermsOfService.defaultProps = {
errorMessage: '',
value: false,
};
TermsOfService.propTypes = {
intl: intlShape.isRequired,
errorMessage: PropTypes.string,
onChangeHandler: PropTypes.func.isRequired,
value: PropTypes.bool,
};
export default injectIntl(TermsOfService);

View File

@@ -2,51 +2,33 @@ import React from 'react';
import PropTypes, { string } from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, IconButton, Icon } from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import { Button } from '@edx/paragon';
import { FormGroup } from '../common-components';
import messages from './messages';
const UsernameField = (props) => {
const { intl, usernameSuggestions, errorMessage } = props;
let className = '';
let suggestedUsernameDiv = <></>;
let iconButton = <></>;
const suggestedUsernames = () => (
<div className={className}>
<span className="text-gray username-suggestion-label">{intl.formatMessage(messages['registration.username.suggestion.label'])}</span>
<div className="scroll-suggested-username">
{usernameSuggestions.map((username, index) => (
<Button
type="button"
name="username"
variant="outline-dark"
className="username-suggestion data-hj-suppress"
key={`suggestion-${index.toString()}`}
onClick={(e) => props.handleSuggestionClick(e, username)}
>
{username}
</Button>
))}
</div>
{iconButton}
</div>
);
if (usernameSuggestions.length > 0 && errorMessage && props.value === ' ') {
className = 'suggested-username-with-error';
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => props.handleUsernameSuggestionClose()} variant="black" size="sm" className="suggested-username-close-button" />;
suggestedUsernameDiv = suggestedUsernames();
} else if (usernameSuggestions.length > 0 && props.value === ' ') {
className = 'suggested-username';
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => props.handleUsernameSuggestionClose()} variant="black" size="sm" className="suggested-username-close-button" />;
suggestedUsernameDiv = suggestedUsernames();
} else if (usernameSuggestions.length > 0 && errorMessage) {
suggestedUsernameDiv = suggestedUsernames();
}
return (
<FormGroup {...props}>
{suggestedUsernameDiv}
{usernameSuggestions.length > 0 && errorMessage ? (
<div>
<span className="text-gray username-suggestion-label">{intl.formatMessage(messages['registration.username.suggestion.label'])}</span>
{usernameSuggestions.map((username, index) => (
<Button
type="button"
name="username"
variant="outline-dark"
className="username-suggestion data-hj-suppress"
key={`suggestion-${index.toString()}`}
onClick={(e) => props.handleSuggestionClick(e, username)}
>
{username}
</Button>
))}
</div>
) : <></>}
</FormGroup>
);
};
@@ -54,14 +36,12 @@ const UsernameField = (props) => {
UsernameField.defaultProps = {
usernameSuggestions: [],
handleSuggestionClick: () => {},
handleUsernameSuggestionClose: () => {},
errorMessage: '',
};
UsernameField.propTypes = {
usernameSuggestions: PropTypes.arrayOf(string),
handleSuggestionClick: PropTypes.func,
handleUsernameSuggestionClose: PropTypes.func,
errorMessage: PropTypes.string,
intl: intlShape.isRequired,
name: PropTypes.string.isRequired,

View File

@@ -1,10 +1,3 @@
// Registration Fields
export const FIELDS = {
COUNTRY: 'country',
HONOR_CODE: 'honor_code',
TERMS_OF_SERVICE: 'terms_of_service',
};
// Registration Error Codes
export const FORBIDDEN_REQUEST = 'forbidden-request';
export const FORM_SUBMISSION_ERROR = 'form-submission-error';
@@ -36,16 +29,18 @@ export const EDUCATION_LEVELS = [
'other',
];
export const REGISTRATION_REASONS = ['', 'job', 'learn', 'boss'];
export const GENDER_OPTIONS = ['', 'f', 'm', 'o'];
// Other constants
export const FORM_FIELDS = ['name', 'email', 'password', 'username', 'country'];
export const COMMON_EMAIL_PROVIDERS = [
'hotmail.com', 'yahoo.com', 'outlook.com', 'live.com', 'gmail.com',
'hotmail.com', 'yahoo.com', 'outlook.com', 'live.com', 'gmail.com', 'aol.com',
];
export const DEFAULT_SERVICE_PROVIDER_DOMAINS = ['yahoo', 'hotmail', 'live', 'outlook', 'gmail'];
export const DEFAULT_SERVICE_PROVIDER_DOMAINS = ['yahoo', 'aol', 'hotmail', 'live', 'outlook', 'gmail'];
export const DEFAULT_TOP_LEVEL_DOMAINS = [
'aaa', 'aarp', 'abarth', 'abb', 'abbott', 'abbvie', 'abc', 'able', 'abogado', 'abudhabi', 'ac', 'academy',

View File

@@ -1,14 +1,8 @@
import {
REGISTRATION_FORM,
REGISTER_NEW_USER,
REGISTER_FORM_VALIDATIONS,
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTRATION_FORM, REGISTER_NEW_USER, REGISTER_FORM_VALIDATIONS, REGISTER_CLEAR_USERNAME_SUGGESTIONS,
} from './actions';
import {
DEFAULT_STATE,
PENDING_STATE,
} from '../../data/constants';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
export const defaultState = {
registrationError: {},
@@ -17,9 +11,6 @@ export const defaultState = {
validations: null,
statusCode: null,
usernameSuggestions: [],
extendedProfile: [],
fieldDescriptions: {},
formRenderState: DEFAULT_STATE,
};
const reducer = (state = defaultState, action) => {

View File

@@ -24,6 +24,7 @@ export function* handleNewUserRegistration(action) {
yield put(registerNewUserBegin());
const { redirectUrl, success } = yield call(registerRequest, action.payload.registrationInfo);
yield put(registerNewUserSuccess(
redirectUrl,
success,
@@ -55,6 +56,7 @@ export function* fetchRealtimeValidations(action) {
}
}
}
export default function* saga() {
yield takeEvery(REGISTER_NEW_USER.BASE, handleNewUserRegistration);
yield takeEvery(REGISTER_FORM_VALIDATIONS.BASE, fetchRealtimeValidations);

View File

@@ -1,6 +1,6 @@
import { getConfig } from '@edx/frontend-platform';
import { getHttpClient, getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as QueryString from 'query-string';
import querystring from 'querystring';
export async function registerRequest(registrationInformation) {
const requestConfig = {
@@ -11,7 +11,7 @@ export async function registerRequest(registrationInformation) {
const { data } = await getAuthenticatedHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/api/user/v2/account/registration/`,
QueryString.stringify(registrationInformation),
querystring.stringify(registrationInformation),
requestConfig,
)
.catch((e) => {
@@ -32,7 +32,7 @@ export async function getFieldsValidations(formPayload) {
const { data } = await getHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/api/user/v1/validation/registration`,
QueryString.stringify(formPayload),
querystring.stringify(formPayload),
requestConfig,
)
.catch((e) => {

View File

@@ -14,9 +14,6 @@ describe('register reducer', () => {
validations: null,
statusCode: null,
usernameSuggestions: [],
extendedProfile: [],
fieldDescriptions: {},
formRenderState: DEFAULT_STATE,
},
);
});

View File

@@ -32,11 +32,6 @@ const messages = defineMessages({
defaultMessage: 'Country/Region',
description: 'Placeholder for the country options dropdown.',
},
'registration.opt.in.label': {
id: 'registration.opt.in.label',
defaultMessage: 'I agree that {siteName} may send me marketing messages.',
description: 'Text for opt in option on register page.',
},
// Help text
'help.text.name': {
id: 'help.text.name',
@@ -59,10 +54,10 @@ const messages = defineMessages({
description: 'Help text for email field on registration page',
},
// Form buttons
'create.account.for.free.button': {
id: 'create.account.for.free.button',
defaultMessage: 'Create an account for free',
description: 'Label text for registration form submission button',
'create.account.button': {
id: 'create.account.button',
defaultMessage: 'Create an account',
description: 'Button label that appears on register page',
},
'create.an.account.btn.pending.state': {
id: 'create.an.account.btn.pending.state',
@@ -101,16 +96,6 @@ const messages = defineMessages({
defaultMessage: 'Enter your email',
description: 'Error message for empty email field',
},
'empty.username.field.error': {
id: 'empty.username.field.error',
defaultMessage: 'Username must be between 2 and 30 characters',
description: 'Error message for empty username field',
},
'empty.password.field.error': {
id: 'empty.password.field.error',
defaultMessage: 'Password criteria has not been met',
description: 'Error message for empty password field',
},
'empty.country.field.error': {
id: 'empty.country.field.error',
defaultMessage: 'Select your country or region of residence',
@@ -131,11 +116,6 @@ const messages = defineMessages({
defaultMessage: 'Username must be between 2 and 30 characters',
description: 'Error message for empty username field',
},
'name.validation.message': {
id: 'name.validation.message',
defaultMessage: 'Enter a valid name',
description: 'Validation message that appears when fullname contain URL',
},
'password.validation.message': {
id: 'password.validation.message',
defaultMessage: 'Password criteria has not been met',
@@ -143,7 +123,7 @@ const messages = defineMessages({
},
'username.format.validation.message': {
id: 'username.format.validation.message',
defaultMessage: 'Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.',
defaultMessage: 'Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-).',
description: 'Validation message that appears when username format is invalid',
},
'support.education.research': {
@@ -188,16 +168,6 @@ const messages = defineMessages({
defaultMessage: 'Privacy Policy',
description: 'Text for the hyperlink that redirects user to privacy policy',
},
'honor.code': {
id: 'honor.code',
defaultMessage: 'Honor Code',
description: 'Text for the hyperlink that redirects user to the honor code',
},
'terms.of.service': {
id: 'terms.of.service',
defaultMessage: 'Terms of Service',
description: 'Text for the hyperlink that redirects user to the terms of service',
},
// Optional fields
'registration.year.of.birth.label': {
id: 'registration.year.of.birth.label',
@@ -282,8 +252,8 @@ const messages = defineMessages({
// miscellaneous strings
'registration.username.suggestion.label': {
id: 'registration.username.suggestion.label',
defaultMessage: 'Suggested:',
description: 'Suggested usernames label text.',
defaultMessage: 'Available:',
description: 'Available usernames label text.',
},
'registration.using.tpa.form.heading': {
id: 'registration.using.tpa.form.heading',
@@ -295,11 +265,6 @@ const messages = defineMessages({
defaultMessage: 'Did you mean',
description: 'Did you mean alert suggestion',
},
'certificate.msg': {
id: 'certificate.msg',
defaultMessage: '*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.',
description: 'Text for the 15% discount experiment',
},
});
export default messages;

View File

@@ -1,59 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mergeConfig } from '@edx/frontend-platform';
import HonorCode from '../HonorCode';
const IntlHonorCode = injectIntl(HonorCode);
describe('HonorCodeTest', () => {
mergeConfig({
PRIVACY_POLICY: 'http://privacy-policy.com',
TOS_AND_HONOR_CODE: 'http://tos-and-honot-code.com',
});
let value = false;
const changeHandler = (e) => {
value = e.target.checked;
};
beforeEach(() => {
value = false;
});
it('should render error msg if honor code is not checked', () => {
const honorCode = mount(
<IntlProvider locale="en">
<IntlHonorCode errorMessage="You must agree to the edx Honor Code" onChangeHandler={changeHandler} />
</IntlProvider>,
);
expect(honorCode.find('.form-text-size').last().text()).toEqual('You must agree to the edx Honor Code');
});
it('should render Honor code field', () => {
const expectedMsg = 'I agree to the Your Platform Name Here Honor Codein a new tab';
const honorCode = mount(
<IntlProvider locale="en">
<IntlHonorCode onChangeHandler={changeHandler} />
</IntlProvider>,
);
honorCode.find('#honor-code').last().simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(honorCode.find('#honor-code').find('label').text()).toEqual(expectedMsg);
expect(value).toEqual(true);
});
it('should render Terms of Service and Honor code field', () => {
const HonorCodeProps = mount(
<IntlProvider locale="en">
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
</IntlProvider>,
);
const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Codein a new tab and you '
+ 'acknowledge that Your Platform Name Here and each Member process your personal data in '
+ 'accordance with the Privacy Policyin a new tab.';
const field = HonorCodeProps.find('#honor-code');
expect(field.text()).toEqual(expectedMsg);
});
});

View File

@@ -16,9 +16,7 @@ import {
registerNewUser,
resetRegistrationForm,
} from '../data/actions';
import {
FIELDS, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED,
} from '../data/constants';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED } from '../data/constants';
import RegistrationFailureMessage from '../RegistrationFailure';
import RegistrationPage from '../RegistrationPage';
@@ -36,6 +34,7 @@ const mockStore = configureStore();
describe('RegistrationPage', () => {
mergeConfig({
PRIVACY_POLICY: 'http://privacy-policy.com',
REGISTRATION_OPTIONAL_FIELDS: 'gender,goals,levelOfEducation,yearOfBirth',
TOS_AND_HONOR_CODE: 'http://tos-and-honot-code.com',
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME,
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME,
@@ -208,16 +207,15 @@ describe('RegistrationPage', () => {
it('should update errors for frontend validations', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#name').simulate('blur', { target: { value: 'http://test.com', name: 'name' } });
registrationPage.find('input#password').simulate('blur', { target: { value: 'pas', name: 'password' } });
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual({
email: '', name: 'Enter a valid name', username: '', password: 'Password criteria has not been met', country: '',
email: '', name: '', username: '', password: 'Password criteria has not been met', country: '',
});
registrationPage.find('input#password').simulate('blur', { target: { value: 'invalid-email', name: 'email' } });
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual({
email: 'Enter a valid email address', name: 'Enter a valid name', username: '', password: 'Password criteria has not been met', country: '',
email: 'Enter a valid email address', name: '', username: '', password: 'Password criteria has not been met', country: '',
});
});
@@ -272,14 +270,14 @@ describe('RegistrationPage', () => {
...initialState.register,
registrationError: {
username: [{ userMessage: 'It looks like this username is already taken' }],
email: [{ userMessage: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account` }],
email: [{ userMessage: 'It looks like this email address is already registered' }],
},
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
expect(registrationPage.prop('validationDecisions')).toEqual({
country: '',
email: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account`,
email: 'It looks like this email address is already registered',
name: '',
password: '',
username: 'It looks like this username is already taken',
@@ -336,12 +334,6 @@ describe('RegistrationPage', () => {
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
});
it('should set readOnly state false if focus on country field', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#country').simulate('focus');
expect(registrationPage.find('RegistrationPage').state('readOnly')).toEqual(false);
});
// ******** test alert messages ********
it('should match third party auth alert', () => {
@@ -356,8 +348,8 @@ describe('RegistrationPage', () => {
},
});
const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before '
+ 'you start learning with '}${ getConfig().SITE_NAME }.`;
const expectedMessage = 'You\'ve successfully signed into Apple! We just need a little more information before '
+ 'you start learning with edX.';
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registerPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage);
@@ -406,7 +398,7 @@ describe('RegistrationPage', () => {
it('should match default button state', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account for free');
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account');
});
it('should match pending button state', () => {
@@ -508,47 +500,13 @@ describe('RegistrationPage', () => {
});
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registerPage.find('RegistrationPage').instance().shouldComponentUpdate(props);
registerPage.find('RegistrationPage').setState({ errors: { username: 'It looks like this username is already taken' } });
expect(registerPage.find('button.username-suggestion').length).toEqual(3);
registerPage.find('button.username-suggestion').at(0).simulate('click');
expect(registerPage.find('RegistrationPage').state('username')).toEqual('test_1');
});
it('should show username suggestions when full name is populated', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
usernameSuggestions: ['testname', 't.name', 'test_0'],
},
});
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registerPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
expect(registerPage.find('button.username-suggestion').length).toEqual(3);
registerPage.find('button.username-suggestion').at(0).simulate('click');
expect(registerPage.find('RegistrationPage').state('username')).toEqual('testname');
});
it('should clear username suggestions when close icon is clicked', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
usernameSuggestions: ['testname', 't.name', 'test_0'],
},
});
store.dispatch = jest.fn(store.dispatch);
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registerPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
registerPage.find('button.suggested-username-close-button').at(0).simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
});
it('should redirect to url returned in registration result after successful account creation', () => {
const dasboardUrl = 'http://test.com/testing-dashboard/';
store = mockStore({
@@ -704,14 +662,6 @@ describe('RegistrationPage', () => {
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
});
it('should send track event for save_for_later param', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/register'), search: '?save_for_later=true' };
renderer.create(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.saveforlater.course.enroll.clicked',
{ category: 'save-for-later' });
});
// ******** shouldComponentUpdate tests ********
it('should populate form with pipeline user details', () => {
@@ -761,62 +711,55 @@ describe('RegistrationPage', () => {
expect(registrationPage.find('RegistrationPage').state('errorCode')).toEqual(INTERNAL_SERVER_ERROR);
});
it('should display opt-in/opt-out checkbox', () => {
mergeConfig({
MARKETING_EMAILS_OPT_IN: 'true',
});
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registerPage.find('div.opt-checkbox').length).toEqual(1);
mergeConfig({
MARKETING_EMAILS_OPT_IN: '',
});
});
});
describe('TestDynamicFields', () => {
it('should render fields returned by backend', () => {
mergeConfig({
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
});
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
country: { name: 'country', error_message: true },
profession: { name: 'profession', type: 'text', label: 'Profession' },
honor_code: { name: FIELDS.HONOR_CODE, error_message: 'You must agree to Honor Code of our site' },
terms_of_service: {
name: FIELDS.TERMS_OF_SERVICE,
error_message: 'You must agree to the Terms and Service agreement of our site',
},
},
},
});
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registerPage.find('#country').exists()).toBeTruthy();
expect(registerPage.find('#profession').exists()).toBeTruthy();
expect(registerPage.find('#honor-code').exists()).toBeTruthy();
expect(registerPage.find('#tos').exists()).toBeTruthy();
describe('TestOptionalFields', () => {
it('should toggle optional fields state', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('click', { target: { name: 'optionalFields', checked: true } });
expect(registrationPage.find('RegistrationPage').state('showOptionalField')).toEqual(true);
// it should also works when change is made directly instead of click
registrationPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: false } });
expect(registrationPage.find('RegistrationPage').state('showOptionalField')).toEqual(false);
});
it('should submit form with fields returned by backend in payload', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
country: { name: 'country', error_message: true },
profession: { name: 'profession', type: 'text', label: 'Profession' },
honor_code: { name: 'honor_code', type: 'tos_and_honor_code' },
},
extendedProfile: ['profession'],
},
it('should show optional fields section on optional check enabled', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: true } });
registrationPage.update();
expect(registrationPage.find('textarea#goals').length).toEqual(1);
expect(registrationPage.find('select#levelOfEducation').length).toEqual(1);
expect(registrationPage.find('select#yearOfBirth').length).toEqual(1);
expect(registrationPage.find('select#gender').length).toEqual(1);
});
it('should show optional field check based on environment variable', () => {
mergeConfig({
REGISTRATION_OPTIONAL_FIELDS: '',
});
let registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('input#optional-field-checkbox').length).toEqual(0);
mergeConfig({
REGISTRATION_OPTIONAL_FIELDS: 'gender,goals,levelOfEducation,yearOfBirth',
});
registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('input#optional-field-checkbox').length).toEqual(1);
});
it('send tracking event on optional checkbox enabled', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: true } });
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.register.optional_fields_selected', {});
});
it('should submit form with optional fields', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const payload = {
name: 'John Doe',
@@ -824,38 +767,31 @@ describe('RegistrationPage', () => {
email: 'john.doe@example.com',
password: 'password1',
country: 'Pakistan',
gender: 'm',
year_of_birth: '1997',
level_of_education: 'other',
goals: 'edX goals',
honor_code: true,
totalRegistrationTime: 0,
is_authn_mfe: true,
honor_code: true,
extended_profile: [{ field_name: 'profession', field_value: 'Engineer' }],
};
store.dispatch = jest.fn(store.dispatch);
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
delete window.location;
window.location = { href: getConfig().BASE_URL };
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
populateRequiredFields(registerPage, payload);
registerPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
// submit optional fields
registerPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: true } });
registerPage.find('select#gender').simulate('change', { target: { value: 'm', name: 'gender' } });
registerPage.find('select#yearOfBirth').simulate('change', { target: { value: '1997', name: 'yearOfBirth' } });
registerPage.find('select#levelOfEducation').simulate('change', { target: { value: 'other', name: 'levelOfEducation' } });
registerPage.find('textarea#goals').simulate('change', { target: { value: 'edX goals', name: 'goals' } });
registerPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
it('should show error message for fields returned by backend', () => {
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
profession: {
name: 'profession', type: 'text', label: 'Profession', error_message: 'Enter profession',
},
},
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('#profession-error').last().text()).toEqual('Enter profession');
});
});
});

View File

@@ -1,55 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mergeConfig } from '@edx/frontend-platform';
import TermsOfService from '../TermsOfService';
const IntlTermsOfService = injectIntl(TermsOfService);
describe('TermsOfServiceTest', () => {
mergeConfig({
TOS_LINK: 'http://tos-and-honot-code.com',
});
let value = false;
const changeHandler = (e) => {
value = e.target.checked;
};
beforeEach(() => {
value = false;
});
it('should render error msg if Terms of Service checkbox is not checked', () => {
const errorMessage = 'You must agree to the edx Terms of Service';
const termsOfService = mount(
<IntlProvider locale="en">
<IntlTermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
</IntlProvider>,
);
expect(termsOfService.find('.form-text-size').last().text()).toEqual(errorMessage);
});
it('should render Terms of Service field', () => {
const termsOfService = mount(
<IntlProvider locale="en">
<IntlTermsOfService onChangeHandler={changeHandler} />
</IntlProvider>,
);
const expectedMsg = 'I agree to the Your Platform Name Here Terms of Servicein a new tab';
expect(termsOfService.find('#terms-of-service').find('label').text()).toEqual(expectedMsg);
expect(value).toEqual(false);
});
it('should change value when Terms of Service field is checked', () => {
const termsOfService = mount(
<IntlProvider locale="en">
<IntlTermsOfService onChangeHandler={changeHandler} />
</IntlProvider>,
);
const field = termsOfService.find('input#tos');
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(value).toEqual(true);
});
});

View File

@@ -53,11 +53,7 @@ const ResetPasswordPage = (props) => {
const validatePasswordFromBackend = async (password) => {
let errorMessage = '';
try {
const payload = {
reset_password_page: true,
password,
};
errorMessage = await validatePassword(payload);
errorMessage = await validatePassword(password);
} catch (err) {
errorMessage = '';
}
@@ -89,24 +85,6 @@ const ResetPasswordPage = (props) => {
return !Object.values(formErrors).some(x => (x !== ''));
};
const handleOnBlur = (event) => {
let { name, value } = event.target;
// Do not validate when focus out from 'newPassword' and focus on 'passwordValidation' icon
// for better user experience.
if (event.relatedTarget
&& event.relatedTarget.name === 'passwordValidation'
&& name === 'newPassword'
) {
return;
}
if (name === 'passwordValidation') {
name = 'newPassword';
value = newPassword;
}
validateInput(name, value);
};
const handleConfirmPasswordChange = (e) => {
const { value } = e.target;
@@ -148,7 +126,7 @@ const ResetPasswordPage = (props) => {
const { token } = props.match.params;
if (token) {
props.validateToken(token);
return <Spinner animation="border" variant="primary" className="centered-align-spinner" />;
return <Spinner animation="border" variant="primary" className="mt-5" />;
}
} else if (props.status === PASSWORD_RESET_ERROR) {
return <Redirect to={updatePathWithQueryParams(RESET_PAGE)} />;
@@ -174,12 +152,12 @@ const ResetPasswordPage = (props) => {
<ResetPasswordFailure errorCode={errorCode} errorMsg={props.errorMsg} />
<h4>{intl.formatMessage(messages['reset.password'])}</h4>
<p className="mb-4">{intl.formatMessage(messages['reset.password.page.instructions'])}</p>
<Form id="set-reset-password-form" name="set-reset-password-form">
<Form>
<PasswordField
name="newPassword"
value={newPassword}
handleChange={(e) => setNewPassword(e.target.value)}
handleBlur={handleOnBlur}
handleBlur={(e) => validateInput(e.target.name, e.target.value)}
handleFocus={handleOnFocus}
errorMessage={formErrors.newPassword}
floatingLabel={intl.formatMessage(messages['new.password.label'])}
@@ -194,8 +172,6 @@ const ResetPasswordPage = (props) => {
floatingLabel={intl.formatMessage(messages['confirm.password.label'])}
/>
<StatefulButton
id="submit-new-password"
name="submit-new-password"
type="submit"
variant="brand"
className="stateful-button-width"

View File

@@ -39,14 +39,14 @@ export async function resetPassword(payload, token, queryParams) {
return data;
}
export async function validatePassword(payload) {
export async function validatePassword(password) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
const { data } = await getHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/api/user/v1/validation/registration`,
formurlencoded(payload),
formurlencoded({ password }),
requestConfig,
)
.catch((e) => {

View File

@@ -1,237 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
configure as configureAuth,
AxiosJwtAuthService,
ensureAuthenticatedUser,
hydrateAuthenticatedUser,
getAuthenticatedUser,
} from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getLoggingService } from '@edx/frontend-platform/logging';
import {
Alert,
Form,
StatefulButton,
Hyperlink,
Spinner,
} from '@edx/paragon';
import { Error } from '@edx/paragon/icons';
import { getFieldData, saveUserProfile } from './data/actions';
import { welcomePageSelector } from './data/selectors';
import messages from './messages';
import { RedirectLogistration } from '../common-components';
import {
DEFAULT_REDIRECT_URL, DEFAULT_STATE, FAILURE_STATE, COMPLETE_STATE,
} from '../data/constants';
import FormFieldRenderer from '../field-renderer';
import WelcomePageModal from './WelcomePageModal';
import BaseComponent from '../base-component';
const ProgressiveProfiling = (props) => {
const {
extendedProfile, fieldDescriptions, formRenderState, intl, submitState, showError,
} = props;
const [ready, setReady] = useState(false);
const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' });
const [values, setValues] = useState({});
const [openDialog, setOpenDialog] = useState(false);
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
useEffect(() => {
configureAuth(AxiosJwtAuthService, { loggingService: getLoggingService(), config: getConfig() });
ensureAuthenticatedUser(DASHBOARD_URL).then(() => {
hydrateAuthenticatedUser().then(() => {
props.getFieldData();
setReady(true);
});
});
if (props.location.state && props.location.state.registrationResult) {
setRegistrationResult(props.location.state.registrationResult);
sendPageEvent('login_and_registration', 'welcome');
}
}, []);
if (!props.location.state || !props.location.state.registrationResult || formRenderState === FAILURE_STATE) {
global.location.assign(DASHBOARD_URL);
return null;
}
if (!ready) {
return null;
}
const handleSubmit = (e) => {
e.preventDefault();
const authenticatedUser = getAuthenticatedUser();
const payload = { ...values, extendedProfile: [] };
extendedProfile.forEach(fieldName => {
if (values[fieldName]) {
payload.extendedProfile.push({ fieldName, fieldValue: values[fieldName] });
}
delete payload[fieldName];
});
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
sendTrackEvent(
'edx.bi.welcome.page.submit.clicked',
{
isGenderSelected: !!values.gender,
isYearOfBirthSelected: !!values.year_of_birth,
isLevelOfEducationSelected: !!values.level_of_education,
},
);
};
const handleSkip = (e) => {
e.preventDefault();
setOpenDialog(true);
sendTrackEvent('edx.bi.welcome.page.skip.link.clicked');
};
const onChangeHandler = (e) => {
if (e.target.type === 'checkbox') {
setValues({ ...values, [e.target.name]: e.target.checked });
} else {
setValues({ ...values, [e.target.name]: e.target.value });
}
};
const formFields = Object.keys(fieldDescriptions).map((fieldName) => {
const fieldData = fieldDescriptions[fieldName];
return (
<span key={fieldData.name}>
<FormFieldRenderer
fieldData={fieldData}
value={values[fieldData.name]}
onChangeHandler={onChangeHandler}
/>
</span>
);
});
if (formRenderState === COMPLETE_STATE) {
return (
<>
<BaseComponent showWelcomeBanner>
<Helmet>
<title>{intl.formatMessage(messages['progressive.profiling.page.title'],
{ siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<WelcomePageModal isOpen={openDialog} redirectUrl={registrationResult.redirectUrl} />
{props.shouldRedirect ? (
<RedirectLogistration
success
redirectUrl={registrationResult.redirectUrl}
/>
) : null}
<div className="mw-xs pp-page-content">
<div className="pp-page-heading">
<h2 className="h3 text-primary">{intl.formatMessage(messages['progressive.profiling.page.heading'])}</h2>
</div>
<hr className="border-light-700 mb-4" />
{showError ? (
<Alert id="pp-page-errors" className="mb-3" variant="danger" icon={Error}>
<Alert.Heading>{intl.formatMessage(messages['welcome.page.error.heading'])}</Alert.Heading>
<p>{intl.formatMessage(messages['welcome.page.error.message'])}</p>
</Alert>
) : null}
<Form>
{formFields}
<span className="progressive-profiling-support">
<Hyperlink
isInline
variant="muted"
destination={getConfig().WELCOME_PAGE_SUPPORT_LINK}
target="_blank"
showLaunchIcon={false}
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
>
{intl.formatMessage(messages['optional.fields.information.link'])}
</Hyperlink>
</span>
<div className="d-flex mt-4 mb-3">
<StatefulButton
type="submit"
variant="brand"
className="login-button-width"
state={submitState}
labels={{
default: intl.formatMessage(messages['optional.fields.submit.button']),
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
<StatefulButton
className="text-gray-700 font-weight-500"
type="submit"
variant="link"
labels={{
default: intl.formatMessage(messages['optional.fields.skip.button']),
}}
onClick={handleSkip}
onMouseDown={(e) => e.preventDefault()}
/>
</div>
</Form>
</div>
</BaseComponent>
</>
);
}
return <Spinner id="loader" animation="border" variant="primary" className="centered-align-spinner" />;
};
ProgressiveProfiling.propTypes = {
extendedProfile: PropTypes.arrayOf(PropTypes.string),
fieldDescriptions: PropTypes.shape({}),
formRenderState: PropTypes.string.isRequired,
intl: intlShape.isRequired,
location: PropTypes.shape({
state: PropTypes.object,
}),
getFieldData: PropTypes.func.isRequired,
saveUserProfile: PropTypes.func.isRequired,
showError: PropTypes.bool,
shouldRedirect: PropTypes.bool,
submitState: PropTypes.string,
};
ProgressiveProfiling.defaultProps = {
extendedProfile: [],
fieldDescriptions: {},
location: { state: {} },
shouldRedirect: false,
showError: false,
submitState: DEFAULT_STATE,
};
const mapStateToProps = state => ({
extendedProfile: welcomePageSelector(state).extendedProfile,
fieldDescriptions: welcomePageSelector(state).fieldDescriptions,
formRenderState: welcomePageSelector(state).formRenderState,
shouldRedirect: welcomePageSelector(state).success,
submitState: welcomePageSelector(state).submitState,
showError: welcomePageSelector(state).showError,
});
export default connect(
mapStateToProps,
{
saveUserProfile,
getFieldData,
},
)(injectIntl(ProgressiveProfiling));

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { sendPageEvent } from '@edx/frontend-platform/analytics';
import {
configure as configureAuth,
AxiosJwtAuthService,
@@ -30,7 +30,7 @@ import messages from './messages';
import { RedirectLogistration } from '../common-components';
import { DEFAULT_REDIRECT_URL, DEFAULT_STATE } from '../data/constants';
import { EDUCATION_LEVELS, GENDER_OPTIONS, YEAR_OF_BIRTH_OPTIONS } from '../register/data/constants';
import { EDUCATION_LEVELS, GENDER_OPTIONS, YEAR_OF_BIRTH_OPTIONS, REGISTRATION_REASONS } from '../register/data/constants';
import WelcomePageModal from './WelcomePageModal';
import BaseComponent from '../base-component';
@@ -39,7 +39,8 @@ const WelcomePage = (props) => {
const [ready, setReady] = useState(false);
const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' });
const [values, setValues] = useState({ levelOfEducation: '', yearOfBirth: '', gender: '' });
const [values, setValues] = useState({ levelOfEducation: '', yearOfBirth: '', gender: '', reason: '' });
const [openCareerDialog, setOpenCareerDialog] = useState(false);
const [openDialog, setOpenDialog] = useState(false);
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
@@ -68,11 +69,6 @@ const WelcomePage = (props) => {
return null;
}
if (getConfig().ENABLE_COPPA_COMPLIANCE && EDUCATION_LEVELS) {
const index = EDUCATION_LEVELS.indexOf('el');
EDUCATION_LEVELS.splice(index, 1);
}
const getOptions = (fieldName) => {
const options = {
yearOfBirth: YEAR_OF_BIRTH_OPTIONS.map(({ value, label }) => (
@@ -88,12 +84,19 @@ const WelcomePage = (props) => {
{intl.formatMessage(messages[`gender.options.${key || 'label'}`])}
</option>
)),
reason: REGISTRATION_REASONS.map(key => (
<option className="data-hj-suppress" key={key} value={key}>
{intl.formatMessage(messages[`reason.options.${key || 'label'}`])}
</option>
)),
};
return options[fieldName];
};
const handleSubmit = (e) => {
// console.log("HAHAHAHAHAHA");
console.log(values.reason);
e.preventDefault();
const payload = {};
const authenticatedUser = getAuthenticatedUser();
@@ -105,16 +108,6 @@ const WelcomePage = (props) => {
});
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
sendTrackEvent(
'edx.bi.welcome.page.submit.clicked',
{
isGenderSelected: !!payload.gender,
isYearOfBirthSelected: !!payload.yearOfBirth,
isLevelOfEducationSelected: !!payload.levelOfEducation,
},
);
window.optimizely.push({
type: 'event',
eventName: 'authn_welcome_page_submit_btn_clicked',
@@ -124,7 +117,6 @@ const WelcomePage = (props) => {
const handleSkip = (e) => {
e.preventDefault();
setOpenDialog(true);
sendTrackEvent('edx.bi.welcome.page.skip.link.clicked');
window.optimizely.push({
type: 'event',
@@ -138,7 +130,7 @@ const WelcomePage = (props) => {
return (
<>
<BaseComponent showWelcomeBanner>
<BaseComponent>
<Helmet>
<title>{intl.formatMessage(messages['progressive.profiling.page.title'],
{ siteName: getConfig().SITE_NAME })}
@@ -162,7 +154,9 @@ const WelcomePage = (props) => {
<p>{intl.formatMessage(messages['welcome.page.error.message'])}</p>
</Alert>
) : null}
<Form id="welcome-page-profile-form" name="welcome-page-profile-form">
<Form>
<Form.Group controlId="levelOfEducation">
<Form.Control
as="select"
@@ -175,22 +169,25 @@ const WelcomePage = (props) => {
{getOptions('levelOfEducation')}
</Form.Control>
</Form.Group>
{(!getConfig().ENABLE_COPPA_COMPLIANCE)
&& (
<Form.Group controlId="yearOfBirth">
<Form.Control
as="select"
name="yearOfBirth"
value={values.yearOfBirth}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={intl.formatMessage(messages['year.of.birth.label'])}
>
<option value="">{intl.formatMessage(messages['year.of.birth.label'])}</option>
{getOptions('yearOfBirth')}
</Form.Control>
</Form.Group>
)}
<Form.Group controlId="yearOfBirth">
<Form.Control
as="select"
name="yearOfBirth"
value={values.yearOfBirth}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={intl.formatMessage(messages['year.of.birth.label'])}
>
<option value="">{intl.formatMessage(messages['year.of.birth.label'])}</option>
{getOptions('yearOfBirth')}
</Form.Control>
</Form.Group>
<Form.Group controlId="gender" className="mb-3">
<Form.Control
as="select"
@@ -203,6 +200,22 @@ const WelcomePage = (props) => {
{getOptions('gender')}
</Form.Control>
</Form.Group>
<Form.Group controlId="purposeOfAccountCreation" className="mb-3">
<Form.Control
as="select"
name="reason"
value={values.reason}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={intl.formatMessage(messages['reason.options.label'])}
>
{getOptions('reason')}
</Form.Control>
</Form.Group>
<span className="progressive-profiling-support">
<Hyperlink
isInline
@@ -210,15 +223,12 @@ const WelcomePage = (props) => {
destination={getConfig().WELCOME_PAGE_SUPPORT_LINK}
target="_blank"
showLaunchIcon={false}
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
>
{intl.formatMessage(messages['optional.fields.information.link'])}
</Hyperlink>
</span>
<div className="d-flex mt-4">
<StatefulButton
name="submit-profile"
id="submit-profile"
type="submit"
variant="brand"
className="login-button-width"
@@ -231,8 +241,6 @@ const WelcomePage = (props) => {
onMouseDown={(e) => e.preventDefault()}
/>
<StatefulButton
id="skip-profile"
name="skip-profile"
className="text-gray-700 font-weight-500"
type="submit"
variant="link"

View File

@@ -1,6 +1,5 @@
import { AsyncActionType } from '../../data/utils';
export const GET_FIELDS_DATA = new AsyncActionType('FIELD_DESCRIPTION', 'GET_FIELDS_DATA');
export const SAVE_USER_PROFILE = new AsyncActionType('USER_PROFILE', 'SAVE_USER_PROFILE');
// save additional user information
@@ -20,21 +19,3 @@ export const saveUserProfileSuccess = () => ({
export const saveUserProfileFailure = () => ({
type: SAVE_USER_PROFILE.FAILURE,
});
// get field data from platform
export const getFieldData = () => ({
type: GET_FIELDS_DATA.BASE,
});
export const getFieldDataBegin = () => ({
type: GET_FIELDS_DATA.BEGIN,
});
export const getFieldDataSuccess = (data, extendedProfile) => ({
type: GET_FIELDS_DATA.SUCCESS,
payload: { data, extendedProfile },
});
export const getFieldDataFailure = () => ({
type: GET_FIELDS_DATA.FAILURE,
});

View File

@@ -1,12 +1,7 @@
import { GET_FIELDS_DATA, SAVE_USER_PROFILE } from './actions';
import {
DEFAULT_STATE, PENDING_STATE, COMPLETE_STATE, FAILURE_STATE,
} from '../../data/constants';
import { SAVE_USER_PROFILE } from './actions';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
export const defaultState = {
extendedProfile: [],
fieldDescriptions: {},
formRenderState: DEFAULT_STATE,
success: false,
submitState: DEFAULT_STATE,
showError: false,
@@ -14,23 +9,6 @@ export const defaultState = {
const reducer = (state = defaultState, action) => {
switch (action.type) {
case GET_FIELDS_DATA.BEGIN:
return {
...state,
formRenderState: PENDING_STATE,
};
case GET_FIELDS_DATA.SUCCESS:
return {
...state,
extendedProfile: action.payload.extendedProfile,
fieldDescriptions: action.payload.data,
formRenderState: COMPLETE_STATE,
};
case GET_FIELDS_DATA.FAILURE:
return {
...state,
formRenderState: FAILURE_STATE,
};
case SAVE_USER_PROFILE.BEGIN:
return {
...state,

View File

@@ -1,17 +1,13 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import {
GET_FIELDS_DATA,
getFieldDataBegin,
getFieldDataFailure,
getFieldDataSuccess,
SAVE_USER_PROFILE,
saveUserProfileBegin,
saveUserProfileFailure,
saveUserProfileSuccess,
} from './actions';
import { patchAccount, getOptionalFieldData } from './service';
import patchAccount from './service';
export function* saveUserProfileInformation(action) {
try {
@@ -24,17 +20,6 @@ export function* saveUserProfileInformation(action) {
}
}
export function* getFieldData() {
try {
yield put(getFieldDataBegin());
const data = yield call(getOptionalFieldData);
yield put(getFieldDataSuccess(data.fields, data.extended_profile));
} catch (e) {
yield put(getFieldDataFailure());
}
}
export default function* saga() {
yield takeEvery(SAVE_USER_PROFILE.BASE, saveUserProfileInformation);
yield takeEvery(GET_FIELDS_DATA.BASE, getFieldData);
}

View File

@@ -1,7 +1,7 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
export async function patchAccount(username, commitValues) {
export default async function patchAccount(username, commitValues) {
const requestConfig = {
headers: { 'Content-Type': 'application/merge-patch+json' },
};
@@ -16,20 +16,3 @@ export async function patchAccount(username, commitValues) {
throw (error);
});
}
export async function getOptionalFieldData() {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
const { data } = await getAuthenticatedHttpClient()
.get(
`${getConfig().LMS_BASE_URL}/api/optional_fields`,
requestConfig,
)
.catch((e) => {
throw (e);
});
return data;
}

View File

@@ -1,5 +1,4 @@
export { default } from './WelcomePage';
export { default as ProgressiveProfiling } from './ProgressiveProfiling';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';

View File

@@ -31,6 +31,26 @@ const messages = defineMessages({
defaultMessage: 'Other/Prefer not to say',
description: 'The label for catch-all gender option.',
},
'reason.options.label': {
id: 'reason.options.label',
defaultMessage: 'Purpose (optional)',
description: 'Placeholder for the reasons options dropdown',
},
'reason.options.job': {
id: 'reason.options.job',
defaultMessage: 'Finding a job',
description: 'edX grants unlimited opportunities to expand and grow one\' career',
},
'reason.options.learn': {
id: 'reason.options.learn',
defaultMessage: 'Learning tons of shit',
description: 'edX ever increasing catalog of a wide array of courses allows one to explore new ares of knowledge and challenge their perception of the world.',
},
'reason.options.boss': {
id: 'reason.options.boss',
defaultMessage: 'My boss made me',
description: 'edX allows businesses and institutions to empower their employees on a mass scale, offering opportunities to learn to countless learners.',
},
'education.levels.label': {
id: 'education.levels.label',
defaultMessage: 'Highest level of education completed (optional)',

View File

@@ -1,200 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import * as logging from '@edx/frontend-platform/logging';
import { injectIntl, IntlProvider, configure } from '@edx/frontend-platform/i18n';
import { getFieldData, saveUserProfile } from '../data/actions';
import ProgressiveProfiling from '../ProgressiveProfiling';
import {
COMPLETE_STATE, DEFAULT_REDIRECT_URL, FAILURE_STATE, PENDING_STATE,
} from '../../data/constants';
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
const mockStore = configureStore();
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-platform/auth');
jest.mock('@edx/frontend-platform/logging');
analytics.sendTrackEvent = jest.fn();
analytics.sendPageEvent = jest.fn();
logging.getLoggingService = jest.fn();
auth.configure = jest.fn();
auth.ensureAuthenticatedUser = jest.fn().mockImplementation(() => Promise.resolve(true));
auth.hydrateAuthenticatedUser = jest.fn().mockImplementation(() => Promise.resolve(true));
describe('ProgressiveProfilingTests', () => {
mergeConfig({
WELCOME_PAGE_SUPPORT_LINK: 'http://localhost:1999/welcome',
});
const registrationResult = { redirectUrl: 'http://localhost:18000/dashboard', success: true };
let props = {};
let store = {};
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
const initialState = {
welcomePage: {
formRenderState: COMPLETE_STATE,
},
};
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
const getProgressiveProfilingPage = async () => {
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage {...props} />));
await act(async () => {
await Promise.resolve(progressiveProfilingPage);
await new Promise(resolve => setImmediate(resolve));
progressiveProfilingPage.update();
});
return progressiveProfilingPage;
};
beforeEach(() => {
store = mockStore(initialState);
configure({
loggingService: { logError: jest.fn() },
config: {
ENVIRONMENT: 'production',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
props = {
getFieldData: jest.fn(),
location: {
state: {
registrationResult,
},
},
};
});
it('should fire action to get form fields', async () => {
store.dispatch = jest.fn(store.dispatch);
const progressiveProfilingPage = await getProgressiveProfilingPage();
progressiveProfilingPage.find('button.btn-link').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(getFieldData());
});
it('should show spinner until fields are fetched', async () => {
store = mockStore({
welcomePage: {
formRenderState: PENDING_STATE,
},
});
const progressiveProfilingPage = await getProgressiveProfilingPage();
expect(progressiveProfilingPage.find('#loader').exists()).toBeTruthy();
});
it('should render fields returned by backend api', async () => {
store = mockStore({
welcomePage: {
...initialState.welcomePage,
fieldDescriptions: {
gender: {
name: 'gender',
type: 'select',
label: 'Gender',
options: [['m', 'Male'], ['f', 'Female'], ['o', 'Other/Prefer Not to Say']],
},
},
},
});
const progressiveProfilingPage = await getProgressiveProfilingPage();
expect(progressiveProfilingPage.find('#gender').exists()).toBeTruthy();
});
it('should submit user profile details on form submission', async () => {
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' }));
const formPayload = {
gender: 'm',
extended_profile: [{ field_name: 'company', field_value: 'edx' }],
};
store = mockStore({
welcomePage: {
...initialState.welcomePage,
extendedProfile: ['company'],
fieldDescriptions: {
gender: {
name: 'gender',
type: 'select',
label: 'Gender',
options: [['m', 'Male'], ['f', 'Female'], ['o', 'Other/Prefer Not to Say']],
},
company: {
name: 'company',
type: 'text',
label: 'Company',
},
},
},
});
store.dispatch = jest.fn(store.dispatch);
const progressiveProfilingPage = await getProgressiveProfilingPage();
progressiveProfilingPage.find('select#gender').simulate('change', { target: { value: 'm', name: 'gender' } });
progressiveProfilingPage.find('input#company').simulate('change', { target: { value: 'edx', name: 'company' } });
progressiveProfilingPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(saveUserProfile('abc123', formPayload));
});
it('should open modal on pressing skip for now button', async () => {
const progressiveProfilingPage = await getProgressiveProfilingPage();
progressiveProfilingPage.find('button.btn-link').simulate('click');
expect(progressiveProfilingPage.find('.pgn__modal-content-container').exists()).toBeTruthy();
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked');
});
it('should send analytic event for support link click', async () => {
const progressiveProfilingPage = await getProgressiveProfilingPage();
progressiveProfilingPage.find('.progressive-profiling-support a[target="_blank"]').simulate('click');
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
});
it('should show error message when patch request fails', async () => {
store = mockStore({
welcomePage: {
...initialState.welcomePage,
showError: true,
},
});
const progressiveProfilingPage = await getProgressiveProfilingPage();
expect(progressiveProfilingPage.find('#pp-page-errors').exists()).toBeTruthy();
});
it('should redirect to dashboard if no form fields are configured', async () => {
store = mockStore({
welcomePage: {
formRenderState: FAILURE_STATE,
},
});
delete window.location;
window.location = {
href: getConfig().BASE_URL,
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
};
await getProgressiveProfilingPage();
expect(window.location.href).toBe(DASHBOARD_URL);
});
});

Some files were not shown because too many files have changed in this diff Show More