Compare commits
121 Commits
open-relea
...
Ali-Abbas/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1398e486fe | ||
|
|
f098fe1a3a | ||
|
|
a989fabb92 | ||
|
|
1257e81781 | ||
|
|
b08890b794 | ||
|
|
7f1c7b86ef | ||
|
|
a5fd3a7f7e | ||
|
|
4513cc8834 | ||
|
|
40103a2386 | ||
|
|
b6c18bb439 | ||
|
|
ee51939f2d | ||
|
|
fc127ccd98 | ||
|
|
22faebac50 | ||
|
|
5ce3995f5b | ||
|
|
e181269703 | ||
|
|
5d212ec6b5 | ||
|
|
363096c4f0 | ||
|
|
85c5902559 | ||
|
|
fca6da2df7 | ||
|
|
7a74e5d29b | ||
|
|
4b94344dd9 | ||
|
|
5245254de5 | ||
|
|
afda76ff11 | ||
|
|
da52ddd35a | ||
|
|
bb2aef3878 | ||
|
|
4d6f76d9b3 | ||
|
|
869e083a2a | ||
|
|
ee876b5c84 | ||
|
|
a2b25449de | ||
|
|
16218252f1 | ||
|
|
757e446be7 | ||
|
|
db0f8f80bc | ||
|
|
49bc817f2d | ||
|
|
c31beeef96 | ||
|
|
09970d7935 | ||
|
|
d15b0baf74 | ||
|
|
b9802a130e | ||
|
|
7c0ea75e21 | ||
|
|
e1b02de7de | ||
|
|
6c4dbc5db0 | ||
|
|
d180626122 | ||
|
|
a9518b7388 | ||
|
|
51b18e9c52 | ||
|
|
3d98558bf6 | ||
|
|
e7e7f518bf | ||
|
|
cb06c8778a | ||
|
|
ab5c205a7f | ||
|
|
2f85902d2c | ||
|
|
b75e78bdda | ||
|
|
182a0251a4 | ||
|
|
89881c64a6 | ||
|
|
7321e2a159 | ||
|
|
9b45aa3bc9 | ||
|
|
dfadac08d3 | ||
|
|
2e87f0bd9f | ||
|
|
7f8086545c | ||
|
|
de6e3c2010 | ||
|
|
eb6d0125c6 | ||
|
|
e8f754c10b | ||
|
|
a72bbf2f58 | ||
|
|
c465f51e66 | ||
|
|
599e658742 | ||
|
|
929a669cad | ||
|
|
503b8b5176 | ||
|
|
d2aa727c12 | ||
|
|
d66dcecd2f | ||
|
|
80d0c44b40 | ||
|
|
0e5cd30d01 | ||
|
|
56b9fe3998 | ||
|
|
a22f1298eb | ||
|
|
b7bd6a2846 | ||
|
|
a52ab171de | ||
|
|
13a7508f26 | ||
|
|
a499aa4cc5 | ||
|
|
e7207878d4 | ||
|
|
47d64c1cf5 | ||
|
|
c75dc86263 | ||
|
|
105a8d1a3c | ||
|
|
c74cf52e38 | ||
|
|
5c9b448b14 | ||
|
|
4e95495dcc | ||
|
|
82f86f5fbe | ||
|
|
f737d6e158 | ||
|
|
f9feb94668 | ||
|
|
112afa7e51 | ||
|
|
1694ea38ab | ||
|
|
0f3b7caa0f | ||
|
|
a85e1e1e15 | ||
|
|
55d00027a9 | ||
|
|
bf012619a6 | ||
|
|
f47795bf40 | ||
|
|
848e7a2f85 | ||
|
|
ecc6138833 | ||
|
|
d6600eb876 | ||
|
|
bc237de755 | ||
|
|
89abb51734 | ||
|
|
be8570edac | ||
|
|
09cce6802d | ||
|
|
c164dd7dcf | ||
|
|
00435fb27d | ||
|
|
0ebaa0b991 | ||
|
|
0d7b529233 | ||
|
|
2c2f7e8e98 | ||
|
|
768b8a2417 | ||
|
|
192714629c | ||
|
|
f2f761e8db | ||
|
|
410aa14b28 | ||
|
|
e857293414 | ||
|
|
492f911930 | ||
|
|
0002d84a6c | ||
|
|
9b49b38496 | ||
|
|
7cf9294e09 | ||
|
|
129f73aa4f | ||
|
|
7bdb93784b | ||
|
|
b32c001fec | ||
|
|
5f134d7b99 | ||
|
|
81b0823632 | ||
|
|
95e3af7487 | ||
|
|
77047bab2a | ||
|
|
c1cf6d65de | ||
|
|
f626fc2f89 |
4
.env
4
.env
@@ -1,6 +1,5 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
BASE_URL=''
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
DEMOGRAPHICS_BASE_URL=''
|
||||
@@ -26,8 +25,11 @@ STUDIO_BASE_URL=''
|
||||
SUPPORT_URL=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_ACCOUNT_DELETION=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
PASSWORD_RESET_SUPPORT_LINK=''
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
@@ -27,9 +26,11 @@ STUDIO_BASE_URL=''
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_ACCOUNT_DELETION=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
PASSWORD_RESET_SUPPORT_LINK='mailto:support@example.com'
|
||||
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
@@ -26,7 +25,10 @@ STUDIO_BASE_URL=''
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_ACCOUNT_DELETION=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,5 +10,5 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
|
||||
|
||||
18
Makefile
18
Makefile
@@ -2,12 +2,13 @@ export TRANSIFEX_RESOURCE = frontend-app-account
|
||||
transifex_resource = frontend-app-account
|
||||
transifex_langs = "ar,de,es_419,fa_IR,fr,fr_CA,hi,it,pt,ru,uk,zh_CN,it_IT,pt_PT,de_DE"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
@@ -50,9 +51,24 @@ push_translations:
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-app-account/src/i18n/messages:frontend-app-account
|
||||
|
||||
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-account
|
||||
endif
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
13
README.rst
13
README.rst
@@ -63,17 +63,18 @@ Examples:
|
||||
|
||||
The fully-qualified URL to the support page or email to request the support from in the target environment.
|
||||
|
||||
``ENABLE_ACCOUNT_DELETION``
|
||||
|
||||
Example: ``'false'`` | ``''`` (empty strings are true)
|
||||
|
||||
Enable the account deletion option, defaults to true.
|
||||
To disable account deletion set ``ENABLE_ACCOUNT_DELETION`` to ``'false'`` (string), otherwise it will default to true.
|
||||
|
||||
edX-specific Environment Variables
|
||||
**********************************
|
||||
|
||||
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and are unsupported in Open edX. Enabling these environment variables will result in undefined behavior in Open edX installations:
|
||||
|
||||
``COACHING_ENABLED``
|
||||
|
||||
Example: ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
Enables support for a section of the micro-frontend that helps users arrange for coaching sessions. Integrates with a private coaching plugin and is only used by edx.org.
|
||||
|
||||
``ENABLE_DEMOGRAPHICS_COLLECTION``
|
||||
|
||||
Example: ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
14270
package-lock.json
generated
14270
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
@@ -28,10 +28,10 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-component-footer": "11.7.1",
|
||||
"@edx/frontend-component-header": "3.7.2",
|
||||
"@edx/frontend-platform": "4.0.2",
|
||||
"@edx/paragon": "20.28.5",
|
||||
"@edx/frontend-component-footer": "12.3.0",
|
||||
"@edx/frontend-component-header": "4.7.0",
|
||||
"@edx/frontend-platform": "5.5.4",
|
||||
"@edx/paragon": "20.46.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
@@ -42,11 +42,10 @@
|
||||
"@tensorflow/tfjs-core": "3.21.0",
|
||||
"bowser": "2.11.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.27.1",
|
||||
"core-js": "3.33.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "6.1.0",
|
||||
"form-urlencoded": "6.1.4",
|
||||
"formdata-polyfill": "4.0.10",
|
||||
"history": "4.10.1",
|
||||
"jslib-html5-camera-photo": "3.3.4",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
@@ -57,37 +56,38 @@
|
||||
"lodash.omit": "4.5.0",
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.pickby": "4.6.0",
|
||||
"long": "5.2.1",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"long": "5.2.3",
|
||||
"memoize-one": "5.2.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.11.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"qs": "6.11.2",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"react-router": "6.14.2",
|
||||
"react-router-dom": "6.14.2",
|
||||
"react-router-hash-link": "2.4.3",
|
||||
"react-scrollspy": "3.4.3",
|
||||
"react-transition-group": "4.4.5",
|
||||
"redux": "4.2.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.0.0",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.0",
|
||||
"reselect": "4.1.8",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.1.1",
|
||||
"@edx/frontend-build": "12.8.6",
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/frontend-build": "13.0.1",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.7",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
}
|
||||
|
||||
@@ -1,17 +1,148 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Account | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></script>
|
||||
<% } %>
|
||||
<title>Account | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<!-- begin usabilla live embed code -->
|
||||
<script defer type="text/javascript">
|
||||
window.lightningjs ||
|
||||
(function (n) {
|
||||
var e = "lightningjs";
|
||||
function t(e, t) {
|
||||
var r, i, a, o, d, c;
|
||||
return (
|
||||
t && (t += (/\?/.test(t) ? "&" : "?") + "lv=1"),
|
||||
n[e] ||
|
||||
((r = window),
|
||||
(i = document),
|
||||
(a = e),
|
||||
(o = i.location.protocol),
|
||||
(d = "load"),
|
||||
(c = 0),
|
||||
(function () {
|
||||
n[a] = function () {
|
||||
var t = arguments,
|
||||
i = this,
|
||||
o = ++c,
|
||||
d = (i && i != r && i.id) || 0;
|
||||
function s() {
|
||||
return (s.id = o), n[a].apply(s, arguments);
|
||||
}
|
||||
return (
|
||||
(e.s = e.s || []).push([o, d, t]),
|
||||
(s.then = function (n, t, r) {
|
||||
var i = (e.fh[o] = e.fh[o] || []),
|
||||
a = (e.eh[o] = e.eh[o] || []),
|
||||
d = (e.ph[o] = e.ph[o] || []);
|
||||
return (
|
||||
n && i.push(n), t && a.push(t), r && d.push(r), s
|
||||
);
|
||||
}),
|
||||
s
|
||||
);
|
||||
};
|
||||
var e = (n[a]._ = {});
|
||||
function s() {
|
||||
e.P(d), (e.w = 1), n[a]("_load");
|
||||
}
|
||||
(e.fh = {}),
|
||||
(e.eh = {}),
|
||||
(e.ph = {}),
|
||||
(e.l = t
|
||||
? t.replace(/^\/\//, ("https:" == o ? o : "http:") + "//")
|
||||
: t),
|
||||
(e.p = { 0: +new Date() }),
|
||||
(e.P = function (n) {
|
||||
e.p[n] = new Date() - e.p[0];
|
||||
}),
|
||||
e.w && s(),
|
||||
r.addEventListener
|
||||
? r.addEventListener(d, s, !1)
|
||||
: r.attachEvent("onload", s);
|
||||
var l = function () {
|
||||
function n() {
|
||||
return [
|
||||
"<!DOCTYPE ",
|
||||
o,
|
||||
"><",
|
||||
o,
|
||||
"><head></head><",
|
||||
t,
|
||||
"><",
|
||||
r,
|
||||
' src="',
|
||||
e.l,
|
||||
'"></',
|
||||
r,
|
||||
"></",
|
||||
t,
|
||||
"></",
|
||||
o,
|
||||
">",
|
||||
].join("");
|
||||
}
|
||||
var t = "body",
|
||||
r = "script",
|
||||
o = "html",
|
||||
d = i[t];
|
||||
if (!d) return setTimeout(l, 100);
|
||||
e.P(1);
|
||||
var c,
|
||||
s = i.createElement("div"),
|
||||
h = s.appendChild(i.createElement("div")),
|
||||
u = i.createElement("iframe");
|
||||
(s.style.display = "none"),
|
||||
(d.insertBefore(s, d.firstChild).id = "lightningjs-" + a),
|
||||
(u.frameBorder = "0"),
|
||||
(u.id = "lightningjs-frame-" + a),
|
||||
/MSIE[ ]+6/.test(navigator.userAgent) &&
|
||||
(u.src = "javascript:false"),
|
||||
(u.allowTransparency = "true"),
|
||||
h.appendChild(u);
|
||||
try {
|
||||
u.contentWindow.document.open();
|
||||
} catch (n) {
|
||||
(e.domain = i.domain),
|
||||
(c =
|
||||
"javascript:var d=document.open();d.domain='" +
|
||||
i.domain +
|
||||
"';"),
|
||||
(u.src = c + "void(0);");
|
||||
}
|
||||
try {
|
||||
var p = u.contentWindow.document;
|
||||
p.write(n()), p.close();
|
||||
} catch (e) {
|
||||
u.src =
|
||||
c +
|
||||
'd.write("' +
|
||||
n().replace(/"/g, String.fromCharCode(92) + '"') +
|
||||
'");d.close();';
|
||||
}
|
||||
e.P(2);
|
||||
};
|
||||
e.l && l();
|
||||
})()),
|
||||
(n[e].lv = "1"),
|
||||
n[e]
|
||||
);
|
||||
}
|
||||
var r = (window.lightningjs = t(e));
|
||||
(r.require = t), (r.modules = n);
|
||||
})({});
|
||||
</script>
|
||||
<!-- end usabilla live embed code -->
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx"],
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { getConfig, history, getQueryParameters } from '@edx/frontend-platform';
|
||||
import { getConfig, getQueryParameters } from '@edx/frontend-platform';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -48,21 +48,15 @@ import {
|
||||
getStatesList,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
import CoachingToggle from './coaching/CoachingToggle';
|
||||
import DemographicsSection from './demographics/DemographicsSection';
|
||||
import { fetchCourseList } from '../notification-preferences/data/thunks';
|
||||
import { withLocation, withNavigate } from './hoc';
|
||||
|
||||
class AccountSettingsPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// If there is a "duplicate_provider" query parameter, that's the backend's
|
||||
// way of telling us that the provider account the user tried to link is already linked
|
||||
// to another user account on the platform. We use this to display a message to that effect,
|
||||
// and remove the parameter from the URL.
|
||||
const duplicateTpaProvider = getQueryParameters().duplicate_provider;
|
||||
if (duplicateTpaProvider !== undefined) {
|
||||
history.replace(history.location.pathname);
|
||||
}
|
||||
this.state = {
|
||||
duplicateTpaProvider,
|
||||
};
|
||||
@@ -79,8 +73,9 @@ class AccountSettingsPage extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchCourseList();
|
||||
this.props.fetchSettings();
|
||||
this.props.fetchSiteLanguages();
|
||||
this.props.fetchSiteLanguages(this.props.navigate);
|
||||
sendTrackingLogEvent('edx.user.settings.viewed', {
|
||||
page: 'account',
|
||||
visibility: null,
|
||||
@@ -200,6 +195,12 @@ class AccountSettingsPage extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there is a "duplicate_provider" query parameter, that's the backend's
|
||||
// way of telling us that the provider account the user tried to link is already linked
|
||||
// to another user account on the platform. We use this to display a message to that effect,
|
||||
// and remove the parameter from the URL.
|
||||
this.props.navigate(this.props.location, { replace: true });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert variant="danger">
|
||||
@@ -687,15 +688,6 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{getConfig().COACHING_ENABLED
|
||||
&& this.props.formValues.coaching.eligible_for_coaching
|
||||
&& (
|
||||
<CoachingToggle
|
||||
name="coaching"
|
||||
phone_number={this.props.formValues.phone_number}
|
||||
coaching={this.props.formValues.coaching}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
|
||||
<div className="account-section pt-3 mb-5" id="social-media">
|
||||
@@ -777,12 +769,15 @@ class AccountSettingsPage extends React.Component {
|
||||
<ThirdPartyAuth />
|
||||
</div>
|
||||
|
||||
<div className="account-section pt-3 mb-5" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
|
||||
<DeleteAccount
|
||||
isVerifiedAccount={this.props.isActive}
|
||||
hasLinkedTPA={hasLinkedTPA}
|
||||
/>
|
||||
</div>
|
||||
{getConfig().ENABLE_ACCOUNT_DELETION
|
||||
&& (
|
||||
<div className="account-section pt-3 mb-5" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
|
||||
<DeleteAccount
|
||||
isVerifiedAccount={this.props.isActive}
|
||||
hasLinkedTPA={hasLinkedTPA}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
@@ -819,12 +814,12 @@ class AccountSettingsPage extends React.Component {
|
||||
</h1>
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<div className="col-md-2">
|
||||
<JumpNav
|
||||
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
<div className="col-md-10">
|
||||
{loading ? this.renderLoading() : null}
|
||||
{loaded ? this.renderContent() : null}
|
||||
{loadingError ? this.renderError() : null}
|
||||
@@ -862,11 +857,6 @@ AccountSettingsPage.propTypes = {
|
||||
social_link_facebook: PropTypes.string,
|
||||
social_link_twitter: PropTypes.string,
|
||||
time_zone: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}),
|
||||
state: PropTypes.string,
|
||||
shouldDisplayDemographicsSection: PropTypes.bool,
|
||||
useVerifiedNameForCerts: PropTypes.bool.isRequired,
|
||||
@@ -908,6 +898,7 @@ AccountSettingsPage.propTypes = {
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
beginNameChange: PropTypes.func.isRequired,
|
||||
fetchCourseList: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
connected: PropTypes.bool,
|
||||
})),
|
||||
@@ -931,6 +922,8 @@ AccountSettingsPage.propTypes = {
|
||||
proctored_exam_attempt_id: PropTypes.number,
|
||||
}),
|
||||
),
|
||||
navigate: PropTypes.func.isRequired,
|
||||
location: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
AccountSettingsPage.defaultProps = {
|
||||
@@ -958,11 +951,12 @@ AccountSettingsPage.defaultProps = {
|
||||
verifiedNameHistory: [],
|
||||
};
|
||||
|
||||
export default connect(accountSettingsPageSelector, {
|
||||
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {
|
||||
fetchCourseList,
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
saveMultipleSettings,
|
||||
updateDraft,
|
||||
fetchSiteLanguages,
|
||||
beginNameChange,
|
||||
})(injectIntl(AccountSettingsPage));
|
||||
})(injectIntl(AccountSettingsPage))));
|
||||
|
||||
@@ -565,6 +565,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'No value set.',
|
||||
description: 'The placeholder for an empty but uneditable field when there is no administrator',
|
||||
},
|
||||
'notification.preferences.notifications.label': {
|
||||
id: 'notification.preferences.notifications.label',
|
||||
defaultMessage: 'Notifications',
|
||||
description: 'Label for Notifications',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
import { breakpoints, useWindowSize, Icon } from '@edx/paragon';
|
||||
import { OpenInNew } from '@edx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { NavHashLink } from 'react-router-hash-link';
|
||||
import Scrollspy from 'react-scrollspy';
|
||||
import { Link } from 'react-router-dom';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { selectShowPreferences } from '../notification-preferences/data/selectors';
|
||||
|
||||
const JumpNav = ({
|
||||
intl,
|
||||
displayDemographicsLink,
|
||||
}) => {
|
||||
const stickToTop = useWindowSize().width > breakpoints.small.minWidth;
|
||||
const showPreferences = useSelector(selectShowPreferences());
|
||||
|
||||
return (
|
||||
<div className={classNames('jump-nav', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
|
||||
<div className={classNames('jump-nav px-2.25', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
|
||||
<Scrollspy
|
||||
items={[
|
||||
'basic-information',
|
||||
@@ -61,12 +67,30 @@ const JumpNav = ({
|
||||
{intl.formatMessage(messages['account.settings.section.linked.accounts'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#delete-account">
|
||||
{intl.formatMessage(messages['account.settings.jump.nav.delete.account'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
{getConfig().ENABLE_ACCOUNT_DELETION
|
||||
&& (
|
||||
<li>
|
||||
<NavHashLink to="#delete-account">
|
||||
{intl.formatMessage(messages['account.settings.jump.nav.delete.account'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
)}
|
||||
</Scrollspy>
|
||||
{showPreferences && (
|
||||
<>
|
||||
<hr />
|
||||
<Scrollspy
|
||||
className="list-unstyled"
|
||||
>
|
||||
<li>
|
||||
<Link to="/notifications" target="_blank" rel="noopener noreferrer">
|
||||
<span>{intl.formatMessage(messages['notification.preferences.notifications.label'])}</span>
|
||||
<Icon className="d-inline-block align-bottom ml-1" src={OpenInNew} />
|
||||
</Link>
|
||||
</li>
|
||||
</Scrollspy>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,10 @@ import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<div
|
||||
className="container-fluid d-flex py-5 justify-content-center align-items-start text-center"
|
||||
data-testid="not-found-page"
|
||||
>
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -28,8 +27,6 @@ jest.mock('react-redux', () => ({
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const IntlCertificatePreference = injectIntl(CertificatePreference);
|
||||
|
||||
const mockStore = configureStore();
|
||||
@@ -42,7 +39,7 @@ describe('NameChange', () => {
|
||||
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig, getQueryParameters } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import PageLoading from '../PageLoading';
|
||||
import CoachingConsentForm from './CoachingConsentForm';
|
||||
import messages from './CoachingConsent.messages';
|
||||
import LogoSVG from '../../logo.svg';
|
||||
import { fetchSettings } from '../data/actions';
|
||||
import { coachingConsentPageSelector } from '../data/selectors';
|
||||
|
||||
const Logo = ({ src, alt, ...attributes }) => <img src={src} alt={alt} {...attributes} />;
|
||||
|
||||
const SuccessMessage = (props) => (
|
||||
<div className="col-12 col-lg-6 shadow-lg mx-auto mt-4 p-5">
|
||||
<FontAwesomeIcon className="text-success" icon={faCheck} size="5x" />
|
||||
<div className="h3">{props.header}</div>
|
||||
<div>{props.message}</div>
|
||||
<Hyperlink destination={props.continueUrl} className="d-block p-2 my-3 text-center text-white bg-primary rounded">
|
||||
{props.continue}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AutoRedirect = (props) => {
|
||||
window.location.href = props.redirectUrl;
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const VIEWS = {
|
||||
NOT_LOADED: 'NOT_LOADED',
|
||||
LOADED: 'LOADED',
|
||||
SUCCESS: 'SUCCESS',
|
||||
SUCCESS_PENDING: 'SUCCESS_PENDING',
|
||||
DECLINED: 'DECLINED',
|
||||
DECLINE_PENDING: 'DECLINE_PENDING',
|
||||
};
|
||||
|
||||
class CoachingConsent extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// Used to redirect back to the courseware.
|
||||
const nextUrl = this.sanitizeForwardingUrl(getQueryParameters().next);
|
||||
this.state = {
|
||||
redirectUrl: nextUrl || `${getConfig().LMS_BASE_URL}/dashboard/`,
|
||||
formErrors: {},
|
||||
formSubmitted: false,
|
||||
declineSubmitted: false,
|
||||
submissionSuccess: false,
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.declineCoaching = this.declineCoaching.bind(this);
|
||||
this.patchUsingCoachingConsentForm = this.patchUsingCoachingConsentForm.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchSettings();
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const fullName = e.target.fullName.value;
|
||||
const phoneNumber = e.target.phoneNumber.value;
|
||||
const body = {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: true,
|
||||
phone_number: phoneNumber,
|
||||
full_name: fullName,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: true,
|
||||
declineSubmitted: false,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
}
|
||||
|
||||
sanitizeForwardingUrl(url) {
|
||||
// Redirect to root of MFE if invalid next param is sent
|
||||
return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
|
||||
}
|
||||
|
||||
async patchUsingCoachingConsentForm(body) {
|
||||
const { userId } = getAuthenticatedUser();
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/coaching_consent/${userId}/`;
|
||||
let formErrors = {};
|
||||
const data = await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, body)
|
||||
.catch((error) => {
|
||||
if (get(error, 'customAttributes.httpErrorResponseData')) {
|
||||
formErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
} else {
|
||||
formErrors = { full_name: 'Something went wrong. Please try again.' };
|
||||
}
|
||||
this.setState({
|
||||
submissionSuccess: false,
|
||||
formErrors,
|
||||
formSubmitted: false,
|
||||
});
|
||||
});
|
||||
if (get(data, 'status') === 200) {
|
||||
this.setState({ submissionSuccess: true });
|
||||
}
|
||||
}
|
||||
|
||||
declineCoaching(e) {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
coaching_consent: false,
|
||||
consent_form_seen: true,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: false,
|
||||
declineSubmitted: true,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
}
|
||||
|
||||
renderView(currentView) {
|
||||
switch (currentView) {
|
||||
case VIEWS.NOT_LOADED:
|
||||
return <PageLoading srMessage="" />;
|
||||
case VIEWS.LOADED:
|
||||
return (
|
||||
<CoachingConsentForm
|
||||
onSubmit={this.handleSubmit}
|
||||
declineCoaching={this.declineCoaching}
|
||||
formErrors={this.state.formErrors}
|
||||
formValues={this.props.formValues}
|
||||
redirectUrl={this.state.redirectUrl}
|
||||
profileDataManager={this.props.profileDataManager}
|
||||
/>
|
||||
);
|
||||
case VIEWS.SUCCESS_PENDING:
|
||||
return <PageLoading srMessage="Submitting..." />;
|
||||
case VIEWS.SUCCESS:
|
||||
return (
|
||||
<SuccessMessage
|
||||
continueUrl={this.state.redirectUrl}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
|
||||
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
|
||||
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
|
||||
/>
|
||||
);
|
||||
case VIEWS.DECLINE_PENDING:
|
||||
return <PageLoading srMessage="Redirecting..." />;
|
||||
case VIEWS.DECLINED:
|
||||
return <AutoRedirect redirectUrl={this.state.redirectUrl} />;
|
||||
default:
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loaded } = this.props;
|
||||
const formHasErrors = Object.keys(this.state.formErrors).length > 0;
|
||||
let currentView = null;
|
||||
// This amount of logic was making the template very hard to read, so I broke it out into views.
|
||||
if (!loaded) {
|
||||
currentView = VIEWS.NOT_LOADED;
|
||||
} else if (this.state.formSubmitted && !formHasErrors) {
|
||||
if (this.state.submissionSuccess) {
|
||||
currentView = VIEWS.SUCCESS;
|
||||
} else {
|
||||
currentView = VIEWS.SUCCESS_PENDING;
|
||||
}
|
||||
} else if (this.state.declineSubmitted && !formHasErrors) {
|
||||
if (this.state.submissionSuccess) {
|
||||
currentView = VIEWS.DECLINED;
|
||||
} else {
|
||||
currentView = VIEWS.DECLINE_PENDING;
|
||||
}
|
||||
} else {
|
||||
currentView = VIEWS.LOADED;
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="w-100 d-flex justify-content-center align-items-center shadow coaching-header">
|
||||
<Logo
|
||||
className="logo"
|
||||
src={LogoSVG}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
{this.renderView(currentView)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Logo.defaultProps = {
|
||||
src: '',
|
||||
alt: '',
|
||||
};
|
||||
|
||||
Logo.propTypes = {
|
||||
src: PropTypes.string,
|
||||
alt: PropTypes.string,
|
||||
};
|
||||
|
||||
SuccessMessage.defaultProps = {
|
||||
header: '',
|
||||
message: '',
|
||||
continueUrl: '',
|
||||
continue: '',
|
||||
};
|
||||
|
||||
SuccessMessage.propTypes = {
|
||||
header: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
continueUrl: PropTypes.string,
|
||||
continue: PropTypes.string,
|
||||
};
|
||||
|
||||
AutoRedirect.defaultProps = {
|
||||
redirectUrl: '',
|
||||
};
|
||||
|
||||
AutoRedirect.propTypes = {
|
||||
redirectUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
CoachingConsent.defaultProps = {
|
||||
loaded: false,
|
||||
profileDataManager: null,
|
||||
};
|
||||
|
||||
CoachingConsent.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
loaded: PropTypes.bool,
|
||||
formValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
consent_form_seen: PropTypes.bool.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
coaching: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
confirmationValues: PropTypes.shape({
|
||||
coaching: PropTypes.shape({}),
|
||||
name: PropTypes.shape({}),
|
||||
phone_number: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
profileDataManager: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(coachingConsentPageSelector, {
|
||||
fetchSettings,
|
||||
})(injectIntl(CoachingConsent));
|
||||
@@ -1,66 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.coaching.consent.welcome.header': {
|
||||
id: 'account.settings.coaching.consent.welcome.header',
|
||||
defaultMessage: 'Let’s get started.',
|
||||
description: 'The welcome header for consent form.',
|
||||
},
|
||||
'account.settings.coaching.consent.welcome.subheader': {
|
||||
id: 'account.settings.coaching.consent.welcome.subheader',
|
||||
defaultMessage: "We're here for you from start to finish",
|
||||
description: 'The welcome subheader for consent form.',
|
||||
},
|
||||
'account.settings.coaching.consent.description': {
|
||||
id: 'account.settings.coaching.consent.description',
|
||||
defaultMessage: "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
description: 'Text describing what Coaching is.',
|
||||
},
|
||||
'account.settings.coaching.consent.text-messaging.disclaimer': {
|
||||
id: 'account.settings.coaching.consent.text-messaging.disclaimer',
|
||||
defaultMessage: '* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.',
|
||||
description: 'Text describing what Coaching is.',
|
||||
},
|
||||
'account.settings.coaching.consent.accept-coaching': {
|
||||
id: 'account.settings.coaching.consent.accept-coaching',
|
||||
defaultMessage: 'Sign up for coaching',
|
||||
description: 'Text to confirm coaching enablement',
|
||||
},
|
||||
'account.settings.coaching.consent.decline-coaching': {
|
||||
id: 'account.settings.coaching.consent.decline-coaching',
|
||||
defaultMessage: 'I prefer not to be contacted with free coaching services',
|
||||
description: 'Text to decline coaching enablement',
|
||||
},
|
||||
'account.settings.coaching.consent.label.name': {
|
||||
id: 'account.settings.coaching.consent.label.name',
|
||||
defaultMessage: 'Please confirm your name',
|
||||
description: 'Label for name input',
|
||||
},
|
||||
'account.settings.coaching.consent.label.phone-number': {
|
||||
id: 'account.settings.coaching.consent.label.phone-number',
|
||||
defaultMessage: 'Enter your mobile number',
|
||||
description: 'Label for mobile phone number input',
|
||||
},
|
||||
'account.settings.coaching.consent.success.header': {
|
||||
id: 'account.settings.coaching.consent.success.header',
|
||||
defaultMessage: 'Success!',
|
||||
description: 'Heading announcing that submission succeeded',
|
||||
},
|
||||
'account.settings.coaching.consent.success.message': {
|
||||
id: 'account.settings.coaching.consent.success.message',
|
||||
defaultMessage: "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
description: 'Text announcing that you have signed up and will receive texts',
|
||||
},
|
||||
'account.settings.coaching.consent.success.continue': {
|
||||
id: 'account.settings.coaching.consent.success.continue',
|
||||
defaultMessage: 'Start my course',
|
||||
description: 'Text that the user will be sent back to the courseware',
|
||||
},
|
||||
'account.settings.coaching.managed.support': {
|
||||
id: 'account.settings.coaching.managed.support',
|
||||
defaultMessage: 'support',
|
||||
description: 'website support',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,129 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Alert from '../Alert';
|
||||
import messages from './CoachingConsent.messages';
|
||||
|
||||
const ErrorMessage = (props) => <div className="alert-warning mb-2">{props.message}</div>;
|
||||
|
||||
const ManagedProfileAlert = ({ profileDataManager }) => (
|
||||
<Alert className="alert alert-primary" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.coaching.managed.alert"
|
||||
defaultMessage="Your name is managed by {managerTitle}. Contact your administrator for help."
|
||||
description="Alert message informing the user their account data is managed by a third party"
|
||||
values={{
|
||||
managerTitle: <b>{profileDataManager}</b>,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
const CoachingForm = (props) => (
|
||||
<div className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg">
|
||||
<h2 className="h2">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.welcome.header'])}
|
||||
</h2>
|
||||
<p>{props.intl.formatMessage(messages['account.settings.coaching.consent.description'])}</p>
|
||||
<div>
|
||||
<form onSubmit={props.onSubmit}>
|
||||
<div className="py-3">
|
||||
{!!props.profileDataManager && (
|
||||
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
|
||||
)}
|
||||
<ErrorMessage message={props.formErrors.full_name} />
|
||||
<label className="h6" htmlFor="fullName">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
|
||||
</label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="full-name"
|
||||
id="fullName"
|
||||
disabled={!!props.profileDataManager}
|
||||
defaultValue={props.formValues.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-3">
|
||||
<ErrorMessage message={props.formErrors.phone_number} />
|
||||
<label className="h6" htmlFor="phoneNumber">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
|
||||
</label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="phone_number"
|
||||
id="phoneNumber"
|
||||
defaultValue={props.formValues.phone_number}
|
||||
/>
|
||||
</div>
|
||||
<div className=" py-3">
|
||||
<p className="small font-italic">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.text-messaging.disclaimer'])}
|
||||
</p>
|
||||
</div>
|
||||
<ErrorMessage message={props.formErrors.coaching} />
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Button variant="outline-primary" className="w-100" type="submit">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Hyperlink
|
||||
className="mt-3 text-dark btn-link small"
|
||||
destination={props.redirectUrl}
|
||||
onClick={props.declineCoaching}
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.decline-coaching'])}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
CoachingForm.defaultProps = {
|
||||
formErrors: {
|
||||
coaching: '',
|
||||
name: '',
|
||||
phone_number: '',
|
||||
},
|
||||
};
|
||||
|
||||
CoachingForm.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
declineCoaching: PropTypes.func.isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
consent_form_seen: PropTypes.bool.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
coaching: PropTypes.string,
|
||||
full_name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
}),
|
||||
redirectUrl: PropTypes.string.isRequired,
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ErrorMessage.defaultProps = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
message: PropTypes.string,
|
||||
};
|
||||
|
||||
ManagedProfileAlert.propTypes = {
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoachingForm);
|
||||
@@ -1,103 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import messages from './CoachingToggle.messages';
|
||||
import { editableFieldSelector } from '../data/selectors';
|
||||
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
|
||||
import EditableField from '../EditableField';
|
||||
|
||||
const CoachingToggle = (props) => (
|
||||
<>
|
||||
<EditableField
|
||||
name="phone_number"
|
||||
type="text"
|
||||
value={props.phone_number}
|
||||
label={props.intl.formatMessage(messages['account.settings.field.phone_number'])}
|
||||
emptyLabel={props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
|
||||
onChange={props.updateDraft}
|
||||
onSubmit={() => {
|
||||
const { coaching } = props;
|
||||
if (coaching.coaching_consent === true) {
|
||||
return props.saveMultipleSettings([
|
||||
{
|
||||
formId: 'coaching',
|
||||
commitValues: {
|
||||
...coaching,
|
||||
phone_number: props.phone_number,
|
||||
},
|
||||
},
|
||||
{
|
||||
formId: 'phone_number',
|
||||
commitValues: props.phone_number,
|
||||
},
|
||||
], 'phone_number');
|
||||
}
|
||||
return props.saveSettings('phone_number', props.phone_number);
|
||||
}}
|
||||
/>
|
||||
<Form.Group
|
||||
isInvalid={!!props.error}
|
||||
className="custom-control custom-switch"
|
||||
>
|
||||
<Form.Switch
|
||||
name={props.name}
|
||||
className="custom-control-input"
|
||||
disabled={props.saveState === 'pending'}
|
||||
type="checkbox"
|
||||
id="coachingConsent"
|
||||
checked={props.coaching.coaching_consent}
|
||||
helperText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
|
||||
value={props.coaching.coaching_consent}
|
||||
onChange={async (e) => {
|
||||
const { name } = e.target;
|
||||
// eslint-disable-next-line camelcase
|
||||
const { user, eligible_for_coaching } = props.coaching;
|
||||
const value = {
|
||||
user,
|
||||
// eslint-disable-next-line camelcase
|
||||
eligible_for_coaching,
|
||||
coaching_consent: e.target.checked,
|
||||
};
|
||||
props.saveSettings(name, value);
|
||||
}}
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}
|
||||
</Form.Switch>
|
||||
{!!props.error && (
|
||||
<Form.Control.Feedback>
|
||||
{props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
CoachingToggle.defaultProps = {
|
||||
phone_number: '',
|
||||
error: '',
|
||||
saveState: undefined,
|
||||
};
|
||||
|
||||
CoachingToggle.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
error: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
phone_number: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
saveMultipleSettings,
|
||||
})(injectIntl(CoachingToggle));
|
||||
@@ -1,31 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.field.phone_number': {
|
||||
id: 'account.settings.field.phone_number',
|
||||
defaultMessage: 'Phone Number',
|
||||
description: 'The label for a phone numbers setting in the user profile',
|
||||
},
|
||||
'account.settings.field.phone_number.empty': {
|
||||
id: 'account.settings.field.phone_number.empty',
|
||||
defaultMessage: 'Add a phone number',
|
||||
description: 'placeholder for a profiles empty phone number field',
|
||||
},
|
||||
'account.settings.field.coaching_consent': {
|
||||
id: 'account.settings.field.coaching_consent',
|
||||
defaultMessage: 'Coaching consent',
|
||||
description: 'The label for the coaching consent setting in the user profile',
|
||||
},
|
||||
'account.settings.field.coaching_consent.tooltip': {
|
||||
id: 'account.settings.field.coaching_consent.tooltip',
|
||||
defaultMessage: 'MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.',
|
||||
description: 'A tooltip explaining what coaching is and who it is for',
|
||||
},
|
||||
'account.settings.field.coaching_consent.error': {
|
||||
id: 'account.settings.field.coaching_consent.error',
|
||||
defaultMessage: 'A valid US phone number is required to opt into coaching',
|
||||
description: 'An error message that displays when a user attempts to consent to coaching without first providing a phone number in their profile',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,51 +0,0 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import get from 'lodash.get';
|
||||
|
||||
/**
|
||||
* get all settings related to the coaching plugin. Settings used
|
||||
* by Microbachelors students.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getCoachingPreferences(userId) {
|
||||
let data = {};
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`));
|
||||
} catch (error) {
|
||||
// If a user isn't active the API call will fail with a lack of credentials.
|
||||
data = {
|
||||
coaching_consent: false,
|
||||
user: userId,
|
||||
eligible_for_coaching: false,
|
||||
consent_form_seen: false,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* patch all of the settings related to coaching.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { coaching }
|
||||
*/
|
||||
export async function patchCoachingPreferences(userId, commitValues) {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`;
|
||||
const { coaching } = commitValues;
|
||||
coaching.user = userId;
|
||||
|
||||
await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, coaching)
|
||||
.catch((error) => {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
if (get(apiError, 'fieldErrors.phone_number')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
|
||||
delete apiError.fieldErrors.phone_number;
|
||||
}
|
||||
throw apiError;
|
||||
});
|
||||
return commitValues;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import CoachingConsent from '../CoachingConsent';
|
||||
import * as selectors from '../../data/selectors';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlCoachingConsent = injectIntl(CoachingConsent);
|
||||
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('CoachingConsent', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
selectors.mockClear();
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
fetchSettings: jest.fn(),
|
||||
loaded: true,
|
||||
saveState: undefined,
|
||||
formValues: {
|
||||
name: 'edx edx',
|
||||
phone_number: '1234567890',
|
||||
coaching: {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: false,
|
||||
eligible_for_coaching: true,
|
||||
user: 1,
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
confirmationValues: {},
|
||||
profileDataManager: '',
|
||||
intl: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('disables name field on enterprise user', () => {
|
||||
props = {
|
||||
...props,
|
||||
profileDataManager: 'test person',
|
||||
};
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('display completed box when successfully submitted', async () => {
|
||||
const fakeEvent = {
|
||||
preventDefault: () => {},
|
||||
target: {
|
||||
fullName: { value: 'edx edx' },
|
||||
phoneNumber: { value: '9783028731' },
|
||||
},
|
||||
};
|
||||
const wrapper = renderer.create(
|
||||
reduxWrapper(<IntlCoachingConsent {...props} />),
|
||||
{
|
||||
// bypass the forward-ref. we don't care about focus for this one test
|
||||
createNodeMock: (element) => {
|
||||
if (element.type === 'button') {
|
||||
// mock a focus function
|
||||
return {
|
||||
focus: async () => wrapper.root.findByType('form').props.onSubmit(fakeEvent),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
);
|
||||
const form = wrapper.root.findByType('form');
|
||||
await act(async () => { await form.props.onSubmit(fakeEvent); });
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,300 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
|
||||
>
|
||||
<h2
|
||||
className="h2"
|
||||
>
|
||||
Let’s get started.
|
||||
</h2>
|
||||
<p>
|
||||
MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*
|
||||
</p>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-primary"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
Your name is managed by
|
||||
<b>
|
||||
test person
|
||||
</b>
|
||||
. Contact your administrator for help.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="fullName"
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={true}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="phoneNumber"
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
>
|
||||
<p
|
||||
className="small font-italic"
|
||||
>
|
||||
* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
I prefer not to be contacted with free coaching services
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
exports[`CoachingConsent display completed box when successfully submitted 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={
|
||||
Object {
|
||||
"height": "50vh",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="spinner-border text-primary"
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Submitting...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
exports[`CoachingConsent should render 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
|
||||
>
|
||||
<h2
|
||||
className="h2"
|
||||
>
|
||||
Let’s get started.
|
||||
</h2>
|
||||
<p>
|
||||
MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*
|
||||
</p>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="fullName"
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={false}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="phoneNumber"
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
>
|
||||
<p
|
||||
className="small font-italic"
|
||||
>
|
||||
* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
I prefer not to be contacted with free coaching services
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
@@ -106,11 +106,6 @@ const isEditingSelector = createSelector(
|
||||
(name, accountSettings) => accountSettings.openFormId === name,
|
||||
);
|
||||
|
||||
const confirmationValuesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.confirmationValues,
|
||||
);
|
||||
|
||||
const errorSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.errors,
|
||||
@@ -289,35 +284,6 @@ export const certPreferenceSelector = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
export const coachingConsentPageSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
activeAccountSelector,
|
||||
profileDataManagerSelector,
|
||||
saveStateSelector,
|
||||
confirmationValuesSelector,
|
||||
errorSelector,
|
||||
(
|
||||
accountSettings,
|
||||
formValues,
|
||||
activeAccount,
|
||||
profileDataManager,
|
||||
saveState,
|
||||
confirmationValues,
|
||||
errors,
|
||||
) => ({
|
||||
loading: accountSettings.loading,
|
||||
loaded: accountSettings.loaded,
|
||||
loadingError: accountSettings.loadingError,
|
||||
isActive: activeAccount,
|
||||
profileDataManager,
|
||||
formValues,
|
||||
saveState,
|
||||
confirmationValues,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const demographicsSectionSelector = createSelector(
|
||||
formValuesSelector,
|
||||
draftsSelector,
|
||||
|
||||
@@ -8,7 +8,6 @@ import isEmpty from 'lodash.isempty';
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
||||
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
||||
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
||||
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
||||
|
||||
@@ -214,7 +213,7 @@ export async function postVerifiedName(data) {
|
||||
|
||||
/**
|
||||
* A single function to GET everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
|
||||
* Currently encapsulates Account, Preferences, ThirdPartyAuth, and Demographics
|
||||
*/
|
||||
export async function getSettings(username, userRoles, userId) {
|
||||
const [
|
||||
@@ -223,7 +222,6 @@ export async function getSettings(username, userRoles, userId) {
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsQuestionsResponse,
|
||||
demographics,
|
||||
demographicsOptions,
|
||||
@@ -233,7 +231,6 @@ export async function getSettings(username, userRoles, userId) {
|
||||
getThirdPartyAuthProviders(),
|
||||
getProfileDataManager(username, userRoles),
|
||||
getTimeZones(),
|
||||
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
|
||||
@@ -245,7 +242,6 @@ export async function getSettings(username, userRoles, userId) {
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
|
||||
...demographics,
|
||||
demographicsOptions,
|
||||
@@ -254,26 +250,23 @@ export async function getSettings(username, userRoles, userId) {
|
||||
|
||||
/**
|
||||
* A single function to PATCH everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, coaching and ThirdPartyAuth
|
||||
* Currently encapsulates Account, Preferences, ThirdPartyAuth
|
||||
*/
|
||||
export async function patchSettings(username, commitValues, userId) {
|
||||
// Note: time_zone exists in the return value from user/v1/accounts
|
||||
// but it is always null and won't update. It also exists in
|
||||
// user/v1/preferences where it does update. This is the one we use.
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const coachingKeys = ['coaching'];
|
||||
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
||||
const certificateKeys = ['useVerifiedNameForCerts'];
|
||||
const isDemographicsKey = (value, key) => key.includes('demographics');
|
||||
const accountCommitValues = omit(
|
||||
commitValues,
|
||||
preferenceKeys,
|
||||
coachingKeys,
|
||||
demographicsKeys,
|
||||
certificateKeys,
|
||||
);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const coachingCommitValues = pick(commitValues, coachingKeys);
|
||||
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
||||
const certCommitValues = pick(commitValues, certificateKeys);
|
||||
const patchRequests = [];
|
||||
@@ -284,9 +277,6 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
if (!isEmpty(preferenceCommitValues)) {
|
||||
patchRequests.push(patchPreferences(username, preferenceCommitValues));
|
||||
}
|
||||
if (!isEmpty(coachingCommitValues)) {
|
||||
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
|
||||
}
|
||||
if (!isEmpty(demographicsCommitValues)) {
|
||||
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) {
|
||||
export default function* handleFailure(error, navigate, failureAction = null, failureRedirectPath = null) {
|
||||
if (error.fieldErrors && failureAction !== null) {
|
||||
yield put(failureAction({ fieldErrors: error.fieldErrors }));
|
||||
}
|
||||
@@ -11,6 +10,6 @@ export default function* handleFailure(error, failureAction = null, failureRedir
|
||||
yield put(failureAction(error.message));
|
||||
}
|
||||
if (failureRedirectPath !== null) {
|
||||
history.push(failureRedirectPath);
|
||||
navigate(failureRedirectPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,12 @@ const BeforeProceedingBanner = (props) => {
|
||||
defaultMessage="Before proceeding, please {actionLink}."
|
||||
description="Error that appears if you are trying to delete your account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
|
||||
values={{
|
||||
actionLink: (
|
||||
actionLink: supportArticleUrl ? (
|
||||
<Hyperlink destination={supportArticleUrl}>
|
||||
{intl.formatMessage(messages[instructionMessageId])}
|
||||
</Hyperlink>
|
||||
) : (
|
||||
intl.formatMessage(messages[instructionMessageId])
|
||||
),
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl, createIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import BeforeProceedingBanner from './BeforeProceedingBanner'; // eslint-disable-line import/first
|
||||
|
||||
const IntlBeforeProceedingBanner = injectIntl(BeforeProceedingBanner);
|
||||
|
||||
describe('BeforeProceedingBanner', () => {
|
||||
it('should match the snapshot if SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT does not have a support link', () => {
|
||||
const props = {
|
||||
instructionMessageId: 'account.settings.delete.account.please.unlink',
|
||||
intl: createIntl({ locale: 'en' }),
|
||||
supportArticleUrl: '',
|
||||
};
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlBeforeProceedingBanner
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match the snapshot when SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT has a support link', () => {
|
||||
const props = {
|
||||
instructionMessageId: 'account.settings.delete.account.please.unlink',
|
||||
intl: createIntl({ locale: 'en' }),
|
||||
supportArticleUrl: 'http://test-support.edx',
|
||||
};
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlBeforeProceedingBanner
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -59,6 +59,7 @@ export class DeleteAccount extends React.Component {
|
||||
hasLinkedTPA, isVerifiedAccount, status, errorType, intl,
|
||||
} = this.props;
|
||||
const canDelete = isVerifiedAccount && !hasLinkedTPA;
|
||||
const supportArticleUrl = process.env.SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT;
|
||||
|
||||
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
|
||||
// to allow edx.org to fulfill its business requirements.
|
||||
@@ -122,7 +123,7 @@ export class DeleteAccount extends React.Component {
|
||||
{hasLinkedTPA ? (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.unlink"
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
supportArticleUrl={supportArticleUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Testing the modals separately, they just clutter up the snapshots if included here.
|
||||
jest.mock('./ConfirmationModal', () => function () {
|
||||
jest.mock('./ConfirmationModal', () => function ConfirmationModalMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('./SuccessModal', () => function () {
|
||||
jest.mock('./SuccessModal', () => function SuccessModalMock() {
|
||||
return <></>;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BeforeProceedingBanner should match the snapshot if SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT does not have a support link 1`] = `
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
Before proceeding, please unlink all social media accounts.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT has a support link 1`] = `
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://test-support.edx"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -43,7 +43,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@@ -689,7 +689,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1347,7 +1347,7 @@ exports[`DemographicsSection should render an Alert when demographicsOptions pro
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1418,7 +1418,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@@ -2057,7 +2057,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@@ -2696,7 +2696,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@@ -3342,7 +3342,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
19
src/account-settings/hoc.jsx
Normal file
19
src/account-settings/hoc.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export const withNavigate = Component => {
|
||||
const WrappedComponent = props => {
|
||||
const navigate = useNavigate();
|
||||
return <Component {...props} navigate={navigate} />;
|
||||
};
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
export const withLocation = Component => {
|
||||
const WrappedComponent = props => {
|
||||
const location = useLocation();
|
||||
return <Component {...props} location={location.pathname} />;
|
||||
};
|
||||
return WrappedComponent;
|
||||
};
|
||||
38
src/account-settings/hoc.test.jsx
Normal file
38
src/account-settings/hoc.test.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { withLocation, withNavigate } from './hoc';
|
||||
|
||||
const mockedNavigator = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockedNavigator,
|
||||
useLocation: () => ({
|
||||
pathname: '/current-location',
|
||||
}),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const MockComponent = ({ navigate, location }) => (
|
||||
// eslint-disable-next-line react/button-has-type, react/prop-types
|
||||
<button id="btn" onClick={() => navigate('/some-route')}>{location}</button>
|
||||
);
|
||||
const WrappedComponent = withNavigate(withLocation(MockComponent));
|
||||
|
||||
test('Provide Navigation to Component', () => {
|
||||
const wrapper = mount(
|
||||
<WrappedComponent />,
|
||||
);
|
||||
const btn = wrapper.find('#btn');
|
||||
btn.simulate('click');
|
||||
|
||||
expect(mockedNavigator).toHaveBeenCalledWith('/some-route');
|
||||
});
|
||||
|
||||
test('Provide Location Pathname to Component', () => {
|
||||
const wrapper = mount(
|
||||
<WrappedComponent />,
|
||||
);
|
||||
|
||||
expect(wrapper.find('#btn').text()).toContain('/current-location');
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
@@ -29,7 +29,7 @@ const NameChangeModal = ({
|
||||
saveState,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { push } = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const { username } = getAuthenticatedUser();
|
||||
const [verifiedNameInput, setVerifiedNameInput] = useState(formValues.verified_name || '');
|
||||
const [confirmedWarning, setConfirmedWarning] = useState(false);
|
||||
@@ -69,9 +69,9 @@ const NameChangeModal = ({
|
||||
useEffect(() => {
|
||||
if (saveState === 'complete') {
|
||||
handleClose();
|
||||
push(`/id-verification?next=${encodeURIComponent('account/settings')}`);
|
||||
navigate(`/id-verification?next=${encodeURIComponent('account/settings')}`);
|
||||
}
|
||||
}, [handleClose, push, saveState]);
|
||||
}, [handleClose, navigate, saveState]);
|
||||
|
||||
function renderErrors() {
|
||||
if (Object.keys(errors).length > 0) {
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -28,8 +27,6 @@ jest.mock('react-redux', () => ({
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ nameChangeSelector: () => ({}) })));
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const IntlNameChange = injectIntl(NameChange);
|
||||
|
||||
const mockStore = configureStore();
|
||||
@@ -39,7 +36,7 @@ describe('NameChange', () => {
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
@@ -168,6 +165,6 @@ describe('NameChange', () => {
|
||||
props.saveState = 'complete';
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
expect(history.location.pathname).toEqual('/id-verification');
|
||||
expect(window.location.pathname).toEqual('/id-verification');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,9 @@ import { AsyncActionType } from '../data/utils';
|
||||
|
||||
export const FETCH_SITE_LANGUAGES = new AsyncActionType('SITE_LANGUAGE', 'FETCH_SITE_LANGUAGES');
|
||||
|
||||
export const fetchSiteLanguages = () => ({
|
||||
export const fetchSiteLanguages = handleNavigation => ({
|
||||
type: FETCH_SITE_LANGUAGES.BASE,
|
||||
payload: { handleNavigation },
|
||||
});
|
||||
|
||||
export const fetchSiteLanguagesBegin = () => ({
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
import { getSiteLanguageList } from './service';
|
||||
import { handleFailure } from '../data/utils';
|
||||
|
||||
function* handleFetchSiteLanguages() {
|
||||
function* handleFetchSiteLanguages(action) {
|
||||
try {
|
||||
yield put(fetchSiteLanguagesBegin());
|
||||
const siteLanguageList = yield call(getSiteLanguageList);
|
||||
yield put(fetchSiteLanguagesSuccess(siteLanguageList));
|
||||
} catch (e) {
|
||||
yield call(handleFailure, e, fetchSiteLanguagesFailure);
|
||||
yield call(handleFailure, e, action.payload.handleNavigation, fetchSiteLanguagesFailure);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,65 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp, mergeConfig, setConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { mergeConfig, setConfig } from '@edx/frontend-platform';
|
||||
import JumpNav from '../JumpNav';
|
||||
import configureStore from '../../data/configureStore';
|
||||
|
||||
const IntlJumpNav = injectIntl(JumpNav);
|
||||
|
||||
describe('JumpNav', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: false,
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
});
|
||||
|
||||
let props = {};
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
props = {
|
||||
intl: {},
|
||||
displayDemographicsLink: false,
|
||||
};
|
||||
store = configureStore({
|
||||
notificationPreferences: {
|
||||
showPreferences: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render Optional Information link', () => {
|
||||
it('should not render Optional Information or delete account link', () => {
|
||||
setConfig({
|
||||
ENABLE_ACCOUNT_DELETION: false,
|
||||
});
|
||||
|
||||
const tree = renderer.create((
|
||||
// Had to wrap the following in a router or I will receive an error stating:
|
||||
// "Invariant failed: You should not use <NavLink> outside a <Router>"
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<IntlJumpNav {...props} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render Optional Information link', () => {
|
||||
it('should render Optional Information and delete account link', () => {
|
||||
setConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: true,
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
});
|
||||
|
||||
props = {
|
||||
@@ -48,12 +68,11 @@ describe('JumpNav', () => {
|
||||
};
|
||||
|
||||
const tree = renderer.create((
|
||||
// Same as previous test
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<IntlJumpNav {...props} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
exports[`JumpNav should not render Optional Information or delete account link 1`] = `
|
||||
<div
|
||||
className="jump-nav jump-nav-sm position-sticky pt-3"
|
||||
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
@@ -12,8 +12,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#basic-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Account Information
|
||||
@@ -23,8 +25,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#profile-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Profile Information
|
||||
@@ -34,8 +38,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#social-media"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Social Media Links
|
||||
@@ -45,8 +51,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#site-preferences"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Site Preferences
|
||||
@@ -56,31 +64,22 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#linked-accounts"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
href="/#delete-account"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Delete My Account
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`JumpNav should render Optional Information link 1`] = `
|
||||
exports[`JumpNav should render Optional Information and delete account link 1`] = `
|
||||
<div
|
||||
className="jump-nav jump-nav-sm position-sticky pt-3"
|
||||
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
@@ -90,8 +89,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#basic-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Account Information
|
||||
@@ -101,8 +102,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#profile-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Profile Information
|
||||
@@ -112,8 +115,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#demographics-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Optional Information
|
||||
@@ -123,8 +128,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#social-media"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Social Media Links
|
||||
@@ -134,8 +141,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#site-preferences"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Site Preferences
|
||||
@@ -145,8 +154,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#linked-accounts"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Linked Accounts
|
||||
@@ -156,8 +167,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#delete-account"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Delete My Account
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
reducer as accountSettingsReducer,
|
||||
storeName as accountSettingsStoreName,
|
||||
} from '../account-settings';
|
||||
import notificationPreferencesReducer from '../notification-preferences/data/reducers';
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
[accountSettingsStoreName]: accountSettingsReducer,
|
||||
notificationPreferences: notificationPreferencesReducer,
|
||||
});
|
||||
export default createRootReducer;
|
||||
|
||||
13
src/hooks.js
13
src/hooks.js
@@ -1,4 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import {
|
||||
IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS, FAILURE_STATUS,
|
||||
@@ -53,3 +55,14 @@ export function useRedirect() {
|
||||
|
||||
return redirect;
|
||||
}
|
||||
|
||||
export function useFeedbackWrapper() {
|
||||
useEffect(() => {
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
window.usabilla_live = lightningjs?.require('usabilla_live', getConfig().LEARNER_FEEDBACK_URL);
|
||||
} catch (error) {
|
||||
logError('Error loading usabilla_live', error);
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
import arMessages from './messages/ar.json';
|
||||
import deMessages from './messages/de.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
@@ -15,7 +18,7 @@ import ititCAMessages from './messages/it_IT.json';
|
||||
import ptptCAMessages from './messages/pt_PT.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
|
||||
const messages = {
|
||||
const appMessages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
'fa-ir': faIRMessages,
|
||||
@@ -33,4 +36,9 @@ const messages = {
|
||||
'pt-pt': ptptCAMessages,
|
||||
};
|
||||
|
||||
export default messages;
|
||||
export default [
|
||||
headerMessages,
|
||||
paragonMessages,
|
||||
footerMessages,
|
||||
appMessages,
|
||||
];
|
||||
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "تعديل",
|
||||
"account.settings.static.field.empty": "لا قيمة محددة. رجاءً اتصل بمديرك في {enterprise} ليقوم بالتعديلات.",
|
||||
"account.settings.static.field.empty.no.admin": "لا قيمة محددة.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "في حال التأشير، سيظهر هذا الاسم في شهاداتك و سجلاتك العامة.",
|
||||
"account.settings.field.name.modal.certificate.title": "اختر اسمًا مفضلًا للشهادات والسجلات العامة",
|
||||
"account.settings.field.name.modal.certificate.select": "اختر اسمًا",
|
||||
"account.settings.field.name.modal.certificate.option.full": "الاسم الكامل",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "اسم متحقَّق منه",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "اختيار الاسم",
|
||||
"account.settings.coaching.consent.welcome.header": "لنبدأ.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية",
|
||||
"account.settings.coaching.consent.description": "تتضمن برامج MicroBachelors مرافقة تركز على مهنتك و تعليمك و كيفية تحقيقك للنتائج، و ذلك من خلال التواصل الفردي مع خبير متمرس. إن كنت مهتمًا، فيرجى تزويدنا بالمعلومات أدناه و النقر على \"إرسال\"، و سيتصل بك شريكنا في المرافقة عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* خدمات المرافقة مشمولة دون تكاليف إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. تتضمن المرافقة رسائل نصية دورية. قد تنطبق أسعار على الرسائل والبيانات. أرسل STOP للانسحاب.",
|
||||
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات المرافقة",
|
||||
"account.settings.coaching.consent.decline-coaching": "أفضّل ألا يٌتصَل بي بخصوص خدمات المرافقة المجانية",
|
||||
"account.settings.coaching.consent.label.name": "رجاءً أكّد اسمك",
|
||||
"account.settings.coaching.consent.label.phone-number": "أدخل رقم هاتفك الجوّال",
|
||||
"account.settings.coaching.consent.success.header": "نجحت العملية!",
|
||||
"account.settings.coaching.consent.success.message": "أنت الآن مشترك في المرافقة. ترقّب رسالة عبر البريد الإلكتروني أو خدمة الرسائل القصيرة في في الأيام المقبلة.",
|
||||
"account.settings.coaching.consent.success.continue": "البدء في مساقي",
|
||||
"account.settings.coaching.managed.support": "الدعم",
|
||||
"account.settings.coaching.managed.alert": "اسمك يديره {ManagerTitle}. اتصل بمديرك للحصول على المساعدة.",
|
||||
"account.settings.field.phone_number": "رقم الهاتف",
|
||||
"account.settings.field.phone_number.empty": "إضافة رقم هاتف",
|
||||
"account.settings.field.coaching_consent": "الموافقة على المرافقة",
|
||||
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج MicroBachelors مرافقة قائمة على الرسائل النصية، تساعدك على إقران تجاربك التعلّمية مع أهدافك المهنية من خلال النصائح الفردية. خدمات المرافقة مشمولة دون تكاليف إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف جوالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل 'STOP' في أي وقت للانسحاب من الرسائل.",
|
||||
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صحيح للتسجيل في المرافقة",
|
||||
"account.settings.delete.account.before.proceeding": "قبل المتابعة، يرجى {actionLink}.",
|
||||
"account.settings.delete.account.header": "حذف حسابي",
|
||||
"account.settings.delete.account.subheader": "نأسف لذهابك!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلبًا من المتصفح للوصول إلى كاميرا جهازك، فتأكد رجاءً من النقر على {السماح}.",
|
||||
"id.verification.account.name.summary.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
|
||||
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مجددًا بعد بضع دقائق\nإن استمرت المشكلة، فيرجى الذهاب إلى {support_link} للحصول على المساعدة.",
|
||||
"id.verification.account.name.edit": "تعديل {sr}"
|
||||
"id.verification.account.name.edit": "تعديل {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s get started.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
|
||||
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"id.verification.account.name.edit": "Edit {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Bearbeiten",
|
||||
"account.settings.static.field.empty": "Kein Wert eingestellt. Wenden Sie sich an Ihren {enterprise} Administrator, um Änderungen vorzunehmen.",
|
||||
"account.settings.static.field.empty.no.admin": "Kein Wert eingestellt.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "Wenn diese Option aktiviert ist, erscheint dieser Name auf Ihren Zertifikaten und öffentlich zugänglichen Aufzeichnungen.",
|
||||
"account.settings.field.name.modal.certificate.title": "Wählen Sie einen bevorzugten Namen für Zertifikate und öffentlich zugängliche Aufzeichnungen",
|
||||
"account.settings.field.name.modal.certificate.select": "Wählen Sie einen Namen aus",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Vollständiger Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verifizierter Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Wähle Name",
|
||||
"account.settings.coaching.consent.welcome.header": "Starten Sie jetzt.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "Wir begleiten Sie von Anfang bis Ende. ",
|
||||
"account.settings.coaching.consent.description": "Zu den MicroBachelors-Programmen gehört ein Coaching, das sich auf Ihre Karriere, Ihre Ausbildung und die Art und Weise konzentriert, wie Sie durch persönliche Kommunikation mit einem erfahrenen Fachmann Ergebnisse erzielen. Wenn Sie interessiert sind, geben Sie die folgenden Informationen ein und klicken Sie auf \"Senden\". Unser Coaching-Partner wird sich per E-Mail und / oder SMS mit Ihnen in Verbindung setzen, um Sie voranzubringen. Geschäftsbedingungen gelten.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Die Coaching Dienste erfordern keine zusätzlichen Kosten für Teilnehmer mit US-Telefonnummern. Das Coaching beinhaltet regelmäßige SMS-Nachrichten. Es gelten die üblichen Tarife für SMS-Nachrichten. Mit dem Text STOP können Sie sich jederzeit abmelden.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Anmeldung zum Coaching ",
|
||||
"account.settings.coaching.consent.decline-coaching": "Ich bevorzuge es, ohne den coaching Service fortzufahren",
|
||||
"account.settings.coaching.consent.label.name": "Bitte bestätigen Sie Ihren Namen",
|
||||
"account.settings.coaching.consent.label.phone-number": "Bitte geben Sie Ihre Handynummer an",
|
||||
"account.settings.coaching.consent.success.header": "Super!",
|
||||
"account.settings.coaching.consent.success.message": "Sie sind nun für das Coaching angemeldet. Sie erhalten eine SMS-Nachricht als Bestätigung in den kommenden Tagen.",
|
||||
"account.settings.coaching.consent.success.continue": "Kurs starten",
|
||||
"account.settings.coaching.managed.support": "Support",
|
||||
"account.settings.coaching.managed.alert": "Ihr Name wird von {managerTitle} verwaltet. Wenden Sie sich an Ihren Administrator, um Hilfe zu erhalten.",
|
||||
"account.settings.field.phone_number": "Telefonnummer",
|
||||
"account.settings.field.phone_number.empty": "Eine Telefonnummer hinzufügen",
|
||||
"account.settings.field.coaching_consent": "Einverständnis zum Coaching",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors-Programme umfassen SMS-basiertes Coaching, das Ihnen hilft, Bildungserfahrungen mit Ihren Karrierezielen durch persönliche Beratung zu verbinden. Coaching-Services sind ohne zusätzliche Kosten enthalten und stehen Lernenden mit US-Handynummern zur Verfügung. Es gelten die Standard-Messaging-Gebühren. Senden Sie jederzeit eine SMS mit „STOP“, um Nachrichten abzubestellen.",
|
||||
"account.settings.field.coaching_consent.error": "Eine gültige Telefonnummer ist nötig, um sich für das Coaching anzumelden",
|
||||
"account.settings.delete.account.before.proceeding": "Bevor Sie fortfahren, bitte {actionLink}.",
|
||||
"account.settings.delete.account.header": "Meinen Account löschen",
|
||||
"account.settings.delete.account.subheader": "Es tut uns leid, dass Sie Ihren Account löschen möchten!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "Sie benötigen ein Gerät mit Kamera. Wenn Sie eine Browser-Eingabeaufforderung für den Zugriff auf Ihre Kamera erhalten, stellen Sie bitte sicher, dass Sie auf {allow} klicken.",
|
||||
"id.verification.account.name.summary.alert": "Ihre Kontoeinstellungen werden von {managerTitle} verwaltet. Wenn der Name auf Ihrem Lichtbildausweis nicht mit dem Namen auf Ihrem Konto übereinstimmt, wenden Sie sich bitte an Ihren {profileDataManager}-Administrator oder {support}, um Hilfe zu erhalten.",
|
||||
"idv.submission.alert.error": "Beim Versuch, die ID-Bestätigung einzureichen, ist ein technischer Fehler aufgetreten. Dies könnte ein vorübergehendes Problem sein, versuchen Sie es in ein paar Minuten erneut. Wenn das Problem weiterhin besteht, rufen Sie bitte {support_link} auf, um Hilfe zu erhalten.",
|
||||
"id.verification.account.name.edit": "{sr} bearbeiten"
|
||||
"id.verification.account.name.edit": "{sr} bearbeiten",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Editar",
|
||||
"account.settings.static.field.empty": "No hay valor establecido. Contacte su administrador {enterprise} para hacer cambios.",
|
||||
"account.settings.static.field.empty.no.admin": "No hay valor establecido.",
|
||||
"notification.preferences.notifications.label": "Notificaciones",
|
||||
"account.settings.field.name.certificate.select": "En caso de ser seleccionado, este nombre aparecerá en tus certificados y registros públicos. ",
|
||||
"account.settings.field.name.modal.certificate.title": "Escoge un nombre de preferencia para tus certificados y registros públicos.",
|
||||
"account.settings.field.name.modal.certificate.select": "Selecciona un nombre",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Nombre completo",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Nombre verificado",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Escoge un nombre",
|
||||
"account.settings.coaching.consent.welcome.header": "Empecemos",
|
||||
"account.settings.coaching.consent.welcome.subheader": "Estamos aquí para ustede desde el inicio hasta el final",
|
||||
"account.settings.coaching.consent.description": "Los programas de MicroBachelors incluyen entrenamiento que se enfoca en su carrera, educación y cómo logrará resultados a través de la comunicación individual con un profesional experimentado. Si está interesado, proporcione la información a continuación y haga clic en \"Enviar\", y nuestro socio asesor se comunicará con usted por correo electrónico y / o mensaje de texto para ayudarlo a avanzar. Los términos y Condiciones aplican.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Los servicios de entrenamiento se incluyen sin costo adicional para los alumnos con números de teléfono de EE. UU. El entrenamiento incluye mensajes de texto recurrentes. Se pueden aplicar tarifas por mensajes y datos. Envía STOP para cancelar la suscripción.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Registrarse para coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "Prefiero no ser contactado con servicios de coaching gratuitos.",
|
||||
"account.settings.coaching.consent.label.name": "Por favor confirme su nombre",
|
||||
"account.settings.coaching.consent.label.phone-number": "Ingrese su número de teléfono móvil",
|
||||
"account.settings.coaching.consent.success.header": "¡Éxito!",
|
||||
"account.settings.coaching.consent.success.message": "Estás inscrito para coaching. Puedes esperar un mensaje por correo electrónico o SMS en los próximos días.",
|
||||
"account.settings.coaching.consent.success.continue": "Iniciar mi curso",
|
||||
"account.settings.coaching.managed.support": "soporte",
|
||||
"account.settings.coaching.managed.alert": "{ManagerTitle} administra su Nombre. Póngase en contacto con su administrador para obtener ayuda.",
|
||||
"account.settings.field.phone_number": "Teléfono",
|
||||
"account.settings.field.phone_number.empty": "Añadir un número de teléfono",
|
||||
"account.settings.field.coaching_consent": "Consentimiento de coaching",
|
||||
"account.settings.field.coaching_consent.tooltip": "Los programas de MicroBachelors incluyen entrenamiento basado en mensajes de texto que lo ayuda a emparejar experiencias educativas con sus objetivos profesionales a través de asesoramiento personalizado. Los servicios de entrenamiento se incluyen sin costo adicional y están disponibles para estudiantes con números de teléfono móvil de EE. UU. Se aplican tarifas de mensajería estándar. Envíe \"STOP\" en cualquier momento para cancelar la suscripción a los mensajes.",
|
||||
"account.settings.field.coaching_consent.error": "Se requiere un número de teléfono válido de EE. UU. Para optar por el coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Antes de continuar, por favor {actionLink}.",
|
||||
"account.settings.delete.account.header": "Eliminar mi cuenta",
|
||||
"account.settings.delete.account.subheader": "¡Sentimos que te vayas!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "Necesitas un dispositivo que tenga una cámara. Si has recibido un aviso del navegador para habilitar acceso a tu cámara, por favor asegúrate de seleccionar [allow].",
|
||||
"id.verification.account.name.summary.alert": "La configuración de tu perfil es administrada por {managerTitle}. Por lo tanto si el nombre en tu ID de foto coincide con tu nombre de cuenta por favor ponte en contacto con tu administrador de {profileDataManager} o con {soporte} para solicitar ayuda.",
|
||||
"idv.submission.alert.error": "\n Se produjo un error técnico al intentar enviar la verificación de ID.\n Es posible que sea una cuestión temporal, así que inténtalo de nuevo en unos minutos.\n Si el problema continúa, dirígete a {support_link} para obtener ayuda.\n ",
|
||||
"id.verification.account.name.edit": "Editar {sr}"
|
||||
"id.verification.account.name.edit": "Editar {sr}",
|
||||
"notification.preference.heading": "Notificaciones",
|
||||
"notification.preference.app.title": "{ clave, seleccionar, discusión {Debates} trabajo del curso {Trabajo del curso} otro {{clave}} }",
|
||||
"notification.preference.title": "{ text, select, core {Notificaciones principales} newDiscussionPost {Nuevas publicaciones de discusión} newQuestionPost {Nuevas publicaciones de preguntas} other { {text} } }",
|
||||
"notification.preference.type.label": "Tipo",
|
||||
"notification.preference.web,label": "Sitio Web",
|
||||
"notification.preference.help.email": "Correo electrónico",
|
||||
"notification.preference.help.push": "Empujar",
|
||||
"notification.preference.load.more.courses": "Cargar más cursos",
|
||||
"notification.preference.guide.link": "como se detalla aquí",
|
||||
"notification.preference.guide.body": "Las notificaciones para determinadas actividades están habilitadas de forma predeterminada,"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"account.settings.message.duplicate.tpa.provider": "حساب کاربری {provider} که برگزیدید قبلاً به حساب کاربری دیگر {siteName} پیوند داده شدهاست.",
|
||||
"account.settings.message.managed.settings": "تنظیمات پرونده کاربری شما بهدست {managerTitle} مدیریت میشود. برای راهنمایی با سرپرست یا {support} خود تماس بگیرید.",
|
||||
"account.settings.message.managed.settings": "تنظیمات پرونده کاربری شما بهدست {managerTitle} مدیریت میشود. برای راهنمایی با مدیر یا {support} خود تماس بگیرید.",
|
||||
"account.settings.message.managed.settings.support": "پشتیبانی",
|
||||
"account.settings.page.heading": "تنظیمات حساب کاربری",
|
||||
"account.settings.loading.message": "در حال بارگیری ...",
|
||||
@@ -57,12 +57,12 @@
|
||||
"account.settings.field.year_of_birth.options.empty": "انتخاب سال تولد",
|
||||
"account.settings.field.dob.month": "ماه",
|
||||
"account.settings.field.dob.year": "سال",
|
||||
"account.settings.field.month.year.default": "Select month",
|
||||
"account.settings.field.dob.year.default": "Select year",
|
||||
"account.settings.field.month.year.default": "ماه را انتخاب کنید",
|
||||
"account.settings.field.dob.year.default": "سال را انتخاب کنید",
|
||||
"account.settings.field.dob.form.button": "لطفا تاریخ تولد را وارد کنید",
|
||||
"account.settings.field.dob.form.title": "Enter your birth month and year",
|
||||
"account.settings.field.dob.form.help.text": "We ask for birth month and year information to help us comply with our legal obligations.",
|
||||
"account.settings.field.dob.form.success": "Thank you for entering your information.",
|
||||
"account.settings.field.dob.form.title": "ماه و سال تولد خود را وارد کنید",
|
||||
"account.settings.field.dob.form.help.text": "ما اطلاعات ماه و سال تولد را درخواست می کنیم تا به ما در انجام تعهدات قانونی خود کمک کند.",
|
||||
"account.settings.field.dob.form.success": "از اینکه اطلاعات خود را وارد کردید متشکرم",
|
||||
"account.settings.field.month_of_birth.options.empty": "ماه تولد خود را برگزینید",
|
||||
"account.settingsfield.dob.error.general": "مشکل فنی رخ داده است. دوباره تلاش کنید.",
|
||||
"account.settings.field.country": "کشور",
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": " ویرایش",
|
||||
"account.settings.static.field.empty": "مقداری برای آن تعیین نشده است. برای ایجاد تغییرات با مدیر {enterprise} خود تماس بگیرید.",
|
||||
"account.settings.static.field.empty.no.admin": "هیچ مقداری تنظیم نشده است.",
|
||||
"notification.preferences.notifications.label": "اعلانها",
|
||||
"account.settings.field.name.certificate.select": "اگر علامت زده شود، این نام در گواهیها و سوابق عمومی شما درج میشود.",
|
||||
"account.settings.field.name.modal.certificate.title": "نامی مرجح برای گواهیها و سوابق عمومی انتخاب کنید",
|
||||
"account.settings.field.name.modal.certificate.select": "یک نام انتخاب کنید",
|
||||
"account.settings.field.name.modal.certificate.option.full": "نام و نام خانوادگی",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "تغییر نام",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "انتخاب نام",
|
||||
"account.settings.coaching.consent.welcome.header": "بیا آغاز کنیم.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "ما از آغاز تا پایان با شما هستیم",
|
||||
"account.settings.coaching.consent.description": "برنامههای MicroBachelors شامل مربیگری است که بر حرفه، تحصیلات و نحوه دستیابی به نتایج از طریق ارتباط یکبهیک با یک متخصص با تجربه تمرکز دارد. اگر علاقهمند هستید، اطلاعات زیر را ارائه دهید و روی «ارسال» کلیک کنید، و شریک مربی ما از طریق رایانامه و/یا پیام متنی با شما در ارتباط خواهد بود تا به شما در حرکت به جلو کمک کند. شرایط و ضوابط اعمال میشود.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* خدمات مربیگری بدون هزینه اضافی برای یادگیرندگان با شماره تلفن ایالات متحده گنجانده شده است. مربیگری شامل پیامهای متنی مکرر است. ممکن است نرخ پیام و داده اعمال شود. برای انصراف، STOP را پیامک کنید.",
|
||||
"account.settings.coaching.consent.accept-coaching": "ثبتنام برای مربیگری",
|
||||
"account.settings.coaching.consent.decline-coaching": "ترجیح میدهم با خدمات مربیگری رایگان تماس نگیرم",
|
||||
"account.settings.coaching.consent.label.name": "لطفا نام خود را تایید کنید",
|
||||
"account.settings.coaching.consent.label.phone-number": "شماره تلفن همراه خود را وارد کنید",
|
||||
"account.settings.coaching.consent.success.header": "موفق شدید!",
|
||||
"account.settings.coaching.consent.success.message": "شما برای مربیگری ثبتنام کردید. در روزهای آینده منتظر پیامک یا رایانامه باشید.",
|
||||
"account.settings.coaching.consent.success.continue": "آغاز دوره آموزشی ",
|
||||
"account.settings.coaching.managed.support": "پشتیبانی",
|
||||
"account.settings.coaching.managed.alert": "نام شما توسط {managerTitle} مدیریت میشود. برای راهنمایی با مدیر خود تماس بگیرید.",
|
||||
"account.settings.field.phone_number": "شماره تلفن",
|
||||
"account.settings.field.phone_number.empty": "افزودن شماره تلفن",
|
||||
"account.settings.field.coaching_consent": "رضایت مربیگری",
|
||||
"account.settings.field.coaching_consent.tooltip": "برنامه های MicroBachelors شامل مربیگری مبتنی بر پیام متنی است که به شما کمک میکند تجربیات آموزشی را با اهداف شغلی خود از طریق مشاوره یک به یک، جفت کنید. خدمات مربیگری بدون هزینه اضافی گنجانده شده است و برای زبان آموزان با شماره تلفن همراه ایالات متحده در دسترس است. نرخ استاندارد پیامرسانی اعمال میشود. برای انصراف از پیامها، در هر زمان «توقف» را پیامک کنید.",
|
||||
"account.settings.field.coaching_consent.error": "ارائه یک شماره تلفن معتبر ایالات متحده برای شرکت در مربیگری ضروری است",
|
||||
"account.settings.delete.account.before.proceeding": "پیش از ادامه، لطفاً {actionLink}.",
|
||||
"account.settings.delete.account.header": "حذف حساب کاربری من",
|
||||
"account.settings.delete.account.subheader": "از رفتن شما متأسفیم. ",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "شما به دستگاهی نیاز دارید که دوربین داشته باشد. اگر درخواست مرورگر برای دسترسی به دوربین خود دریافت کردید، لطفاً روی {allow} کلیک کنید.",
|
||||
"id.verification.account.name.summary.alert": "تنظیمات حساب کاربری شما توسط {manager Title} مدیریت میشود. نام روی تصویر کارت شناسایی شما با نام حساب کاربری شما مطابقت ندارد، لطفاً برای راهنمایی با مدیر {profile DataManager} یا {support} خود تماس بگیرید.",
|
||||
"idv.submission.alert.error": "\nهنگام تلاش برای ارسال تأیید هویت با خطای فنی مواجه شدیم. \nاین مشکل ممکن است موقتی باشد، پس لطفاً چند دقیقه دیگر دوباره امتحان کنید. \nاگر مشکل همچنان ادامه داشت، لطفاً برای راهنمایی به {support_link} بروید.",
|
||||
"id.verification.account.name.edit": "ویرایش {sr}"
|
||||
"id.verification.account.name.edit": "ویرایش {sr}",
|
||||
"notification.preference.heading": "اعلانها",
|
||||
"notification.preference.app.title": "{\n کلید, انتخاب,\n بحث {Discussions}\n کار دوره {Course Work}\n سایر {{key}}\n }",
|
||||
"notification.preference.title": "{ متن، انتخاب، هسته {اعلانهای اصلی} newDiscussionPost {پستهای بحث جدید} newQuestionPost {پستهای سؤال جدید} سایر { {text} } }",
|
||||
"notification.preference.type.label": "نوع",
|
||||
"notification.preference.web,label": "شبکه",
|
||||
"notification.preference.help.email": "نشانی رایانامه",
|
||||
"notification.preference.help.push": "فشار دادن",
|
||||
"notification.preference.load.more.courses": "بارگیری دوره های بیشتر",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Modifier",
|
||||
"account.settings.static.field.empty": "Aucune valeur n'a été fixée. Contactez l'administrateur de votre {entreprise} pour effectuer des modifications.",
|
||||
"account.settings.static.field.empty.no.admin": "Pas de valeur définie.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "Si sélectionné, ce nom va apparaître sur vos attestations et dossiers publics.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choisissez le nom qui va apparaîtres sur vos attestations et dossiers publics.",
|
||||
"account.settings.field.name.modal.certificate.select": "Sélectionnez un nom",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Nom complet",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Nom vérifié",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choisissez un nom",
|
||||
"account.settings.coaching.consent.welcome.header": "Commençons.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "Nous sommes là pour vous du début à la fin",
|
||||
"account.settings.coaching.consent.description": "Les programmes MicroBachelors comprennent un coaching axé sur votre carrière, vos études et la façon dont vous obtiendrez des résultats grâce à une communication individuelle avec un professionnel expérimenté. Si vous êtes intéressé, fournissez les informations ci-dessous et cliquez sur \"Soumettre\". Notre partenaire de coaching vous contactera par courrier électronique et/ou par SMS pour vous aider à progresser. Les conditions générales s'appliquent.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Les services de coaching sont inclus sans frais supplémentaires pour les apprenants ayant un numéro de téléphone aux États-Unis. Le coaching comprend des messages textes récurrents. Les tarifs des messages et des données peuvent s'appliquer. Text STOP pour se désengager.",
|
||||
"account.settings.coaching.consent.accept-coaching": "S'inscrire pour le coaching.",
|
||||
"account.settings.coaching.consent.decline-coaching": "Je préfère ne pas être contacté par les services de coaching gratuit.",
|
||||
"account.settings.coaching.consent.label.name": "Veuillez confirmer votre nom",
|
||||
"account.settings.coaching.consent.label.phone-number": "Entrer un numéro de cellulaire",
|
||||
"account.settings.coaching.consent.success.header": "Opération réussie!",
|
||||
"account.settings.coaching.consent.success.message": "Vous êtes inscrit au coaching. Vous pouvez vous attendre à un message par courriel ou SMS dans les prochains jours.",
|
||||
"account.settings.coaching.consent.success.continue": "Démarrer mon cours",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Votre nom est géré par {managerTitle}. Contactez votre administrateur pour obtenir de l'aide.",
|
||||
"account.settings.field.phone_number": "Numéro de téléphone",
|
||||
"account.settings.field.phone_number.empty": "Ajouter un numéro de téléphone",
|
||||
"account.settings.field.coaching_consent": "Consentement pour le coaching",
|
||||
"account.settings.field.coaching_consent.tooltip": "Un parcours MicroBachelors inclus du coaching par texto avec un professionnel expériementé qui vous aidera à combiner votre expérience éducationnelle et vos objectifs de carrière. Les services de coaching sont inclus sans coût additionnel aux apprenant ayant un numéro de téléphone US. Le coût standard des textos s'applique. Texter 'STOP' en tout temp pour arrêter les messages.",
|
||||
"account.settings.field.coaching_consent.error": "Un numéro de téléphone US valide est requis pour s'inscrire pour du coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Avant de poursuivre, veuillez {actionLink}.",
|
||||
"account.settings.delete.account.header": "Supprimer mon compte",
|
||||
"account.settings.delete.account.subheader": "Nous sommes désolés de vous voir quitter!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "Vous avez besoin d'un appareil équipé d'une caméra. Si vous recevez une invite du navigateur pour accéder à votre caméra, assurez-vous de cliquer sur {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Les paramètres de votre compte sont gérés par {managerTitle}. Si le nom sur votre pièce d'identité avec photo ne correspond pas au nom de votre compte, veuillez contacter votre administrateur {profileDataManager} ou {support} pour obtenir de l'aide.",
|
||||
"idv.submission.alert.error": "\n Nous avons rencontré une erreur technique en essayant de soumettre la vérification d'identité.\n Il peut s'agir d'un problème temporaire. Veuillez réessayer dans quelques minutes.\n Si le problème persiste, veuillez consulter {support_link} pour obtenir de l'aide.\n ",
|
||||
"id.verification.account.name.edit": "Modifier {sr}"
|
||||
"id.verification.account.name.edit": "Modifier {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
"account.settings.field.email": "Adresse courriel (Connexion)",
|
||||
"account.settings.field.email.empty": "Ajouter une adresse courriel",
|
||||
"account.settings.field.email.confirmation": "Nous avons envoyé un message de confirmation à {value}. Cliquez sur le lien dans le message pour mettre à jour votre adresse courriel.",
|
||||
"account.settings.field.email.help.text": "Vous recevrez des messages de {siteName} et des équipes pédagogiques à cette adresse.",
|
||||
"account.settings.field.email.help.text": "Vous recevrez des messages de {siteName} et des équipes de cours à cette adresse.",
|
||||
"account.settings.field.secondary.email": "Adresse courriel de récupération",
|
||||
"account.settings.field.secondary.email.empty": "Ajouter une adresse courriel de récupération",
|
||||
"account.settings.field.secondary.email.confirmation": "Nous avons envoyé un message de confirmation à {value}. Cliquez sur le lien dans le message pour mettre à jour votre adresse courriel de récupération.",
|
||||
@@ -84,7 +84,7 @@
|
||||
"account.settings.field.education.levels.jhs": "Collège / enseignement secondaire inférieur",
|
||||
"account.settings.field.education.levels.el": "Enseignement primaire",
|
||||
"account.settings.field.education.levels.none": "Sans diplôme",
|
||||
"account.settings.field.education.levels.o": "Autre",
|
||||
"account.settings.field.education.levels.o": "Autre niveau d'études",
|
||||
"account.settings.field.gender": "Genre",
|
||||
"account.settings.field.gender.empty": "Ajouter le genre",
|
||||
"account.settings.field.gender.options.empty": "Sélectionner un genre",
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Modifier",
|
||||
"account.settings.static.field.empty": "Aucune valeur définie. Contactez l'administrateur de votre {enterprise} pour effectuer des modifications.",
|
||||
"account.settings.static.field.empty.no.admin": "Aucune valeur définie.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "Si sélectionné, ce nom va apparaître sur vos attestations et dossiers publics.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choisissez le nom qui va apparaître sur vos attestations et dossiers publics",
|
||||
"account.settings.field.name.modal.certificate.select": "Sélectionnez un nom",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Nom complet",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Nom vérifié",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choisissez un nom",
|
||||
"account.settings.coaching.consent.welcome.header": "Commençons.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "Nous vous accompagnons du début jusqu'à la fin",
|
||||
"account.settings.coaching.consent.description": "Un parcours MicroBachelors inclus du coaching avec un professionnel expérimenté qui vous guidera sur votre carrière, votre éducation et comment atteindre vos objectifs. Si vous êtes intéressés, entrez les informations requises ci-dessous et cliquez \"Soumettre\". Un guide vous contactera par courriel ou texto pour vous aider à progresser. Termes et conditions s'appliquent. *",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Nos services de coaching sont inclus sans coût additionnel aux apprenant ayant un numéro de téléphone US. Le coaching comprend des textos récurrents. Le coût des messages et des données peuvent s'appliquer. Texter STOP pour arrêter.",
|
||||
"account.settings.coaching.consent.accept-coaching": "S'inscrire pour le coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "Je préfère ne pas être contacté par les services de coaching gratuit",
|
||||
"account.settings.coaching.consent.label.name": "Veuillez confirmer votre nom",
|
||||
"account.settings.coaching.consent.label.phone-number": "Entrer un numéro de cellulaire",
|
||||
"account.settings.coaching.consent.success.header": "Succès!",
|
||||
"account.settings.coaching.consent.success.message": "Vous êtes inscrit au coaching. Vous pouvez vous attendre à un message par courriel ou SMS dans les prochains jours.",
|
||||
"account.settings.coaching.consent.success.continue": "Commencer mon cours",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Votre nom est géré par {managerTitle}. Contactez votre administrateur pour obtenir de l'aide.",
|
||||
"account.settings.field.phone_number": "Numéro de téléphone",
|
||||
"account.settings.field.phone_number.empty": "Ajouter un numéro de téléphone",
|
||||
"account.settings.field.coaching_consent": "Consentement pour le coaching",
|
||||
"account.settings.field.coaching_consent.tooltip": "Un parcours MicroBachelors inclus du coaching par texto avec un professionnel expériementé qui vous aidera à combiner votre expérience éducationnelle et vos objectifs de carrière. Les services de coaching sont inclus sans coût additionnel aux apprenant ayant un numéro de téléphone US. Le coût standard des textos s'applique. Texter 'STOP' en tout temp pour arrêter les messages.",
|
||||
"account.settings.field.coaching_consent.error": "Un numéro de téléphone US valide est requis pour s'inscrire pour du coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Avant de continuer, veuillez {actionLink}.",
|
||||
"account.settings.delete.account.header": "Supprimer mon compte",
|
||||
"account.settings.delete.account.subheader": "Nous sommes désolés de vous voir quitter!",
|
||||
@@ -239,7 +222,7 @@
|
||||
"id.verification.access.blocked.enrollment": "Vous n'êtes actuellement pas inscrit à un cours nécessitant une vérification d'identité.",
|
||||
"id.verification.access.blocked.pending": "Vous avez déjà soumis vos informations de vérification. Vous verrez un message sur votre tableau de bord lorsque le processus de vérification sera terminé (généralement dans les 5 jours).",
|
||||
"id.verification.photo.take": "Prendre une photo",
|
||||
"id.verification.photo.retake": "Reprendre la photo ?",
|
||||
"id.verification.photo.retake": "Reprendre la photo?",
|
||||
"id.verification.photo.enable.detection": "Activer la détection des visages",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "Si coché, une case apparaîtra autour de votre visage. Votre visage peut être vu clairement si la boîte qui l'entoure est bleue. Si votre visage n'est pas dans une bonne position ou indétectable, la case sera rouge.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "Si coché, une case apparaîtra autour du visage sur votre pièce d'identité. Le visage peut être vu clairement si la boîte qui l'entoure est bleue. Si le visage n'est pas dans une bonne position ou indétectable, la case sera rouge.",
|
||||
@@ -270,7 +253,7 @@
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2": "Accédez à Plus> Paramètres.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "Pour Windows: Alt+F, Alt+E ou F10 suivi de la barre d'espace",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "Pour Mac : Command+,",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "Sous l'onglet «Confidentialité et sécurité», sélectionnez «Paramètres du site», puis «Appareil photo».",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "Sous l'onglet \"Confidentialité et sécurité\", sélectionnez \"Paramètres du site\", puis \"Appareil photo\".",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step4": "Sous \"Bloqué\", recherchez \"edulib.org\" et sélectionnez-le.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step5": "Dans la section \"Autorisations\", mettez à jour les autorisations de la caméra pour \"Autoriser\".",
|
||||
"id.verification.camera.access.failure.temporary.ie11": "Pour activer l'accès à la caméra dans Internet Explorer :",
|
||||
@@ -304,7 +287,7 @@
|
||||
"id.verification.camera.help.sight.question": "Que faire si je ne peux pas voir l'image de la caméra ou si je ne peux pas voir ma photo pour déterminer quel côté est visible?",
|
||||
"id.verification.camera.help.sight.answer.portrait": "Vous pourrez peut-être terminer la procédure de capture d'image sans aide, mais cela peut prendre quelques tentatives de soumission pour obtenir le bon positionnement de la caméra. Le positionnement optimal de la caméra varie avec chaque ordinateur, mais généralement la meilleure position pour une prise de vue de la tête est d'environ 12-18 pouces (30-45 centimètres) de la caméra, la tête étant centrée par rapport à l'écran de l'ordinateur. Si les photos que vous soumettez sont rejetées, essayez de déplacer l’orientation de l’ordinateur ou de l’appareil photo pour modifier l’angle d’éclairage.",
|
||||
"id.verification.camera.help.sight.answer.id": "Vous pourrez peut-être terminer la procédure de capture d'image sans aide, mais cela peut prendre quelques tentatives de soumission pour obtenir le bon positionnement de la caméra. Le positionnement optimal de la caméra varie avec chaque ordinateur, mais généralement, la meilleure position pour une photo d'une pièce d'identité est de 8-12 pouces (20-30 centimètres) de la caméra, la pièce d'identité étant centrée par rapport à la caméra. Si les photos que vous soumettez sont rejetées, essayez de déplacer l’orientation de l’ordinateur ou de l’appareil photo pour modifier l’angle d’éclairage. La raison la plus courante de rejet est l'incapacité de lire le texte sur la pièce d'identité.",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "Que faire si j'ai de la difficulté à tenir ma tête en position relativement à la caméra ?",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "Que faire si j'ai de la difficulté à tenir ma tête en position relativement à la caméra?",
|
||||
"id.verification.camera.help.difficulty.question.id": "Que faire si j'ai de la difficulté à tenir ma pièce d'identité en position relativement à la caméra?",
|
||||
"id.verification.camera.help.difficulty.answer": "Si vous avez besoin d'aide pour prendre une photo à soumettre, contactez le support {siteName} pour des suggestions supplémentaires.",
|
||||
"id.verification.id.photo.unclear.question": "L'image de votre pièce d'identité n'est pas claire ou trop floue?",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "Vous avez besoin d'un appareil équipé d'une caméra. Si vous recevez une invite du navigateur pour accéder à votre caméra, assurez-vous de cliquer sur {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Les paramètres de votre compte sont gérés par {managerTitle}. Si le nom sur votre pièce d'identité avec photo ne correspond pas au nom de votre compte, veuillez contacter votre administrateur {profileDataManager} ou {support} pour obtenir de l'aide.",
|
||||
"idv.submission.alert.error": "\n Nous avons rencontré une erreur technique en essayant de soumettre la vérification d'identité.\n Il peut s'agir d'un problème temporaire. Veuillez réessayer dans quelques minutes.\n Si le problème persiste, veuillez consulter {support_link} pour obtenir de l'aide.\n ",
|
||||
"id.verification.account.name.edit": "Modifier {sr}"
|
||||
"id.verification.account.name.edit": "Modifier {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Courriel",
|
||||
"notification.preference.help.push": "Pousser",
|
||||
"notification.preference.load.more.courses": "Charger plus de cours",
|
||||
"notification.preference.guide.link": "comme détaillé ici",
|
||||
"notification.preference.guide.body": "Les notifications pour certaines activités sont activées par défaut,"
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s get started.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
|
||||
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"id.verification.account.name.edit": "Edit {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s get started.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
|
||||
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"id.verification.account.name.edit": "Edit {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Modifica",
|
||||
"account.settings.static.field.empty": "Nessun valore impostato. Contatta il tuo amministratore {enterprise} per apportare modifiche. ",
|
||||
"account.settings.static.field.empty.no.admin": "Nessun valore impostato. ",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "Se selezionato, questo nome apparirà sui tuoi certificati e record pubblici.",
|
||||
"account.settings.field.name.modal.certificate.title": "Scegli un nome preferito per certificati e record pubblici",
|
||||
"account.settings.field.name.modal.certificate.select": "Seleziona un nome",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Nome e Cognome",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Nome verificato",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Scegli il nome",
|
||||
"account.settings.coaching.consent.welcome.header": "Partiamo!",
|
||||
"account.settings.coaching.consent.welcome.subheader": "Saremo qui per te dall'inizio alla fine",
|
||||
"account.settings.coaching.consent.description": "I programmi MicroBachelors prevedono un servizio di coaching con un professionista esperto incentrato sulla tua carriera, istruzione e su come ottenere risultati attraverso la comunicazione individuale. Se sei interessato, fornisci le informazioni richieste di seguito e fai clic su \"Invia\" e il nostro partner di insegnamento si metterà in contatto con te tramite e-mail e/o messaggi sms per aiutarti a procedere. Si applicano Termini e Condizioni.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* I servizi di coaching sono inclusi senza costi aggiuntivi per gli studenti con numeri di telefono statunitensi. Il coaching include messaggi di testo ricorrenti. Potrebbero essere applicate tariffe per messaggi e dati. Invia un messaggio con il testo STOP per disattivare l'offerta.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Iscriviti al coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "Preferisco non essere contattato con servizi di coaching gratuiti ",
|
||||
"account.settings.coaching.consent.label.name": "Conferma il tuo nome",
|
||||
"account.settings.coaching.consent.label.phone-number": "Immetti il tuo numero di cellulare",
|
||||
"account.settings.coaching.consent.success.header": "Completato correttamente!",
|
||||
"account.settings.coaching.consent.success.message": "Sei iscritto al coaching. Puoi aspettarti un messaggio via e-mail o SMS nei prossimi giorni. ",
|
||||
"account.settings.coaching.consent.success.continue": "Inizia il mio corso",
|
||||
"account.settings.coaching.managed.support": "supporto",
|
||||
"account.settings.coaching.managed.alert": "Il tuo nome è gestito da {managerTitle}. Rivolgiti all'amministratore per assistenza.",
|
||||
"account.settings.field.phone_number": "Numero di telefono",
|
||||
"account.settings.field.phone_number.empty": "Aggiungi un numero di telefono",
|
||||
"account.settings.field.coaching_consent": "Autorizza Coaching",
|
||||
"account.settings.field.coaching_consent.tooltip": "I programmi MicroBachelors includono un servizio di coaching basato su messaggi SMS che ti aiuta ad abbinare le esperienze formative ai tuoi obiettivi di carriera attraverso consigli personali. I servizi di coaching sono inclusi senza costi aggiuntivi e sono disponibili per gli studenti con numeri di telefono cellulare statunitensi. Si applicano le tariffe standard per la messaggistica SMS. Invia in qualsiasi momento un SMSM con scritto ‘STOP’ per disattivare i messaggi.",
|
||||
"account.settings.field.coaching_consent.error": "Per attivare il servizio di coaching è necessario un numero di telefono statunitense valido",
|
||||
"account.settings.delete.account.before.proceeding": "Prima di procedere, {actionLink}.",
|
||||
"account.settings.delete.account.header": "Elimina il mio Account",
|
||||
"account.settings.delete.account.subheader": "Ci dispiace che tu ci lasci!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "Hai bisogno di un dispositivo dotato di fotocamera. Se ricevi una richiesta del browser per l'accesso alla fotocamera, assicurati di fare clic su {allow}. ",
|
||||
"id.verification.account.name.summary.alert": "Le impostazioni del tuo account sono gestite da {managerTitle}. Se il nome riportato sul tuo documento d'identità con foto non corrisponde al nome nel tuo account, contatta il tuo amministratore di {profileDataManager} o {support} per assistenza. ",
|
||||
"idv.submission.alert.error": "\n Si è verificato un errore tecnico durante il tentativo di inviare la verifica del documento d'identità.\nÈ possibile che ciò sia dovuto ad un problema temporaneo, quindi riprova tra qualche minuto.\nSe il problema persiste, accedi alla pagina {support_link} per assistenza.",
|
||||
"id.verification.account.name.edit": "Modifica {sr}"
|
||||
"id.verification.account.name.edit": "Modifica {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s get started.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
|
||||
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"id.verification.account.name.edit": "Edit {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -20,30 +20,30 @@
|
||||
"account.settings.field.full.name": "Nome completo",
|
||||
"account.settings.field.full.name.empty": "Adicionar nome",
|
||||
"account.settings.field.full.name.help.text": "O nome que é utilizado para verificação de ID e que aparece nos seus certificados.",
|
||||
"account.settings.field.full.name.help.text.default": "The name that appears on your public profile.",
|
||||
"account.settings.field.full.name.help.text.default.certificate": "This name is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified": "Verified name",
|
||||
"account.settings.field.name.verified.help.text.verified": "This name has been verified by photo ID.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored": "This name has been verified by proctoring.",
|
||||
"account.settings.field.name.verified.help.text.verified.certificate": "This name has been verified by photo ID, and is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "This name has been verified by proctoring, and is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored": "Your proctored exam has been submitted. Verified name cannot be changed at this time. Please check back in 2-5 days.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "Once your proctored exam passes review, this name will appear on your certificate and public-facing records. Verified Name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your unexpired student, work, or government-issued identification card.",
|
||||
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored": "Your proctored exam has been submitted. Full name cannot be changed at this time. Please check back in 2-5 days.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "Once your proctored exam passes review, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.success.message": "Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.",
|
||||
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!",
|
||||
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.",
|
||||
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.",
|
||||
"account.settings.field.full.name.help.text.default": "O nome que aparece no seu perfil público.",
|
||||
"account.settings.field.full.name.help.text.default.certificate": "Este nome está selecionado para aparecer nos seus certificados e registos públicos.",
|
||||
"account.settings.field.name.verified": "Nome verificado",
|
||||
"account.settings.field.name.verified.help.text.verified": "Este nome foi verificado por identificação com foto.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored": "Este nome foi verificado por supervisão.",
|
||||
"account.settings.field.name.verified.help.text.verified.certificate": "Esse nome foi verificado por identificação com foto e foi selecionado para aparecer em seus certificados e registros públicos.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "Esse nome foi verificado por supervisão e foi selecionado para aparecer em seus certificados e registros públicos.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "A verificação foi submetida. Normalmente demora 48 horas ou menos. O nome verificado não pode ser alterado neste momento. ",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored": "Seu exame supervisionado foi enviado. O nome verificado não pode ser alterado neste momento. Por favor, verifique novamente em 2-5 dias.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "Quando a verificação de identidade for bem sucedida, este nome aparecerá nos seus certificados e registos públicos. O nome verificado não pode ser alterado neste momento.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "Depois que seu exame supervisionado passar na revisão, esse nome aparecerá no seu certificado e nos registros públicos. O Nome Verificado não pode ser alterado neste momento.",
|
||||
"account.settings.field.name.verified.verification.help": "Digite seu nome como ele aparece em seu cartão de estudante, trabalho ou documento de identificação emitido pelo governo.",
|
||||
"account.settings.field.full.name.help.text.submitted": "A verificação foi submetida. Normalmente demora 48 horas ou menos. O nome completo não pode ser alterado neste momento.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored": "Seu exame supervisionado foi enviado. O nome completo não pode ser alterado neste momento. Por favor, verifique novamente em 2-5 dias.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "Quando a verificação de identidade for bem sucedida, este nome aparecerá nos seus certificados e registos públicos. O nome completo não pode ser alterado neste momento.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "Depois que seu exame supervisionado passar na revisão, esse nome aparecerá em seus certificados e registros públicos. O nome completo não pode ser alterado neste momento.",
|
||||
"account.settings.field.name.verified.success.message": "O seu pedido de verificação de identidade foi concluído com êxito. Agora tem a opção de selecionar qual o nome que prefere que apareça nos seus certificados e registos públicos.",
|
||||
"account.settings.field.name.verified.success.message.header": "O seu pedido de alteração de nome está completo!",
|
||||
"account.settings.field.name.verified.failure.message": "A sua tentativa de verificação de identidade mais recente não passou. As definições de conta relacionadas foram restauradas.",
|
||||
"account.settings.field.name.verified.failure.message.header": "Não foi possível verificar a sua identidade.",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "Saiba mais sobre a verificação da ID",
|
||||
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!",
|
||||
"account.settings.field.name.verified.submitted.message": "O seu pedido de verificação de identidade foi submetido e geralmente demora entre 24 e 48 horas a ser concluído.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "Quando o seu pedido for aprovado, o seu nome atualizado aparecerá em todos os certificados associados e registos públicos.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "O seu pedido de alteração de nome está quase completo!",
|
||||
"account.settings.field.email": "Endereço de e-mail (Iniciar sessão)",
|
||||
"account.settings.field.email.empty": "Adicionar endereço de e-mail",
|
||||
"account.settings.field.email.confirmation": "Foi enviada uma mensagem de confirmação para {value}. Cique no link da mensagem para atualizar o seu endereço de e-mail.",
|
||||
@@ -55,16 +55,16 @@
|
||||
"account.settings.field.dob": "Ano de nascimento",
|
||||
"account.settings.field.dob.empty": "Adicione ano de nascimento",
|
||||
"account.settings.field.year_of_birth.options.empty": "Selecionar um ano de nascimento",
|
||||
"account.settings.field.dob.month": "Month",
|
||||
"account.settings.field.dob.year": "Year",
|
||||
"account.settings.field.month.year.default": "Select month",
|
||||
"account.settings.field.dob.year.default": "Select year",
|
||||
"account.settings.field.dob.form.button": "Please confirm your date of birth",
|
||||
"account.settings.field.dob.form.title": "Enter your birth month and year",
|
||||
"account.settings.field.dob.form.help.text": "We ask for birth month and year information to help us comply with our legal obligations.",
|
||||
"account.settings.field.dob.form.success": "Thank you for entering your information.",
|
||||
"account.settings.field.month_of_birth.options.empty": "Select a month of birth",
|
||||
"account.settingsfield.dob.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.field.dob.month": "Mês",
|
||||
"account.settings.field.dob.year": "Ano",
|
||||
"account.settings.field.month.year.default": "Selecione um mês",
|
||||
"account.settings.field.dob.year.default": "Selecione um ano",
|
||||
"account.settings.field.dob.form.button": "Por favor confirme a sua data de nascimento",
|
||||
"account.settings.field.dob.form.title": "Introduza o seu mês e ano de nascimento",
|
||||
"account.settings.field.dob.form.help.text": "Pedimos informações sobre o mês e ano de nascimento para nos ajudar a cumprir as nossas obrigações legais.",
|
||||
"account.settings.field.dob.form.success": "Obrigado por introduzir a sua informação.",
|
||||
"account.settings.field.month_of_birth.options.empty": "Selecione um mês de nascimento",
|
||||
"account.settingsfield.dob.error.general": "Ocorreu um erro técnico. Por favor, tente novamente.",
|
||||
"account.settings.field.country": "País",
|
||||
"account.settings.field.country.empty": "Adicionar país",
|
||||
"account.settings.field.country.options.empty": "Selecionar um País",
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Editar",
|
||||
"account.settings.static.field.empty": "Sem conjunto de valores. Contacte o seu administrador {enterprise} para fazer alterações.",
|
||||
"account.settings.static.field.empty.no.admin": "Nenhum valor definido.",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "Se verificado, este nome irá aparecer nos seus certificados e registos públicos.",
|
||||
"account.settings.field.name.modal.certificate.title": "Escolha um nome preferido para certificados e registos públicos.",
|
||||
"account.settings.field.name.modal.certificate.select": "Selecione um nome",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Nome Completo",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Vamos começar.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "Estamos aqui para si do princípio ao fim",
|
||||
"account.settings.coaching.consent.description": "Os programas MicroBachelors incluem formação que se concentra na sua carreira, educação, e como alcançará resultados através da comunicação personalizada com um profissional experiente. Se estiver interessado, forneça as informações abaixo e clique em \"Submeter\", e o nosso parceiro de formação ligar-se-á a si através de e-mail e/ou mensagem de texto para o ajudar a seguir em frente. Aplicam-se os termos e condições.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Os serviços de formação estão incluídos, sem custos adicionais para os alunos com números de telefone dos EUA. A formação inclui mensagens de texto recorrentes. Podem aplicar-se taxas de mensagens e dados. Digite STOP para optar por não participar .",
|
||||
"account.settings.coaching.consent.accept-coaching": "Inscrever-se para a formação",
|
||||
"account.settings.coaching.consent.decline-coaching": "Prefiro não ser contactado com serviços gratuitos de formação",
|
||||
"account.settings.coaching.consent.label.name": "Por favor confirme o seu nome",
|
||||
"account.settings.coaching.consent.label.phone-number": "Introduza o seu número de telemóvel",
|
||||
"account.settings.coaching.consent.success.header": "Sucesso!",
|
||||
"account.settings.coaching.consent.success.message": "Está inscrito como formador. Pode esperar uma mensagem via e-mail ou SMS nos próximos dias.",
|
||||
"account.settings.coaching.consent.success.continue": "Iniciar o meu curso",
|
||||
"account.settings.coaching.managed.support": "suporte",
|
||||
"account.settings.coaching.managed.alert": "O seu nome é gerido por {managerTitle}. Contacte o seu administrador para obter ajuda.",
|
||||
"account.settings.field.phone_number": "Telefone",
|
||||
"account.settings.field.phone_number.empty": "Adicione um número de telefone",
|
||||
"account.settings.field.coaching_consent": "Autorização para formação",
|
||||
"account.settings.field.coaching_consent.tooltip": "Os programas MicroBachelors incluem treino baseado em mensagens de texto que o ajudam a associar experiências educacionais com os seus objectivos de carreira através de conselhos personalizados. Os serviços de formação estão incluídos sem custos adicionais, e estão disponíveis para estudantes com números de telemóvel nos EUA. Aplicam-se as taxas normais de mensagens. Escreva 'STOP' em qualquer altura para optar por não receber mensagens.",
|
||||
"account.settings.field.coaching_consent.error": "É necessário um número de telefone válido nos EUA para optar pela formação",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Nome Verificado",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Escolha um nome",
|
||||
"account.settings.delete.account.before.proceeding": "Antes de prosseguir, por favor {actionLink}.",
|
||||
"account.settings.delete.account.header": "Eliminar a Minha Conta",
|
||||
"account.settings.delete.account.subheader": "Lamentamos vê-lo/a partir!",
|
||||
@@ -149,7 +132,7 @@
|
||||
"account.settings.delete.account.text.change.instead": "Quer alterar o seu e-mail, nome, ou palavra-passe em alternativa?",
|
||||
"account.settings.delete.account.button": "Eliminar a Minha Conta",
|
||||
"account.settings.delete.account.please.activate": "ative a sua conta",
|
||||
"account.settings.delete.account.please.confirm": "confirm your account",
|
||||
"account.settings.delete.account.please.confirm": "confirme a sua conta",
|
||||
"account.settings.delete.account.please.unlink": "debloquear todas as contas dos meios de comunicação social",
|
||||
"account.settings.delete.account.modal.header": "Tem a certeza?",
|
||||
"account.settings.delete.account.modal.text.1": "Seleccionou \"Apagar a Minha Conta\". Eliminar a sua conta e os seus dados pessoais é permanente e não pode ser desfeito. A plataforma {siteName} não poderá recuperar a sua conta ou os dados que forem apagados.",
|
||||
@@ -198,14 +181,14 @@
|
||||
"account.settings.field.demographics.future_work_sector.empty": "Adicione sector de trabalho",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "Adicione sector de trabalho",
|
||||
"account.settings.section.demographics.why": "Porque é que a plataforma {siteName} recolhe esta informação?",
|
||||
"account.settings.name.change.title.id": "This name change requires identity verification",
|
||||
"account.settings.name.change.title.begin": "Before we begin",
|
||||
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.",
|
||||
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.",
|
||||
"account.settings.name.change.id.name.label": "Enter your name as it appears on your unexpired student, work, or government-issued identification card.",
|
||||
"account.settings.name.change.id.name.placeholder": "Enter the name on your photo ID",
|
||||
"account.settings.name.change.error.valid.name": "Please enter a valid name.",
|
||||
"account.settings.name.change.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.name.change.title.id": "Esta mudança de nome requer verificação de identidade",
|
||||
"account.settings.name.change.title.begin": "Antes de começarmos",
|
||||
"account.settings.name.change.warning.one": "Aviso: Esta ação atualiza o nome que aparece em todos os certificados que foram obtidos nesta conta no passado e quaisquer certificados que esteja atualmente a obter ou irá obter no futuro.",
|
||||
"account.settings.name.change.warning.two": "Esta ação não pode ser desfeita sem verificar a sua identidade.",
|
||||
"account.settings.name.change.id.name.label": "Digite seu nome como ele aparece em seu cartão de estudante, trabalho ou documento de identificação emitido pelo governo.",
|
||||
"account.settings.name.change.id.name.placeholder": "Digite o nome em sua identificação com foto",
|
||||
"account.settings.name.change.error.valid.name": "Por favor insira um nome válido.",
|
||||
"account.settings.name.change.error.general": "Ocorreu um erro técnico. Por favor, tente novamente.",
|
||||
"account.settings.name.change.continue": "Continuar",
|
||||
"account.settings.name.change.cancel": "Cancelar",
|
||||
"error.notfound.message": "A página não foi encontrada ou o URL está errado. Por favor, verifique o URL e tente novamente.",
|
||||
@@ -219,17 +202,17 @@
|
||||
"account.settings.sso.account.disconnect.error": "Houve um problema a desconectar esta conta. Se o problema persistir contacte a equipa de suporte.",
|
||||
"account.settings.sso.unlink.account": "Desbloqueio da conta {name}",
|
||||
"account.settings.sso.no.providers": "Nenhuma conta pode ser associada neste momento.",
|
||||
"account.page.title": "Account | {siteName}",
|
||||
"id.verification.access.blocked.denied": "We cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
|
||||
"account.page.title": "Conta | {siteName}",
|
||||
"id.verification.access.blocked.denied": "Não conseguimos verificar a sua identidade neste momento. Se ainda não ativou a sua conta, por favor verifique a sua pasta de spam para o email de ativação de {email}.",
|
||||
"id.verification.next": "Seguinte",
|
||||
"id.verification.support": "suporte",
|
||||
"id.verification.example.card.alt": "Exemplo de um cartão de identificação válido com nome completo e fotografia.",
|
||||
"id.verification.requirements.title": "Requisitos de Verificação de Fotos",
|
||||
"id.verification.requirements.description": "In order to complete Photo Verification, you will need the following:",
|
||||
"id.verification.requirements.description": "Para completar a Verificação Fotográfica, necessitará do seguinte:",
|
||||
"id.verification.requirements.card.device.title": "Dispositivo com câmara",
|
||||
"id.verification.requirements.card.device.allow": "Permitir",
|
||||
"id.verification.requirements.card.id.title": "Photo Identification Card",
|
||||
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a driver’s license or passport.",
|
||||
"id.verification.requirements.card.id.title": "Cartão de Identificação Fotográfica",
|
||||
"id.verification.requirements.card.id.text": "É necessário um cartão de identificação válido que contenha o seu nome completo e fotografia, como uma carta de condução ou passaporte.",
|
||||
"id.verification.privacy.title": "Informação de Privacidade",
|
||||
"id.verification.privacy.need.photo.question": "Porque é que a plataforma {siteName} precisa da minha fotografia?",
|
||||
"id.verification.privacy.need.photo.answer": "Usamos as suas fotografias de verificação para confirmar a sua identidade e garantir a validade do seu certificado.",
|
||||
@@ -307,22 +290,22 @@
|
||||
"id.verification.camera.help.difficulty.question.portrait": "E se eu tiver dificuldade em manter a minha cabeça em posição em relação à câmara?",
|
||||
"id.verification.camera.help.difficulty.question.id": "E se eu tiver dificuldade em manter o meu documento de identificação em posição em relação à câmara?",
|
||||
"id.verification.camera.help.difficulty.answer": "Se necessitar de assistência para tirar uma fotografia para submissão, contactar o apoio {siteName} para sugestões adicionais.",
|
||||
"id.verification.id.photo.unclear.question": "Is your ID card image not clear or too blurry?",
|
||||
"id.verification.id.tips.title": "Helpful Identification Card Tips",
|
||||
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a driver’s license or passport. Please have your ID ready.",
|
||||
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
|
||||
"id.verification.id.photo.unclear.question": "A imagem do seu cartão de identificação está pouco clara ou demasiado desfocada?",
|
||||
"id.verification.id.tips.title": "Dicas Úteis de Cartões de Identificação",
|
||||
"id.verification.id.tips.description": "A seguir, vamos precisar que tire uma fotografia de um cartão de identificação válido que inclua o seu nome completo e fotografia, como uma carta de condução ou passaporte. Por favor, tenha a sua identificação pronta.",
|
||||
"id.verification.id.tips.list.well.lit": "O seu cartão de identificação está bem iluminado.",
|
||||
"id.verification.id.tips.list.clear": "Certifique-se de que consegue ver a sua fotografia e ler claramente o seu nome.",
|
||||
"id.verification.id.photo.title.camera": "Take a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.title.camera": "Tire uma fotografia do Seu Cartão de Identificação",
|
||||
"id.verification.id.photo.title.upload": "Carregue uma fotografia do seu Cartão de Identificação",
|
||||
"id.verification.id.photo.preview.alt": "Pré-visualização da foto do ID.",
|
||||
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, driver’s license, or another identification card that includes your full name and a picture of your face.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
|
||||
"id.verification.id.photo.instructions.camera": "Quando a sua identificação estiver em posição, use o botão Tirar Foto abaixo para tirar a sua fotografia. Utilize um passaporte, carta de condução, ou outro cartão de identificação que inclua o seu nome completo e uma fotografia da sua cara.",
|
||||
"id.verification.id.photo.instructions.upload": "Por favor carregue uma fotografia do seu cartão de identificação. Assegure-se de que todo o documento de identificação se encontra dentro da moldura e está bem iluminado. O tamanho do ficheiro deve ser inferior a 10 MB. Formatos suportados: ",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "O ficheiro que seleccionou não é um tipo de imagem suportada. Por favor, escolha entre os seguintes formatos: ",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "O ficheiro que seleccionou é demasiado grande. Por favor, tente novamente com um ficheiro de menos de 10MB.",
|
||||
"id.verification.name.check.title": "Double-Check Your Name",
|
||||
"id.verification.name.check.instructions": "Does the name below match the name on your photo ID? If not, update the name below to match your photo ID.",
|
||||
"id.verification.name.check.mismatch.information": "If the name below does not match your photo ID, your identity verification will be denied.",
|
||||
"id.verification.name.error": "Please enter your name as it appears on your photo ID.",
|
||||
"id.verification.name.check.title": "Verifique o Seu Nome",
|
||||
"id.verification.name.check.instructions": "O nome abaixo corresponde ao nome na sua identificação com foto? Caso contrário, atualize o nome abaixo para corresponder ao seu documento de identidade com foto.",
|
||||
"id.verification.name.check.mismatch.information": "Se o nome abaixo não corresponder ao seu documento de identidade com foto, sua verificação de identidade será negada.",
|
||||
"id.verification.name.error": "Por favor, digite seu nome como aparece em seu documento de identidade com foto.",
|
||||
"id.verification.account.name.warning.prefix": "Por Favor Note:",
|
||||
"id.verification.account.name.settings": "Configurações de Conta",
|
||||
"id.verification.name.label": "Nome",
|
||||
@@ -332,8 +315,8 @@
|
||||
"id.verification.review.portrait.label": "O Seu Perfil",
|
||||
"id.verification.review.portrait.alt": "Fotografia do seu rosto a ser submetida.",
|
||||
"id.verification.review.portrait.retake": "Tirar de Novo a Fotografia de Perfil",
|
||||
"id.verification.review.id.label": "Your Identification Card",
|
||||
"id.verification.review.id.alt": "Photo of your identification card to be submitted.",
|
||||
"id.verification.review.id.label": "O seu Cartão de Identificação",
|
||||
"id.verification.review.id.alt": "Fotografia do seu cartão de identificação a ser submetida.",
|
||||
"id.verification.review.id.retake": "Tirar de Novo a Fotografia do documento de identificação",
|
||||
"id.verification.review.confirm": "Submeter",
|
||||
"id.verification.submission.alert.error.face": "É necessária uma fotografia do seu rosto. Por favor, volte a tirar a sua foto de perfil.",
|
||||
@@ -342,10 +325,10 @@
|
||||
"id.verification.submission.alert.error.unsupported": "Um ou mais dos ficheiros que carregou estão num formato não suportado. Por favor, escolha entre o seguinte: ",
|
||||
"id.verification.review.error": "{siteName} Página de Suporte",
|
||||
"id.verification.submitted.title": "Verificação de Identidade em Curso",
|
||||
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will be notified when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.submitted.text": "Recebemos suas informações e estamos verificando a sua identidade. Será notificado quando o processo de verificação estiver concluído (geralmente em 5 dias). Entretanto, poderá na mesma aceder a todo o conteúdo disponível do curso.",
|
||||
"id.verification.return.dashboard": "Regressar ao Seu Painel de Controlo",
|
||||
"id.verification.return.course": "Voltar ao Curso",
|
||||
"id.verification.return.generic": "Return",
|
||||
"id.verification.return.generic": "Voltar",
|
||||
"id.verification.photo.upload.help.title": "Carregar uma Fotografia em alternativa",
|
||||
"id.verification.photo.camera.help.title": "Use Antes a Sua Câmara",
|
||||
"id.verification.photo.upload.help.text": "Se estiver a ter problemas com a captura de fotografias acima, talvez queira carregar uma fotografia em seu lugar. Para carregar uma fotografia, clique no botão abaixo.",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "É necessário um dispositivo que tenha uma câmara. Se receber um pedido de acesso à sua câmara através de um navegador, por favor, certifique-se de que clica em {allow}.",
|
||||
"id.verification.account.name.summary.alert": "As definições da sua conta são geridas por {managerTitle}. Se o nome visível na fotografia do seu documento de identificação não corresponder ao nome da sua conta, contacte o seu administrador {profileDataManager} ou {support} para obter ajuda.",
|
||||
"idv.submission.alert.error": "\n Encontrámos um erro técnico ao tentar submeter a verificação da identificação.\n Isto pode ser uma questão temporária, por isso tente novamente dentro de alguns minutos.\n Se o problema persistir, por favor vá a {support_link} para obter ajuda.\n ",
|
||||
"id.verification.account.name.edit": "Editar {sr}"
|
||||
"id.verification.account.name.edit": "Editar {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s get started.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
|
||||
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"id.verification.account.name.edit": "Edit {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -114,30 +114,13 @@
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s get started.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
|
||||
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
@@ -357,5 +340,15 @@
|
||||
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"id.verification.account.name.edit": "Edit {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -1,361 +1,354 @@
|
||||
{
|
||||
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another {siteName} account.",
|
||||
"account.settings.message.managed.settings": "Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help.",
|
||||
"account.settings.message.managed.settings.support": "support",
|
||||
"account.settings.page.heading": "Account Settings",
|
||||
"account.settings.loading.message": "Loading...",
|
||||
"account.settings.loading.error": "Error: {error}",
|
||||
"account.settings.banner.beta.language": "You have set your language to {beta_language}, which is currently not fully translated. You can help us translate this language fully by joining the Transifex community and adding translations from English for learners that speak {beta_language}.",
|
||||
"account.settings.banner.beta.language.action.switch.back": "Switch Back to {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "Help Translate into {beta_language}",
|
||||
"account.settings.section.account.information": "Account Information",
|
||||
"account.settings.section.account.information.description": "These settings include basic information about your account.",
|
||||
"account.settings.section.profile.information": "Profile Information",
|
||||
"account.settings.section.demographics.information": "Optional Information",
|
||||
"account.settings.section.site.preferences": "Site Preferences",
|
||||
"account.settings.section.linked.accounts": "Linked Accounts",
|
||||
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to {siteName}.",
|
||||
"account.settings.field.username": "Username",
|
||||
"account.settings.field.username.help.text": "The name that identifies you on {siteName}. You cannot change your username.",
|
||||
"account.settings.field.full.name": "Full name",
|
||||
"account.settings.field.full.name.empty": "Add name",
|
||||
"account.settings.field.full.name.help.text": "The name that is used for ID verification and that appears on your certificates.",
|
||||
"account.settings.field.full.name.help.text.default": "The name that appears on your public profile.",
|
||||
"account.settings.field.full.name.help.text.default.certificate": "This name is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified": "Verified name",
|
||||
"account.settings.field.name.verified.help.text.verified": "This name has been verified by photo ID.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored": "This name has been verified by proctoring.",
|
||||
"account.settings.field.name.verified.help.text.verified.certificate": "This name has been verified by photo ID, and is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "This name has been verified by proctoring, and is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored": "Your proctored exam has been submitted. Verified name cannot be changed at this time. Please check back in 2-5 days.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "Once your proctored exam passes review, this name will appear on your certificate and public-facing records. Verified Name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your unexpired student, work, or government-issued identification card.",
|
||||
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored": "Your proctored exam has been submitted. Full name cannot be changed at this time. Please check back in 2-5 days.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "Once your proctored exam passes review, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.success.message": "Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.",
|
||||
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!",
|
||||
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.",
|
||||
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "Learn more about ID verification",
|
||||
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!",
|
||||
"account.settings.field.email": "Email address (Sign in)",
|
||||
"account.settings.field.email.empty": "Add email address",
|
||||
"account.settings.field.email.confirmation": "We’ve sent a confirmation message to {value}. Click the link in the message to update your email address.",
|
||||
"account.settings.field.email.help.text": "You receive messages from {siteName} and course teams at this address.",
|
||||
"account.settings.field.secondary.email": "Recovery email address",
|
||||
"account.settings.field.secondary.email.empty": "Add a recovery email address",
|
||||
"account.settings.field.secondary.email.confirmation": "We’ve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.",
|
||||
"account.settings.email.field.confirmation.header": "Pending confirmation",
|
||||
"account.settings.field.dob": "Year of birth",
|
||||
"account.settings.field.dob.empty": "Add year of birth",
|
||||
"account.settings.field.year_of_birth.options.empty": "Select a year of birth",
|
||||
"account.settings.field.dob.month": "Month",
|
||||
"account.settings.field.dob.year": "Year",
|
||||
"account.settings.field.month.year.default": "Select month",
|
||||
"account.settings.field.dob.year.default": "Select year",
|
||||
"account.settings.field.dob.form.button": "Please confirm your date of birth",
|
||||
"account.settings.field.dob.form.title": "Enter your birth month and year",
|
||||
"account.settings.field.dob.form.help.text": "We ask for birth month and year information to help us comply with our legal obligations.",
|
||||
"account.settings.field.dob.form.success": "Thank you for entering your information.",
|
||||
"account.settings.field.month_of_birth.options.empty": "Select a month of birth",
|
||||
"account.settingsfield.dob.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.field.country": "Country",
|
||||
"account.settings.field.country.empty": "Add country",
|
||||
"account.settings.field.country.options.empty": "Select a Country",
|
||||
"account.settings.field.state": "State",
|
||||
"account.settings.field.state.empty": "Add state",
|
||||
"account.settings.field.state.options.empty": "Select a State",
|
||||
"account.settings.field.site.language": "Site language",
|
||||
"account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
|
||||
"account.settings.field.education": "Education",
|
||||
"account.settings.field.education.empty": "Add level of education",
|
||||
"account.settings.field.education.levels.empty": "Select a level of education",
|
||||
"account.settings.field.education.levels.p": "Doctorate",
|
||||
"account.settings.field.education.levels.m": "Master's or professional degree",
|
||||
"account.settings.field.education.levels.b": "Bachelor's Degree",
|
||||
"account.settings.field.education.levels.a": "Associate's degree",
|
||||
"account.settings.field.education.levels.hs": "Secondary/high school",
|
||||
"account.settings.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"account.settings.field.education.levels.el": "Elementary/primary school",
|
||||
"account.settings.field.education.levels.none": "No formal education",
|
||||
"account.settings.field.education.levels.o": "Other education",
|
||||
"account.settings.field.gender": "Gender",
|
||||
"account.settings.field.gender.empty": "Add gender",
|
||||
"account.settings.field.gender.options.empty": "Select a gender",
|
||||
"account.settings.field.gender.options.f": "Female",
|
||||
"account.settings.field.gender.options.m": "Male",
|
||||
"account.settings.field.gender.options.o": "Other",
|
||||
"account.settings.field.language.proficiencies": "Spoken language",
|
||||
"account.settings.field.language.proficiencies.empty": "Add a spoken language",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Select a Language",
|
||||
"account.settings.field.time.zone": "Time zone",
|
||||
"account.settings.field.time.zone.empty": "Set time zone",
|
||||
"account.settings.field.time.zone.description": "Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your browser’s local time zone.",
|
||||
"account.settings.field.time.zone.default": "Default (Local Time Zone)",
|
||||
"account.settings.field.time.zone.all": "All time zones",
|
||||
"account.settings.field.time.zone.country": "Country time zones",
|
||||
"account.settings.section.social.media": "Social Media Links",
|
||||
"account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your {siteName} profile.",
|
||||
"account.settings.field.social.platform.name.linkedin": "LinkedIn",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
|
||||
"account.settings.jump.nav.delete.account": "Delete My Account",
|
||||
"account.settings.field.social.platform.name.twitter": "Twitter",
|
||||
"account.settings.field.social.platform.name.twitter.empty": "Add Twitter profile",
|
||||
"account.settings.field.social.platform.name.facebook": "Facebook",
|
||||
"account.settings.field.social.platform.name.facebook.empty": "Add Facebook profile",
|
||||
"account.settings.editable.field.action.save": "Save",
|
||||
"account.settings.editable.field.action.cancel": "Cancel",
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s get started.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
|
||||
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
"account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.",
|
||||
"account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on {siteName}.",
|
||||
"account.settings.delete.account.text.2.edX": "Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
|
||||
"account.settings.delete.account.text.3.link": "Follow these instructions for printing or downloading a certificate",
|
||||
"account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on {siteName}.",
|
||||
"account.settings.delete.account.text.change.instead": "Want to change your email, name, or password instead?",
|
||||
"account.settings.delete.account.button": "Delete My Account",
|
||||
"account.settings.delete.account.please.activate": "activate your account",
|
||||
"account.settings.delete.account.please.confirm": "confirm your account",
|
||||
"account.settings.delete.account.please.unlink": "unlink all social media accounts",
|
||||
"account.settings.delete.account.modal.header": "Are you sure?",
|
||||
"account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.",
|
||||
"account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on {siteName}.",
|
||||
"account.settings.delete.account.modal.text.2.edX": "If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
|
||||
"account.settings.delete.account.modal.enter.password": "If you still wish to continue and delete your account, please enter your account password:",
|
||||
"account.settings.delete.account.modal.confirm.delete": "Yes, Delete",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
|
||||
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
|
||||
"account.settings.delete.account.error.no.password": "A password is required",
|
||||
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
|
||||
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
|
||||
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
|
||||
"account.settings.delete.account.modal.after.button": "Close",
|
||||
"account.settings.delete.account.text.3.edX": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. You can make a copy of these for your records before proceeding with deletion. {actionLink}.",
|
||||
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.",
|
||||
"account.settings.message.demographics.service.issue": "An error occurred attempting to retrieve or save your account information. Please try again later.",
|
||||
"account.settings.field.demographics.gender": "Gender identity",
|
||||
"account.settings.field.demographics.gender.empty": "Add gender identity",
|
||||
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
|
||||
"account.settings.field.demographics.gender_description": "Gender identity description",
|
||||
"account.settings.field.demographics.gender_description.empty": "Enter description",
|
||||
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
|
||||
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
|
||||
"account.settings.field.demographics.income": "Family income",
|
||||
"account.settings.field.demographics.income.empty": "Add family income",
|
||||
"account.settings.field.demographics.income.options.empty": "Select a family income range",
|
||||
"account.settings.field.demographics.military_history": "U.S. Military status",
|
||||
"account.settings.field.demographics.military_history.empty": "Add military status",
|
||||
"account.settings.field.demographics.military_history.options.empty": "Select military status",
|
||||
"account.settings.field.demographics.learner_education_level": "Your education level",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "Add education level",
|
||||
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "Add education level",
|
||||
"account.settings.field.demographics.education_level.options.empty": "Select education level",
|
||||
"account.settings.field.demographics.work_status": "Employment status",
|
||||
"account.settings.field.demographics.work_status.empty": "Add employment status",
|
||||
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
|
||||
"account.settings.field.demographics.work_status_description": "Employment status description",
|
||||
"account.settings.field.demographics.work_status_description.empty": "Enter description",
|
||||
"account.settings.field.demographics.current_work_sector": "Current work industry",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "Add work industry",
|
||||
"account.settings.field.demographics.future_work_sector": "Future work industry",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
|
||||
"account.settings.section.demographics.why": "Why does {siteName} collect this information?",
|
||||
"account.settings.name.change.title.id": "This name change requires identity verification",
|
||||
"account.settings.name.change.title.begin": "Before we begin",
|
||||
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.",
|
||||
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.",
|
||||
"account.settings.name.change.id.name.label": "Enter your name as it appears on your unexpired student, work, or government-issued identification card.",
|
||||
"account.settings.name.change.id.name.placeholder": "Enter the name on your photo ID",
|
||||
"account.settings.name.change.error.valid.name": "Please enter a valid name.",
|
||||
"account.settings.name.change.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.name.change.continue": "Continue",
|
||||
"account.settings.name.change.cancel": "Cancel",
|
||||
"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.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Reset Password",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "Your previous request is in progress, please try again in few moments.",
|
||||
"account.settings.editable.field.password.reset.label": "Password",
|
||||
"account.settings.sso.link.account": "Sign in with {name}",
|
||||
"account.settings.sso.account.connected": "Linked",
|
||||
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
|
||||
"account.settings.sso.unlink.account": "Unlink {name} account",
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
|
||||
"account.page.title": "Account | {siteName}",
|
||||
"id.verification.access.blocked.denied": "We cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
|
||||
"id.verification.next": "Next",
|
||||
"id.verification.support": "support",
|
||||
"id.verification.example.card.alt": "Example of a valid identification card with a full name and photo.",
|
||||
"id.verification.requirements.title": "Photo Verification Requirements",
|
||||
"id.verification.requirements.description": "In order to complete Photo Verification, you will need the following:",
|
||||
"id.verification.requirements.card.device.title": "Device with Camera",
|
||||
"id.verification.requirements.card.device.allow": "Allow",
|
||||
"id.verification.requirements.card.id.title": "Photo Identification Card",
|
||||
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a driver’s license or passport.",
|
||||
"id.verification.privacy.title": "Privacy Information",
|
||||
"id.verification.privacy.need.photo.question": "Why does {siteName} need my photo?",
|
||||
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
|
||||
"id.verification.privacy.do.with.photo.question": "What does {siteName} do with this photo?",
|
||||
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on {siteName} after the verification process is complete.",
|
||||
"id.verification.access.blocked.title": "Identity Verification",
|
||||
"id.verification.access.blocked.enrollment": "You are not currently enrolled in a course that requires identity verification.",
|
||||
"id.verification.access.blocked.pending": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
|
||||
"id.verification.photo.take": "Take Photo",
|
||||
"id.verification.photo.retake": "Retake Photo?",
|
||||
"id.verification.photo.enable.detection": "Enable Face Detection",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.photo.feedback.correct": "Face is in a good position.",
|
||||
"id.verification.photo.feedback.two.faces": "More than one face detected.",
|
||||
"id.verification.photo.feedback.no.faces": "No face detected.",
|
||||
"id.verification.photo.feedback.top.left": "Incorrect position. Top left.",
|
||||
"id.verification.photo.feedback.top.center": "Incorrect position. Top center.",
|
||||
"id.verification.photo.feedback.top.right": "Incorrect position. Top right.",
|
||||
"id.verification.photo.feedback.center.left": "Incorrect position. Center left.",
|
||||
"id.verification.photo.feedback.center.center": "Incorrect position. Too close to camera.",
|
||||
"id.verification.photo.feedback.center.right": "Incorrect position. Center right.",
|
||||
"id.verification.photo.feedback.bottom.left": "Incorrect position. Bottom left.",
|
||||
"id.verification.photo.feedback.bottom.center": "Incorrect position. Bottom center.",
|
||||
"id.verification.photo.feedback.bottom.right": "Incorrect position. Bottom right.",
|
||||
"id.verification.camera.access.title": "Camera Permissions",
|
||||
"id.verification.camera.access.title.success": "Camera Access Enabled",
|
||||
"id.verification.camera.access.title.failed": "Camera Access Failed",
|
||||
"id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
|
||||
"id.verification.camera.access.enable": "Enable Camera",
|
||||
"id.verification.camera.access.problems": "Having problems?",
|
||||
"id.verification.camera.access.skip": "Skip and upload image files instead",
|
||||
"id.verification.camera.access.success": "Looks like your camera is working and ready.",
|
||||
"id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
|
||||
"id.verification.camera.access.failure.temporary": "It looks like we're unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.",
|
||||
"id.verification.camera.access.failure.temporary.chrome": "To enable camera access in Chrome:",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step1": "Open Chrome.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2": "Navigate to More > Settings.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "For Windows: Alt+F, Alt+E, or F10 followed by the spacebar",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "For Mac: Command+,",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "Under the \"Privacy and security\" tab, select \"Site Settings\" and then \"Camera.\"",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step4": "Under \"Blocked,\" find \"edx.org\" and select it.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step5": "In the \"Permissions\" section, update the camera permissions to \"Allow.\"",
|
||||
"id.verification.camera.access.failure.temporary.ie11": "To enable camera access in Internet Explorer:",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step1": "Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step2": "Select the \"Camera and Mic\" tab, and then select the \"Camera and Microphone Settings by Site\" button.",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step3": "Choose \"edx.org\" from the list of websites and change the permissions by selecting \"Allow\" in the dropdown menu.",
|
||||
"id.verification.camera.access.failure.temporary.firefox": "To enable camera access in Firefox:",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step1": "Open Firefox.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step2": "Enter \"about:preferences\" in the URL bar.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step3": "Select the \"Privacy & Security\" tab, and navigate to the \"Permissions\" section.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step4": "Next to \"Camera,\" select the \"Settings…\" button.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step5": "In the search bar, enter \"edx.org.\"",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step6": "In the status column for \"edx.org,\" select \"Allow\" from the drop down.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step7": "Select \"Save Changes.\"",
|
||||
"id.verification.camera.access.failure.temporary.safari": "To enable camera access in Safari:",
|
||||
"id.verification.camera.access.failure.temporary.safari.step1": "Open Safari.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step2": "Click on the Safari app menu, then select \"Preferences.\" You can also use Command+, as a keyboard shortcut.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step3": "Select the \"Websites\" tab and then select \"Camera.\"",
|
||||
"id.verification.camera.access.failure.temporary.safari.step4": "Select \"edx.org\" and change the camera permissions to \"Allow.\"",
|
||||
"id.verification.camera.access.failure.unsupported": "It looks like your browser does not support camera access.",
|
||||
"id.verification.camera.access.failure.unsupported.chrome.explanation": "The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.",
|
||||
"id.verification.camera.access.failure.unsupported.instructions": "Please use another browser to complete Identity Verification.",
|
||||
"id.verification.photo.tips.title": "Helpful Photo Tips",
|
||||
"id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
|
||||
"id.verification.photo.tips.list.title": "Photo Tips",
|
||||
"id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
|
||||
"id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
|
||||
"id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
|
||||
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself",
|
||||
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
|
||||
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
|
||||
"id.verification.camera.help.sight.answer.portrait": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.",
|
||||
"id.verification.camera.help.sight.answer.id": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
|
||||
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
|
||||
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.",
|
||||
"id.verification.id.photo.unclear.question": "Is your ID card image not clear or too blurry?",
|
||||
"id.verification.id.tips.title": "Helpful Identification Card Tips",
|
||||
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a driver’s license or passport. Please have your ID ready.",
|
||||
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
|
||||
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
|
||||
"id.verification.id.photo.title.camera": "Take a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
|
||||
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, driver’s license, or another identification card that includes your full name and a picture of your face.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.name.check.title": "Double-Check Your Name",
|
||||
"id.verification.name.check.instructions": "Does the name below match the name on your photo ID? If not, update the name below to match your photo ID.",
|
||||
"id.verification.name.check.mismatch.information": "If the name below does not match your photo ID, your identity verification will be denied.",
|
||||
"id.verification.name.error": "Please enter your name as it appears on your photo ID.",
|
||||
"id.verification.account.name.warning.prefix": "Please Note:",
|
||||
"id.verification.account.name.settings": "Account Settings",
|
||||
"id.verification.name.label": "Name",
|
||||
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.review.title": "Review Your Photos",
|
||||
"id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
|
||||
"id.verification.review.portrait.label": "Your Portrait",
|
||||
"id.verification.review.portrait.alt": "Photo of your face to be submitted.",
|
||||
"id.verification.review.portrait.retake": "Retake Portrait Photo",
|
||||
"id.verification.review.id.label": "Your Identification Card",
|
||||
"id.verification.review.id.alt": "Photo of your identification card to be submitted.",
|
||||
"id.verification.review.id.retake": "Retake ID Photo",
|
||||
"id.verification.review.confirm": "Submit",
|
||||
"id.verification.submission.alert.error.face": "A photo of your face is required. Please retake your portrait photo.",
|
||||
"id.verification.submission.alert.error.id": "A photo of your ID card is required. Please retake your ID photo.",
|
||||
"id.verification.submission.alert.error.name": "A valid account name is required. Please update your account name to match the name on your ID.",
|
||||
"id.verification.submission.alert.error.unsupported": "One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ",
|
||||
"id.verification.review.error": "{siteName} Support Page",
|
||||
"id.verification.submitted.title": "Identity Verification in Progress",
|
||||
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will be notified when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"id.verification.return.generic": "Return",
|
||||
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
|
||||
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
|
||||
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
|
||||
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
|
||||
"id.verification.upload.help.button": "Switch to Upload Mode",
|
||||
"id.verification.camera.help.button": "Switch to Camera Mode",
|
||||
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
|
||||
"id.verification.requirements.account.managed.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process.",
|
||||
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"account.settings.message.duplicate.tpa.provider": "所选的{provider}账户已与其他{siteName}账户关联。",
|
||||
"account.settings.message.managed.settings": "您的账号设置由{managerTitle}管理,如需帮助请联系您的管理员或 {support}。",
|
||||
"account.settings.message.managed.settings.support": "支持",
|
||||
"account.settings.page.heading": "账号设置",
|
||||
"account.settings.loading.message": "载入中...",
|
||||
"account.settings.loading.error": "错误: {error}",
|
||||
"account.settings.banner.beta.language": "您已将语言设置为{beta_language},目前尚未完全翻译。 您可以加入Transifex社区,并为使用{beta_language}的学员添加英语翻译,从而帮助我们充分翻译这门语言。",
|
||||
"account.settings.banner.beta.language.action.switch.back": "切换回{previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "帮助翻译(测试版) {beta_language}",
|
||||
"account.settings.section.account.information": "账号信息",
|
||||
"account.settings.section.account.information.description": "这些设置包括您账号的基本信息。",
|
||||
"account.settings.section.profile.information": "用户资料信息",
|
||||
"account.settings.section.demographics.information": "可选信息",
|
||||
"account.settings.section.site.preferences": "站点首选项",
|
||||
"account.settings.section.linked.accounts": "关联的账号",
|
||||
"account.settings.section.linked.accounts.description": "您可以关联您的身份账户以简化登录{siteName}的步骤。",
|
||||
"account.settings.field.username": "用户名",
|
||||
"account.settings.field.username.help.text": "此名称用于在{siteName}上确认您的身份。请勿更改您的用户名。",
|
||||
"account.settings.field.full.name": "全名",
|
||||
"account.settings.field.full.name.empty": "添加名字",
|
||||
"account.settings.field.full.name.help.text": "用于身份认证和显示在证书上的姓名。",
|
||||
"account.settings.field.full.name.help.text.default": "公开显示您的个人资料上的名字。",
|
||||
"account.settings.field.full.name.help.text.default.certificate": "此名字已经选定,会用在您的证书和面向公众的网页上。",
|
||||
"account.settings.field.name.verified": "验证过的名字",
|
||||
"account.settings.field.name.verified.help.text.verified": "此姓名已通过照片 ID 验证。",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored": "此名字已通过监考验证。",
|
||||
"account.settings.field.name.verified.help.text.verified.certificate": "此姓名已通过照片 ID 验证,并被选中显示在您的证书和面向公开网页中。",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "此名字已经过监考措施验证,并被选择用在您的证书和面向公众网页上。",
|
||||
"account.settings.field.name.verified.help.text.submitted": "验证已经提交。这通常会花不到 48 小时的时间。在此期间,验证过的名字无法更改。",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored": "您的考试是在监考下进行的,试卷已经提交。验证过的名字无法在提交期间更改。请 2-5 天后再来查看。",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "如果身份验证成功,则此名字会用在您的证书和面向公众的网页上。验证过的名字无法在此期间更改。",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "您的监考考试通过审查,该名称将出现在您的证书和面向公众的网页中。目前无法更改已验证的名称。",
|
||||
"account.settings.field.name.verified.verification.help": "输入您未过期的学生证、工作证或身份证上显示的姓名。",
|
||||
"account.settings.field.full.name.help.text.submitted": "验证已提交。这通常需要 48 小时或更短时间。目前无法更改全名。",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored": "您的监考考试已提交。目前无法更改全名。请在 2-5 天内回来查看。",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "身份验证成功后,此名称将出现在您的证书和面向公众网页中。目前无法更改全名。",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "一旦您的监考考试通过审核,该名称将出现在您的证书和面向公众网页中。目前无法更改全名。",
|
||||
"account.settings.field.name.verified.success.message": "您的身份验证请求已成功完成。您现在可以选择希望在您的证书和公共网页中显示的名称。",
|
||||
"account.settings.field.name.verified.success.message.header": "您的姓名更改请求已完成!",
|
||||
"account.settings.field.name.verified.failure.message": "您最近的身份验证尝试未通过。相关账户设置已恢复。",
|
||||
"account.settings.field.name.verified.failure.message.header": "我们无法验证您的身份。",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "详细了解身份验证",
|
||||
"account.settings.field.name.verified.submitted.message": "您的身份验证请求已提交,通常需要 24 到 48 小时才能完成。",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "当您的请求获得批准后,您更新后的姓名将出现在所有相关证书和面向公众的网页中。",
|
||||
"account.settings.field.name.verified.submitted.message.header": "您的姓名更改请求即将完成!",
|
||||
"account.settings.field.email": "电子邮箱地址(登录)",
|
||||
"account.settings.field.email.empty": "添加电子邮箱地址",
|
||||
"account.settings.field.email.confirmation": "验证信息已发送至{value}。请点击消息中的链接以更新您的电子邮箱地址。",
|
||||
"account.settings.field.email.help.text": "您会在此地址收到来自 {siteName} 和课程团队的消息。",
|
||||
"account.settings.field.secondary.email": "恢复电子邮箱地址",
|
||||
"account.settings.field.secondary.email.empty": "请添加已恢复的电子邮箱地址",
|
||||
"account.settings.field.secondary.email.confirmation": "验证信息已发送至{value}。请点击消息中的链接以恢复您的电子邮箱地址。",
|
||||
"account.settings.email.field.confirmation.header": "待确认",
|
||||
"account.settings.field.dob": "出生年份",
|
||||
"account.settings.field.dob.empty": "添加出生年份",
|
||||
"account.settings.field.year_of_birth.options.empty": "选择出生年份",
|
||||
"account.settings.field.dob.month": "月",
|
||||
"account.settings.field.dob.year": "年",
|
||||
"account.settings.field.month.year.default": "选择月份",
|
||||
"account.settings.field.dob.year.default": "选择年份",
|
||||
"account.settings.field.dob.form.button": "请确认您的出生日期",
|
||||
"account.settings.field.dob.form.title": "输入您的出生月份和年份",
|
||||
"account.settings.field.dob.form.help.text": "我们要求提供出生月份和年份信息,以帮助我们履行我们的法律义务。",
|
||||
"account.settings.field.dob.form.success": "感谢您输入信息。",
|
||||
"account.settings.field.month_of_birth.options.empty": "选择出生月份",
|
||||
"account.settingsfield.dob.error.general": "出现错误。请再试一次。",
|
||||
"account.settings.field.country": "国家/地区",
|
||||
"account.settings.field.country.empty": "添加国家",
|
||||
"account.settings.field.country.options.empty": "请选择国家",
|
||||
"account.settings.field.state": "省/市",
|
||||
"account.settings.field.state.empty": "添加省/市",
|
||||
"account.settings.field.state.options.empty": "选择一个省/市",
|
||||
"account.settings.field.site.language": "网站语言",
|
||||
"account.settings.field.site.language.help.text": "整个网站显示的语言。目前仅限于几种语言。",
|
||||
"account.settings.field.education": "教育程度",
|
||||
"account.settings.field.education.empty": "添加受教育水平",
|
||||
"account.settings.field.education.levels.empty": "请选择受教育程度",
|
||||
"account.settings.field.education.levels.p": "博士",
|
||||
"account.settings.field.education.levels.m": "硕士",
|
||||
"account.settings.field.education.levels.b": "学士学位",
|
||||
"account.settings.field.education.levels.a": "本科",
|
||||
"account.settings.field.education.levels.hs": "高中",
|
||||
"account.settings.field.education.levels.jhs": "初中",
|
||||
"account.settings.field.education.levels.el": "小学",
|
||||
"account.settings.field.education.levels.none": "未受正规教育",
|
||||
"account.settings.field.education.levels.o": "其他教育程度",
|
||||
"account.settings.field.gender": "性别",
|
||||
"account.settings.field.gender.empty": "添加性别",
|
||||
"account.settings.field.gender.options.empty": "请选择性别",
|
||||
"account.settings.field.gender.options.f": "女",
|
||||
"account.settings.field.gender.options.m": "男",
|
||||
"account.settings.field.gender.options.o": "其他",
|
||||
"account.settings.field.language.proficiencies": "语言",
|
||||
"account.settings.field.language.proficiencies.empty": "请添加语言",
|
||||
"account.settings.field.language_proficiencies.options.empty": "请选择语言",
|
||||
"account.settings.field.time.zone": "时区",
|
||||
"account.settings.field.time.zone.empty": "请设置时区",
|
||||
"account.settings.field.time.zone.description": "请选择课程日期的时区。若未设置时区,那么如作业截止日期等课程日期将根据您浏览器的本地时区显示。",
|
||||
"account.settings.field.time.zone.default": "默认 (本地时区)",
|
||||
"account.settings.field.time.zone.all": "所有时区",
|
||||
"account.settings.field.time.zone.country": "国家时区",
|
||||
"account.settings.section.social.media": "社交媒体链接",
|
||||
"account.settings.section.social.media.description": "或者,将您的个人帐户链接到 {siteName} 个人资料上的社交媒体图标。",
|
||||
"account.settings.field.social.platform.name.linkedin": "领英",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "添加领英账户",
|
||||
"account.settings.jump.nav.delete.account": "删除我的账号",
|
||||
"account.settings.field.social.platform.name.twitter": "推特",
|
||||
"account.settings.field.social.platform.name.twitter.empty": "添加推特账户",
|
||||
"account.settings.field.social.platform.name.facebook": "脸书",
|
||||
"account.settings.field.social.platform.name.facebook.empty": "添加脸书账户",
|
||||
"account.settings.editable.field.action.save": "保存",
|
||||
"account.settings.editable.field.action.cancel": "取消",
|
||||
"account.settings.editable.field.action.edit": "编辑",
|
||||
"account.settings.static.field.empty": "没有设置值。请联系您的{enterprise}管理员进行更改。",
|
||||
"account.settings.static.field.empty.no.admin": "尚无值",
|
||||
"notification.preferences.notifications.label": "通知",
|
||||
"account.settings.field.name.certificate.select": "如果选中,此名称将出现在您的证书和面向公众的网页中。",
|
||||
"account.settings.field.name.modal.certificate.title": "为证书和面向公众的网页选择一个首选名称",
|
||||
"account.settings.field.name.modal.certificate.select": "选择名称",
|
||||
"account.settings.field.name.modal.certificate.option.full": "全名",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "验证名称",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "选择名字",
|
||||
"account.settings.delete.account.before.proceeding": "再进行下一步之前,请{actionLink}。",
|
||||
"account.settings.delete.account.header": "删除我的账号",
|
||||
"account.settings.delete.account.subheader": "很遗憾看到您要离开了!",
|
||||
"account.settings.delete.account.text.1": "请注意:删除您的帐户和个人数据是永久性的,无法撤消。 {siteName} 将无法恢复您的帐户或已删除的数据。",
|
||||
"account.settings.delete.account.text.2": "一旦您的帐户被删除,您将无法使用它来参加 {siteName} 上的课程。",
|
||||
"account.settings.delete.account.text.2.edX": "一旦删除了账号,您将无法在edX 应用, edx.org 任何由edX支持的站点上学习课程。这也包括从您的雇主或者大学系统进入edx.org 还有任何由 MIT Open Learning, Wharton Executive Education, 和Harvard Medical School支持的私人站点。",
|
||||
"account.settings.delete.account.text.3.link": "按照这些说明打印或下载证书",
|
||||
"account.settings.delete.account.text.warning": "警告:帐户删除是永久性的。请在继续之前仔细阅读以上内容。这是不可逆的操作,您将无法再在 {siteName} 上使用同一电子邮件。",
|
||||
"account.settings.delete.account.text.change.instead": "是否想要更改您的电子邮箱、名字或密码?",
|
||||
"account.settings.delete.account.button": "删除我的账号",
|
||||
"account.settings.delete.account.please.activate": "请激活您的账号",
|
||||
"account.settings.delete.account.please.confirm": "确认您的帐户",
|
||||
"account.settings.delete.account.please.unlink": "解绑所有社交媒体账号",
|
||||
"account.settings.delete.account.modal.header": "您确定吗?",
|
||||
"account.settings.delete.account.modal.text.1": "您选择了“删除我的帐户”。删除您的帐户和个人数据是永久性的,无法撤消。 {siteName} 将无法恢复您的帐户或已删除的数据。",
|
||||
"account.settings.delete.account.modal.text.2": "如果继续,您将无法使用此帐户参加 {siteName} 上的课程。",
|
||||
"account.settings.delete.account.modal.text.2.edX": "如果继续此操作,您将无法在edX 应用, edx.org 任何由edX支持的站点上学习课程。这也包括从您的雇主或者大学系统进入edx.org 还有任何由 MIT Open Learning, Wharton Executive Education, 和Harvard Medical School支持的私人站点。",
|
||||
"account.settings.delete.account.modal.enter.password": "如果您仍然要删除账号,请输入您的账号密码:",
|
||||
"account.settings.delete.account.modal.confirm.delete": "是的,删除",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "取消",
|
||||
"account.settings.delete.account.error.unable.to.delete": "无法删除账号",
|
||||
"account.settings.delete.account.error.no.password": "需要输入密码",
|
||||
"account.settings.delete.account.error.invalid.password": "密码错误",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "抱歉,处理您的请求时发生错误,请稍后重试。",
|
||||
"account.settings.delete.account.modal.after.header": "很遗憾您要离开了!您的账号即将被删除。",
|
||||
"account.settings.delete.account.modal.after.text": "删除账号包括从邮箱列表中移除您的邮箱,我们的系统可能需要耗时数周才能完成处理。如果在此期间您不希望收到邮件,请从任意邮件的页脚取消订阅。",
|
||||
"account.settings.delete.account.modal.after.button": "关闭",
|
||||
"account.settings.delete.account.text.3.edX": "您还可能无法访问经过验证的证书和其他项目认证,例如微型硕士证书。在继续删除之前,您可以复制这些文件作为记录。 {actionLink}。",
|
||||
"account.settings.delete.account.text.3": "您还可能无法访问经过验证的证书和其他项目认证。在继续删除之前,您可以复制这些文件作为记录。",
|
||||
"account.settings.message.demographics.service.issue": "尝试检索或保存您的帐户信息时发生错误。请稍后再试。",
|
||||
"account.settings.field.demographics.gender": "性别",
|
||||
"account.settings.field.demographics.gender.empty": "添加性别",
|
||||
"account.settings.field.demographics.gender.options.empty": "选择性别",
|
||||
"account.settings.field.demographics.gender_description": "性别描述",
|
||||
"account.settings.field.demographics.gender_description.empty": "输入描述",
|
||||
"account.settings.field.demographics.ethnicity": "种族/民族",
|
||||
"account.settings.field.demographics.ethnicity.empty": "添加种族/民族",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "选择所有符合条件的",
|
||||
"account.settings.field.demographics.income": "家庭收入",
|
||||
"account.settings.field.demographics.income.empty": "添加家庭收入",
|
||||
"account.settings.field.demographics.income.options.empty": "选择家庭收入范围",
|
||||
"account.settings.field.demographics.military_history": "军事单位",
|
||||
"account.settings.field.demographics.military_history.empty": "添加军事状态",
|
||||
"account.settings.field.demographics.military_history.options.empty": "选择军籍",
|
||||
"account.settings.field.demographics.learner_education_level": "你的教育程度",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "添加教育水平",
|
||||
"account.settings.field.demographics.parent_education_level": "父母/监护人的教育水平",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "添加教育水平",
|
||||
"account.settings.field.demographics.education_level.options.empty": "选择教育程度",
|
||||
"account.settings.field.demographics.work_status": "就业状况",
|
||||
"account.settings.field.demographics.work_status.empty": "添加就业状况",
|
||||
"account.settings.field.demographics.work_status.options.empty": "选择就业状况",
|
||||
"account.settings.field.demographics.work_status_description": "就业状况说明",
|
||||
"account.settings.field.demographics.work_status_description.empty": "输入描述",
|
||||
"account.settings.field.demographics.current_work_sector": "目前工作行业",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "添加工作行业",
|
||||
"account.settings.field.demographics.future_work_sector": "未来工作行业",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "添加工作行业",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "选择工作行业",
|
||||
"account.settings.section.demographics.why": "为什么 {siteName} 收集这些信息?",
|
||||
"account.settings.name.change.title.id": "此名称更改需要身份验证",
|
||||
"account.settings.name.change.title.begin": "在我们开始之前",
|
||||
"account.settings.name.change.warning.one": "警告:此操作会更新出现在过去通过此帐户获得的所有证书以及您当前正在获得或将来将获得的任何证书上的名称。",
|
||||
"account.settings.name.change.warning.two": "如果不验证您的身份,则无法撤消此操作。",
|
||||
"account.settings.name.change.id.name.label": "输入您未过期的学生证、工作证或身份证上显示的姓名。",
|
||||
"account.settings.name.change.id.name.placeholder": "输入您带照片的身份证件上的姓名",
|
||||
"account.settings.name.change.error.valid.name": "请输入一个有效的名字。",
|
||||
"account.settings.name.change.error.general": "出现错误。请再试一次。",
|
||||
"account.settings.name.change.continue": "继续",
|
||||
"account.settings.name.change.cancel": "取消",
|
||||
"error.notfound.message": "您访问的地址不存在或有误。请检查URL后重新尝试访问。",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "技术支持",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "密码重置邮件已发送至{email},请点击邮件中的链接来重置密码。如未收到邮件,请联系{technicalSupportLink}。",
|
||||
"account.settings.editable.field.password.reset.button": "重设密码",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "您之前的请求正在处理中,请稍后重试。",
|
||||
"account.settings.editable.field.password.reset.label": "密码",
|
||||
"account.settings.sso.link.account": "以{name}登陆",
|
||||
"account.settings.sso.account.connected": "已关联",
|
||||
"account.settings.sso.account.disconnect.error": "断开此帐户的链接时出现了一个问题。如果问题仍然存在,请联系支持部门。",
|
||||
"account.settings.sso.unlink.account": "解绑{name}账号",
|
||||
"account.settings.sso.no.providers": "目前无法连接任何帐户。",
|
||||
"account.page.title": "帐户 | {siteName}",
|
||||
"id.verification.access.blocked.denied": "我们目前无法验证您的身份。如果您尚未激活您的帐户,请检查您的垃圾邮件文件夹中是否有来自 {email} 的激活电子邮件。",
|
||||
"id.verification.next": "下一节",
|
||||
"id.verification.support": "支持",
|
||||
"id.verification.example.card.alt": "带有全名和照片的有效身份证示例。",
|
||||
"id.verification.requirements.title": "照片验证要求",
|
||||
"id.verification.requirements.description": "为了完成照片验证,您需要以下内容:",
|
||||
"id.verification.requirements.card.device.title": "带摄像头的设备",
|
||||
"id.verification.requirements.card.device.allow": "允许",
|
||||
"id.verification.requirements.card.id.title": "带照片的身份证",
|
||||
"id.verification.requirements.card.id.text": "您需要一张包含您的全名和照片的有效身份证,例如驾照或护照。",
|
||||
"id.verification.privacy.title": "隐私信息",
|
||||
"id.verification.privacy.need.photo.question": "为什么 {siteName} 需要我的照片?",
|
||||
"id.verification.privacy.need.photo.answer": "我们使用您的验证照片来确认您的身份并确保您的证书的有效性。",
|
||||
"id.verification.privacy.do.with.photo.question": "{siteName} 对这张照片做了什么?",
|
||||
"id.verification.privacy.do.with.photo.answer": "我们对您的照片进行安全加密并将其发送给我们的授权服务以供审核。验证过程完成后,您的照片和信息不会保存或显示在 {siteName} 上的任何地方。",
|
||||
"id.verification.access.blocked.title": "身份验证",
|
||||
"id.verification.access.blocked.enrollment": "您目前没有注册需要身份验证的课程。",
|
||||
"id.verification.access.blocked.pending": "您已经提交了验证信息。验证过程完成后(通常在 5 天内),您会在控制面板上看到一条消息。",
|
||||
"id.verification.photo.take": "拍照",
|
||||
"id.verification.photo.retake": "重拍照片?",
|
||||
"id.verification.photo.enable.detection": "启用人脸检测",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "如果选中,您的脸部周围会出现一个方框。如果周围的方框是蓝色的,则可以清楚地看到您的脸。如果您的脸位置不佳或无法检测到,则该框将为红色。",
|
||||
"id.verification.photo.enable.detection.id.help.text": "如果选中,您的身份证上的脸部周围将出现一个方框。如果周围的方框是蓝色的,则可以清楚地看到脸部。如果面部位置不佳或无法检测到,则框为红色。",
|
||||
"id.verification.photo.feedback.correct": "脸的位置很好。",
|
||||
"id.verification.photo.feedback.two.faces": "检测到不止一张脸。",
|
||||
"id.verification.photo.feedback.no.faces": "未检测到人脸。",
|
||||
"id.verification.photo.feedback.top.left": "位置不正确。左上方。",
|
||||
"id.verification.photo.feedback.top.center": "位置不正确。顶部中心。",
|
||||
"id.verification.photo.feedback.top.right": "位置不正确。右上。",
|
||||
"id.verification.photo.feedback.center.left": "位置不正确。居中偏左。",
|
||||
"id.verification.photo.feedback.center.center": "位置不正确。离相机太近。",
|
||||
"id.verification.photo.feedback.center.right": "位置不正确。中右。",
|
||||
"id.verification.photo.feedback.bottom.left": "位置不正确。左下方。",
|
||||
"id.verification.photo.feedback.bottom.center": "位置不正确。底部中心。",
|
||||
"id.verification.photo.feedback.bottom.right": "位置不正确。右下角。",
|
||||
"id.verification.camera.access.title": "相机权限",
|
||||
"id.verification.camera.access.title.success": "启用相机访问",
|
||||
"id.verification.camera.access.title.failed": "摄像头访问失败",
|
||||
"id.verification.camera.access.click.allow": "请务必点击“允许”",
|
||||
"id.verification.camera.access.enable": "启用相机",
|
||||
"id.verification.camera.access.problems": "遇到问题?",
|
||||
"id.verification.camera.access.skip": "跳过并上传图片文件",
|
||||
"id.verification.camera.access.success": "看起来你的相机正在工作并准备就绪。",
|
||||
"id.verification.camera.access.failure": "我们似乎无法访问您的相机。您将需要上传您的图像文件和您的带照片身份证件。",
|
||||
"id.verification.camera.access.failure.temporary": "我们似乎无法访问您的相机。请确认您的网络摄像头已连接并且您已允许您的浏览器访问它。",
|
||||
"id.verification.camera.access.failure.temporary.chrome": "要在Chrome中启用相机访问:",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step1": "打开Chrome。",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2": "导航到更多 〉设置。",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "如果是Windows:Alt+F、Alt+E 或 F10 后跟空格键",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "如果是 Mac:Command+,",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "在“隐私和安全”选项卡下,选择“站点设置”,然后选择“相机”。",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step4": "在“已阻止”下,找到“edx.org”并选择它。",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step5": "在“权限”部分,将相机权限更新为“允许”。",
|
||||
"id.verification.camera.access.failure.temporary.ie11": "要在 Internet Explorer 中启用摄像头访问:",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step1": "通过导航到 Windows 设置〉控制面板〉Flash Player 打开 Flash Player 设置管理器。",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step2": "选择“摄像头和麦克风”选项卡,然后选择“按站点设置摄像头和麦克风”按钮。",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step3": "从网站列表中选择“edx.org”并通过在下拉菜单中选择“允许”来更改权限。",
|
||||
"id.verification.camera.access.failure.temporary.firefox": "要在 Firefox 中启用相机访问:",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step1": "打开火狐游览器。",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step2": "在 URL 栏中输入“关于:喜好”。",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step3": "选择“隐私和安全”选项卡,然后导航到“权限”部分。",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step4": "在“相机”旁边,选择“设置...”按钮。",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step5": "在搜索栏中,输入“网址”。",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step6": "在“网址”的状态列中,从下拉列表中选择“允许”。",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step7": "选择“保存更改”。",
|
||||
"id.verification.camera.access.failure.temporary.safari": "要在 Safari 中启用相机访问:",
|
||||
"id.verification.camera.access.failure.temporary.safari.step1": "打开Safari浏览器。",
|
||||
"id.verification.camera.access.failure.temporary.safari.step2": "单击 Safari 应用程序菜单,然后选择“首选项”。您还可以使用 Command+ 作为键盘快捷键。",
|
||||
"id.verification.camera.access.failure.temporary.safari.step3": "选择“网站”选项卡,然后选择“相机”。",
|
||||
"id.verification.camera.access.failure.temporary.safari.step4": "选择“edx.org”并将相机权限更改为“允许”。",
|
||||
"id.verification.camera.access.failure.unsupported": "您的浏览器似乎不支持摄像头访问。",
|
||||
"id.verification.camera.access.failure.unsupported.chrome.explanation": "Chrome 浏览器目前不支持 iOS 设备(例如 iPhone 和 iPad)上的相机访问。",
|
||||
"id.verification.camera.access.failure.unsupported.instructions": "请使用其他浏览器完成身份验证。",
|
||||
"id.verification.photo.tips.title": "有用的照片提示",
|
||||
"id.verification.photo.tips.description": "接下来,我们需要您拍一张脸部照片。请查看以下有用的提示。",
|
||||
"id.verification.photo.tips.list.title": "拍照技巧",
|
||||
"id.verification.photo.tips.list.description": "为了照相成功,请确保:",
|
||||
"id.verification.photo.tips.list.well.lit": "您的面部光照很好。",
|
||||
"id.verification.photo.tips.list.inside.frame": "您的整张脸都在框内。",
|
||||
"id.verification.portrait.photo.title.camera": "给自己拍张照片",
|
||||
"id.verification.portrait.photo.instructions.camera": "当您的脸就位后,使用下面的拍照按钮拍照。",
|
||||
"id.verification.camera.help.sight.question": "如果我看不到相机图像或看不到我的照片以确定哪一面可见怎么办?",
|
||||
"id.verification.camera.help.sight.answer.portrait": "您也许可以在没有帮助的情况下完成图像捕获过程,但可能需要几次提交尝试才能使相机定位正确。相机的最佳位置因每台电脑而异,但一般来说,头部最佳位置是距离相机大约 12-18 英寸(30-45 厘米),头部相对于电脑屏幕居中。如果您提交的照片被拒绝,请尝试移动计算机或相机方向以改变照明角度。",
|
||||
"id.verification.camera.help.sight.answer.id": "您也许可以在没有帮助的情况下完成图像捕获过程,但可能需要几次提交尝试才能使相机定位正确。相机的最佳位置因每台计算机而异,但一般来说,身份证照片的最佳位置是距相机 8-12 英寸(20-30 厘米),身份证相对于相机居中。如果您提交的照片被拒绝,请尝试移动计算机或相机方向以改变照明角度。最常见的拒绝原因是无法阅读身份证上的文字。",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "如果我相对相机无法保持头部姿势怎么办?",
|
||||
"id.verification.camera.help.difficulty.question.id": "如果我难以将我的 ID 保持在相对于相机的位置怎么办?",
|
||||
"id.verification.camera.help.difficulty.answer": "如果您在提交照片方面需要帮助,请联系 {siteName} 支持以获得更多建议。",
|
||||
"id.verification.id.photo.unclear.question": "您的身份证图像是否不清晰或太模糊?",
|
||||
"id.verification.id.tips.title": "有用的身份证提示",
|
||||
"id.verification.id.tips.description": "接下来,我们需要您为包含您的全名和照片的有效身份证件拍照,例如驾照或护照。请准备好您的身份证件。",
|
||||
"id.verification.id.tips.list.well.lit": "您的身份证光线充足。",
|
||||
"id.verification.id.tips.list.clear": "确保您可以看到您的照片并清楚地读出您的名字。",
|
||||
"id.verification.id.photo.title.camera": "为您的身份证拍照",
|
||||
"id.verification.id.photo.title.upload": "上传您的身份证照片",
|
||||
"id.verification.id.photo.preview.alt": "带照片的身份证件预览。",
|
||||
"id.verification.id.photo.instructions.camera": "当您的身份证就位后,请使用下面的拍照按钮拍照。请使用护照、驾照或其他包含您的全名和面部照片的身份证件。",
|
||||
"id.verification.id.photo.instructions.upload": "请上传您的身份证照片。确保整个 ID 在框架内且光线充足。文件大小必须小于 10 MB。支持的格式:",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "您选择的文件不是受支持的图像类型。请从以下格式中选择:",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "您选择的文件太大。请使用小于 10MB 的文件重试。",
|
||||
"id.verification.name.check.title": "仔细检查你的名字",
|
||||
"id.verification.name.check.instructions": "下面的姓名与您带照片的身份证件上的姓名相符吗?如果不是,请更新以下姓名以匹配您的带照片身份证件。",
|
||||
"id.verification.name.check.mismatch.information": "如果以下姓名与您带照片的身份证件不符,您的身份验证将被拒绝。",
|
||||
"id.verification.name.error": "请输入您照片身份证件上显示的姓名。",
|
||||
"id.verification.account.name.warning.prefix": "请注意:",
|
||||
"id.verification.account.name.settings": "账号设置",
|
||||
"id.verification.name.label": "姓名",
|
||||
"id.verification.account.name.photo.alt": "要提交的身份证件照片。",
|
||||
"id.verification.review.title": "检查您的照片",
|
||||
"id.verification.review.description": "请确保我们可以通过您提供的照片及信息来验证您的身份。",
|
||||
"id.verification.review.portrait.label": "你的肖像",
|
||||
"id.verification.review.portrait.alt": "要提交的脸部照片。",
|
||||
"id.verification.review.portrait.retake": "重拍人像照片",
|
||||
"id.verification.review.id.label": "您的身份证",
|
||||
"id.verification.review.id.alt": "要提交的身份证照片。",
|
||||
"id.verification.review.id.retake": "重拍证件照",
|
||||
"id.verification.review.confirm": "提交",
|
||||
"id.verification.submission.alert.error.face": "需要一张您的脸部照片。请重新拍摄您的人像照片。",
|
||||
"id.verification.submission.alert.error.id": "需要您的身份证照片。请重新拍摄您的证件照。",
|
||||
"id.verification.submission.alert.error.name": "需要一个有效的帐户名。请更新您的帐户名称以匹配您身份证件上的名称。",
|
||||
"id.verification.submission.alert.error.unsupported": "您上传的一个或多个文件的格式不受支持。请从以下选项中选择:",
|
||||
"id.verification.review.error": "{siteName} 支持页面",
|
||||
"id.verification.submitted.title": "正在进行身份验证",
|
||||
"id.verification.submitted.text": "我们已收到您的信息,正在验证您的身份。验证过程完成后(通常在 5 天内),您会收到通知。在此期间,您仍然可以访问所有可用的课程内容。",
|
||||
"id.verification.return.dashboard": "返回您的控制面板",
|
||||
"id.verification.return.course": "返回课程",
|
||||
"id.verification.return.generic": "返回",
|
||||
"id.verification.photo.upload.help.title": "改为上传照片",
|
||||
"id.verification.photo.camera.help.title": "改用你的相机",
|
||||
"id.verification.photo.upload.help.text": "如果您在使用上面的照片捕获时遇到问题,您可能需要上传照片。要上传照片,请单击下面的按钮。",
|
||||
"id.verification.photo.camera.help.text": "如果您在上传上面的照片时遇到问题,您可能想改用相机。要使用您的相机,请单击下面的按钮。",
|
||||
"id.verification.upload.help.button": "切换到上传模式",
|
||||
"id.verification.camera.help.button": "切换到相机模式",
|
||||
"id.verification.request.camera.access.instructions": "为了使用您的网络摄像头拍照,您可能会收到一个浏览器提示,提示您访问您的相机。 {clickAllow}",
|
||||
"id.verification.requirements.account.managed.alert": "您的帐户设置由 {managerTitle} 管理。如果您照片 ID 上的姓名与您帐户上的姓名不符,请在完成照片验证过程之前联系您的 {profileDataManager} 管理员或 {support} 寻求帮助。",
|
||||
"id.verification.requirements.card.device.text": "你需要一个有摄像头的设备。如果您收到访问相机的浏览器提示,请确保单击 {allow}。",
|
||||
"id.verification.account.name.summary.alert": "您的帐户设置由 {managerTitle} 管理。如果您照片 ID 上的姓名与您帐户上的姓名不符,请联系您的 {profileDataManager} 管理员或 {support} 寻求帮助。",
|
||||
"idv.submission.alert.error": "我们在尝试提交身份验证时遇到技术错误。这可能是暂时性问题,因此请过几分钟再试。如果问题仍然存在,请前往 {support_link} 寻求帮助。",
|
||||
"id.verification.account.name.edit": "编辑 {sr}",
|
||||
"notification.preference.heading": "通知",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "邮箱",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses",
|
||||
"notification.preference.guide.link": "as detailed here",
|
||||
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Route, Switch, Redirect, useRouteMatch, useLocation,
|
||||
Route, Routes, useLocation, useNavigate,
|
||||
} from 'react-router-dom';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import qs from 'qs';
|
||||
@@ -27,8 +27,8 @@ import messages from './IdVerification.messages';
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
const IdVerificationPage = (props) => {
|
||||
const { path } = useRouteMatch();
|
||||
const { search } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
@@ -45,81 +45,82 @@ const IdVerificationPage = (props) => {
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* If user reloads, redirect to the beginning of the process */}
|
||||
<Redirect to={`${path}/review-requirements`} />
|
||||
<div className="page__id-verification container-fluid py-5">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 col-md-8">
|
||||
<VerifiedNameContextProvider>
|
||||
<IdVerificationContextProvider>
|
||||
<Switch>
|
||||
<Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} />
|
||||
<Route path={`${path}/request-camera-access`} component={RequestCameraAccessPanel} />
|
||||
<Route path={`${path}/portrait-photo-context`} component={PortraitPhotoContextPanel} />
|
||||
<Route path={`${path}/take-portrait-photo`} component={TakePortraitPhotoPanel} />
|
||||
<Route path={`${path}/id-context`} component={IdContextPanel} />
|
||||
<Route path={`${path}/get-name-id`} component={GetNameIdPanel} />
|
||||
<Route path={`${path}/take-id-photo`} component={TakeIdPhotoPanel} />
|
||||
<Route path={`${path}/summary`} component={SummaryPanel} />
|
||||
<Route path={`${path}/submitted`} component={SubmittedPanel} />
|
||||
</Switch>
|
||||
</IdVerificationContextProvider>
|
||||
</VerifiedNameContextProvider>
|
||||
</div>
|
||||
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
|
||||
<Button variant="link" className="px-0" onClick={() => setIsModalOpen(true)}>
|
||||
Privacy Information
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ModalDialog
|
||||
isOpen={isModalOpen}
|
||||
title="Id modal"
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
size="lg"
|
||||
hasCloseButton={false}
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title data-testid="Id-modal">
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<div className="p-3">
|
||||
<h6>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.need.photo.question'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.do.with.photo.question'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.do.with.photo.answer'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="p-2">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="link">
|
||||
Close
|
||||
</ModalDialog.CloseButton>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
useEffect(() => {
|
||||
navigate('/id-verification/review-requirements');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page__id-verification container-fluid py-5">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 col-md-8">
|
||||
<VerifiedNameContextProvider>
|
||||
<IdVerificationContextProvider>
|
||||
<Routes>
|
||||
<Route path="/review-requirements" element={<ReviewRequirementsPanel />} />
|
||||
<Route path="/request-camera-access" element={<RequestCameraAccessPanel />} />
|
||||
<Route path="/portrait-photo-context" element={<PortraitPhotoContextPanel />} />
|
||||
<Route path="/take-portrait-photo" element={<TakePortraitPhotoPanel />} />
|
||||
<Route path="/id-context" element={<IdContextPanel />} />
|
||||
<Route path="/get-name-id" element={<GetNameIdPanel />} />
|
||||
<Route path="/take-id-photo" element={<TakeIdPhotoPanel />} />
|
||||
<Route path="/summary" element={<SummaryPanel />} />
|
||||
<Route path="/submitted" element={<SubmittedPanel />} />
|
||||
</Routes>
|
||||
</IdVerificationContextProvider>
|
||||
</VerifiedNameContextProvider>
|
||||
</div>
|
||||
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
|
||||
<Button variant="link" className="px-0" onClick={() => setIsModalOpen(true)}>
|
||||
Privacy Information
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<ModalDialog
|
||||
isOpen={isModalOpen}
|
||||
title="Id modal"
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
size="lg"
|
||||
hasCloseButton={false}
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title data-testid="Id-modal">
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<div className="p-3">
|
||||
<h6>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.need.photo.question'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.do.with.photo.question'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.do.with.photo.answer'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="p-2">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="link">
|
||||
Close
|
||||
</ModalDialog.CloseButton>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useVerificationRedirectSlug } from '../routing-utilities';
|
||||
|
||||
const BasePanel = ({
|
||||
@@ -20,7 +20,7 @@ const BasePanel = ({
|
||||
|
||||
const redirectSlug = useVerificationRedirectSlug(name);
|
||||
if (redirectSlug) {
|
||||
return <Redirect to={redirectSlug} />;
|
||||
return <Navigate replace to={`/id-verification/${redirectSlug}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, {
|
||||
useContext, useEffect, useRef,
|
||||
} from 'react';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
@@ -12,7 +12,8 @@ import IdVerificationContext from '../IdVerificationContext';
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
const GetNameIdPanel = (props) => {
|
||||
const { push, location } = useHistory();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const nameInputRef = useRef();
|
||||
const panelSlug = 'get-name-id';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
@@ -33,7 +34,7 @@ const GetNameIdPanel = (props) => {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (idPhotoName) {
|
||||
push(nextPanelSlug);
|
||||
navigate(`/id-verification/${nextPanelSlug}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,7 +80,7 @@ const GetNameIdPanel = (props) => {
|
||||
|
||||
<div className="action-row">
|
||||
<Link
|
||||
to={nextPanelSlug}
|
||||
to={`/id-verification/${nextPanelSlug}`}
|
||||
className={`btn btn-primary ${!idPhotoName && 'disabled'}`}
|
||||
data-testid="next-button"
|
||||
aria-disabled={!idPhotoName}
|
||||
|
||||
@@ -41,7 +41,7 @@ const IdContextPanel = (props) => {
|
||||
</div>
|
||||
<CameraHelp isOpen />
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ const PortraitPhotoContextPanel = (props) => {
|
||||
</div>
|
||||
<CameraHelp isOpen isPortrait />
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ const RequestCameraAccessPanel = (props) => {
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.success'])}
|
||||
</p>
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@ const ReviewRequirementsPanel = (props) => {
|
||||
</p>
|
||||
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { getConfig, history } from '@edx/frontend-platform';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
Alert, Hyperlink, Form, Button, Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { submitIdVerification } from '../data/service';
|
||||
@@ -31,6 +31,7 @@ const SummaryPanel = (props) => {
|
||||
const nameToBeUsed = idPhotoName || nameOnAccount || '';
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submissionError, setSubmissionError] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => setReachedSummary(true), [setReachedSummary]);
|
||||
|
||||
@@ -80,7 +81,7 @@ const SummaryPanel = (props) => {
|
||||
const result = await submitIdVerification(verificationData);
|
||||
if (result.success) {
|
||||
stopUserMedia();
|
||||
history.push(nextPanelSlug);
|
||||
navigate(`/id-verification/${nextPanelSlug}`);
|
||||
} else {
|
||||
stopUserMedia();
|
||||
setIsSubmitting(false);
|
||||
@@ -171,10 +172,8 @@ const SummaryPanel = (props) => {
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-outline-primary"
|
||||
to={{
|
||||
pathname: 'take-portrait-photo',
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
to="/id-verification/take-portrait-photo"
|
||||
state={{ fromSummary: true }}
|
||||
data-testid="portrait-retake"
|
||||
>
|
||||
{props.intl.formatMessage(messages['id.verification.review.portrait.retake'])}
|
||||
@@ -191,10 +190,8 @@ const SummaryPanel = (props) => {
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-outline-primary"
|
||||
to={{
|
||||
pathname: 'take-id-photo',
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
to="/id-verification/take-id-photo"
|
||||
state={{ fromSummary: true }}
|
||||
data-testid="id-retake"
|
||||
>
|
||||
{props.intl.formatMessage(messages['id.verification.review.id.retake'])}
|
||||
@@ -219,10 +216,8 @@ const SummaryPanel = (props) => {
|
||||
{!profileDataManager && (
|
||||
<Link
|
||||
className="btn btn-link ml-3 px-0"
|
||||
to={{
|
||||
pathname: 'get-name-id',
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
to="/id-verification/get-name-id"
|
||||
state={{ fromSummary: true }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.edit"
|
||||
|
||||
@@ -61,7 +61,7 @@ const TakeIdPhotoPanel = (props) => {
|
||||
{useCameraForId && <CameraHelp />}
|
||||
<CollapsibleImageHelp />
|
||||
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ const TakePortraitPhotoPanel = (props) => {
|
||||
</div>
|
||||
<CameraHelp isPortrait />
|
||||
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -12,8 +11,6 @@ import AccessBlocked from '../AccessBlocked';
|
||||
|
||||
const IntlAccessBlocked = injectIntl(AccessBlocked);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('AccessBlocked', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
@@ -28,7 +25,7 @@ describe('AccessBlocked', () => {
|
||||
defaultProps.error = ERROR_REASONS.EXISTING_REQUEST;
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccessBlocked {...defaultProps} />
|
||||
</IntlProvider>
|
||||
@@ -44,7 +41,7 @@ describe('AccessBlocked', () => {
|
||||
defaultProps.error = ERROR_REASONS.COURSE_ENROLLMENT;
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccessBlocked {...defaultProps} />
|
||||
</IntlProvider>
|
||||
@@ -60,7 +57,7 @@ describe('AccessBlocked', () => {
|
||||
defaultProps.error = ERROR_REASONS.CANNOT_VERIFY;
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccessBlocked {...defaultProps} />
|
||||
</IntlProvider>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, screen, act, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
@@ -22,8 +21,6 @@ window.HTMLMediaElement.prototype.play = () => {};
|
||||
|
||||
const IntlCamera = injectIntl(Camera);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('SubmittedPanel', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
@@ -45,7 +42,7 @@ describe('SubmittedPanel', () => {
|
||||
|
||||
it('takes photo', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
@@ -61,7 +58,7 @@ describe('SubmittedPanel', () => {
|
||||
|
||||
it('shows correct help text for portrait photo capture', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
@@ -75,7 +72,7 @@ describe('SubmittedPanel', () => {
|
||||
|
||||
it('shows correct help text for id photo capture', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...idProps} />
|
||||
@@ -90,7 +87,7 @@ describe('SubmittedPanel', () => {
|
||||
it('shows spinner when loading face detection', async () => {
|
||||
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
@@ -108,7 +105,7 @@ describe('SubmittedPanel', () => {
|
||||
it('canvas is visible when detection is enabled', async () => {
|
||||
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
@@ -128,7 +125,7 @@ describe('SubmittedPanel', () => {
|
||||
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
@@ -147,7 +144,7 @@ describe('SubmittedPanel', () => {
|
||||
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
@@ -168,7 +165,7 @@ describe('SubmittedPanel', () => {
|
||||
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...idProps} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, screen, act,
|
||||
} from '@testing-library/react';
|
||||
@@ -20,8 +19,6 @@ window.HTMLMediaElement.prototype.play = () => {};
|
||||
|
||||
const IntlCollapsible = injectIntl(CollapsibleImageHelp);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('CollapsibleImageHelpPanel', () => {
|
||||
const defaultProps = { intl: {} };
|
||||
|
||||
@@ -36,7 +33,7 @@ describe('CollapsibleImageHelpPanel', () => {
|
||||
|
||||
it('shows the correct text if user should switch to upload', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCollapsible {...defaultProps} />
|
||||
@@ -56,7 +53,7 @@ describe('CollapsibleImageHelpPanel', () => {
|
||||
it('shows the correct text if user should switch to camera', async () => {
|
||||
contextValue.useCameraForId = false;
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCollapsible {...defaultProps} />
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { MemoryRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {
|
||||
render, act, screen, fireEvent,
|
||||
@@ -20,37 +19,36 @@ jest.mock('../VerifiedNameContext', () => {
|
||||
VerifiedNameContextProvider: jest.fn(({ children }) => children),
|
||||
};
|
||||
});
|
||||
jest.mock('../panels/ReviewRequirementsPanel', () => function () {
|
||||
jest.mock('../panels/ReviewRequirementsPanel', () => function ReviewRequirementsPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('../panels/RequestCameraAccessPanel', () => function () {
|
||||
jest.mock('../panels/RequestCameraAccessPanel', () => function RequestCameraAccessPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('../panels/PortraitPhotoContextPanel', () => function () {
|
||||
jest.mock('../panels/PortraitPhotoContextPanel', () => function PortraitPhotoContextPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('../panels/TakePortraitPhotoPanel', () => function () {
|
||||
jest.mock('../panels/TakePortraitPhotoPanel', () => function TakePortraitPhotoPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('../panels/IdContextPanel', () => function () {
|
||||
jest.mock('../panels/IdContextPanel', () => function IdContextPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('../panels/GetNameIdPanel', () => function () {
|
||||
jest.mock('../panels/GetNameIdPanel', () => function GetNameIdPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('../panels/TakeIdPhotoPanel', () => function () {
|
||||
jest.mock('../panels/TakeIdPhotoPanel', () => function TakeIdPhotoPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('../panels/SummaryPanel', () => function () {
|
||||
jest.mock('../panels/SummaryPanel', () => function SummaryPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('../panels/SubmittedPanel', () => function () {
|
||||
jest.mock('../panels/SubmittedPanel', () => function SubmittedPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
|
||||
const IntlIdVerificationPage = injectIntl(IdVerificationPage);
|
||||
const mockStore = configureStore();
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('IdVerificationPage', () => {
|
||||
selectors.mockClear();
|
||||
@@ -60,9 +58,8 @@ describe('IdVerificationPage', () => {
|
||||
intl: {},
|
||||
};
|
||||
it('decodes and stores course_id', async () => {
|
||||
history.push(`/?course_id=${encodeURIComponent('course-v1:edX+DemoX+Demo_Course')}`);
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router initialEntries={[`/?course_id=${encodeURIComponent('course-v1:edX+DemoX+Demo_Course')}`]}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<IntlIdVerificationPage {...props} />
|
||||
@@ -77,9 +74,8 @@ describe('IdVerificationPage', () => {
|
||||
});
|
||||
|
||||
it('stores `next` value', async () => {
|
||||
history.push('/?next=dashboard');
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router initialEntries={['/?next=dashboard']}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<IntlIdVerificationPage {...props} />
|
||||
@@ -93,9 +89,8 @@ describe('IdVerificationPage', () => {
|
||||
);
|
||||
});
|
||||
it('shows modal on click of button', async () => {
|
||||
history.push('/?next=dashboard');
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router initialEntries={['/?next=dashboard']}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<IntlIdVerificationPage {...props} />
|
||||
@@ -108,9 +103,8 @@ describe('IdVerificationPage', () => {
|
||||
expect(screen.getByTestId('Id-modal')).toBeInTheDocument();
|
||||
});
|
||||
it('shows modal on click of button', async () => {
|
||||
history.push('/?next=dashboard');
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router initialEntries={['/?next=dashboard']}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<IntlIdVerificationPage {...props} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
@@ -16,8 +15,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
|
||||
const IntlGetNameIdPanel = injectIntl(GetNameIdPanel);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('GetNameIdPanel', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
@@ -36,7 +33,7 @@ describe('GetNameIdPanel', () => {
|
||||
|
||||
const getPanel = async (idVerificationContextValue = IDVerificationContextValue) => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<VerifiedNameContext.Provider value={verifiedNameContextValue}>
|
||||
<IdVerificationContext.Provider value={idVerificationContextValue}>
|
||||
@@ -82,6 +79,6 @@ describe('GetNameIdPanel', () => {
|
||||
const button = await screen.findByTestId('next-button');
|
||||
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/summary');
|
||||
expect(window.location.pathname).toEqual('/id-verification/summary');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
@@ -15,8 +14,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
|
||||
const IntlIdContextPanel = injectIntl(IdContextPanel);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('IdContextPanel', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
@@ -33,7 +30,7 @@ describe('IdContextPanel', () => {
|
||||
|
||||
it('routes to TakeIdPhotoPanel normally', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlIdContextPanel {...defaultProps} />
|
||||
@@ -43,13 +40,13 @@ describe('IdContextPanel', () => {
|
||||
)));
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/take-id-photo');
|
||||
expect(window.location.pathname).toEqual('/id-verification/take-id-photo');
|
||||
});
|
||||
|
||||
it('routes to TakeIdPhotoPanel if reachedSummary is true', async () => {
|
||||
contextValue.reachedSummary = true;
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlIdContextPanel {...defaultProps} />
|
||||
@@ -59,6 +56,6 @@ describe('IdContextPanel', () => {
|
||||
)));
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/take-id-photo');
|
||||
expect(window.location.pathname).toEqual('/id-verification/take-id-photo');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
@@ -15,8 +14,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
|
||||
const IntlPortraitPhotoContextPanel = injectIntl(PortraitPhotoContextPanel);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('PortraitPhotoContextPanel', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
@@ -30,7 +27,7 @@ describe('PortraitPhotoContextPanel', () => {
|
||||
|
||||
it('routes to TakePortraitPhotoPanel normally', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlPortraitPhotoContextPanel {...defaultProps} />
|
||||
@@ -40,13 +37,13 @@ describe('PortraitPhotoContextPanel', () => {
|
||||
)));
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/take-portrait-photo');
|
||||
expect(window.location.pathname).toEqual('/id-verification/take-portrait-photo');
|
||||
});
|
||||
|
||||
it('routes to TakePortraitPhotoPanel if reachedSummary is true', async () => {
|
||||
contextValue.reachedSummary = true;
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlPortraitPhotoContextPanel {...defaultProps} />
|
||||
@@ -56,6 +53,6 @@ describe('PortraitPhotoContextPanel', () => {
|
||||
)));
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/take-portrait-photo');
|
||||
expect(window.location.pathname).toEqual('/id-verification/take-portrait-photo');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import Bowser from 'bowser';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import {
|
||||
render, screen, cleanup, act, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
@@ -16,8 +15,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
|
||||
jest.mock('bowser');
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const IntlRequestCameraAccessPanel = injectIntl(RequestCameraAccessPanel);
|
||||
|
||||
describe('RequestCameraAccessPanel', () => {
|
||||
@@ -38,7 +35,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
contextValue.mediaAccess = 'pending';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -54,7 +51,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
contextValue.mediaAccess = 'granted';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -66,14 +63,14 @@ describe('RequestCameraAccessPanel', () => {
|
||||
expect(text).toHaveTextContent(/Looks like your camera is working and ready./);
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/portrait-photo-context');
|
||||
expect(window.location.pathname).toEqual('/id-verification/portrait-photo-context');
|
||||
});
|
||||
|
||||
it('renders correctly with media access denied', async () => {
|
||||
contextValue.mediaAccess = 'denied';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -89,7 +86,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
contextValue.mediaAccess = 'unsupported';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Chrome' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -106,7 +103,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
contextValue.mediaAccess = 'unsupported';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -123,7 +120,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
contextValue.mediaAccess = 'denied';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Chrome' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -139,7 +136,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
contextValue.mediaAccess = 'denied';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Firefox' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -155,7 +152,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
contextValue.mediaAccess = 'denied';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Safari' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -171,7 +168,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
contextValue.mediaAccess = 'denied';
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Internet Explorer' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -188,7 +185,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -205,7 +202,7 @@ describe('RequestCameraAccessPanel', () => {
|
||||
|
||||
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlRequestCameraAccessPanel {...defaultProps} />
|
||||
@@ -215,6 +212,6 @@ describe('RequestCameraAccessPanel', () => {
|
||||
)));
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/portrait-photo-context');
|
||||
expect(window.location.pathname).toEqual('/id-verification/portrait-photo-context');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
@@ -15,8 +14,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
|
||||
const IntlReviewRequirementsPanel = injectIntl(ReviewRequirementsPanel);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('ReviewRequirementsPanel', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
@@ -26,7 +23,7 @@ describe('ReviewRequirementsPanel', () => {
|
||||
|
||||
const getPanel = async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={context}>
|
||||
<IntlReviewRequirementsPanel {...defaultProps} />
|
||||
@@ -44,7 +41,7 @@ describe('ReviewRequirementsPanel', () => {
|
||||
await getPanel();
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/request-camera-access');
|
||||
expect(window.location.pathname).toEqual('/id-verification/request-camera-access');
|
||||
});
|
||||
|
||||
it('displays an alert if the user\'s account information is managed by a third party', async () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -15,8 +14,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
|
||||
const IntlSubmittedPanel = injectIntl(SubmittedPanel);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('SubmittedPanel', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
@@ -43,7 +40,7 @@ describe('SubmittedPanel', () => {
|
||||
|
||||
it('links to dashboard without courseId or next value', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlSubmittedPanel {...defaultProps} />
|
||||
@@ -59,7 +56,7 @@ describe('SubmittedPanel', () => {
|
||||
it('links to course when courseId is stored', async () => {
|
||||
sessionStorage.setItem('courseId', 'course-v1:edX+DemoX+Demo_Course');
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlSubmittedPanel {...defaultProps} />
|
||||
@@ -75,7 +72,7 @@ describe('SubmittedPanel', () => {
|
||||
it('links to specified page when `next` value is provided', async () => {
|
||||
sessionStorage.setItem('next', 'some_page');
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlSubmittedPanel {...defaultProps} />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen, fireEvent, waitFor,
|
||||
} from '@testing-library/react';
|
||||
@@ -21,8 +20,6 @@ dataService.submitIdVerification = jest.fn().mockReturnValue({ success: true });
|
||||
|
||||
const IntlSummaryPanel = injectIntl(SummaryPanel);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('SummaryPanel', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
@@ -41,7 +38,7 @@ describe('SummaryPanel', () => {
|
||||
|
||||
const getPanel = async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<VerifiedNameContext.Provider value={verifiedNameContextValue}>
|
||||
<IdVerificationContext.Provider value={appContextValue}>
|
||||
@@ -61,16 +58,14 @@ describe('SummaryPanel', () => {
|
||||
await getPanel();
|
||||
const button = await screen.findByTestId('portrait-retake');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/take-portrait-photo');
|
||||
expect(history.location.state.fromSummary).toEqual(true);
|
||||
expect(window.location.pathname).toEqual('/id-verification/take-portrait-photo');
|
||||
});
|
||||
|
||||
it('routes back to TakeIdPhotoPanel', async () => {
|
||||
await getPanel();
|
||||
const button = await screen.findByTestId('id-retake');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/take-id-photo');
|
||||
expect(history.location.state.fromSummary).toEqual(true);
|
||||
expect(window.location.pathname).toEqual('/id-verification/take-id-photo');
|
||||
});
|
||||
|
||||
it('allows user to upload ID photo', async () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
@@ -14,8 +13,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
|
||||
jest.mock('../../Camera');
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const IntlTakeIdPhotoPanel = injectIntl(TakeIdPhotoPanel);
|
||||
|
||||
describe('TakeIdPhotoPanel', () => {
|
||||
@@ -37,7 +34,7 @@ describe('TakeIdPhotoPanel', () => {
|
||||
|
||||
it('doesn\'t show next button before photo is taken', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlTakeIdPhotoPanel {...defaultProps} />
|
||||
@@ -52,7 +49,7 @@ describe('TakeIdPhotoPanel', () => {
|
||||
it('shows next button after photo is taken and routes to GetNameIdPanel', async () => {
|
||||
contextValue.idPhotoFile = 'test.jpg';
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlTakeIdPhotoPanel {...defaultProps} />
|
||||
@@ -63,14 +60,14 @@ describe('TakeIdPhotoPanel', () => {
|
||||
const button = await screen.findByTestId('next-button');
|
||||
expect(button).toBeVisible();
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/get-name-id');
|
||||
expect(window.location.pathname).toEqual('/id-verification/get-name-id');
|
||||
});
|
||||
|
||||
it('routes back to SummaryPanel if that was the source', async () => {
|
||||
contextValue.idPhotoFile = 'test.jpg';
|
||||
contextValue.reachedSummary = true;
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlTakeIdPhotoPanel {...defaultProps} />
|
||||
@@ -80,12 +77,12 @@ describe('TakeIdPhotoPanel', () => {
|
||||
)));
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/summary');
|
||||
expect(window.location.pathname).toEqual('/id-verification/summary');
|
||||
});
|
||||
|
||||
it('shows correct text if user should use upload', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlTakeIdPhotoPanel {...defaultProps} />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
render, cleanup, act, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
@@ -13,12 +12,10 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendTrackEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../Camera', () => function () {
|
||||
jest.mock('../../Camera', () => function CameraMock() {
|
||||
return <></>;
|
||||
});
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const IntlTakePortraitPhotoPanel = injectIntl(TakePortraitPhotoPanel);
|
||||
|
||||
describe('TakePortraitPhotoPanel', () => {
|
||||
@@ -39,7 +36,7 @@ describe('TakePortraitPhotoPanel', () => {
|
||||
|
||||
it('doesn\'t show next button before photo is taken', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlTakePortraitPhotoPanel {...defaultProps} />
|
||||
@@ -54,7 +51,7 @@ describe('TakePortraitPhotoPanel', () => {
|
||||
it('shows next button after photo is taken and routes to IdContextPanel', async () => {
|
||||
contextValue.facePhotoFile = 'test.jpg';
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlTakePortraitPhotoPanel {...defaultProps} />
|
||||
@@ -65,7 +62,7 @@ describe('TakePortraitPhotoPanel', () => {
|
||||
const button = await screen.findByTestId('next-button');
|
||||
expect(button).toBeVisible();
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/id-context');
|
||||
expect(window.location.pathname).toEqual('/id-verification/id-context');
|
||||
});
|
||||
|
||||
it('routes back to SummaryPanel if that was the source', async () => {
|
||||
@@ -73,7 +70,7 @@ describe('TakePortraitPhotoPanel', () => {
|
||||
contextValue.idPhotoFile = 'test.jpg';
|
||||
contextValue.reachedSummary = true;
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlTakePortraitPhotoPanel {...defaultProps} />
|
||||
@@ -83,6 +80,6 @@ describe('TakePortraitPhotoPanel', () => {
|
||||
)));
|
||||
const button = await screen.findByTestId('next-button');
|
||||
fireEvent.click(button);
|
||||
expect(history.location.pathname).toEqual('/summary');
|
||||
expect(window.location.pathname).toEqual('/id-verification/summary');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,39 +8,44 @@ import {
|
||||
} from '@edx/frontend-platform';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Route, Routes, Outlet } from 'react-router-dom';
|
||||
|
||||
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
|
||||
import configureStore from './data/configureStore';
|
||||
import AccountSettingsPage, { NotFoundPage } from './account-settings';
|
||||
import IdVerificationPage from './id-verification';
|
||||
import CoachingConsent from './account-settings/coaching/CoachingConsent';
|
||||
import appMessages from './i18n';
|
||||
import messages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
import Head from './head/Head';
|
||||
import NotificationCourses from './notification-preferences/NotificationCourses';
|
||||
import NotificationPreferences from './notification-preferences/NotificationPreferences';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
<Switch>
|
||||
<Route path="/coaching_consent" component={CoachingConsent} />
|
||||
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
|
||||
<Header />
|
||||
<main className="flex-grow-1">
|
||||
<Switch>
|
||||
<Route path="/id-verification" component={IdVerificationPage} />
|
||||
<Route exact path="/" component={AccountSettingsPage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</Switch>
|
||||
<Routes>
|
||||
<Route element={(
|
||||
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
|
||||
<Header />
|
||||
<main className="flex-grow-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Route path="/notifications/:courseId" element={<NotificationPreferences />} />
|
||||
<Route path="/notifications" element={<NotificationCourses />} />
|
||||
<Route path="/id-verification/*" element={<IdVerificationPage />} />
|
||||
<Route path="/" element={<AccountSettingsPage />} />
|
||||
<Route path="/notfound" element={<NotFoundPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
@@ -51,24 +56,21 @@ subscribe(APP_INIT_ERROR, (error) => {
|
||||
});
|
||||
|
||||
initialize({
|
||||
messages: [
|
||||
appMessages,
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
],
|
||||
messages,
|
||||
requireAuthenticatedUser: true,
|
||||
hydrateAuthenticatedUser: true,
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
SUPPORT_URL: process.env.SUPPORT_URL,
|
||||
COACHING_ENABLED: (process.env.COACHING_ENABLED || false),
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: (process.env.ENABLE_DEMOGRAPHICS_COLLECTION || false),
|
||||
DEMOGRAPHICS_BASE_URL: process.env.DEMOGRAPHICS_BASE_URL,
|
||||
ENABLE_COPPA_COMPLIANCE: (process.env.ENABLE_COPPA_COMPLIANCE || false),
|
||||
ENABLE_ACCOUNT_DELETION: (process.env.ENABLE_ACCOUNT_DELETION !== 'false'),
|
||||
ENABLE_DOB_UPDATE: (process.env.ENABLE_DOB_UPDATE || false),
|
||||
MARKETING_EMAILS_OPT_IN: (process.env.MARKETING_EMAILS_OPT_IN || false),
|
||||
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK,
|
||||
LEARNER_FEEDBACK_URL: process.env.LEARNER_FEEDBACK_URL,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -38,21 +38,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
}
|
||||
}
|
||||
|
||||
.coaching-header {
|
||||
.logo {
|
||||
display: block;
|
||||
box-sizing: content-box;
|
||||
height: 1.75rem;
|
||||
padding: .75rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.coaching-consent {
|
||||
.disclaimer {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxOption {
|
||||
input:focus {
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
@@ -62,3 +47,58 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-heading {
|
||||
line-height: 36px;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.px-2\.25 {
|
||||
padding-left: 0.625rem;
|
||||
}
|
||||
|
||||
.notification-sub-heading {
|
||||
font-size: 14px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pgn__hyperlink__external-icon{
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.notification-preferences {
|
||||
input[type="checkbox"] {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.header-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
height: 28px;
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
.notification-course-title {
|
||||
line-height: 28px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.usabilla_live_button_container {
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768.98px) {
|
||||
.usabilla_live_button_container {
|
||||
right: -22px !important;
|
||||
top: 50% !important;
|
||||
transform: rotate(270deg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
86
src/notification-preferences/NotificationCourses.jsx
Normal file
86
src/notification-preferences/NotificationCourses.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Container, Icon, Spinner, Button,
|
||||
} from '@edx/paragon';
|
||||
import { ArrowForwardIos } from '@edx/paragon/icons';
|
||||
import { fetchCourseList } from './data/thunks';
|
||||
import { selectCourseListStatus, selectCourseList, selectPagination } from './data/selectors';
|
||||
import {
|
||||
IDLE_STATUS,
|
||||
LOADING_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
} from '../constants';
|
||||
import { messages } from './messages';
|
||||
import { NotFoundPage } from '../account-settings';
|
||||
import { useFeedbackWrapper } from '../hooks';
|
||||
|
||||
const NotificationCourses = ({ intl }) => {
|
||||
useFeedbackWrapper();
|
||||
const dispatch = useDispatch();
|
||||
const coursesList = useSelector(selectCourseList());
|
||||
const courseListStatus = useSelector(selectCourseListStatus());
|
||||
const { hasMore, currentPage } = useSelector(selectPagination());
|
||||
|
||||
const loadMore = useCallback((page = 1, pageSize = 10) => {
|
||||
dispatch(fetchCourseList(page, pageSize));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (courseListStatus === IDLE_STATUS) { loadMore(); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (courseListStatus === SUCCESS_STATUS && coursesList.length === 0) {
|
||||
return <NotFoundPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size="md">
|
||||
<h2 className="notification-heading mt-6 mb-5.5">
|
||||
{intl.formatMessage(messages.notificationHeading)}
|
||||
</h2>
|
||||
<div data-testid="courses-list">
|
||||
{coursesList.map(course => (
|
||||
<Link
|
||||
key={course.id}
|
||||
to={`/notifications/${course.id}`}
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<div className="mb-4 d-flex text-gray-700">
|
||||
<span className="ml-0 mr-auto">
|
||||
{course.name}
|
||||
</span>
|
||||
<span className="ml-auto mr-0">
|
||||
<Icon src={ArrowForwardIos} />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{courseListStatus === LOADING_STATUS ? (
|
||||
<div className="d-flex">
|
||||
<Spinner
|
||||
variant="primary"
|
||||
animation="border"
|
||||
className="mx-auto my-auto"
|
||||
size="lg"
|
||||
data-testid="loading-spinner"
|
||||
/>
|
||||
</div>
|
||||
) : hasMore && (
|
||||
<Button variant="primary" className="w-100 bg-primary-500" onClick={() => loadMore(currentPage + 1)}>
|
||||
{intl.formatMessage(messages.loadMoreCourses)}
|
||||
</Button>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationCourses.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(NotificationCourses);
|
||||
95
src/notification-preferences/NotificationCourses.test.jsx
Normal file
95
src/notification-preferences/NotificationCourses.test.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import NotificationCourses from './NotificationCourses';
|
||||
import { defaultState } from './data/reducers';
|
||||
import { LOADING_STATUS, SUCCESS_STATUS } from '../constants';
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const courseList = [
|
||||
{ id: 'course-id-1', name: 'Course Name 1' },
|
||||
{ id: 'course-id-2', name: 'Course Name 2' },
|
||||
{ id: 'course-id-3', name: 'Course Name 3' },
|
||||
];
|
||||
|
||||
const setupStore = (override = {}) => {
|
||||
const storeState = defaultState;
|
||||
storeState.courses = {
|
||||
...storeState.courses,
|
||||
...override,
|
||||
};
|
||||
const store = mockStore({
|
||||
notificationPreferences: storeState,
|
||||
});
|
||||
return store;
|
||||
};
|
||||
|
||||
const renderComponent = (store = {}) => (
|
||||
render(
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<NotificationCourses />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</Router>,
|
||||
)
|
||||
);
|
||||
|
||||
describe('Notification Courses', () => {
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
store = setupStore({
|
||||
courses: courseList,
|
||||
status: SUCCESS_STATUS,
|
||||
pagination: {
|
||||
count: 3,
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
totalPages: 1,
|
||||
},
|
||||
});
|
||||
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
|
||||
window.lightningjs = null;
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('tests if all courses are available', async () => {
|
||||
await renderComponent(store);
|
||||
expect(screen.queryByTestId('courses-list').children).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('show spinner if api call is in progress', async () => {
|
||||
store = setupStore({ status: LOADING_STATUS });
|
||||
await renderComponent(store);
|
||||
expect(screen.queryByTestId('loading-spinner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('show not found page if course list is empty', async () => {
|
||||
store = setupStore({ status: SUCCESS_STATUS, courses: [] });
|
||||
await renderComponent(store);
|
||||
expect(screen.queryByTestId('not-found-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('show load more courses button when hasMore True', async () => {
|
||||
store = setupStore({ status: SUCCESS_STATUS, pagination: { ...store.pagination, hasMore: true, totalPages: 2 } });
|
||||
await renderComponent(store);
|
||||
|
||||
expect(screen.queryByText('Load more courses')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
76
src/notification-preferences/NotificationPreferenceApp.jsx
Normal file
76
src/notification-preferences/NotificationPreferenceApp.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
import { messages } from './messages';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import {
|
||||
selectPreferenceAppToggleValue,
|
||||
selectPreferencesOfApp,
|
||||
selectSelectedCourseId,
|
||||
} from './data/selectors';
|
||||
import NotificationPreferenceRow from './NotificationPreferenceRow';
|
||||
import { updateAppPreferenceToggle } from './data/thunks';
|
||||
|
||||
const NotificationPreferenceApp = ({ appId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
const appPreferences = useSelector(selectPreferencesOfApp(appId));
|
||||
const appToggle = useSelector(selectPreferenceAppToggleValue(appId));
|
||||
|
||||
const preferences = useMemo(() => (
|
||||
appPreferences.map(preference => (
|
||||
<NotificationPreferenceRow
|
||||
key={preference.id}
|
||||
appId={appId}
|
||||
preferenceName={preference.id}
|
||||
/>
|
||||
))), [appId, appPreferences]);
|
||||
|
||||
const onChangeAppSettings = useCallback((event) => {
|
||||
dispatch(updateAppPreferenceToggle(courseId, appId, event.target.checked));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appId]);
|
||||
|
||||
if (!courseId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Collapsible.Advanced open={appToggle} data-testid="notification-app" className="mb-5">
|
||||
<Collapsible.Trigger>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="mr-auto">
|
||||
{intl.formatMessage(messages.notificationAppTitle, { key: appId })}
|
||||
</span>
|
||||
<span className="d-flex" id={`${appId}-app-toggle`}>
|
||||
<ToggleSwitch
|
||||
name={appId}
|
||||
value={appToggle}
|
||||
onChange={onChangeAppSettings}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<hr className="border-light-400 my-3" />
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body>
|
||||
<div className="d-flex flex-row header-label">
|
||||
<span className="col-8 px-0">{intl.formatMessage(messages.typeLabel)}</span>
|
||||
<span className="d-flex col-4 px-0">
|
||||
<span className="ml-auto">{intl.formatMessage(messages.webLabel)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="my-3">
|
||||
{ preferences }
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationPreferenceApp.propTypes = {
|
||||
appId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(NotificationPreferenceApp);
|
||||
93
src/notification-preferences/NotificationPreferenceRow.jsx
Normal file
93
src/notification-preferences/NotificationPreferenceRow.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { InfoOutline } from '@edx/paragon/icons';
|
||||
import { messages } from './messages';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import {
|
||||
selectPreference,
|
||||
selectPreferenceNonEditableChannels,
|
||||
selectSelectedCourseId,
|
||||
selectNotificationPreferencesStatus,
|
||||
} from './data/selectors';
|
||||
import { updatePreferenceToggle } from './data/thunks';
|
||||
import { LOADING_STATUS } from '../constants';
|
||||
|
||||
const NotificationPreferenceRow = ({ appId, preferenceName }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
const preference = useSelector(selectPreference(appId, preferenceName));
|
||||
const nonEditable = useSelector(selectPreferenceNonEditableChannels(appId, preferenceName));
|
||||
const preferencesStatus = useSelector(selectNotificationPreferencesStatus());
|
||||
|
||||
const onToggle = useCallback((event) => {
|
||||
const {
|
||||
checked,
|
||||
name: notificationChannel,
|
||||
} = event.target;
|
||||
dispatch(updatePreferenceToggle(
|
||||
courseId,
|
||||
appId,
|
||||
preferenceName,
|
||||
notificationChannel,
|
||||
checked,
|
||||
));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appId, preferenceName]);
|
||||
|
||||
const tooltipId = `${preferenceName}-tooltip`;
|
||||
return (
|
||||
<div className="d-flex mb-3" data-testid="notification-preference">
|
||||
<div className="d-flex align-items-center mr-auto">
|
||||
{intl.formatMessage(messages.notificationTitle, { text: preferenceName })}
|
||||
{preference.info !== '' && (
|
||||
<OverlayTrigger
|
||||
id={tooltipId}
|
||||
className="d-inline"
|
||||
placement="right"
|
||||
overlay={(
|
||||
<Tooltip id={tooltipId}>
|
||||
{preference.info}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<span className="ml-2">
|
||||
<Icon src={InfoOutline} />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
{['web'].map((channel) => (
|
||||
<div
|
||||
id={`${preferenceName}-${channel}`}
|
||||
className={classNames(
|
||||
'd-flex',
|
||||
{ 'ml-auto': channel === 'web' },
|
||||
{ 'mx-auto': channel === 'email' },
|
||||
{ 'ml-auto mr-0': channel === 'push' },
|
||||
)}
|
||||
>
|
||||
<ToggleSwitch
|
||||
name={channel}
|
||||
value={preference[channel]}
|
||||
onChange={onToggle}
|
||||
disabled={nonEditable.includes(channel) || preferencesStatus === LOADING_STATUS}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationPreferenceRow.propTypes = {
|
||||
appId: PropTypes.string.isRequired,
|
||||
preferenceName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(NotificationPreferenceRow);
|
||||
101
src/notification-preferences/NotificationPreferences.jsx
Normal file
101
src/notification-preferences/NotificationPreferences.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Container, Icon, Spinner, Hyperlink,
|
||||
} from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
import {
|
||||
selectCourseListStatus,
|
||||
selectCourse,
|
||||
selectPreferenceAppsId,
|
||||
selectNotificationPreferencesStatus,
|
||||
selectCourseList,
|
||||
} from './data/selectors';
|
||||
import { fetchCourseList, fetchCourseNotificationPreferences } from './data/thunks';
|
||||
import { messages } from './messages';
|
||||
import NotificationPreferenceApp from './NotificationPreferenceApp';
|
||||
import {
|
||||
FAILURE_STATUS,
|
||||
IDLE_STATUS,
|
||||
LOADING_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
} from '../constants';
|
||||
import { NotFoundPage } from '../account-settings';
|
||||
|
||||
const NotificationPreferences = () => {
|
||||
const { courseId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseStatus = useSelector(selectCourseListStatus());
|
||||
const coursesList = useSelector(selectCourseList());
|
||||
const course = useSelector(selectCourse(courseId));
|
||||
const notificationStatus = useSelector(selectNotificationPreferencesStatus());
|
||||
const preferenceAppsIds = useSelector(selectPreferenceAppsId());
|
||||
const isLoading = notificationStatus === LOADING_STATUS || courseStatus === LOADING_STATUS;
|
||||
|
||||
const preferencesList = useMemo(() => (
|
||||
preferenceAppsIds.map(appId => (
|
||||
<NotificationPreferenceApp appId={appId} key={appId} />
|
||||
))
|
||||
), [preferenceAppsIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if ([IDLE_STATUS, FAILURE_STATUS].includes(courseStatus)) {
|
||||
dispatch(fetchCourseList());
|
||||
}
|
||||
dispatch(fetchCourseNotificationPreferences(courseId));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [courseId]);
|
||||
|
||||
if (
|
||||
(courseStatus === SUCCESS_STATUS && coursesList.length === 0)
|
||||
|| (notificationStatus === FAILURE_STATUS && coursesList.length !== 0)
|
||||
) {
|
||||
return <NotFoundPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size="sm" className="notification-preferences">
|
||||
<h2 className="notification-heading mt-6 mb-4.5">
|
||||
{intl.formatMessage(messages.notificationHeading)}
|
||||
</h2>
|
||||
<div className="mb-6 text-gray-700">
|
||||
{intl.formatMessage(messages.notificationPreferenceGuideBody)}
|
||||
<Hyperlink
|
||||
destination="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/sfd_notifications/managing_notifications.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-underline"
|
||||
>
|
||||
{intl.formatMessage(messages.notificationPreferenceGuideLink)}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
<div className="h-100">
|
||||
<div className="d-flex mb-5">
|
||||
<Link to="/notifications">
|
||||
<Icon className="text-primary-500" src={ArrowBack} />
|
||||
</Link>
|
||||
<span className="notification-course-title ml-auto mr-auto text-primary-500">
|
||||
{course?.name}
|
||||
</span>
|
||||
</div>
|
||||
{preferencesList}
|
||||
{isLoading && (
|
||||
<div className="d-flex">
|
||||
<Spinner
|
||||
variant="primary"
|
||||
animation="border"
|
||||
className="mx-auto my-auto"
|
||||
size="lg"
|
||||
data-testid="loading-spinner"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationPreferences;
|
||||
145
src/notification-preferences/NotificationPreferences.test.jsx
Normal file
145
src/notification-preferences/NotificationPreferences.test.jsx
Normal file
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import NotificationPreferences from './NotificationPreferences';
|
||||
import { defaultState } from './data/reducers';
|
||||
import { FAILURE_STATUS, LOADING_STATUS, SUCCESS_STATUS } from '../constants';
|
||||
|
||||
const courseId = 'selected-course-id';
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const defaultPreferences = {
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: courseId,
|
||||
apps: [
|
||||
{ id: 'discussion', enabled: true },
|
||||
{ id: 'coursework', enabled: true },
|
||||
],
|
||||
preferences: [
|
||||
{
|
||||
id: 'newPost',
|
||||
appId: 'discussion',
|
||||
web: false,
|
||||
push: false,
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
id: 'newComment',
|
||||
appId: 'discussion',
|
||||
web: false,
|
||||
push: false,
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
id: 'newAssignment',
|
||||
appId: 'coursework',
|
||||
web: false,
|
||||
push: false,
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
id: 'newGrade',
|
||||
appId: 'coursework',
|
||||
web: false,
|
||||
push: false,
|
||||
mobile: false,
|
||||
},
|
||||
],
|
||||
nonEditable: {},
|
||||
};
|
||||
|
||||
const setupStore = (override = {}) => {
|
||||
const storeState = defaultState;
|
||||
storeState.courses = {
|
||||
status: SUCCESS_STATUS,
|
||||
courses: [
|
||||
{ id: 'selected-course-id', name: 'Selected Course' },
|
||||
],
|
||||
};
|
||||
storeState.preferences = {
|
||||
...storeState.preferences,
|
||||
...override,
|
||||
};
|
||||
const store = mockStore({
|
||||
notificationPreferences: storeState,
|
||||
});
|
||||
return store;
|
||||
};
|
||||
|
||||
const renderComponent = (store = {}) => render(
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<NotificationPreferences />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</Router>,
|
||||
);
|
||||
describe('Notification Preferences', () => {
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
store = setupStore({
|
||||
...defaultPreferences,
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: courseId,
|
||||
});
|
||||
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('tests if all notification apps are listed', async () => {
|
||||
await renderComponent(store);
|
||||
expect(screen.queryAllByTestId('notification-app')).toHaveLength(2);
|
||||
});
|
||||
it('show spinner if api call is in progress', async () => {
|
||||
store = setupStore({ status: LOADING_STATUS });
|
||||
await renderComponent(store);
|
||||
expect(screen.queryByTestId('loading-spinner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('tests if all notification preferences are listed', async () => {
|
||||
await renderComponent(store);
|
||||
expect(screen.queryAllByTestId('notification-preference')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('update group on click', async () => {
|
||||
const wrapper = await renderComponent(store);
|
||||
const element = wrapper.container.querySelector('#discussion-app-toggle');
|
||||
await fireEvent.click(element);
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update preference on click', async () => {
|
||||
const wrapper = await renderComponent(store);
|
||||
const element = wrapper.container.querySelector('#newPost-web');
|
||||
expect(element).not.toBeChecked();
|
||||
await fireEvent.click(element);
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('show not found page if invalid course id is entered in url', async () => {
|
||||
store = setupStore({ status: FAILURE_STATUS, selectedCourse: 'invalid-course-id' });
|
||||
await renderComponent(store);
|
||||
expect(screen.queryByTestId('not-found-page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
31
src/notification-preferences/ToggleSwitch.jsx
Normal file
31
src/notification-preferences/ToggleSwitch.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Form } from '@edx/paragon';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ToggleSwitch = ({
|
||||
name,
|
||||
value,
|
||||
disabled,
|
||||
onChange,
|
||||
}) => (
|
||||
<Form.Switch
|
||||
name={name}
|
||||
checked={value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
ToggleSwitch.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.bool.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
ToggleSwitch.defaultProps = {
|
||||
onChange: () => null,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default React.memo(ToggleSwitch);
|
||||
58
src/notification-preferences/data/actions.js
Normal file
58
src/notification-preferences/data/actions.js
Normal file
@@ -0,0 +1,58 @@
|
||||
export const Actions = {
|
||||
FETCHED_PREFERENCES: 'fetchedPreferences',
|
||||
FETCHING_PREFERENCES: 'fetchingPreferences',
|
||||
FAILED_PREFERENCES: 'failedPreferences',
|
||||
FETCHING_COURSE_LIST: 'fetchingCourseList',
|
||||
FETCHED_COURSE_LIST: 'fetchedCourseList',
|
||||
FAILED_COURSE_LIST: 'failedCourseList',
|
||||
UPDATE_SELECTED_COURSE: 'updateSelectedCourse',
|
||||
UPDATE_PREFERENCE: 'updatePreference',
|
||||
UPDATE_APP_PREFERENCE: 'updateAppValue',
|
||||
};
|
||||
|
||||
export const fetchNotificationPreferenceSuccess = (courseId, payload) => dispatch => (
|
||||
dispatch({ type: Actions.FETCHED_PREFERENCES, courseId, payload })
|
||||
);
|
||||
|
||||
export const fetchNotificationPreferenceFetching = () => dispatch => (
|
||||
dispatch({ type: Actions.FETCHING_PREFERENCES })
|
||||
);
|
||||
|
||||
export const fetchNotificationPreferenceFailed = () => dispatch => (
|
||||
dispatch({ type: Actions.FAILED_PREFERENCES })
|
||||
);
|
||||
|
||||
export const fetchCourseListSuccess = payload => dispatch => (
|
||||
dispatch({ type: Actions.FETCHED_COURSE_LIST, payload })
|
||||
);
|
||||
|
||||
export const fetchCourseListFetching = () => dispatch => (
|
||||
dispatch({ type: Actions.FETCHING_COURSE_LIST })
|
||||
);
|
||||
|
||||
export const fetchCourseListFailed = () => dispatch => (
|
||||
dispatch({ type: Actions.FAILED_COURSE_LIST })
|
||||
);
|
||||
|
||||
export const updateSelectedCourse = courseId => dispatch => (
|
||||
dispatch({ type: Actions.UPDATE_SELECTED_COURSE, courseId })
|
||||
);
|
||||
|
||||
export const updatePreferenceValue = (appId, preferenceName, notificationChannel, value) => dispatch => (
|
||||
dispatch({
|
||||
type: Actions.UPDATE_PREFERENCE,
|
||||
appId,
|
||||
preferenceName,
|
||||
notificationChannel,
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
export const updateAppToggle = (courseId, appId, value) => dispatch => (
|
||||
dispatch({
|
||||
type: Actions.UPDATE_APP_PREFERENCE,
|
||||
courseId,
|
||||
appId,
|
||||
value,
|
||||
})
|
||||
);
|
||||
125
src/notification-preferences/data/reducers.js
Normal file
125
src/notification-preferences/data/reducers.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Actions } from './actions';
|
||||
import {
|
||||
IDLE_STATUS,
|
||||
LOADING_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
FAILURE_STATUS,
|
||||
} from '../../constants';
|
||||
|
||||
export const defaultState = {
|
||||
showPreferences: false,
|
||||
courses: {
|
||||
status: IDLE_STATUS,
|
||||
courses: [],
|
||||
pagination: {},
|
||||
},
|
||||
preferences: {
|
||||
status: IDLE_STATUS,
|
||||
selectedCourse: null,
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
},
|
||||
};
|
||||
|
||||
const notificationPreferencesReducer = (state = defaultState, action = {}) => {
|
||||
const {
|
||||
courseId, appId, notificationChannel, preferenceName, value,
|
||||
} = action;
|
||||
switch (action.type) {
|
||||
case Actions.FETCHING_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
...state.courses,
|
||||
status: LOADING_STATUS,
|
||||
},
|
||||
};
|
||||
case Actions.FETCHED_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
status: SUCCESS_STATUS,
|
||||
courses: [...state.courses.courses, ...action.payload.courseList],
|
||||
pagination: action.payload.pagination,
|
||||
},
|
||||
showPreferences: action.payload.showPreferences,
|
||||
};
|
||||
case Actions.FAILED_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
...state.courses,
|
||||
status: FAILURE_STATUS,
|
||||
},
|
||||
};
|
||||
case Actions.FETCHING_PREFERENCES:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
status: LOADING_STATUS,
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
},
|
||||
};
|
||||
case Actions.FETCHED_PREFERENCES:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
status: SUCCESS_STATUS,
|
||||
...action.payload,
|
||||
},
|
||||
};
|
||||
case Actions.FAILED_PREFERENCES:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
status: FAILURE_STATUS,
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
},
|
||||
};
|
||||
case Actions.UPDATE_SELECTED_COURSE:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
selectedCourse: courseId,
|
||||
},
|
||||
};
|
||||
case Actions.UPDATE_PREFERENCE:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
preferences: state.preferences.preferences.map((preference) => (
|
||||
preference.id === preferenceName
|
||||
? { ...preference, [notificationChannel]: value }
|
||||
: preference
|
||||
)),
|
||||
status: LOADING_STATUS,
|
||||
},
|
||||
};
|
||||
case Actions.UPDATE_APP_PREFERENCE:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
apps: state.preferences.apps.map(app => (
|
||||
app.id === appId
|
||||
? { ...app, enabled: value }
|
||||
: app
|
||||
)),
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default notificationPreferencesReducer;
|
||||
138
src/notification-preferences/data/reducers.test.js
Normal file
138
src/notification-preferences/data/reducers.test.js
Normal file
@@ -0,0 +1,138 @@
|
||||
// eslint-disable-next-line import/no-named-default
|
||||
import { default as reducer } from './reducers';
|
||||
import { Actions } from './actions';
|
||||
import {
|
||||
FAILURE_STATUS,
|
||||
LOADING_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
} from '../../constants';
|
||||
|
||||
describe('notification-preferences reducer', () => {
|
||||
let state = null;
|
||||
const selectedCourseId = 'selected-course-id';
|
||||
|
||||
const preferenceData = {
|
||||
apps: [{ id: 'discussion', enabled: true }],
|
||||
preferences: [{
|
||||
id: 'newPost',
|
||||
appId: 'discussion',
|
||||
web: false,
|
||||
push: false,
|
||||
mobile: false,
|
||||
}],
|
||||
nonEditable: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
state = reducer();
|
||||
});
|
||||
|
||||
it('updates course list when api call is successful', () => {
|
||||
const data = {
|
||||
pagination: {
|
||||
count: 1,
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
totalPages: 1,
|
||||
},
|
||||
courseList: [
|
||||
{ id: selectedCourseId, name: 'Selected Course' },
|
||||
],
|
||||
};
|
||||
const result = reducer(
|
||||
state,
|
||||
{ type: Actions.FETCHED_COURSE_LIST, payload: data },
|
||||
);
|
||||
expect(result.courses).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
courses: data.courseList,
|
||||
pagination: data.pagination,
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ action: Actions.FETCHING_COURSE_LIST, status: LOADING_STATUS },
|
||||
{ action: Actions.FAILED_COURSE_LIST, status: FAILURE_STATUS },
|
||||
])('course list is empty when api call is %s', ({ action, status }) => {
|
||||
const result = reducer(
|
||||
state,
|
||||
{ type: action },
|
||||
);
|
||||
expect(result.courses).toEqual({
|
||||
status,
|
||||
courses: [],
|
||||
pagination: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates selected course id', () => {
|
||||
const result = reducer(
|
||||
state,
|
||||
{ type: Actions.UPDATE_SELECTED_COURSE, courseId: selectedCourseId },
|
||||
);
|
||||
expect(result.preferences.selectedCourse).toEqual(selectedCourseId);
|
||||
});
|
||||
|
||||
it('updates preferences when api call is successful', () => {
|
||||
const result = reducer(
|
||||
state,
|
||||
{ type: Actions.FETCHED_PREFERENCES, payload: preferenceData },
|
||||
);
|
||||
expect(result.preferences).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: null,
|
||||
...preferenceData,
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ action: Actions.FETCHING_PREFERENCES, status: LOADING_STATUS },
|
||||
{ action: Actions.FAILED_PREFERENCES, status: FAILURE_STATUS },
|
||||
])('preferences are empty when api call is %s', ({ action, status }) => {
|
||||
const result = reducer(
|
||||
state,
|
||||
{ type: action },
|
||||
);
|
||||
expect(result.preferences).toEqual({
|
||||
status,
|
||||
selectedCourse: null,
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('app preference changes when action is dispatched', () => {
|
||||
state = reducer(
|
||||
state,
|
||||
{ type: Actions.FETCHED_PREFERENCES, payload: preferenceData },
|
||||
);
|
||||
const result = reducer(
|
||||
state,
|
||||
{
|
||||
type: Actions.UPDATE_APP_PREFERENCE,
|
||||
appId: 'discussion',
|
||||
value: false,
|
||||
},
|
||||
);
|
||||
expect(result.preferences.apps[0].enabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it('preference changes when action is dispatched', () => {
|
||||
state = reducer(
|
||||
state,
|
||||
{ type: Actions.FETCHED_PREFERENCES, payload: preferenceData },
|
||||
);
|
||||
const result = reducer(
|
||||
state,
|
||||
{
|
||||
type: Actions.UPDATE_PREFERENCE,
|
||||
appId: 'discussion',
|
||||
preferenceName: 'newPost',
|
||||
notificationChannel: 'web',
|
||||
value: true,
|
||||
},
|
||||
);
|
||||
expect(result.preferences.preferences[0].web).toBeTruthy();
|
||||
});
|
||||
});
|
||||
63
src/notification-preferences/data/selectors.js
Normal file
63
src/notification-preferences/data/selectors.js
Normal file
@@ -0,0 +1,63 @@
|
||||
export const selectNotificationPreferencesStatus = () => state => (
|
||||
state.notificationPreferences.preferences.status
|
||||
);
|
||||
|
||||
export const selectPreferences = () => state => (
|
||||
state.notificationPreferences.preferences?.preferences
|
||||
);
|
||||
|
||||
export const selectCourseListStatus = () => state => (
|
||||
state.notificationPreferences.courses.status
|
||||
);
|
||||
|
||||
export const selectCourseList = () => state => (
|
||||
state.notificationPreferences.courses.courses
|
||||
);
|
||||
|
||||
export const selectCourse = courseId => state => (
|
||||
selectCourseList()(state).find(
|
||||
course => course.id === courseId,
|
||||
)
|
||||
);
|
||||
|
||||
export const selectPreferenceAppsId = () => state => (
|
||||
state.notificationPreferences.preferences.apps.map(app => app.id)
|
||||
);
|
||||
|
||||
export const selectPreferencesOfApp = appId => state => (
|
||||
selectPreferences()(state).filter(preference => (
|
||||
preference.appId === appId
|
||||
))
|
||||
);
|
||||
|
||||
export const selectPreferenceApp = appId => state => (
|
||||
state.notificationPreferences.preferences.apps.find(app => (
|
||||
app.id === appId
|
||||
))
|
||||
);
|
||||
|
||||
export const selectPreferenceAppToggleValue = appId => state => (
|
||||
selectPreferenceApp(appId)(state).enabled
|
||||
);
|
||||
|
||||
export const selectPreference = (appId, name) => state => (
|
||||
selectPreferences()(state).find((preference) => (
|
||||
preference.id === name && preference.appId === appId
|
||||
))
|
||||
);
|
||||
|
||||
export const selectPreferenceNonEditableChannels = (appId, name) => state => (
|
||||
state?.notificationPreferences.preferences.nonEditable[appId]?.[name] || []
|
||||
);
|
||||
|
||||
export const selectSelectedCourseId = () => state => (
|
||||
state.notificationPreferences.preferences.selectedCourse
|
||||
);
|
||||
|
||||
export const selectPagination = () => state => (
|
||||
state.notificationPreferences.courses.pagination
|
||||
);
|
||||
|
||||
export const selectShowPreferences = () => state => (
|
||||
state.notificationPreferences.showPreferences
|
||||
);
|
||||
44
src/notification-preferences/data/service.js
Normal file
44
src/notification-preferences/data/service.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
export const getCourseNotificationPreferences = async (courseId) => {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/configurations/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCourseList = async (page, pageSize) => {
|
||||
const params = snakeCaseObject({ page, pageSize });
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/enrollments/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url, { params });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const patchAppPreferenceToggle = async (courseId, appId, value) => {
|
||||
const patchData = snakeCaseObject({
|
||||
notificationApp: appId,
|
||||
value,
|
||||
});
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/configurations/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().patch(url, patchData);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const patchPreferenceToggle = async (
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
) => {
|
||||
const patchData = snakeCaseObject({
|
||||
notificationApp,
|
||||
notificationType: snakeCase(notificationType),
|
||||
notificationChannel,
|
||||
value,
|
||||
});
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/configurations/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().patch(url, patchData);
|
||||
return data;
|
||||
};
|
||||
150
src/notification-preferences/data/thunks.js
Normal file
150
src/notification-preferences/data/thunks.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import {
|
||||
fetchCourseListSuccess,
|
||||
fetchCourseListFetching,
|
||||
fetchCourseListFailed,
|
||||
fetchNotificationPreferenceFailed,
|
||||
fetchNotificationPreferenceFetching,
|
||||
fetchNotificationPreferenceSuccess,
|
||||
updateAppToggle,
|
||||
updatePreferenceValue,
|
||||
updateSelectedCourse,
|
||||
} from './actions';
|
||||
import {
|
||||
getCourseList,
|
||||
getCourseNotificationPreferences,
|
||||
patchAppPreferenceToggle,
|
||||
patchPreferenceToggle,
|
||||
} from './service';
|
||||
|
||||
const normalizeCourses = (responseData) => {
|
||||
const courseList = responseData.results?.map((enrollment) => ({
|
||||
id: enrollment.course.id,
|
||||
name: enrollment.course.displayName,
|
||||
})) || [];
|
||||
|
||||
const pagination = {
|
||||
count: responseData.count,
|
||||
currentPage: responseData.currentPage,
|
||||
hasMore: Boolean(responseData.next),
|
||||
totalPages: responseData.numPages,
|
||||
};
|
||||
|
||||
return {
|
||||
courseList,
|
||||
pagination,
|
||||
showPreferences: responseData.showPreferences,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePreferences = (responseData) => {
|
||||
const preferences = responseData.notificationPreferenceConfig;
|
||||
|
||||
const appKeys = Object.keys(preferences);
|
||||
const apps = appKeys.map((appId) => ({
|
||||
id: appId,
|
||||
enabled: preferences[appId].enabled,
|
||||
}));
|
||||
|
||||
const nonEditable = {};
|
||||
const preferenceList = appKeys.map(appId => {
|
||||
const preferencesKeys = Object.keys(preferences[appId].notificationTypes);
|
||||
const flatPreferences = preferencesKeys.map(preferenceId => (
|
||||
{
|
||||
id: preferenceId,
|
||||
appId,
|
||||
web: preferences[appId].notificationTypes[preferenceId].web,
|
||||
push: preferences[appId].notificationTypes[preferenceId].push,
|
||||
email: preferences[appId].notificationTypes[preferenceId].email,
|
||||
info: preferences[appId].notificationTypes[preferenceId].info || '',
|
||||
}
|
||||
));
|
||||
nonEditable[appId] = preferences[appId].nonEditable;
|
||||
|
||||
return flatPreferences;
|
||||
}).flat();
|
||||
|
||||
const normalizedPreferences = {
|
||||
apps,
|
||||
preferences: preferenceList,
|
||||
nonEditable,
|
||||
};
|
||||
return normalizedPreferences;
|
||||
};
|
||||
|
||||
export const fetchCourseList = (page, pageSize) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchCourseListFetching());
|
||||
const data = await getCourseList(page, pageSize);
|
||||
const normalizedData = normalizeCourses(camelCaseObject(data));
|
||||
dispatch(fetchCourseListSuccess(normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(fetchCourseListFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchCourseNotificationPreferences = (courseId) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(updateSelectedCourse(courseId));
|
||||
dispatch(fetchNotificationPreferenceFetching());
|
||||
const data = await getCourseNotificationPreferences(courseId);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data));
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateAppPreferenceToggle = (courseId, appId, value) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(updateAppToggle(courseId, appId, value));
|
||||
const data = await patchAppPreferenceToggle(courseId, appId, value);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data));
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(updateAppToggle(courseId, appId, !value));
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updatePreferenceToggle = (
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
const data = await patchPreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data));
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
66
src/notification-preferences/messages.js
Normal file
66
src/notification-preferences/messages.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const messages = defineMessages({
|
||||
notificationHeading: {
|
||||
id: 'notification.preference.heading',
|
||||
defaultMessage: 'Notifications',
|
||||
description: 'Notification title',
|
||||
},
|
||||
notificationAppTitle: {
|
||||
id: 'notification.preference.app.title',
|
||||
defaultMessage: `{
|
||||
key, select,
|
||||
discussion {Discussions}
|
||||
coursework {Course Work}
|
||||
other {{key}}
|
||||
}`,
|
||||
description: 'Display text for Notification Types',
|
||||
},
|
||||
notificationTitle: {
|
||||
id: 'notification.preference.title',
|
||||
defaultMessage: `{
|
||||
text, select,
|
||||
core {Core notifications}
|
||||
newDiscussionPost {New discussion posts}
|
||||
newQuestionPost {New question posts}
|
||||
other {{text}}
|
||||
}`,
|
||||
description: 'Display text for Notification Types',
|
||||
},
|
||||
typeLabel: {
|
||||
id: 'notification.preference.type.label',
|
||||
defaultMessage: 'Type',
|
||||
description: 'Display text for type',
|
||||
},
|
||||
webLabel: {
|
||||
id: 'notification.preference.web,label',
|
||||
defaultMessage: 'Web',
|
||||
description: 'Display text for web',
|
||||
},
|
||||
notificationHelpEmail: {
|
||||
id: 'notification.preference.help.email',
|
||||
defaultMessage: 'Email',
|
||||
description: 'Display text for email',
|
||||
},
|
||||
notificationHelpPush: {
|
||||
id: 'notification.preference.help.push',
|
||||
defaultMessage: 'Push',
|
||||
description: 'Display text for push',
|
||||
},
|
||||
loadMoreCourses: {
|
||||
id: 'notification.preference.load.more.courses',
|
||||
defaultMessage: 'Load more courses',
|
||||
description: 'Load more button to load more courses',
|
||||
},
|
||||
notificationPreferenceGuideLink: {
|
||||
id: 'notification.preference.guide.link',
|
||||
defaultMessage: 'as detailed here',
|
||||
description: 'Link of the notification preference for learner guide',
|
||||
},
|
||||
notificationPreferenceGuideBody: {
|
||||
id: 'notification.preference.guide.body',
|
||||
defaultMessage: 'Notifications for certain activities are enabled by default, ',
|
||||
description: 'Body of the notification preferences for learner guide',
|
||||
},
|
||||
});
|
||||
@@ -3,6 +3,6 @@ import 'regenerator-runtime/runtime';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
Reference in New Issue
Block a user