Compare commits

...

148 Commits

Author SHA1 Message Date
edx-semantic-release
3f57f39187 chore(i18n): update translations 2022-04-10 11:21:30 -04:00
Usama Sadiq
0e8c8f5412 build: update transifex pull translations command (#554) 2022-04-04 17:45:25 +05:00
Zainab Amir
2099e62bb4 feat: add password change modal (#552)
* feat: add password change modal

Based on the error code sent from the platform, the modal will either
be to nudge users to change password or block users from logging in.

VAN-667
VAN-668
2022-04-04 16:42:07 +05:00
dependabot[bot]
7f7931fec5 build(deps): bump minimist from 1.2.5 to 1.2.6 (#555)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-04 15:27:21 +05:00
Usama Sadiq
1d039de652 Merge pull request #540 from openedx/jenkins/transifex-client-migration-69ede3f
fix: transifex migration to new client
2022-04-04 14:11:13 +05:00
Usama Sadiq
21d24d517f Merge branch 'master' into jenkins/transifex-client-migration-69ede3f 2022-04-04 14:07:35 +05:00
edx-semantic-release
3581007e59 chore(i18n): update translations 2022-04-03 11:20:44 -04:00
Usama Sadiq
df5e11d806 Merge branch 'master' into jenkins/transifex-client-migration-69ede3f 2022-03-30 21:10:55 +05:00
Ivo Branco
75f460bd17 chore(i18n): add more languages (#463)
Add languages it_IT, pt_PT, de_DE, uk, ru, hi
2022-03-29 17:04:00 +05:00
Shafqat Farhan
66261c4cea fix: Fixed the className condition to apply styling properly (#551) 2022-03-25 14:07:56 +05:00
Zainab Amir
ea6854374b fix: password reset page spinner position (#550) 2022-03-24 15:23:56 +05:00
Renovate Bot
e0385ce20d chore(deps): update dependency history to v5.3.0 2022-03-22 12:44:49 +00:00
Renovate Bot
3f53b3057b fix(deps): update dependency core-js to v3.21.1 2022-03-22 12:35:08 +00:00
Renovate Bot
517a390b2f fix(deps): update dependency @redux-devtools/extension to v3.2.2 2022-03-22 12:24:35 +00:00
renovate[bot]
6dc04be641 fix(deps): update dependency @edx/frontend-platform to v1.15.5 (#543)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-03-22 16:30:17 +05:00
renovate[bot]
d852f217cb fix(deps): update dependency @edx/paragon to v19.10.2 (#520)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Waheed Ahmad <waheed.ahmed@arbisoft.com>
2022-03-22 14:23:07 +05:00
Renovate Bot
178fb43a5a chore(deps): update dependency @edx/frontend-build to v9.1.2 2022-03-22 14:14:58 +05:00
Muhammad Soban Javed
76d3a13169 Merge pull request #524 from openedx/jawayria/node-16
build: Added support for Node 16
2022-03-22 14:03:50 +05:00
Muhammad Soban Javed
ce9c6e24a3 Merge branch 'master' into jawayria/node-16 2022-03-21 20:28:14 +05:00
edX Transifex Bot
6dd9d2a1dc chore(i18n): update translations 2022-03-20 11:20:15 -04:00
Jawayria
b493a09c63 feat: add support for node v16 2022-03-18 14:44:22 +05:00
edX requirements bot
5df7130310 fix: transifex migration to new client 2022-03-17 08:25:46 -04:00
Renovate Bot
69ede3f005 fix(deps): update dependency @fortawesome/react-fontawesome to v0.1.18 2022-03-17 01:11:13 +00:00
edX Transifex Bot
ea5ae8f2af chore(i18n): update translations 2022-03-15 10:46:44 -04:00
edX Transifex Bot
d352773a4e chore(i18n): update translations 2022-03-13 11:20:01 -04:00
Waheed Ahmed
3fa3954383 chore: update transifex scripts to api v3
As of April 7th, 2022 the API versions 2 and 2.5 will no longer
be operational and relevant requests will begin to fail.

VAN-894
2022-03-10 11:31:06 +05:00
Shafqat Farhan
182fb34a03 feat: VAN-876 - Rename register experiment with improved metrics (#537) 2022-03-09 16:18:51 +05:00
Zainab Amir
6e99e1e72c feat: add dynamic optional fields support (#534)
Added a new component that renders fields based on the
field descriptions returned from backend

VAN-835
2022-03-01 16:00:37 +05:00
Sarina Canelake
7e82785c7b Merge DEPR automation workflow
Add DEPR workflow automation
2022-02-24 15:22:59 -05:00
Sarina Canelake
74aa00f9cc build: add DEPR workflow automation 2022-02-23 14:40:40 -05:00
Renovate Bot
561be05bac chore(deps): update dependency follow-redirects to 1.14.8 [security] 2022-02-23 13:56:16 +05:00
Renovate Bot
b97e6292dd chore(deps): update dependency jest to v27.5.1 2022-02-09 14:32:48 +00:00
Renovate Bot
aa6f36a293 fix(deps): update dependency sanitize-html to v2.7.0 2022-02-09 14:15:03 +00:00
Zainab Amir
0b21ca3f51 feat: remove optional fields from register form (#528)
* feat: remove optional fields from register form

This is part of the larger ticket where we will move optional
fields to progressive profiling page.

VAN-837
2022-02-09 15:27:18 +05:00
edX Transifex Bot
a04872d7bf chore(i18n): update translations 2022-02-03 00:26:41 -05:00
Renovate Bot
526da727ec fix(deps): update dependency clipboard to v2.0.10 2022-02-02 18:53:12 +00:00
Renovate Bot
747dac26d7 fix(deps): update dependency core-js to v3.21.0 2022-02-01 20:14:11 +00:00
Renovate Bot
a956ccaa93 fix(deps): update dependency @fortawesome/react-fontawesome to v0.1.17 2022-01-31 03:10:51 +00:00
Renovate Bot
b37a059af7 chore(deps): update dependency es-check to v6.2.1 2022-01-31 02:43:59 +00:00
Renovate Bot
3117c9abd4 fix(deps): update dependency clipboard to v2.0.9 2022-01-28 05:18:18 +00:00
Zainab Amir
211c6afc60 feat: improve register response time (#507)
Removed validation call from the submission function

VAN-711
2022-01-26 15:48:43 +05:00
Renovate Bot
261e4cfe86 fix(deps): update dependency sanitize-html to v2.6.1 2022-01-25 15:33:49 +00:00
Waheed Ahmed
dd7ed6d9ee chore(deps): update dependency node-forge to 1.0.0 [security] 2022-01-25 16:26:13 +05:00
Waheed Ahmed
9f38be3318 chore: pin dependencies to fix security vulnerabilities 2022-01-25 16:17:08 +05:00
Renovate Bot
7415a0c0e6 fix(deps): update dependency core-js to v3.20.3 2022-01-24 16:38:03 +00:00
dependabot[bot]
ac9a390d98 build(deps): bump node-fetch from 2.6.6 to 2.6.7
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-24 17:22:16 +05:00
Renovate Bot
61c839bb09 chore(deps): update dependency node-forge to 1.0.0 [security] 2022-01-24 17:14:23 +05:00
Zainab Amir
cd3cda3c4f feat: update save_for_later event name (#511)
updated event names to reflect level of hierarchy in the
events.

VAN-830
2022-01-24 16:41:42 +05:00
Renovate Bot
242629fbdd chore(deps): update dependency follow-redirects to 1.14.7 [security] 2022-01-24 13:08:20 +05:00
edX Transifex Bot
214c383071 chore(i18n): update translations 2022-01-23 10:18:35 -05:00
Mubbshar Anwar
b8f0615434 perf: get csrf token (#504)
do csrf token call on page load time to reduce token call before login and register apis.
VAN-715
2022-01-13 19:39:34 +05:00
Renovate Bot
387e9aedf1 chore(deps): update dependency @edx/frontend-build to v9.0.6 2022-01-11 18:18:47 +00:00
Renovate Bot
d1b5ba74e8 chore(deps): update dependency jest to v27.4.7 2022-01-11 09:32:05 +00:00
Renovate Bot
1c599d8d04 fix(deps): update dependency prop-types to v15.8.1 2022-01-05 06:18:47 +00:00
Renovate Bot
5303b7b95c chore(deps): update dependency jest to v27.4.6 2022-01-05 05:57:56 +00:00
Renovate Bot
f64d9dd940 fix(deps): update dependency core-js to v3.20.2 2022-01-03 02:10:37 +00:00
Renovate Bot
3a68403773 chore(deps): update dependency es-check to v6 2021-12-30 12:25:30 +05:00
Renovate Bot
95c869a6ef chore(deps): update dependency husky to v7 2021-12-30 11:25:51 +05:00
Renovate Bot
35ee6b5a7b fix(deps): update dependency reselect to v4.1.5 2021-12-29 18:08:36 +00:00
Renovate Bot
55e44e5881 fix(deps): update dependency redux-thunk to v2.4.1 2021-12-29 17:52:18 +00:00
Renovate Bot
738af2aaac fix(deps): update dependency redux to v4.1.2 2021-12-29 17:34:08 +00:00
Renovate Bot
0f333b4a03 fix(deps): update dependency react-onclickoutside to v6.12.1 2021-12-29 17:14:26 +00:00
Renovate Bot
06bb7182fb fix(deps): update dependency prop-types to v15.8.0 2021-12-29 16:56:47 +00:00
Renovate Bot
4b8677f34d fix(deps): update dependency core-js to v3.20.1 2021-12-29 16:37:30 +00:00
Renovate Bot
f0df248ac2 chore(deps): update dependency glob to v7.2.0 2021-12-29 16:15:57 +00:00
Renovate Bot
9151cf46aa fix(deps): update react-router monorepo 2021-12-29 15:56:00 +00:00
Renovate Bot
d7f6b1ae0c fix(deps): update font awesome 2021-12-29 15:33:29 +00:00
Renovate Bot
d7c1e119fb chore(deps): update dependency @edx/frontend-build to v9.0.5 2021-12-29 15:10:30 +00:00
Renovate Bot
35d6c9d7bc fix(deps): update dependency react-redux to v7.2.6 2021-12-29 14:46:36 +00:00
Renovate Bot
9f9fe2ed1a fix(deps): update dependency formik to v2.2.9 2021-12-29 14:23:06 +00:00
Renovate Bot
96ec8c3943 chore(deps): update dependency es-check to v5.2.4 2021-12-29 13:58:56 +00:00
Renovate Bot
23bac18450 chore(deps): update dependency codecov to v3.8.2 2021-12-29 13:32:55 +00:00
Renovate Bot
094573be1f chore(deps): update dependency jest to v27 2021-12-29 09:13:32 +00:00
Renovate Bot
b2f2760774 fix(deps): pin dependencies 2021-12-29 11:25:00 +05:00
Renovate Bot
933e8f85fd chore(deps): update dependency history to v5.2.0 2021-12-29 11:17:28 +05:00
Waheed Ahmed
d9b9c4d290 chore: update renovate config 2021-12-28 15:13:22 +05:00
Renovate Bot
892f3d33ca fix(deps): update dependency @edx/frontend-component-cookie-policy-banner to v2.1.14 2021-12-28 15:01:16 +05:00
Waheed Ahmed
c1b402ebd6 fix: npm build issues 2021-12-28 14:43:57 +05:00
Renovate Bot
a081544440 chore(deps): update dependency immer to 9.0.6 [security] 2021-12-28 14:43:57 +05:00
Renovate Bot
6fee02110a fix(deps): update dependency redux-devtools-extension to v2.13.9 2021-12-28 13:46:24 +05:00
Renovate Bot
aef3f6a179 chore(deps): update dependency ws to 7.4.6 [security] 2021-12-28 12:41:38 +05:00
Renovate Bot
d7391dfd66 chore(deps): update dependency hosted-git-info to 2.8.9 [security] 2021-12-28 12:19:11 +05:00
Renovate Bot
4f7fbf82bf chore(deps): update dependency postcss to 7.0.36 [security] 2021-12-28 12:14:52 +05:00
edX Transifex Bot
0d01a9fea3 chore(i18n): update translations 2021-12-26 10:17:45 -05:00
Renovate Bot
88a6e1b260 fix(deps): update dependency classnames to v2.3.1 2021-12-25 02:08:44 +00:00
Waheed Ahmed
b142e159b6 chore: update renovate config 2021-12-23 15:10:42 +05:00
Attiya Ishaque
2a079e484f fix: Replace hardcoded edX name with SITE_NAME env variable. (#486) 2021-12-22 13:10:34 +05:00
edX Transifex Bot
1210fe01ae chore(i18n): update translations 2021-12-12 10:17:23 -05:00
dependabot[bot]
7400da7ba6 build(deps): bump lodash from 4.17.15 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.21)

---
updated-dependencies:
- dependency-name: lodash
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-08 19:33:39 +05:00
Renovate Bot
a86f5c43ad chore(deps): update dependency @edx/frontend-build to v5.6.14 2021-12-08 17:57:22 +05:00
Renovate Bot
531b82745f chore(deps): update dependency ansi-regex to 5.0.1 [security] 2021-12-08 17:42:19 +05:00
Waheed Ahmed
ebdaeb80f1 chore: bump frontend-platform to v1.14.2 2021-12-08 16:35:01 +05:00
Zainab Amir
488b364215 refactor: update account verification messages 2021-12-07 15:16:21 +05:00
Shafqat Farhan
619c75a4b7 feat: VAN-666 - Reject new password that is detected as vulnerable (#471) 2021-12-07 13:46:53 +05:00
Mubbshar Anwar
be783fed99 feat: fire track event (#456)
fire track event for save for latter course
VAN-741
2021-11-22 14:47:42 +05:00
Attiya Ishaque
59d5a400f4 fix: remove oal domain (#469) 2021-11-17 13:08:05 +05:00
Waheed Ahmed
e0e0c1011e feat: enable full name based username suggestions
Enable full name based username suggestions and remove experiment
related changes.
2021-11-15 15:57:10 +05:00
edX Transifex Bot
2070877a84 chore(i18n): update translations 2021-11-14 20:16:47 +05:00
Waheed Ahmed
8c8cd2bcd5 chore: remove opt-in/opt-out experiment code
Removed opt-in/opt-out experiment code to enable it for all users
using feature flag only.
2021-11-10 17:13:58 +05:00
Zainab Amir
a5e7a3a592 feat: add rename register experiment details (#464) 2021-11-10 10:45:06 +05:00
Waheed Ahmed
007dfa82a8 chore: bump frontend-platform version to v1.13.1 2021-11-02 11:40:46 +05:00
edX Transifex Bot
8223f0c988 chore(i18n): update translations 2021-10-31 20:16:50 +05:00
Adeel Ehsan
335f9fcaf8 Merge pull request #459 from edx/aehsan/760/remove_education_level_using_coppa
Remove el education level based on coppa flag
2021-10-28 15:07:17 +05:00
adeelehsan
31a2a8f402 feat: Remove el option from education
remove el option from education levels if coppa flag is set true.

VAN-760
2021-10-28 15:02:56 +05:00
Adeel Ehsan
54fb55fe8f Merge pull request #458 from edx/aehsan/scroll_added_for_username_suggestion
scroll added for username suggestions
2021-10-27 23:56:02 +05:00
adeelehsan
c34360f2fb scroll added for username suggestions 2021-10-27 23:51:42 +05:00
Mubbshar Anwar
82d339c91b fix: update opt in text (#457)
update opt in checkbox text on register page.
VAN-740
2021-10-27 18:09:30 +05:00
Adeel Ehsan
4d9b508e19 Merge pull request #455 from edx/aehsan/username_suggestions_issue_fixed
username suggestions issue fixed
2021-10-26 17:27:24 +05:00
adeelehsan
60e51a8f25 username suggestions issue fixed 2021-10-26 17:21:07 +05:00
Adeel Ehsan
350a6b10ae Merge pull request #440 from edx/aehsan/702/design_updated_for_username_suggestions
username design changed.
2021-10-26 12:21:17 +05:00
adeelehsan
4ba9e1194b username design changed according to the VAN-702 2021-10-25 12:27:31 +05:00
edX Transifex Bot
6cb470522b chore(i18n): update translations 2021-10-24 20:16:43 +05:00
Attiya Ishaque
a3953b672f fix: [VAN-747] Fix tests in which registered email msg changed (#453) 2021-10-22 18:16:46 +05:00
Usama Sadiq
e91a8a4dfa Merge pull request #452 from edx/aht/bom-2901-switch-ci-to-github-actions
BOM-2901: Replace travis with github CI
2021-10-22 15:13:11 +05:00
Usama Sadiq
17048f5caf build: replace travis with github ci 2021-10-22 14:52:30 +05:00
Attiya Ishaque
955cf26860 feat: [VAN-655] User's year of birth field behind the feature flag (#449) 2021-10-20 15:40:27 +05:00
edX Transifex Bot
ee094a1330 chore(i18n): update translations 2021-10-17 20:17:36 +05:00
Attiya Ishaque
75269a3372 feat: Add feature flag for year of birth field (#447) 2021-10-11 21:19:47 +05:00
Mubbshar Anwar
2ac0f83e87 fix: A/B test marketing opt-in (#448)
fire optimizely event to record opt-in/out for marketing emails

VAN-738
2021-10-11 19:12:42 +05:00
edX Transifex Bot
b0df7a0593 chore(i18n): update translations 2021-10-10 20:17:31 +05:00
Mubbshar Anwar
14e89575d1 feat: record opt in/out (#445)
add opt field in register form and record it for marketing emails.

VAN-738
2021-10-08 19:00:18 +05:00
Ned Batchelder
4e7003ca5e build: use the organization commitlint check 2021-10-07 13:50:58 -04:00
Shafqat Farhan
6bc11b01f5 fix: VAN-719 - Handled password field error violation on username change (#444) 2021-10-05 11:58:14 +05:00
edX Transifex Bot
b5ada5d2d5 chore(i18n): update translations 2021-09-26 20:17:37 +05:00
Uzair Rasheed
21ff9b81f9 Merge pull request #441 from edx/update-error-msg-for-username
update error msg for username
2021-09-22 12:13:02 +05:00
uzairr
cdf0c29d01 update error msg for username 2021-09-22 04:29:59 +05:00
Shafqat Farhan
57cc5a4692 fix: VAN-719 - Handled password field error violation (#439) 2021-09-21 16:58:14 +05:00
Waheed Ahmed
7cb58d4f26 bump frontend-platform version to 1.12.7 2021-09-17 18:52:05 +05:00
Attiya Ishaque
b1e9f631dc chore: update paragon version (#436) 2021-09-16 14:46:28 +05:00
edX Transifex Bot
0a25ec2037 chore(i18n): update translations 2021-09-14 13:52:20 +05:00
Attiya Ishaque
3164afa46a fix: SSO *Apple* translated in authn pages. (#434) 2021-09-13 17:39:17 +05:00
edX Transifex Bot
bff75b811d chore(i18n): update translations 2021-09-12 20:17:35 +05:00
Attiya Ishaque
d7b729cc98 Fix: Some design issue on register page. (#431) 2021-09-08 15:32:24 +05:00
Attiya Ishaque
8917b88a5a fix: design issue in discount experiment (#428) 2021-09-07 17:21:37 +05:00
edX Transifex Bot
53e40480f2 fix(i18n): update translations 2021-09-05 20:17:30 +05:00
Waheed Ahmed
9c20e91563 fix: fix translations for 15% discount strings 2021-09-02 20:01:32 +05:00
Zainab Amir
582af7a8b0 fix: use correct experiment variable (#427) 2021-09-01 17:35:15 +05:00
Attiya Ishaque
62ed8a631e chore: add 15% discount incentive to increase registrations. (#426) 2021-09-01 16:50:08 +05:00
Attiya Ishaque
bca605c632 fix: [VAN-685] Failed to execute removeChild on Node (#425) 2021-08-26 17:29:30 +05:00
Waheed Ahmed
c9cf7c5190 chore: upgrade frontend-platform to 1.12.4 2021-08-23 15:14:31 +05:00
Waheed Ahmed
e29e35c093 chore: send analytic events for Welcome page
VAN-679
2021-08-17 15:47:26 +05:00
edX Transifex Bot
85276767eb fix(i18n): update translations 2021-08-15 20:17:29 +05:00
Zainab Amir
1de911122a fix: reset password banner image changed (#420)
In addition to the check for authenticated user, added a welcome
banner check to restrict the welcome banner for only progressive
profiling page.

VAN-688
2021-08-12 18:26:05 +05:00
Attiya Ishaque
68cc93da1c fix: [VAN-332] Full name validation on registration page. (#416) 2021-08-12 13:25:58 +05:00
Waheed Ahmed
419915d752 chore: bump frontend-platform version 2021-08-12 13:00:42 +05:00
Waheed Ahmed
63572d43f6 Revert "chore: upgrade frontend-platform version to 1.12.1"
This reverts commit 5c8d682a83.
2021-08-11 19:54:56 +05:00
Mubbshar Anwar
498325e6e7 fix: stop auto complete country (#415)
fix to stop auto complete country field when address is saved in browser

VAN-671
2021-08-11 16:17:10 +05:00
Waheed Ahmed
5c8d682a83 chore: upgrade frontend-platform version to 1.12.1 2021-08-11 15:25:04 +05:00
83 changed files with 41386 additions and 14862 deletions

4
.env
View File

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

View File

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

View File

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

View File

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

48
.github/workflows/ci.yml vendored Normal file
View File

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

10
.github/workflows/commitlint.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

51774
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,98 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form, Icon } from '@edx/paragon';
import { ExpandMore } from '@edx/paragon/icons';
const FormFieldRenderer = (props) => {
let formField = null;
const { fieldData, onChangeHandler, value } = props;
switch (fieldData.type) {
case 'select': {
if (!fieldData.options) {
return null;
}
formField = (
<Form.Group controlId={fieldData.name} className="mb-3">
<Form.Control
as="select"
name={fieldData.name}
value={value}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={fieldData.label}
>
<option key="default" value="">{fieldData.label}</option>
{fieldData.options.map(option => (
<option className="data-hj-suppress" key={option[0]} value={option[0]}>{option[1]}</option>
))}
</Form.Control>
</Form.Group>
);
break;
}
case 'textarea': {
formField = (
<Form.Group controlId={fieldData.name} className="mb-3">
<Form.Control
as="textarea"
name={fieldData.name}
value={value}
onChange={(e) => onChangeHandler(e)}
floatingLabel={fieldData.label}
/>
</Form.Group>
);
break;
}
case 'text': {
formField = (
<Form.Group controlId={fieldData.name} className="mb-3">
<Form.Control
name={fieldData.name}
value={value}
onChange={(e) => onChangeHandler(e)}
floatingLabel={fieldData.label}
/>
</Form.Group>
);
break;
}
case 'checkbox': {
formField = (
<Form.Group className="mb-3">
<Form.Checkbox
id={fieldData.name}
checked={!!value}
name={fieldData.name}
value={value}
onChange={(e) => onChangeHandler(e)}
>
{fieldData.label}
</Form.Checkbox>
</Form.Group>
);
break;
}
default:
break;
}
return formField;
};
FormFieldRenderer.defaultProps = {
value: '',
};
FormFieldRenderer.propTypes = {
fieldData: PropTypes.shape({
type: PropTypes.string,
label: PropTypes.string,
name: PropTypes.string,
}).isRequired,
onChangeHandler: PropTypes.func.isRequired,
value: PropTypes.string,
};
export default FormFieldRenderer;

View File

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

View File

@@ -0,0 +1,105 @@
import React from 'react';
import { mount } from 'enzyme';
import FieldRenderer from '../FieldRenderer';
describe('FieldRendererTests', () => {
let value = '';
const changeHandler = (e) => {
if (e.target.type === 'checkbox') {
value = e.target.checked;
} else {
value = e.target.value;
}
};
beforeEach(() => {
value = '';
});
it('should render select field type', () => {
const fieldData = {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
options: [['1997', 1997], ['1998', 1998]],
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('select#yob-field');
field.simulate('change', { target: { value: 1997 } });
expect(field.type()).toEqual('select');
expect(fieldRenderer.find('label').text()).toEqual('Year of Birth');
expect(value).toEqual(1997);
});
it('should return null if no options are provided for select field', () => {
const fieldData = {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
});
it('should render textarea field', () => {
const fieldData = {
type: 'textarea',
label: 'Why do you want to join this platform?',
name: 'goals-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#goals-field').last();
field.simulate('change', { target: { value: 'These are my goals.' } });
expect(field.type()).toEqual('textarea');
expect(fieldRenderer.find('label').text()).toEqual('Why do you want to join this platform?');
expect(value).toEqual('These are my goals.');
});
it('should render an input field', () => {
const fieldData = {
type: 'text',
label: 'Company',
name: 'company-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#company-field').last();
field.simulate('change', { target: { value: 'ABC' } });
expect(field.type()).toEqual('input');
expect(fieldRenderer.find('label').text()).toEqual('Company');
expect(value).toEqual('ABC');
});
it('should render checkbox field', () => {
const fieldData = {
type: 'checkbox',
label: 'I agree that edX may send me marketing messages.',
name: 'marketing-emails-opt-in-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('input#marketing-emails-opt-in-field');
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(field.prop('type')).toEqual('checkbox');
expect(fieldRenderer.find('label').text()).toEqual('I agree that edX may send me marketing messages.');
expect(value).toEqual(true);
});
it('should return null if field type is unknown', () => {
const fieldData = {
type: 'unknown',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

221
src/i18n/messages/hi.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,6 @@ initialize({
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
REGISTRATION_OPTIONAL_FIELDS: process.env.REGISTRATION_OPTIONAL_FIELDS || '',
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME || null,
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
WELCOME_PAGE_SUPPORT_LINK: process.env.WELCOME_PAGE_SUPPORT_LINK || null,
@@ -39,6 +38,9 @@ initialize({
INFO_EMAIL: process.env.INFO_EMAIL || '',
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
ENABLE_PROGRESSIVE_PROFILING: process.env.ENABLE_PROGRESSIVE_PROFILING || false,
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
ENABLE_COPPA_COMPLIANCE: process.env.ENABLE_COPPA_COMPLIANCE || '',
SHOW_DYNAMIC_PROFILING_PAGE: process.env.SHOW_DYNAMIC_PROFILING_PAGE || false,
});
},
},

View File

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

View File

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

View File

@@ -15,8 +15,11 @@ import {
INTERNAL_SERVER_ERROR,
INVALID_FORM,
NON_COMPLIANT_PASSWORD_EXCEPTION,
NUDGE_PASSWORD_CHANGE,
REQUIRE_PASSWORD_CHANGE,
} from './data/constants';
import messages from './messages';
import ChangePasswordPrompt from './ChangePasswordPrompt';
const LoginFailureMessage = (props) => {
const { intl } = props;
@@ -118,7 +121,7 @@ const LoginFailureMessage = (props) => {
}
case INCORRECT_EMAIL_PASSWORD:
if (context.failureCount <= 1) {
errorList = intl.formatMessage(messages['login.incorrect.credentials.error']);
errorList = <p>{intl.formatMessage(messages['login.incorrect.credentials.error'])}</p>;
} else if (context.failureCount === 2) {
errorList = (
<p>
@@ -131,6 +134,15 @@ const LoginFailureMessage = (props) => {
);
}
break;
case NUDGE_PASSWORD_CHANGE:
return (
<ChangePasswordPrompt
redirectUrl={props.loginError.redirectUrl}
variant="nudge"
/>
);
case REQUIRE_PASSWORD_CHANGE:
return <ChangePasswordPrompt />;
default:
// TODO: use errorCode instead of processing error messages on frontend
errorList = value.trim().split('\n');
@@ -165,6 +177,7 @@ const LoginFailureMessage = (props) => {
LoginFailureMessage.defaultProps = {
loginError: {
redirectUrl: null,
errorCode: null,
value: '',
},
@@ -176,6 +189,7 @@ LoginFailureMessage.propTypes = {
email: PropTypes.string,
errorCode: PropTypes.string,
value: PropTypes.string,
redirectUrl: PropTypes.string,
}),
intl: intlShape.isRequired,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -215,8 +215,8 @@ describe('LoginPage', () => {
it('should match account activation message', () => {
const activationMessage = 'Success! You have activated your account.'
+ 'You will now receive email updates and alerts from us related '
+ 'to the courses you are enrolled in. Sign in to continue.';
+ 'You will now receive email updates and alerts from us related '
+ 'to the courses you are enrolled in. Sign in to continue.';
delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?account_activation_status=success' };
@@ -238,8 +238,9 @@ describe('LoginPage', () => {
},
});
const expectedMessage = 'You have successfully signed into Apple, but your Apple account does not have a '
+ 'linked edX account. To link your accounts, sign in now using your edX password.';
const expectedMessage = `${'You have successfully signed into Apple, but your Apple account does not have a '
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
getConfig().SITE_NAME } password.`;
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage);
@@ -262,14 +263,14 @@ describe('LoginPage', () => {
// ******** test redirection ********
it('should redirect to url returned by login endpoint', () => {
const dasboardUrl = 'http://localhost:18000/enterprise/select/active/?success_url=/dashboard';
const dashboardUrl = 'http://localhost:18000/enterprise/select/active/?success_url=/dashboard';
store = mockStore({
...initialState,
login: {
...initialState.login,
loginResult: {
success: true,
redirectUrl: dasboardUrl,
redirectUrl: dashboardUrl,
},
},
});
@@ -277,7 +278,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(dasboardUrl);
expect(window.location.href).toBe(dashboardUrl);
});
it('should redirect to finishAuthUrl upon successful login via SSO', () => {

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import React from 'react';
import snakeCase from 'lodash.snakecase';
import { connect } from 'react-redux';
import Skeleton from 'react-loading-skeleton';
import { Helmet } from 'react-helmet';
import PropTypes, { string } from 'prop-types';
import classNames from 'classnames';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -26,7 +26,6 @@ import {
registrationErrorSelector, registrationRequestSelector, validationsSelector, usernameSuggestionsSelector,
} from './data/selectors';
import messages from './messages';
import OptionalFields from './OptionalFields';
import RegistrationFailure from './RegistrationFailure';
import UsernameField from './UsernameField';
@@ -38,7 +37,7 @@ import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import {
DEFAULT_STATE, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, LETTER_REGEX, NUMBER_REGEX,
DEFAULT_STATE, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, LETTER_REGEX, NUMBER_REGEX, VALID_NAME_REGEX,
} from '../data/constants';
import {
getTpaProvider, getTpaHint, getAllPossibleQueryParam, setSurveyCookie, setCookie,
@@ -50,7 +49,6 @@ class RegistrationPage extends React.Component {
constructor(props, context) {
super(props, context);
sendPageEvent('login_and_registration', 'register');
const optionalFields = getConfig().REGISTRATION_OPTIONAL_FIELDS ? getConfig().REGISTRATION_OPTIONAL_FIELDS.split(',') : [];
this.handleOnClose = this.handleOnClose.bind(this);
this.queryParams = getAllPossibleQueryParam();
@@ -61,6 +59,7 @@ class RegistrationPage extends React.Component {
name: '',
password: '',
username: '',
marketingOptIn: true,
errors: {
email: '',
name: '',
@@ -72,16 +71,18 @@ class RegistrationPage extends React.Component {
emailWarningSuggestion: null,
errorCode: null,
failureCount: 0,
optionalFields,
optionalFieldsState: {},
showOptionalField: false,
startTime: Date.now(),
totalRegistrationTime: 0,
optimizelyExperimentName: '', // eslint-disable-line react/no-unused-state
optimizelyExperimentName: '',
readOnly: true,
validatePassword: false,
// TODO: Remove after VAN-876 experimentation is complete.
registerRenameExpVariation: '',
};
}
componentDidMount() {
const payload = { ...this.queryParams };
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'page',
@@ -89,19 +90,39 @@ class RegistrationPage extends React.Component {
isActive: true,
});
const payload = { ...this.queryParams };
if (payload.register_for_free === 'true') {
window.optimizely.push({
type: 'event',
eventName: 'van-876-authn-registration-page',
});
}
if (payload.save_for_later === 'true') {
sendTrackEvent('edx.bi.user.saveforlater.course.enroll.clicked', { category: 'save-for-later' });
}
if (this.tpaHint) {
payload.tpa_hint = this.tpaHint;
}
this.props.resetRegistrationForm();
this.props.getThirdPartyAuthContext(payload);
this.getExperiments();
}
shouldComponentUpdate(nextProps) {
if (this.props.usernameSuggestions.length > 0 && this.state.username === '') {
this.setState({
username: ' ',
});
return false;
}
if (this.props.validationDecisions !== nextProps.validationDecisions) {
const state = { errors: { ...this.state.errors, ...nextProps.validationDecisions } };
let validatePassword = false;
if (state.errors.password) {
validatePassword = true;
}
if (nextProps.registrationErrorCode) {
state.errorCode = nextProps.registrationErrorCode;
}
@@ -124,6 +145,7 @@ class RegistrationPage extends React.Component {
suggestedTopLevelDomain,
suggestedSldMessage,
suggestedServiceLevelDomain,
validatePassword,
});
return false;
}
@@ -152,35 +174,22 @@ class RegistrationPage extends React.Component {
}
getExperiments = () => {
const { optimizelyExperimentName } = window;
const { experimentName, renameRegisterExperiment } = window;
if (optimizelyExperimentName) {
// eslint-disable-next-line react/no-unused-state
this.setState({ optimizelyExperimentName });
if (experimentName) {
this.setState({ optimizelyExperimentName: experimentName });
}
if (renameRegisterExperiment) {
this.setState({ registerRenameExpVariation: renameRegisterExperiment });
}
};
getOptionalFields() {
return (
<OptionalFields
optionalFields={this.state.optionalFields}
values={this.state.optionalFieldsState}
onChangeHandler={
(fieldName, value) => {
this.setState(prevState => ({
optionalFieldsState: { ...prevState.optionalFieldsState, [fieldName]: value },
}));
}
}
/>
);
}
handleSubmit = (e) => {
e.preventDefault();
const { startTime } = this.state;
const totalRegistrationTime = (Date.now() - startTime) / 1000;
let payload = {
const payload = {
name: this.state.name,
username: this.state.username,
email: this.state.email,
@@ -195,12 +204,7 @@ class RegistrationPage extends React.Component {
payload.password = this.state.password;
}
let errors = {};
Object.keys(payload).forEach(key => {
errors = this.validateInput(key, payload[key], { ...payload, form_field_key: key });
});
if (!this.isFormValid(errors)) {
if (!this.isFormValid(payload)) {
this.setState(prevState => ({
errorCode: FORM_SUBMISSION_ERROR,
failureCount: prevState.failureCount + 1,
@@ -208,14 +212,9 @@ class RegistrationPage extends React.Component {
return;
}
// Since optional fields and query params are not validated we can add it to payload after
// required fields have been validated. This will save us unwanted calls to validateInput()
payload = { ...payload, ...this.queryParams };
this.state.optionalFields.forEach((key) => {
if (this.state.optionalFieldsState[key]) {
payload[snakeCase(key)] = this.state.optionalFieldsState[key];
}
});
if (getConfig().MARKETING_EMAILS_OPT_IN) {
payload.marketing_emails_opt_in = this.state.marketingOptIn;
}
payload.totalRegistrationTime = totalRegistrationTime;
this.setState({
@@ -245,12 +244,7 @@ class RegistrationPage extends React.Component {
}
handleOnChange = (e) => {
if (e.target.name === 'optionalFields') {
sendTrackEvent('edx.bi.user.register.optional_fields_selected', {});
this.setState({
showOptionalField: e.target.checked,
});
} else if (!(e.target.name === 'username' && e.target.value.length > 30)) {
if (!(e.target.name === 'username' && e.target.value.length > 30)) {
this.setState({
[e.target.name]: e.target.value,
});
@@ -264,6 +258,9 @@ class RegistrationPage extends React.Component {
if (e.target.name === 'username') {
this.props.clearUsernameSuggestions();
}
if (e.target.name === 'country') {
state.readOnly = false;
}
if (e.target.name === 'passwordValidation') {
state.errors.password = '';
}
@@ -292,15 +289,36 @@ class RegistrationPage extends React.Component {
}
}
isFormValid(validations) {
const keyValidList = Object.entries(validations).map(([key]) => !validations[key]);
return keyValidList.every((current) => current === true);
handleUsernameSuggestionClose = () => {
this.setState({
username: '',
});
this.props.clearUsernameSuggestions();
}
isFormValid(payload) {
const { errors } = this.state;
let isValid = true;
Object.keys(payload).forEach(key => {
if (!payload[key]) {
errors[key] = this.props.intl.formatMessage(messages[`empty.${key}.field.error`]);
}
// Mark form invalid, if there was already a validation error for this key or we added empty field error
if (errors[key]) {
isValid = false;
}
});
this.setState({ errors });
return isValid;
}
validateInput(fieldName, value, payload) {
const { errors } = this.state;
const { intl, statusCode } = this.props;
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
const urlRegex = new RegExp(VALID_NAME_REGEX);
switch (fieldName) {
case 'email':
@@ -359,11 +377,22 @@ class RegistrationPage extends React.Component {
case 'name':
if (!value) {
errors.name = intl.formatMessage(messages['empty.name.field.error']);
} else if (value && value.match(urlRegex)) {
errors.name = intl.formatMessage(messages['name.validation.message']);
} else {
errors.name = '';
}
if (!this.state.username.trim() && value) {
// fetch username suggestions based on the full name
this.props.fetchRealtimeValidations(payload);
}
break;
case 'username':
if (value === ' ' && this.props.usernameSuggestions.length > 0) {
errors.username = '';
break;
}
if (!value || value.length <= 1 || value.length > 30) {
errors.username = intl.formatMessage(messages['username.validation.message']);
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
@@ -373,14 +402,16 @@ class RegistrationPage extends React.Component {
} else {
errors.username = '';
}
if (this.state.validatePassword) {
this.props.fetchRealtimeValidations({ ...payload, form_field_key: 'password' });
}
break;
case 'password':
errors.password = '';
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
errors.password = intl.formatMessage(messages['password.validation.message']);
} else if (payload && statusCode !== 403) {
this.props.fetchRealtimeValidations(payload);
} else {
errors.password = '';
}
break;
case 'country':
@@ -492,6 +523,7 @@ class RegistrationPage extends React.Component {
setSurveyCookie('register');
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
setCookie('authn-returning-user');
const payload = { ...this.queryParams };
// Fire optimizely events
window.optimizely = window.optimizely || [];
@@ -502,6 +534,13 @@ class RegistrationPage extends React.Component {
value: this.state.totalRegistrationTime,
},
});
if (payload.register_for_free === 'true') {
window.optimizely.push({
type: 'event',
eventName: 'van-876-authn-register-for-free-conversion',
});
}
}
return (
@@ -546,18 +585,6 @@ class RegistrationPage extends React.Component {
helpText={[intl.formatMessage(messages['help.text.name'])]}
floatingLabel={intl.formatMessage(messages['registration.fullname.label'])}
/>
<UsernameField
name="username"
value={this.state.username}
handleBlur={this.handleOnBlur}
handleChange={this.handleOnChange}
handleFocus={this.handleOnFocus}
errorMessage={this.state.errors.username}
helpText={[intl.formatMessage(messages['help.text.username.1']), intl.formatMessage(messages['help.text.username.2'])]}
floatingLabel={intl.formatMessage(messages['registration.username.label'])}
handleSuggestionClick={this.handleSuggestionClick}
usernameSuggestions={this.props.usernameSuggestions}
/>
<FormGroup
name="email"
value={this.state.email}
@@ -572,6 +599,20 @@ class RegistrationPage extends React.Component {
{this.renderEmailFeedback()}
</FormGroup>
<UsernameField
name="username"
value={this.state.username}
handleBlur={this.handleOnBlur}
handleChange={this.handleOnChange}
handleFocus={this.handleOnFocus}
errorMessage={this.state.errors.username}
helpText={[intl.formatMessage(messages['help.text.username.1']), intl.formatMessage(messages['help.text.username.2'])]}
floatingLabel={intl.formatMessage(messages['registration.username.label'])}
handleSuggestionClick={this.handleSuggestionClick}
usernameSuggestions={this.props.usernameSuggestions}
handleUsernameSuggestionClose={this.handleUsernameSuggestionClose}
/>
{!currentProvider && (
<PasswordField
name="password"
@@ -595,7 +636,19 @@ class RegistrationPage extends React.Component {
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
handleChange={(value) => this.setState({ country: value })}
errorCode={this.state.errorCode}
readOnly={this.state.readOnly}
/>
{(getConfig().MARKETING_EMAILS_OPT_IN)
&& (
<Form.Checkbox
className="opt-checkbox"
name="marketing_emails_opt_in"
checked={this.state.marketingOptIn}
onChange={(e) => this.setState({ marketingOptIn: e.target.checked })}
>
{intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME })}
</Form.Checkbox>
)}
<div id="honor-code" className="micro text-muted mt-4">
<FormattedMessage
id="register.page.terms.of.service.and.honor.code"
@@ -617,27 +670,19 @@ class RegistrationPage extends React.Component {
}}
/>
</div>
{getConfig().REGISTRATION_OPTIONAL_FIELDS ? (
<Form.Group className="mb-0 mt-2 small">
<Form.Check
id="optional-field-checkbox"
type="checkbox"
name="optionalFields"
value={this.state.showOptionalField}
onClick={this.handleOnChange}
onChange={this.handleOnChange}
label={intl.formatMessage(messages['support.education.research'])}
/>
</Form.Group>
) : null}
{ this.state.showOptionalField ? this.getOptionalFields() : null }
<StatefulButton
type="submit"
variant="brand"
className="stateful-button-width mt-4 mb-4"
className={classNames(
'mt-4 mb-4',
{ 'stateful-button-variation1-width': this.state.registerRenameExpVariation === 'variation1' },
{ 'stateful-button-width': this.state.registerRenameExpVariation !== 'variation1' },
)}
state={submitState}
labels={{
default: intl.formatMessage(messages['create.account.button']),
default: this.state.registerRenameExpVariation === 'variation1' ? (
intl.formatMessage(messages['create.account.for.free.button'])
) : intl.formatMessage(messages['create.account.button']),
pending: '',
}}
onClick={this.handleSubmit}
@@ -649,6 +694,13 @@ class RegistrationPage extends React.Component {
thirdPartyAuthApiStatus,
intl)}
</Form>
{(this.state.optimizelyExperimentName === 'variation1' || this.state.optimizelyExperimentName === 'variation2')
? (
<div id="certificate-msg" className="mt-4 mb-3 micro text-gray-500">
{intl.formatMessage(messages['certificate.msg'])}
</div>
)
: null}
</div>
</>
);
@@ -771,10 +823,10 @@ const mapStateToProps = state => {
export default connect(
mapStateToProps,
{
clearUsernameSuggestions,
getThirdPartyAuthContext,
fetchRealtimeValidations,
registerNewUser,
resetRegistrationForm,
clearUsernameSuggestions,
},
)(injectIntl(RegistrationPage));

View File

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

View File

@@ -35,10 +35,10 @@ export const GENDER_OPTIONS = ['', 'f', 'm', 'o'];
export const FORM_FIELDS = ['name', 'email', 'password', 'username', 'country'];
export const COMMON_EMAIL_PROVIDERS = [
'hotmail.com', 'yahoo.com', 'outlook.com', 'live.com', 'gmail.com', 'aol.com',
'hotmail.com', 'yahoo.com', 'outlook.com', 'live.com', 'gmail.com',
];
export const DEFAULT_SERVICE_PROVIDER_DOMAINS = ['yahoo', 'aol', 'hotmail', 'live', 'outlook', 'gmail'];
export const DEFAULT_SERVICE_PROVIDER_DOMAINS = ['yahoo', 'hotmail', 'live', 'outlook', 'gmail'];
export const DEFAULT_TOP_LEVEL_DOMAINS = [
'aaa', 'aarp', 'abarth', 'abb', 'abbott', 'abbvie', 'abc', 'able', 'abogado', 'abudhabi', 'ac', 'academy',

View File

@@ -24,7 +24,6 @@ export function* handleNewUserRegistration(action) {
yield put(registerNewUserBegin());
const { redirectUrl, success } = yield call(registerRequest, action.payload.registrationInfo);
yield put(registerNewUserSuccess(
redirectUrl,
success,

View File

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

View File

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

View File

@@ -34,7 +34,6 @@ const mockStore = configureStore();
describe('RegistrationPage', () => {
mergeConfig({
PRIVACY_POLICY: 'http://privacy-policy.com',
REGISTRATION_OPTIONAL_FIELDS: 'gender,goals,levelOfEducation,yearOfBirth',
TOS_AND_HONOR_CODE: 'http://tos-and-honot-code.com',
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME,
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME,
@@ -207,15 +206,16 @@ describe('RegistrationPage', () => {
it('should update errors for frontend validations', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#name').simulate('blur', { target: { value: 'http://test.com', name: 'name' } });
registrationPage.find('input#password').simulate('blur', { target: { value: 'pas', name: 'password' } });
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual({
email: '', name: '', username: '', password: 'Password criteria has not been met', country: '',
email: '', name: 'Enter a valid name', username: '', password: 'Password criteria has not been met', country: '',
});
registrationPage.find('input#password').simulate('blur', { target: { value: 'invalid-email', name: 'email' } });
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual({
email: 'Enter a valid email address', name: '', username: '', password: 'Password criteria has not been met', country: '',
email: 'Enter a valid email address', name: 'Enter a valid name', username: '', password: 'Password criteria has not been met', country: '',
});
});
@@ -270,14 +270,14 @@ describe('RegistrationPage', () => {
...initialState.register,
registrationError: {
username: [{ userMessage: 'It looks like this username is already taken' }],
email: [{ userMessage: 'It looks like this email address is already registered' }],
email: [{ userMessage: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account` }],
},
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
expect(registrationPage.prop('validationDecisions')).toEqual({
country: '',
email: 'It looks like this email address is already registered',
email: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account`,
name: '',
password: '',
username: 'It looks like this username is already taken',
@@ -334,6 +334,12 @@ describe('RegistrationPage', () => {
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
});
it('should set readOnly state false if focus on country field', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#country').simulate('focus');
expect(registrationPage.find('RegistrationPage').state('readOnly')).toEqual(false);
});
// ******** test alert messages ********
it('should match third party auth alert', () => {
@@ -348,8 +354,8 @@ describe('RegistrationPage', () => {
},
});
const expectedMessage = 'You\'ve successfully signed into Apple! We just need a little more information before '
+ 'you start learning with edX.';
const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before '
+ 'you start learning with '}${ getConfig().SITE_NAME }.`;
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registerPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage);
@@ -500,13 +506,47 @@ describe('RegistrationPage', () => {
});
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registerPage.find('RegistrationPage').instance().shouldComponentUpdate(props);
registerPage.find('RegistrationPage').setState({ errors: { username: 'It looks like this username is already taken' } });
expect(registerPage.find('button.username-suggestion').length).toEqual(3);
registerPage.find('button.username-suggestion').at(0).simulate('click');
expect(registerPage.find('RegistrationPage').state('username')).toEqual('test_1');
});
it('should show username suggestions when full name is populated', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
usernameSuggestions: ['testname', 't.name', 'test_0'],
},
});
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registerPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
expect(registerPage.find('button.username-suggestion').length).toEqual(3);
registerPage.find('button.username-suggestion').at(0).simulate('click');
expect(registerPage.find('RegistrationPage').state('username')).toEqual('testname');
});
it('should clear username suggestions when close icon is clicked', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
usernameSuggestions: ['testname', 't.name', 'test_0'],
},
});
store.dispatch = jest.fn(store.dispatch);
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registerPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
registerPage.find('button.suggested-username-close-button').at(0).simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
});
it('should redirect to url returned in registration result after successful account creation', () => {
const dasboardUrl = 'http://test.com/testing-dashboard/';
store = mockStore({
@@ -662,6 +702,14 @@ describe('RegistrationPage', () => {
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
});
it('should send track event for save_for_later param', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/register'), search: '?save_for_later=true' };
renderer.create(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.saveforlater.course.enroll.clicked',
{ category: 'save-for-later' });
});
// ******** shouldComponentUpdate tests ********
it('should populate form with pipeline user details', () => {
@@ -711,87 +759,18 @@ describe('RegistrationPage', () => {
expect(registrationPage.find('RegistrationPage').state('errorCode')).toEqual(INTERNAL_SERVER_ERROR);
});
});
describe('TestOptionalFields', () => {
it('should toggle optional fields state', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('click', { target: { name: 'optionalFields', checked: true } });
expect(registrationPage.find('RegistrationPage').state('showOptionalField')).toEqual(true);
// it should also works when change is made directly instead of click
registrationPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: false } });
expect(registrationPage.find('RegistrationPage').state('showOptionalField')).toEqual(false);
});
it('should show optional fields section on optional check enabled', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: true } });
registrationPage.update();
expect(registrationPage.find('textarea#goals').length).toEqual(1);
expect(registrationPage.find('select#levelOfEducation').length).toEqual(1);
expect(registrationPage.find('select#yearOfBirth').length).toEqual(1);
expect(registrationPage.find('select#gender').length).toEqual(1);
});
it('should show optional field check based on environment variable', () => {
it('should display opt-in/opt-out checkbox', () => {
mergeConfig({
REGISTRATION_OPTIONAL_FIELDS: '',
MARKETING_EMAILS_OPT_IN: 'true',
});
let registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('input#optional-field-checkbox').length).toEqual(0);
mergeConfig({
REGISTRATION_OPTIONAL_FIELDS: 'gender,goals,levelOfEducation,yearOfBirth',
});
registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('input#optional-field-checkbox').length).toEqual(1);
});
it('send tracking event on optional checkbox enabled', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: true } });
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.register.optional_fields_selected', {});
});
it('should submit form with optional fields', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
password: 'password1',
country: 'Pakistan',
gender: 'm',
year_of_birth: '1997',
level_of_education: 'other',
goals: 'edX goals',
honor_code: true,
totalRegistrationTime: 0,
is_authn_mfe: true,
};
store.dispatch = jest.fn(store.dispatch);
delete window.location;
window.location = { href: getConfig().BASE_URL };
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
populateRequiredFields(registerPage, payload);
expect(registerPage.find('div.opt-checkbox').length).toEqual(1);
// submit optional fields
registerPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: true } });
registerPage.find('select#gender').simulate('change', { target: { value: 'm', name: 'gender' } });
registerPage.find('select#yearOfBirth').simulate('change', { target: { value: '1997', name: 'yearOfBirth' } });
registerPage.find('select#levelOfEducation').simulate('change', { target: { value: 'other', name: 'levelOfEducation' } });
registerPage.find('textarea#goals').simulate('change', { target: { value: 'edX goals', name: 'goals' } });
registerPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
mergeConfig({
MARKETING_EMAILS_OPT_IN: '',
});
});
});
});

View File

@@ -53,7 +53,11 @@ const ResetPasswordPage = (props) => {
const validatePasswordFromBackend = async (password) => {
let errorMessage = '';
try {
errorMessage = await validatePassword(password);
const payload = {
reset_password_page: true,
password,
};
errorMessage = await validatePassword(payload);
} catch (err) {
errorMessage = '';
}
@@ -85,6 +89,24 @@ const ResetPasswordPage = (props) => {
return !Object.values(formErrors).some(x => (x !== ''));
};
const handleOnBlur = (event) => {
let { name, value } = event.target;
// Do not validate when focus out from 'newPassword' and focus on 'passwordValidation' icon
// for better user experience.
if (event.relatedTarget
&& event.relatedTarget.name === 'passwordValidation'
&& name === 'newPassword'
) {
return;
}
if (name === 'passwordValidation') {
name = 'newPassword';
value = newPassword;
}
validateInput(name, value);
};
const handleConfirmPasswordChange = (e) => {
const { value } = e.target;
@@ -126,7 +148,7 @@ const ResetPasswordPage = (props) => {
const { token } = props.match.params;
if (token) {
props.validateToken(token);
return <Spinner animation="border" variant="primary" className="mt-5" />;
return <Spinner animation="border" variant="primary" className="centered-align-spinner" />;
}
} else if (props.status === PASSWORD_RESET_ERROR) {
return <Redirect to={updatePathWithQueryParams(RESET_PAGE)} />;
@@ -157,7 +179,7 @@ const ResetPasswordPage = (props) => {
name="newPassword"
value={newPassword}
handleChange={(e) => setNewPassword(e.target.value)}
handleBlur={(e) => validateInput(e.target.name, e.target.value)}
handleBlur={handleOnBlur}
handleFocus={handleOnFocus}
errorMessage={formErrors.newPassword}
floatingLabel={intl.formatMessage(messages['new.password.label'])}

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { sendPageEvent } from '@edx/frontend-platform/analytics';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
configure as configureAuth,
AxiosJwtAuthService,
@@ -68,6 +68,11 @@ const WelcomePage = (props) => {
return null;
}
if (getConfig().ENABLE_COPPA_COMPLIANCE && EDUCATION_LEVELS) {
const index = EDUCATION_LEVELS.indexOf('el');
EDUCATION_LEVELS.splice(index, 1);
}
const getOptions = (fieldName) => {
const options = {
yearOfBirth: YEAR_OF_BIRTH_OPTIONS.map(({ value, label }) => (
@@ -100,6 +105,16 @@ const WelcomePage = (props) => {
});
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
sendTrackEvent(
'edx.bi.welcome.page.submit.clicked',
{
isGenderSelected: !!payload.gender,
isYearOfBirthSelected: !!payload.yearOfBirth,
isLevelOfEducationSelected: !!payload.levelOfEducation,
},
);
window.optimizely.push({
type: 'event',
eventName: 'authn_welcome_page_submit_btn_clicked',
@@ -109,6 +124,7 @@ const WelcomePage = (props) => {
const handleSkip = (e) => {
e.preventDefault();
setOpenDialog(true);
sendTrackEvent('edx.bi.welcome.page.skip.link.clicked');
window.optimizely.push({
type: 'event',
@@ -122,7 +138,7 @@ const WelcomePage = (props) => {
return (
<>
<BaseComponent>
<BaseComponent showWelcomeBanner>
<Helmet>
<title>{intl.formatMessage(messages['progressive.profiling.page.title'],
{ siteName: getConfig().SITE_NAME })}
@@ -159,19 +175,22 @@ const WelcomePage = (props) => {
{getOptions('levelOfEducation')}
</Form.Control>
</Form.Group>
<Form.Group controlId="yearOfBirth">
<Form.Control
as="select"
name="yearOfBirth"
value={values.yearOfBirth}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={intl.formatMessage(messages['year.of.birth.label'])}
>
<option value="">{intl.formatMessage(messages['year.of.birth.label'])}</option>
{getOptions('yearOfBirth')}
</Form.Control>
</Form.Group>
{(!getConfig().ENABLE_COPPA_COMPLIANCE)
&& (
<Form.Group controlId="yearOfBirth">
<Form.Control
as="select"
name="yearOfBirth"
value={values.yearOfBirth}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={intl.formatMessage(messages['year.of.birth.label'])}
>
<option value="">{intl.formatMessage(messages['year.of.birth.label'])}</option>
{getOptions('yearOfBirth')}
</Form.Control>
</Form.Group>
)}
<Form.Group controlId="gender" className="mb-3">
<Form.Control
as="select"
@@ -191,6 +210,7 @@ const WelcomePage = (props) => {
destination={getConfig().WELCOME_PAGE_SUPPORT_LINK}
target="_blank"
showLaunchIcon={false}
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
>
{intl.formatMessage(messages['optional.fields.information.link'])}
</Hyperlink>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,6 +47,17 @@ describe('WelcomePageTests', () => {
</IntlProvider>
);
const getWelcomePage = async () => {
const welcomePage = mount(reduxWrapper(<IntlWelcomePage {...props} />));
await act(async () => {
await Promise.resolve(welcomePage);
await new Promise(resolve => setImmediate(resolve));
welcomePage.update();
});
return welcomePage;
};
beforeEach(() => {
store = mockStore({});
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edX' }));
@@ -66,14 +77,8 @@ describe('WelcomePageTests', () => {
level_of_education: 'other',
gender: 'm',
};
store.dispatch = jest.fn(store.dispatch);
const welcomePage = mount(reduxWrapper(<IntlWelcomePage {...props} />));
await act(async () => {
await Promise.resolve(welcomePage);
await new Promise(resolve => setImmediate(resolve));
welcomePage.update();
});
const welcomePage = await getWelcomePage();
welcomePage.find('select#gender').simulate('change', { target: { value: 'm', name: 'gender' } });
welcomePage.find('select#yearOfBirth').simulate('change', { target: { value: 1997, name: 'yearOfBirth' } });
@@ -81,18 +86,28 @@ describe('WelcomePageTests', () => {
welcomePage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(saveUserProfile('edX', formPayload));
const customProps = {
isGenderSelected: true,
isYearOfBirthSelected: true,
isLevelOfEducationSelected: true,
};
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.submit.clicked', customProps);
});
it('should open modal on pressing skip for now button', async () => {
const welcomePage = mount(reduxWrapper(<IntlWelcomePage {...props} />));
await act(async () => {
await Promise.resolve(welcomePage);
await new Promise(resolve => setImmediate(resolve));
welcomePage.update();
});
const welcomePage = await getWelcomePage();
welcomePage.find('button.btn-link').simulate('click');
expect(welcomePage.find('.pgn__modal-content-container').exists()).toBeTruthy();
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked');
});
it('should send analytic event for support link click', async () => {
const welcomePage = await getWelcomePage();
welcomePage.find('.progressive-profiling-support a[target="_blank"]').simulate('click');
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
});
it('should show error message when patch request fails', async () => {
@@ -101,12 +116,7 @@ describe('WelcomePageTests', () => {
showError: true,
},
});
const welcomePage = mount(reduxWrapper(<IntlWelcomePage {...props} />));
await act(async () => {
await Promise.resolve(welcomePage);
await new Promise(resolve => setImmediate(resolve));
welcomePage.update();
});
const welcomePage = await getWelcomePage();
expect(welcomePage.find('#welcome-page-errors').exists()).toBeTruthy();
});
});