Compare commits

..

175 Commits

Author SHA1 Message Date
Syed Sajjad Hussain Shah
a378462480 Merge branch 'master' into embeddable-registration-form 2023-09-11 10:26:29 +05:00
Syed Sajjad Hussain Shah
512deae883 fix: recs skip btn event gets cancelled on redirection (#1058) 2023-09-08 15:37:33 +05:00
Syed Sajjad Hussain Shah
2d11477037 fix: post registration recommendations eventing (#1057) 2023-09-08 15:04:38 +05:00
Zainab Amir
5e15969f4a Recommendations v2 (#1040)
* feat: add personalized recommendations (#1024)

* use Algolia for personalized recommendations
* show personalized recommendations to use that have consented
to functional cookies
* update tests

VAN-1599

* Revert "fix: special characters in redirect url getting decoded to space (#1029)" (#1030)

This reverts commit fc62241332.

* feat: update recommendations page design (#1036)

VAN-1598

* feat: add events for recommendations (#1039)

* feat: remove static recommendations

---------

Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
2023-09-08 12:08:41 +05:00
Syed Sajjad Hussain Shah
0e7e748bc4 feat: final changes 2023-09-07 12:15:48 +05:00
Blue
68be7c65d5 fix: apply frontend validations (#1041)
Description:
Applied frontend validations on all the fields
VAN-1614
2023-09-07 11:57:10 +05:00
Blue
80e940f44e fix: embedded registration form (#1026)
Description:
Adding embedded registration form
VAN-1574
2023-09-07 11:55:41 +05:00
Syed Sajjad Hussain Shah
37e811d7e5 fix: updated optional fields check based on updated response (#1055) 2023-09-07 09:31:19 +05:00
Mashal Malik
a35a1d1ba6 refactor: updated lock file version check to use new workflow (#1048)
Co-authored-by: Muhammad Abdullah Waheed <42172960+abdullahwaheed@users.noreply.github.com>
2023-09-06 14:58:18 +05:00
Syed Sajjad Hussain Shah
41a9c89d71 fix: password validation with correct password value on icon blur (#1054) 2023-09-05 17:52:35 +05:00
Syed Sajjad Hussain Shah
d469102cee refactor: registration component refactoring (#1050)
* refactor: registration component refactoring

* fix: refactored constants
2023-09-05 13:52:25 +05:00
edx-transifex-bot
c685bdd373 chore(i18n): update translations (#1052)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-09-04 16:23:08 +05:00
Emad Rad
daa7ae4d73 feat: Add persian language (#1020)
Co-authored-by: Zainab Amir <zainab.amir@arbisoft.com>
2023-08-30 13:09:40 +05:00
Zainab Amir
c5caaeba60 feat: add registration conversion event (#1049) 2023-08-29 15:22:07 +05:00
Syed Sajjad Hussain Shah
a473d79554 fix: forms data persistence issue (#1046) 2023-08-25 17:48:49 +05:00
Zainab Amir
1b5aa106ab feat: remove hydrate call (#1043) 2023-08-24 12:29:48 +05:00
renovate[bot]
d8b5653224 chore(deps): update dependency @edx/frontend-build to v12.9.8 2023-08-15 22:16:13 +00:00
renovate[bot]
4cc4ff6c4b fix(deps): update react-router monorepo to v6.15.0 2023-08-15 20:09:53 +00:00
renovate[bot]
48a3c57e5f fix(deps): update dependency @edx/paragon to v20.46.2 2023-08-15 15:29:28 +00:00
Syed Ali Abbas Zaidi
efdefc300e feat: upgrade react router to v6 (#936) 2023-08-15 17:23:34 +05:00
Syed Sajjad Hussain Shah
9730a4f55d Revert "fix: special characters in redirect url getting decoded to space (#1029)" (#1030)
This reverts commit fc62241332.
2023-08-15 15:45:31 +05:00
Syed Sajjad Hussain Shah
fc62241332 fix: special characters in redirect url getting decoded to space (#1029) 2023-08-15 13:24:27 +05:00
renovate[bot]
0846001b6d fix(deps): update dependency regenerator-runtime to v0.14.0 2023-08-08 01:57:57 +00:00
renovate[bot]
90658722e1 fix(deps): update dependency @edx/paragon to v20.46.0 2023-08-04 23:08:50 +00:00
renovate[bot]
240752c6cd fix(deps): update dependency @edx/paragon to v20.45.7 2023-08-04 04:27:28 +00:00
renovate[bot]
429d4547e4 fix(deps): update font awesome to v6.4.2 2023-08-03 08:52:23 +00:00
renovate[bot]
e278b5f74a chore(deps): update dependency @edx/frontend-build to v12.9.4 2023-08-02 19:32:04 +00:00
renovate[bot]
a723058bc1 fix(deps): update dependency @edx/paragon to v20.45.6 2023-08-01 16:20:29 +00:00
Shahbaz Shabbir
59fa7d5de3 fix: update username suggestions placement 2023-08-01 19:37:07 +05:00
renovate[bot]
60578189bd fix(deps): update dependency @edx/frontend-platform to v4.6.1 2023-08-01 14:15:25 +00:00
Syed Sajjad Hussain Shah
82cd11e01e refactor: refactor recs dir (#1012) 2023-07-31 16:36:47 +05:00
Blue
4a10540d4a fix: redirection issue fix (#1011)
Description:
Fixed redirection issue
VAN-1573
2023-07-31 15:29:05 +05:00
Blue
aeda262fb0 fix: add location resitriction for popular and trending courses (#1010)
Description:
Filter courses on the basis of location

VAN-1573
2023-07-31 14:47:22 +05:00
Mubbshar Anwar
dff3903617 fix: track event fire multiple times (#1009)
The recommendations group event was firing multiple times due to useEffect dependency because registrationResult is setting in above two useEffects.

VAN-1569
2023-07-31 13:00:53 +05:00
renovate[bot]
1399caf003 chore(deps): update dependency eslint-plugin-import to v2.28.0 2023-07-28 10:31:50 +00:00
renovate[bot]
2dfb6bc528 fix(deps): update dependency core-js to v3.32.0 2023-07-28 01:38:08 +00:00
Mubbshar Anwar
a392395876 feat: Add segments events (#1001)
Add segments events to track popular and trending recommendations stats.

VAN-1569

Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
2023-07-27 17:04:08 +05:00
renovate[bot]
5542311c95 chore(deps): update dependency jest to v29.6.2 (#1004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 15:13:30 +05:00
Zainab Amir
21e6bb6eec feat: update static recommendation placement (#1002)
* feat: update static recommendation placement
* refactor: clean recommendation code (#1003)

---------

Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
2023-07-27 15:04:56 +05:00
Shahbaz Shabbir
bfa7874108 fix: add popular and trending products lists (#1000) 2023-07-27 10:54:57 +05:00
mubbsharanwar
423958c899 feat: setup optimizely experiment
Setup optimizely experiment for popular and trending recommendations for post registration experience

VAN-1566
2023-07-26 18:30:38 +05:00
Blue
cb380a2031 feat: design change for recommendations (#997)
Description:

Design change for recommendations in which covered all the card type

VAN-1564
2023-07-26 10:49:47 +05:00
renovate[bot]
f4e89efdb4 chore(deps): update dependency @edx/frontend-build to v12.9.3 2023-07-25 12:57:49 +00:00
Mubbshar Anwar
f5cb7a1dbd feat: param based redirection (#993)
On welcome page CTA click redirection would be based on next query param.

VAN-1535
2023-07-25 16:03:41 +05:00
Syed Sajjad Hussain Shah
72e601948c fix: fix accessibility issues in register embedded experience (#996)
VAN-1541, VAN-1551, VAN-1543
2023-07-24 16:18:30 +05:00
renovate[bot]
29e30981ae fix(deps): update dependency @edx/paragon to v20.45.5 2023-07-21 17:03:18 +00:00
Zainab Amir
06a61e6a22 feat: add user retention cookie (#992) 2023-07-21 11:16:14 +05:00
renovate[bot]
1c83020b43 fix(deps): update dependency algoliasearch to v4.19.1 2023-07-20 16:50:53 +00:00
Mubbshar Anwar
fa4a0ac2d5 feat: param based cta and redirection (#984)
Register CTA label and authn redirection would be based on query params.

VAN-1535
2023-07-20 11:43:32 +05:00
renovate[bot]
2addf57cbd chore(deps): update dependency @edx/frontend-build to v12.9.2 2023-07-19 21:50:55 +00:00
renovate[bot]
d521fd20ec fix(deps): update dependency @edx/paragon to v20.45.4 2023-07-19 15:32:23 +00:00
renovate[bot]
38d44ac586 fix(deps): update dependency algoliasearch to v4.19.0 2023-07-18 22:47:36 +00:00
renovate[bot]
4768306f53 chore(deps): update dependency @edx/frontend-build to v12.9.1 2023-07-18 18:39:05 +00:00
renovate[bot]
6c6b527dfc fix(deps): update dependency @edx/paragon to v20.45.3 2023-07-18 12:39:08 +00:00
renovate[bot]
e14c9bd1b7 chore(deps): update dependency @edx/frontend-build to v12.8.67 2023-07-18 07:02:32 +00:00
dependabot[bot]
a2bdc4031b chore(deps): bump semver from 5.7.1 to 5.7.2 (#981)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 19:04:57 +05:00
renovate[bot]
8f38eb9e3a fix(deps): update dependency @edx/paragon to v20.45.2 2023-07-17 13:38:21 +00:00
renovate[bot]
c22aa58904 chore(deps): update dependency @edx/frontend-build to v12.8.66 2023-07-17 10:59:27 +00:00
edx-transifex-bot
067bddf892 chore(i18n): update translations (#978)
* chore(i18n): update translations
* feat: add base container tests (#979)

---------

Co-authored-by: Jenkins <sre+jenkins@edx.org>
Co-authored-by: Zainab Amir <zainab.amir@arbisoft.com>
2023-07-17 13:50:14 +05:00
Syed Sajjad Hussain Shah
7e4bccbc29 fix: fix rebrand experiment issue (#976) 2023-07-14 19:14:58 +05:00
renovate[bot]
f39bb35dc8 chore(deps): update dependency @edx/frontend-build to v12.8.64 2023-07-14 03:27:26 +00:00
renovate[bot]
0d760c04b7 chore(deps): update dependency @edx/frontend-build to v12.8.63 2023-07-13 00:22:45 +00:00
renovate[bot]
6f113542f5 chore(deps): update dependency @edx/frontend-build to v12.8.62 2023-07-12 11:26:38 +00:00
renovate[bot]
1e4c342703 chore(deps): update dependency @edx/frontend-build to v12.8.61 2023-07-11 15:33:06 +00:00
renovate[bot]
3ce0585d7e fix(deps): update dependency core-js to v3.31.1 2023-07-11 06:59:31 +00:00
renovate[bot]
5bf6dd6361 fix(deps): update dependency algoliasearch to v4.18.0 2023-07-11 03:33:41 +00:00
renovate[bot]
929abdff69 chore(deps): update dependency jest to v29.6.1 2023-07-11 00:57:30 +00:00
renovate[bot]
f295d69e76 fix(deps): update dependency react-loading-skeleton to v3.3.1 2023-07-10 21:54:41 +00:00
renovate[bot]
32a4c55e4a chore(deps): update dependency babel-plugin-formatjs to v10.5.3 2023-07-10 20:32:20 +00:00
renovate[bot]
615ba91bdb chore(deps): update dependency @edx/frontend-build to v12.8.60 2023-07-10 16:39:31 +00:00
renovate[bot]
dfb2f89a36 fix(deps): update dependency @edx/paragon to v20.45.0 2023-07-10 13:44:30 +00:00
Mashal Malik
c9783234cc feat: update react & react-dom to v17 (#938)
* feat: update react & react-dom to v17

* chore: add enzyme adapter pkg to devDep

* refactor: updated paragon & snapshots

* build: update frontend-platform

---------

Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com>
2023-07-10 15:48:14 +05:00
Zainab Amir
0513e6c2de chore: add configs to env file (#962) 2023-06-28 23:40:41 +05:00
Zainab Amir
3cdc0234ef fix: height of image for larger layout (#961) 2023-06-28 20:06:57 +05:00
Zainab Amir
e06d12be07 feat: add optimizely web to authn (#960) 2023-06-28 16:52:25 +05:00
Zainab Amir
56394881fc feat: Add image layout for base container (#959)
- Added image layout base container
- Added config for optimizely experiment
2023-06-28 16:05:26 +05:00
Zainab Amir
61056240c4 refactor: base component name and folder structure (#958) 2023-06-27 14:17:04 +05:00
Zainab Amir
a59e7c548c Revert "feat: hide privacy banner for embedded form (#954)" (#955)
This reverts commit f63d7674e2.
2023-06-23 10:20:44 +05:00
Zainab Amir
f63d7674e2 feat: hide privacy banner for embedded form (#954) 2023-06-22 15:14:18 +05:00
Zainab Amir
fa3a70e9a9 Revert "feat: fire identify call and register event" (#953)
* Revert "feat: fire identify call and register event (#951)
2023-06-22 11:59:22 +05:00
Syed Sajjad Hussain Shah
b817a8d122 fix: hide zendesk widget for embedded experience (#952) 2023-06-21 17:39:07 +05:00
Syed Sajjad Hussain Shah
2a6668cef3 feat: fire identify call and register event (#951)
VAN-1499
2023-06-21 15:36:58 +05:00
Zainab Amir
a802821ae9 feat: keep username suggestion for embedded experience (#950)
* feat: keep username suggestion for embedded experience
2023-06-21 10:30:26 +05:00
Syed Sajjad Hussain Shah
9e13141f6b fix: render embedded experience through pathname (#949) 2023-06-19 13:44:30 +05:00
Blue
4b64ce2534 fix: make host param compulsory (#948)
Description

Make host param compulsory and host should grab from query params when calling postMessage api

VAN-1485
2023-06-16 14:44:34 +05:00
Blue
c550069e11 fix: avoid validations on blur event (#946)
Description:

When authn is embedded in iframe then validations should not runs on blur event it should only run when button is clicked.

VAN-1481
2023-06-16 10:33:59 +05:00
Mubbshar Anwar
1e10e9c89c feat: registration conversion events (#942)
Passing variant property to registration and progressive profiling events to calculate on ramp conversion rate.

VAN-1478
2023-06-16 09:42:04 +05:00
Zainab Amir
cd6c1c0e42 feat: update welcome page logic (#947) 2023-06-15 17:09:52 +05:00
Zainab Amir
5edcee9eb9 feat: allow welcome page to load for embedded experience (#944) 2023-06-15 15:46:34 +05:00
Syed Sajjad Hussain Shah
d41c06b1fd feat: allow embedded experience to open register page for authenticated users (#945)
VAN-1482
2023-06-15 14:42:09 +05:00
Blue
2a2c5abc81 fix: redirect user to welcome page (#939) (#941)
Description
Redirect the user to welcome page after registration, as Authn is embedded in iframe which is located in Hubspot so we call window.postMessage function from Authn which let the Hubspot knows about redirect URL and then reload the welcome page in parent window.

VAN-1474
2023-06-15 12:29:04 +05:00
Syed Sajjad Hussain Shah
ccdd648603 feat: add iframe resizer script (#943)
VAN-1475
2023-06-14 14:43:09 +05:00
Blue
5c1ea04970 fix: change registration form for onramp (#939)
Description:
In this we have changed the regirstation form on the basis of embed query param and hide the multiple things

VAN-1476
2023-06-13 16:10:39 +05:00
Attiya Ishaque
5ebd22f088 fix: remove survery cookie (#937) 2023-06-08 18:56:00 +05:00
Attiya Ishaque
5aec091156 fix: fix console errors on running test (#930) 2023-06-02 17:49:40 +05:00
renovate[bot]
68993cc21f fix(deps): update dependency @edx/paragon to v20.40.2 2023-06-01 11:16:17 +00:00
renovate[bot]
404fc2b36b fix(deps): update dependency @edx/paragon to v20.40.1 2023-05-31 22:04:26 +00:00
renovate[bot]
7bb06d0bfa fix(deps): update dependency @edx/frontend-platform to v4.5.1 2023-05-30 06:52:39 +00:00
renovate[bot]
abbb64b9c7 fix(deps): update dependency @edx/paragon to v20.40.0 2023-05-30 00:53:33 +00:00
renovate[bot]
99ca582c1a fix(deps): update dependency @edx/frontend-platform to v4.5.0 2023-05-29 10:19:49 +00:00
renovate[bot]
3e4cfc1573 fix(deps): update dependency @edx/paragon to v20.39.3 2023-05-29 08:09:07 +00:00
renovate[bot]
5437e1b7e9 chore(deps): update dependency @edx/frontend-build to v12.8.38 2023-05-29 03:58:43 +00:00
Attiya Ishaque
4d33cd7d69 fix: fix design issue in user name suggestion field (#922) 2023-05-24 18:32:02 +05:00
Attiya Ishaque
cb82e94b53 refactor: BEM convention for modules (#907) 2023-05-23 17:21:29 +05:00
renovate[bot]
dbe14ccedf fix(deps): update dependency @edx/frontend-platform to v4.4.0 2023-05-23 00:09:52 +00:00
renovate[bot]
841edf2d24 fix(deps): update dependency @edx/paragon to v20.39.2 2023-05-22 20:34:23 +00:00
Blue
0c375cc50c refactor: BEM convention for common components (#914)
Description:
Need to apply BEM comvention for common components

VAN-1425
2023-05-22 10:47:53 +05:00
renovate[bot]
50072887d0 fix(deps): update dependency @edx/paragon to v20.39.1 2023-05-19 17:49:26 +00:00
Attiya Ishaque
976814d846 fix: fix console error in login and register's test (#909) 2023-05-19 12:39:52 +05:00
Blue
c22024cf66 refactor: BEM convention for reset password (#906)
Description:

Need to implement BEM convention for reset password page

VAN-1423
2023-05-19 11:16:49 +05:00
Syed Sajjad Hussain Shah
829a219b9f fix: bugs in country field (#913)
VAN-1415
2023-05-19 10:03:39 +05:00
renovate[bot]
13d572ab28 fix(deps): update dependency @edx/paragon to v20.39.0 2023-05-18 15:00:59 +00:00
renovate[bot]
c0c2ffa122 fix(deps): update dependency @edx/paragon to v20.37.0 2023-05-17 21:17:46 +00:00
Syed Sajjad Hussain Shah
84c563fda3 refactor: use paragon Autosuggest field for country field (#911)
VAN-1415

Co-authored-by: Syed Sajjad  Hussain Shah <syed.sajjad@H7FKF7K6XD.local>
2023-05-17 13:13:04 +05:00
Syed Sajjad Hussain Shah
97720d6a46 Revert "refactor: use paragon Autosuggest field for country field (#904)" (#910)
This reverts commit 386982d9c4.
2023-05-17 12:31:21 +05:00
Syed Sajjad Hussain Shah
386982d9c4 refactor: use paragon Autosuggest field for country field (#904)
VAN-1415

Co-authored-by: Syed Sajjad  Hussain Shah <syed.sajjad@H7FKF7K6XD.local>
2023-05-17 12:11:27 +05:00
renovate[bot]
cb38a2a148 fix(deps): update dependency @edx/paragon to v20.36.2 2023-05-16 19:17:29 +00:00
renovate[bot]
07f19209bf fix(deps): update dependency @edx/frontend-platform to v4.3.0 2023-05-16 02:33:21 +00:00
renovate[bot]
69c0ca13fc fix(deps): update dependency @edx/paragon to v20.36.1 2023-05-15 18:26:20 +00:00
Jenkins
e49c50f55c chore(i18n): update translations 2023-05-14 11:16:53 -04:00
Syed Sajjad Hussain Shah
f0105f0094 Revert "refactor: use paragon Autosuggest field for country field (#897)" (#900)
This reverts commit 3c89afea4a.
2023-05-12 13:01:22 +05:00
Syed Sajjad Hussain Shah
3c89afea4a refactor: use paragon Autosuggest field for country field (#897)
VAN-1415

Co-authored-by: Syed Sajjad  Hussain Shah <syed.sajjad@H7FKF7K6XD.local>
2023-05-12 12:45:29 +05:00
renovate[bot]
848f574f3f fix(deps): update dependency @edx/paragon to v20.36.0 2023-05-11 04:41:23 +00:00
Blue
ec9e34cea4 fix: add departments in Zendesk widget (#896)
Description:
Add departments list in Zendesk widget configurations

VAN-1426
2023-05-10 16:58:26 +05:00
Omar Al-Ithawi
f9069df4e6 feat: use atlas in make pull_translations (#890)
Changes
-------
 - Move all i18n imports into `src/i18n/index.js` so `intl-imports.js` can
   override it with latest translations
 - Add `atlas` into `make pull_translations` when `OPENEDX_ATLAS_PULL`
   environment variable is set.

Refs: [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) implementing Translation Infrastructure OEP-58.
2023-05-09 11:12:36 -04:00
renovate[bot]
9035f3eb7e fix(deps): update dependency core-js to v3.30.2 2023-05-08 05:14:07 +00:00
renovate[bot]
e197e788d1 fix(deps): update dependency @edx/paragon to v20.34.0 2023-05-05 19:58:17 +00:00
renovate[bot]
49d33522a8 chore(deps): update dependency @edx/frontend-build to v12.8.27 2023-05-05 17:21:17 +00:00
renovate[bot]
06f0ec3c0b chore(deps): update dependency @edx/frontend-build to v12.8.22 2023-05-04 21:04:47 +00:00
Attiya Ishaque
54319c6949 fix: remove cookie policy banner from authn (#888) 2023-05-04 17:12:21 +05:00
renovate[bot]
86f875ec3e chore(deps): update dependency @edx/frontend-build to v12.8.17 2023-05-03 00:45:13 +00:00
renovate[bot]
754a6ddb12 fix(deps): update dependency @edx/paragon to v20.33.0 2023-05-02 19:39:19 +00:00
Shahbaz Shabbir
62e8f75b96 fix: eliminate the usage of extra OR condition in MFE context API (#882) 2023-05-02 19:17:28 +05:00
Shahbaz Shabbir
c0deb663a6 chore: upgrade the query-string version to 7.1.3 (#869) 2023-05-02 17:08:35 +05:00
Zainab Amir
ce28add152 fix: conditionally initialize optimizely (#862) 2023-05-02 16:11:55 +05:00
renovate[bot]
5f89315947 chore(deps): update dependency babel-plugin-formatjs to v10.5.1 2023-05-01 08:06:15 +00:00
renovate[bot]
56dd194a1a fix(deps): update dependency @edx/paragon to v20.32.3 2023-04-28 08:31:27 +00:00
Attiya Ishaque
adcdcc4c8b chore: remove fortawesome/free-regular-svg-icons package (#867) 2023-04-26 18:39:06 +05:00
renovate[bot]
7fd45f089d chore(deps): update dependency @edx/frontend-build to v12.8.16 2023-04-26 06:17:05 +00:00
renovate[bot]
65d82b2080 fix(deps): update dependency @edx/paragon to v20.32.0 2023-04-24 14:10:11 +00:00
renovate[bot]
f771935e20 fix(deps): update dependency @edx/paragon to v20.31.1 2023-04-24 00:46:26 +00:00
renovate[bot]
70c255fc4f fix(deps): update font awesome to v6.4.0 2023-04-21 00:49:33 +00:00
renovate[bot]
bd1396fc54 fix(deps): update dependency @edx/frontend-platform to v4.2.0 2023-04-20 18:20:03 +00:00
Stanislav
cba5395d5c fix: Remove horizontal scroll in windows (#760) 2023-04-20 17:55:05 +05:00
Zainab Amir
b42c09d919 chore: remove unused lodash dependency (#870) 2023-04-20 14:51:40 +05:00
Blue
b7af2356fa fix: Remove extract-react-intl-messages (#865)
Remove unused package extract-react-intl-messages from package.json

VAN-1378
2023-04-20 14:32:11 +05:00
Syed Sajjad Hussain Shah
22d477e55f fix: import/no-cycle eslint issue (#875)
VAN-1388

Co-authored-by: Syed Sajjad  Hussain Shah <syed.sajjad@H7FKF7K6XD.local>
2023-04-20 12:33:40 +05:00
renovate[bot]
dfa69c27bb chore(deps): update dependency babel-plugin-formatjs to v10.5.0 2023-04-20 05:12:27 +00:00
renovate[bot]
6b78158db2 fix(deps): update dependency reselect to v4.1.8 2023-04-19 23:28:48 +00:00
renovate[bot]
c92cac0eed fix(deps): update dependency react-loading-skeleton to v3.2.1 2023-04-19 19:35:03 +00:00
Attiya Ishaque
b2dce920fa chore: remove semver-regex package (#868) 2023-04-19 20:42:59 +05:00
Attiya Ishaque
44cec762fb fix: replace faSignInAlt with paragon icon (#863) 2023-04-19 16:33:54 +05:00
Shahbaz Shabbir
a98188ead8 fix: change keys to camelCase for mfe_context response (PR# 835) (#857) 2023-04-19 13:03:26 +05:00
Syed Sajjad Hussain Shah
294b1a469f chore: remove sanitize-html package (#866)
VAN-1381

Co-authored-by: Syed Sajjad  Hussain Shah <syed.sajjad@H7FKF7K6XD.local>
2023-04-19 12:21:10 +05:00
Syed Sajjad Hussain Shah
ebed588c1c chore: remove react-onclickoutside package (#864)
VAN-1380

Co-authored-by: Syed Sajjad  Hussain Shah <syed.sajjad@H7FKF7K6XD.local>
2023-04-18 15:13:10 +05:00
Blue
46f8217e1a fix: reset-password code coverage (#855)
Improve the code coverage of reset-password component

VAN-1369
2023-04-18 13:24:59 +05:00
Blue
9ab23cf485 Merge pull request #849 from openedx/ahtesham/VAN-1368/test-coverage-registation
fix: registration test coverage
2023-04-17 15:46:20 +05:00
ahtesham-quraish
7790660fe8 fix: registration test coverage
Increase test coverage of registration flow

VAN-1368
2023-04-17 15:37:53 +05:00
Jenkins
1dd2726beb chore(i18n): update translations 2023-04-16 11:16:49 -04:00
renovate[bot]
ba9ce89d1b fix(deps): update dependency core-js to v3.30.1 2023-04-14 21:03:27 +00:00
renovate[bot]
c9082ac709 chore(deps): update dependency @edx/frontend-build to v12.8.10 2023-04-14 16:39:01 +00:00
renovate[bot]
27c8fa8986 fix(deps): update dependency @edx/frontend-platform to v4.1.0 2023-04-14 16:28:37 +00:00
Syed Sajjad Hussain Shah
a04289d71b Revert "fix: change keys to camelCase for mfe_context response (#835)" (#856)
This reverts commit b0745de672.
2023-04-14 14:43:01 +05:00
Blue
321859e0f5 Merge pull request #854 from openedx/ahtesham/1349/update-conditional-logic
fix: remove conditional logic
2023-04-13 14:34:20 +05:00
ahtesham-quraish
4b4bf413c1 fix: remove conditional logic
Remove conditional logic from autoupdate label as it does not run on master push

VAN-1349
2023-04-13 14:21:14 +05:00
renovate[bot]
06c4f75b4a chore(deps): update dependency eslint-plugin-import to v2.27.5 (#811)
* chore(deps): update dependency eslint-plugin-import to v2.27.5

* fix: fix eslint tests

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: attiyaishaque <atiya.ishaq@arbisoft.com>
2023-04-13 13:41:10 +05:00
Attiya Ishaque
12dd97af61 fix: [VAN-1367] Remove Optimizely web from Authn (#843) 2023-04-13 12:54:29 +05:00
Zainab Amir
8e77197459 fix: update pull translation command (#852) 2023-04-13 12:23:48 +05:00
Zainab Amir
3cc64cada6 fix: nested components in CountryField (#851) 2023-04-13 12:05:32 +05:00
Yoiber
3ac5874df1 chore(i18n): add more languages (#747) 2023-04-13 11:24:38 +05:00
Shahbaz Shabbir
107dd6f360 fix: Update eslint configuration and resolve linting errors and test failures (#847) 2023-04-12 22:52:49 +05:00
Blue
347e0cd336 Merge pull request #850 from openedx/ahtesham/VAN-1349/update-github-token
fix: update github token
2023-04-12 12:20:07 +05:00
ahtesham-quraish
2ba6058ec7 fix: update github token
update github token in autoupdate workflow

VAN-1349
2023-04-12 12:08:59 +05:00
Blue
683aa258b8 Merge pull request #825 from openedx/ahtesham/VAN-1349/add-label-support
fix: add new flag in autoupdate action
2023-04-12 11:59:22 +05:00
ahtesham-quraish
d7ad7e314d fix: add new flag in autoupdate action
trigger autoupdate workflow when autoupdate label is added

VAN-1349
2023-04-12 11:53:53 +05:00
184 changed files with 13927 additions and 12409 deletions

11
.env
View File

@@ -16,21 +16,26 @@ SITE_NAME=null
INFO_EMAIL='' INFO_EMAIL=''
# ***** Cookies ***** # ***** Cookies *****
REGISTER_CONVERSION_COOKIE_NAME=null REGISTER_CONVERSION_COOKIE_NAME=null
USER_SURVEY_COOKIE_NAME=null
# ***** Links ***** # ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK='' LOGIN_ISSUE_SUPPORT_LINK=''
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
POST_REGISTRATION_REDIRECT_URL=''
SEARCH_CATALOG_URL=''
# ***** Features flags ***** # ***** Features flags *****
DISABLE_ENTERPRISE_LOGIN='' DISABLE_ENTERPRISE_LOGIN=''
ENABLE_COOKIE_POLICY_BANNER=''
ENABLE_DYNAMIC_REGISTRATION_FIELDS='' ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='' ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
ENABLE_PERSONALIZED_RECOMMENDATIONS='' ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
MARKETING_EMAILS_OPT_IN='' MARKETING_EMAILS_OPT_IN=''
SHOW_CONFIGURABLE_EDX_FIELDS='' SHOW_CONFIGURABLE_EDX_FIELDS=''
# ***** Zendesk related keys ***** # ***** Zendesk related keys *****
ZENDESK_KEY='' ZENDESK_KEY=''
ZENDESK_LOGO_URL='' ZENDESK_LOGO_URL=''
# ***** Base Container Images *****
BANNER_IMAGE_LARGE=''
BANNER_IMAGE_MEDIUM=''
BANNER_IMAGE_SMALL=''
BANNER_IMAGE_EXTRA_SMALL=''
# ***** Miscellaneous ***** # ***** Miscellaneous *****
APP_ID='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''

View File

@@ -23,13 +23,17 @@ INFO_EMAIL='info@example.com'
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion' REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
SESSION_COOKIE_DOMAIN='localhost' SESSION_COOKIE_DOMAIN='localhost'
USER_INFO_COOKIE_NAME='edx-user-info' USER_INFO_COOKIE_NAME='edx-user-info'
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
# ***** Links ***** # ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url' LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
TOS_AND_HONOR_CODE='http://localhost:18000/honor' TOS_AND_HONOR_CODE='http://localhost:18000/honor'
TOS_LINK='http://localhost:18000/tos' TOS_LINK='http://localhost:18000/tos'
PRIVACY_POLICY='http://localhost:18000/privacy' PRIVACY_POLICY='http://localhost:18000/privacy'
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome' AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
# ***** Base Container Images *****
BANNER_IMAGE_LARGE=''
BANNER_IMAGE_MEDIUM=''
BANNER_IMAGE_SMALL=''
BANNER_IMAGE_EXTRA_SMALL=''
# ***** Miscellaneous ***** # ***** Miscellaneous *****
APP_ID='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''

View File

@@ -1,5 +1,4 @@
# Copy these to the .env.private to enable edX specific functionality on local system # Copy these to the .env.private to enable edX specific functionality on local system
ENABLE_COOKIE_POLICY_BANNER='true'
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true' ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
MARKETING_EMAILS_OPT_IN='true' MARKETING_EMAILS_OPT_IN='true'
SHOW_CONFIGURABLE_EDX_FIELDS='true' SHOW_CONFIGURABLE_EDX_FIELDS='true'

View File

@@ -16,7 +16,6 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY='' SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here' SITE_NAME='Your Platform Name Here'
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion' REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
APP_ID='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''

View File

@@ -48,7 +48,5 @@ module.exports = createConfig('eslint', {
}, },
], ],
'function-paren-newline': 'off', 'function-paren-newline': 'off',
'no-import-assign': 'off',
'react/no-unstable-nested-components': 'off',
}, },
}); });

View File

@@ -3,14 +3,18 @@ on:
push: push:
branches: branches:
- master - master
pull_request:
types: [ labeled ]
branches:
- master
jobs: jobs:
autoupdate: autoupdate:
name: autoupdate name: autoupdate
runs-on: ubuntu-22.04 runs-on: ubuntu-20.04
steps: steps:
- uses: docker://chinthakagodawita/autoupdate-action:v1 - uses: docker://chinthakagodawita/autoupdate-action:v1
env: env:
GITHUB_TOKEN: "${{ secrets.CC_GITHUB_TOKEN }}" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
DRY_RUN: "false" DRY_RUN: "false"
PR_FILTER: "labelled" PR_FILTER: "labelled"
PR_LABELS: "autoupdate" PR_LABELS: "autoupdate"

View File

@@ -10,4 +10,4 @@ on:
jobs: jobs:
version-check: version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master uses: openedx/.github/.github/workflows/lockfile-check.yml@master

View File

@@ -1,6 +1,7 @@
export TRANSIFEX_RESOURCE = frontend-app-authn export TRANSIFEX_RESOURCE = frontend-app-authn
transifex_langs = "ar,fr,es_419,zh_CN,it_IT,pt_PT,de_DE,uk,ru,hi" transifex_langs = "ar,de,de_DE,es_419,fa_IR,fr,fr_CA,hi,it,it_IT,pt,pt_PT,ru,uk,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json transifex_input = $(i18n)/transifex_input.json
@@ -42,11 +43,37 @@ push_translations:
# Pushing comments to Transifex... # Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh ./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex. # Pulls translations from Transifex.
pull_translations: pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs) tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
# This target is used by CI. $(intl_imports) paragon frontend-app-authn
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes: validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes... # Checking for package-lock.json changes...
git diff --exit-code package-lock.json git diff --exit-code package-lock.json
.PHONY: validate
validate:
make validate-no-uncommitted-package-lock-changes
npm run i18n_extract
npm run lint -- --max-warnings 0
npm run test
npm run build
.PHONY: validate.ci
validate.ci:
npm ci
make validate

View File

@@ -112,9 +112,6 @@ The authentication micro-frontend also requires the following additional variabl
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID. - Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
- ``authn`` | ``''`` - ``authn`` | ``''``
* - ``ENABLE_COOKIE_POLICY_BANNER``
- Enables support for displaying the cookies acceptance banner.
- ``true`` | ``''`` (empty strings are falsy)
edX-specific Environment Variables edX-specific Environment Variables
********************************** **********************************

14399
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,59 +33,54 @@
}, },
"dependencies": { "dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-cookie-policy-banner": "2.2.2", "@edx/frontend-platform": "^5.0.0",
"@edx/frontend-platform": "4.2.0", "@edx/paragon": "20.46.2",
"@edx/paragon": "20.30.1", "@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/fontawesome-svg-core": "6.2.1", "@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.2.1", "@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/react-fontawesome": "0.2.0", "@fortawesome/react-fontawesome": "0.2.0",
"@optimizely/react-sdk": "^2.9.1", "@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.2.5", "@redux-devtools/extension": "3.2.5",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"algoliasearch": "^4.14.3", "algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0",
"classnames": "2.3.2", "classnames": "2.3.2",
"core-js": "3.30.0", "core-js": "3.32.0",
"extract-react-intl-messages": "4.1.1",
"fastest-levenshtein": "1.0.16", "fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.0", "form-urlencoded": "6.1.0",
"lodash.camelcase": "4.3.0",
"lodash.snakecase": "4.1.1",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "5.1.1", "query-string": "7.1.3",
"react": "16.14.0", "react": "^17.0.2",
"react-dom": "16.14.0", "react-dom": "^17.0.2",
"react-helmet": "6.1.0", "react-helmet": "6.1.0",
"react-loading-skeleton": "3.2.0", "react-loading-skeleton": "3.3.1",
"react-onclickoutside": "6.13.0",
"react-redux": "7.2.9", "react-redux": "7.2.9",
"react-responsive": "8.2.0", "react-responsive": "8.2.0",
"react-router": "5.3.4", "react-router": "6.15.0",
"react-router-dom": "5.3.4", "react-router-dom": "6.15.0",
"react-zendesk": "^0.1.13", "react-zendesk": "^0.1.13",
"redux": "4.2.0", "redux": "4.2.0",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
"redux-mock-store": "1.5.4", "redux-mock-store": "1.5.4",
"redux-saga": "1.2.3", "redux-saga": "1.2.3",
"redux-thunk": "2.4.2", "redux-thunk": "2.4.2",
"regenerator-runtime": "0.13.11", "regenerator-runtime": "0.14.0",
"reselect": "4.1.7", "reselect": "4.1.8",
"sanitize-html": "2.10.0",
"semver-regex": "3.1.4",
"universal-cookie": "4.0.4" "universal-cookie": "4.0.4"
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "^1.1.1", "@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "12.8.6", "@edx/frontend-build": "12.9.8",
"@edx/reactifex": "1.1.0", "@edx/reactifex": "1.1.0",
"babel-plugin-formatjs": "10.4.0", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"babel-plugin-formatjs": "10.5.3",
"enzyme": "3.11.0", "enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.7", "eslint-plugin-import": "2.28.0",
"eslint-plugin-import": "2.26.0",
"glob": "7.2.3", "glob": "7.2.3",
"history": "5.3.0", "history": "5.3.0",
"husky": "7.0.4", "husky": "7.0.4",
"jest": "29.5.0", "jest": "29.6.2",
"react-test-renderer": "16.14.0" "react-test-renderer": "^17.0.2"
} }
} }

View File

@@ -4,6 +4,11 @@
<title>Authn | <%= process.env.SITE_NAME %></title> <title>Authn | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.min.js"
integrity="sha512-R7Piufj0/o6jG9ZKrAvS2dblFr2kkuG4XVQwStX+/4P+KwOLUXn2DXy0l1AJDxxqGhkM/FJllZHG2PKOAheYzg=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<% if (process.env.OPTIMIZELY_URL) { %> <% if (process.env.OPTIMIZELY_URL) { %>
<script <script
src="<%= process.env.OPTIMIZELY_URL %>" src="<%= process.env.OPTIMIZELY_URL %>"

View File

@@ -3,10 +3,10 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Redirect, Route, Switch } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { import {
Logistration, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk, EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
} from './common-components'; } from './common-components';
import configureStore from './data/configureStore'; import configureStore from './data/configureStore';
import { import {
@@ -15,14 +15,18 @@ import {
PAGE_NOT_FOUND, PAGE_NOT_FOUND,
PASSWORD_RESET_CONFIRM, PASSWORD_RESET_CONFIRM,
RECOMMENDATIONS, RECOMMENDATIONS,
REGISTER_EMBEDDED_PAGE,
REGISTER_PAGE, REGISTER_PAGE,
RESET_PAGE, RESET_PAGE,
} from './data/constants'; } from './data/constants';
import { updatePathWithQueryParams } from './data/utils'; import { updatePathWithQueryParams } from './data/utils';
import { ForgotPasswordPage } from './forgot-password'; import { ForgotPasswordPage } from './forgot-password';
import Logistration from './logistration/Logistration';
import { ProgressiveProfiling } from './progressive-profiling'; import { ProgressiveProfiling } from './progressive-profiling';
import { RecommendationsPage } from './recommendations'; import { RecommendationsPage } from './recommendations';
import { EmbeddableRegistrationPage } from './register';
import { ResetPasswordPage } from './reset-password'; import { ResetPasswordPage } from './reset-password';
import './index.scss'; import './index.scss';
registerIcons(); registerIcons();
@@ -33,21 +37,26 @@ const MainApp = () => (
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" /> <link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet> </Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />} {getConfig().ZENDESK_KEY && <Zendesk />}
<Switch> <Routes>
<Route exact path="/"> <Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} /> <Route
</Route> path={REGISTER_EMBEDDED_PAGE}
<UnAuthOnlyRoute exact path={LOGIN_PAGE} render={() => <Logistration selectedPage={LOGIN_PAGE} />} /> element={<EmbeddedRegistrationRoute><EmbeddableRegistrationPage /></EmbeddedRegistrationRoute>}
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} /> />
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} /> <Route
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} /> path={LOGIN_PAGE}
<Route exact path={AUTHN_PROGRESSIVE_PROFILING} component={ProgressiveProfiling} /> element={
<Route exact path={RECOMMENDATIONS} component={RecommendationsPage} /> <UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} /> }
<Route path="*"> />
<Redirect to={PAGE_NOT_FOUND} /> <Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
</Route> <Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
</Switch> <Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
</Routes>
</AppProvider> </AppProvider>
); );

View File

@@ -1,55 +0,0 @@
import React from 'react';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { getLocale } from '@edx/frontend-platform/i18n';
import { breakpoints } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import MediaQuery from 'react-responsive';
import AuthLargeLayout from './AuthLargeLayout';
import AuthMediumLayout from './AuthMediumLayout';
import AuthSmallLayout from './AuthSmallLayout';
import LargeLayout from './LargeLayout';
import MediumLayout from './MediumLayout';
import SmallLayout from './SmallLayout';
const BaseComponent = ({ children, showWelcomeBanner }) => {
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
const username = authenticatedUser ? authenticatedUser.username : null;
return (
<>
{getConfig().ENABLE_COOKIE_POLICY_BANNER ? <CookiePolicyBanner languageCode={getLocale()} /> : null}
<div className="col-md-12 extra-large-screen-top-stripe" />
<div className="layout">
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
{authenticatedUser ? <AuthSmallLayout username={username} /> : <SmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
{authenticatedUser ? <AuthMediumLayout username={username} /> : <MediumLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
{authenticatedUser ? <AuthLargeLayout username={username} /> : <LargeLayout />}
</MediaQuery>
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
{children}
</div>
</div>
</>
);
};
BaseComponent.defaultProps = {
showWelcomeBanner: false,
};
BaseComponent.propTypes = {
children: PropTypes.node.isRequired,
showWelcomeBanner: PropTypes.bool,
};
export default BaseComponent;

View File

@@ -1,2 +0,0 @@
/* eslint-disable import/prefer-default-export */
export { default as BaseComponent } from './BaseComponent';

View File

@@ -3,16 +3,14 @@ import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import LargeLayout from '../LargeLayout'; import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
import MediumLayout from '../MediumLayout';
import SmallLayout from '../SmallLayout';
describe('ScreenLayout', () => { describe('Default Layout tests', () => {
it('should display the form, pass as a child in SmallScreenLayout', () => { it('should display the form passed as a child in SmallScreenLayout', () => {
const smallScreen = mount( const smallScreen = mount(
<IntlProvider locale="en"> <IntlProvider locale="en">
<div> <div>
<SmallLayout /> <DefaultSmallLayout />
<form> <form>
<input type="text" /> <input type="text" />
</form> </form>
@@ -22,11 +20,11 @@ describe('ScreenLayout', () => {
expect(smallScreen.find('form').exists()).toEqual(true); expect(smallScreen.find('form').exists()).toEqual(true);
}); });
it('should display the form, pass as a child in MediumScreenLayout', () => { it('should display the form passed as a child in MediumScreenLayout', () => {
const mediumScreen = mount( const mediumScreen = mount(
<IntlProvider locale="en"> <IntlProvider locale="en">
<div> <div>
<MediumLayout /> <DefaultMediumLayout />
<form> <form>
<input type="text" /> <input type="text" />
</form> </form>
@@ -36,11 +34,11 @@ describe('ScreenLayout', () => {
expect(mediumScreen.find('form').exists()).toEqual(true); expect(mediumScreen.find('form').exists()).toEqual(true);
}); });
it('should display the form, pass as a child in LargeScreenLayout', () => { it('should display the form passed as a child in LargeScreenLayout', () => {
const largeScreen = mount( const largeScreen = mount(
<IntlProvider locale="en"> <IntlProvider locale="en">
<div> <div>
<LargeLayout /> <DefaultLargeLayout />
<form> <form>
<input type="text" /> <input type="text" />
</form> </form>

View File

@@ -0,0 +1,3 @@
export { default as DefaultLargeLayout } from './LargeLayout';
export { default as DefaultMediumLayout } from './MediumLayout';
export { default as DefaultSmallLayout } from './SmallLayout';

View File

@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'start.learning': {
id: 'start.learning',
defaultMessage: 'Start learning',
description: 'Header text for logistration MFE pages',
},
'with.site.name': {
id: 'with.site.name',
defaultMessage: 'with {siteName}',
description: 'Header text with site name for logistration MFE pages',
},
});
export default messages;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import messages from './messages';
const ExtraSmallLayout = () => {
const { formatMessage } = useIntl();
return (
<span
className="w-100 bg-primary-500 banner__image extra-small-layout"
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
>
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="ml-4.5 mr-1 pb-3.5 pt-3.5">
<h1 className="banner__heading">
<span className="text-light-500">
{formatMessage(messages['your.career.turning.point'])}{' '}
</span>
<span className="text-warning-300">
{formatMessage(messages['is.here'])}
</span>
</h1>
</div>
</span>
);
};
export default ExtraSmallLayout;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import './index.scss';
import messages from './messages';
const LargeLayout = () => {
const { formatMessage } = useIntl();
return (
<div
className="w-50 bg-primary-500 banner__image large-layout"
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_LARGE})` }}
>
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="company-logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="min-vh-100 p-5 d-flex align-items-end">
<h1 className="display-2 mw-sm mb-3 d-flex flex-column flex-shrink-0 justify-content-center">
<span className="text-light-500">
{formatMessage(messages['your.career.turning.point'])}
</span>
<span className="text-warning-300">
{formatMessage(messages['is.here'])}
</span>
</h1>
</div>
</div>
);
};
export default LargeLayout;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import './index.scss';
import messages from './messages';
const MediumLayout = () => {
const { formatMessage } = useIntl();
return (
<div
className="w-100 mb-3 bg-primary-500 banner__image medium-layout"
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_MEDIUM})` }}
>
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="ml-5 pb-4 pt-4">
<h1 className="display-2 banner__heading">
<span className="text-light-500">
{formatMessage(messages['your.career.turning.point'])}{' '}
</span>
<span className="text-warning-300 d-inline-block">
{formatMessage(messages['is.here'])}
</span>
</h1>
</div>
</div>
);
};
export default MediumLayout;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import messages from './messages';
const SmallLayout = () => {
const { formatMessage } = useIntl();
return (
<span
className="w-100 bg-primary-500 banner__image small-layout"
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_SMALL})` }}
>
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="ml-5 mr-1 pb-3.5 pt-3.5">
<h1 className="display-2">
<span className="text-light-500">
{formatMessage(messages['your.career.turning.point'])}{' '}
</span>
<span className="text-warning-300">
{formatMessage(messages['is.here'])}
</span>
</h1>
</div>
</span>
);
};
export default SmallLayout;

View File

@@ -0,0 +1,4 @@
export { default as ImageLargeLayout } from './LargeLayout';
export { default as ImageMediumLayout } from './MediumLayout';
export { default as ImageSmallLayout } from './SmallLayout';
export { default as ImageExtraSmallLayout } from './ExtraSmallLayout';

View File

@@ -0,0 +1,37 @@
.company-logo {
width: 71px;
margin-top: 2rem;
margin-left: 1.5rem;
}
@media (max-width: 576px) {
.company-logo {
width: 44.67px;
margin-top: 1.25rem;
margin-left: 1.5rem;
}
}
.banner__image {
background-size: cover;
background-repeat: no-repeat;
border:none;
}
@media (min-width: 464px) and (max-width: 575.98px) {
.banner__heading {
font-size: 60px;
font-weight: 700;
line-height: 60px;
letter-spacing: -1.2px;
}
}
@media (min-width: 768px) and (max-width: 800px) {
.banner__heading {
font-size: 60px !important;
font-weight: 700 !important;
line-height: 60px !important;
letter-spacing: -2px !important;
}
}

View File

@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'your.career.turning.point': {
id: 'your.career.turning.point',
defaultMessage: 'Your career turning point',
description: 'Part of the heading "Your career turning point is here." shown on Authn MFE',
},
'is.here': {
id: 'is.here',
defaultMessage: 'is here.',
description: 'Part of the heading "Your career turning point is here." shown on Authn MFE',
},
});
export default messages;

View File

@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import messages from './messages'; import messages from './messages';
const AuthLargeLayout = ({ username }) => { const LargeLayout = ({ username }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
@@ -42,8 +42,8 @@ const AuthLargeLayout = ({ username }) => {
); );
}; };
AuthLargeLayout.propTypes = { LargeLayout.propTypes = {
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
}; };
export default AuthLargeLayout; export default LargeLayout;

View File

@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import messages from './messages'; import messages from './messages';
const AuthMediumLayout = ({ username }) => { const MediumLayout = ({ username }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
@@ -45,8 +45,8 @@ const AuthMediumLayout = ({ username }) => {
); );
}; };
AuthMediumLayout.propTypes = { MediumLayout.propTypes = {
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
}; };
export default AuthMediumLayout; export default MediumLayout;

View File

@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import messages from './messages'; import messages from './messages';
const AuthSmallLayout = ({ username }) => { const SmallLayout = ({ username }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
@@ -34,8 +34,8 @@ const AuthSmallLayout = ({ username }) => {
); );
}; };
AuthSmallLayout.propTypes = { SmallLayout.propTypes = {
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
}; };
export default AuthSmallLayout; export default SmallLayout;

View File

@@ -0,0 +1,3 @@
export { default as AuthLargeLayout } from './LargeLayout';
export { default as AuthMediumLayout } from './MediumLayout';
export { default as AuthSmallLayout } from './SmallLayout';

View File

@@ -1,17 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
'start.learning': { 'welcome.to.platform': {
id: 'start.learning', id: 'welcome.to.platform',
defaultMessage: 'Start learning', defaultMessage: 'Welcome to {siteName}, {username}!',
description: 'Header text for logistration MFE pages', description: 'Welcome message that appears on progressive profile page',
}, },
'with.site.name': {
id: 'with.site.name',
defaultMessage: 'with {siteName}',
description: 'Header text with site name for logistration MFE pages',
},
// authenticated user base component text
'complete.your.profile.1': { 'complete.your.profile.1': {
id: 'complete.your.profile.1', id: 'complete.your.profile.1',
defaultMessage: 'Complete', defaultMessage: 'Complete',
@@ -22,11 +16,6 @@ const messages = defineMessages({
defaultMessage: 'your profile', defaultMessage: 'your profile',
description: 'part of text "complete your profile"', description: 'part of text "complete your profile"',
}, },
'welcome.to.platform': {
id: 'welcome.to.platform',
defaultMessage: 'Welcome to {siteName}, {username}!',
description: 'Welcome message that appears on progressive profile page',
},
}); });
export default messages; export default messages;

View File

@@ -0,0 +1,4 @@
const IMAGE_LAYOUT = 'image-layout';
const DEFAULT_LAYOUT = 'default-layout';
export { DEFAULT_LAYOUT, IMAGE_LAYOUT };

View File

@@ -0,0 +1,87 @@
import React, { useEffect, useState } from 'react';
import { breakpoints } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import MediaQuery from 'react-responsive';
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './components/default-layout';
import {
ImageExtraSmallLayout, ImageLargeLayout, ImageMediumLayout, ImageSmallLayout,
} from './components/image-layout';
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
import { DEFAULT_LAYOUT, IMAGE_LAYOUT } from './data/constants';
const BaseContainer = ({ children, showWelcomeBanner, username }) => {
const [baseContainerVersion, setBaseContainerVersion] = useState(DEFAULT_LAYOUT);
useEffect(() => {
const initRebrandExperiment = () => {
if (window.experiments?.rebrandExperiment) {
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
} else {
window.experiments = window.experiments || {};
window.experiments.rebrandExperiment = {};
window.experiments.rebrandExperiment.handleLoaded = () => {
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
};
}
};
initRebrandExperiment();
}, []);
if (baseContainerVersion === IMAGE_LAYOUT) {
return (
<div className="layout">
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageExtraSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth - 1}>
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <ImageMediumLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <ImageLargeLayout />}
</MediaQuery>
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
{children}
</div>
</div>
);
}
return (
<>
<div className="col-md-12 extra-large-screen-top-stripe" />
<div className="layout">
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <DefaultSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <DefaultMediumLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <DefaultLargeLayout />}
</MediaQuery>
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
{children}
</div>
</div>
</>
);
};
BaseContainer.defaultProps = {
showWelcomeBanner: false,
username: null,
};
BaseContainer.propTypes = {
children: PropTypes.node.isRequired,
showWelcomeBanner: PropTypes.bool,
username: PropTypes.string,
};
export default BaseContainer;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { Context as ResponsiveContext } from 'react-responsive';
import BaseContainer from '../index';
const LargeScreen = {
wrappingComponent: ResponsiveContext.Provider,
wrappingComponentProps: { value: { width: 1200 } },
};
describe('Base component tests', () => {
it('should should default layout', () => {
const baseContainer = mount(
<IntlProvider locale="en">
<BaseContainer />
</IntlProvider>,
LargeScreen,
);
expect(baseContainer.find('.banner__image').exists()).toBeFalsy();
expect(baseContainer.find('.large-screen-svg-primary').exists()).toBeTruthy();
});
it('[experiment] should show image layout for treatment group', () => {
window.experiments = {
rebrandExperiment: {
variation: 'image-layout',
},
};
const baseContainer = mount(
<IntlProvider locale="en">
<BaseContainer />
</IntlProvider>,
LargeScreen,
);
expect(baseContainer.find('.banner__image').exists()).toBeTruthy();
});
});

View File

@@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';
import { PAGE_NOT_FOUND } from '../data/constants';
import { isHostAvailableInQueryParams } from '../data/utils';
/**
* This wrapper redirects the requester to embedded register page only if host
* query param is present.
*/
const EmbeddedRegistrationRoute = ({ children }) => {
const registrationEmbedded = isHostAvailableInQueryParams();
// Show registration page for embedded experience even if the user is authenticated
if (registrationEmbedded) {
return children;
}
return <Navigate to={PAGE_NOT_FOUND} replace />;
};
EmbeddedRegistrationRoute.propTypes = {
children: PropTypes.node.isRequired,
};
export default EmbeddedRegistrationRoute;

View File

@@ -4,13 +4,14 @@ import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { import {
Button, Form, Button, Form,
Icon,
} from '@edx/paragon'; } from '@edx/paragon';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'; import { Login } from '@edx/paragon/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import messages from './messages'; import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
/** /**
* This component renders the Single sign-on (SSO) button only for the tpa provider passed * This component renders the Single sign-on (SSO) button only for the tpa provider passed
@@ -47,16 +48,18 @@ const EnterpriseSSO = (props) => {
> >
{tpaProvider.iconImage ? ( {tpaProvider.iconImage ? (
<div aria-hidden="true"> <div aria-hidden="true">
<img className="icon-image" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} /> <img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span> <span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
</div> </div>
) )
: ( : (
<> <>
<div className="font-container" aria-hidden="true"> <div className="btn-tpa__font-container" aria-hidden="true">
<FontAwesomeIcon {SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? (
icon={SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? ['fab', tpaProvider.iconClass] : faSignInAlt} <FontAwesomeIcon icon={['fab', tpaProvider.iconClass]} />)
/> : (
<Icon className="h-75" src={Login} />
)}
</div> </div>
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span> <span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
</> </>

View File

@@ -27,7 +27,7 @@ const FormGroup = (props) => {
readOnly={props.readOnly} readOnly={props.readOnly}
type={props.type} type={props.type}
aria-invalid={props.errorMessage !== ''} aria-invalid={props.errorMessage !== ''}
className="form-field" className="form-group__form-field"
autoComplete={props.autoComplete} autoComplete={props.autoComplete}
spellCheck={props.spellCheck} spellCheck={props.spellCheck}
name={props.name} name={props.name}

View File

@@ -42,7 +42,7 @@ const InstitutionLogistration = props => {
<> <>
<div className="d-flex justify-content-left mb-4 mt-2"> <div className="d-flex justify-content-left mb-4 mt-2">
<div className="flex-column"> <div className="flex-column">
<h4 className="mb-2 font-weight-bold institute-heading"> <h4 className="mb-2 font-weight-bold institutions__heading">
{headingTitle} {headingTitle}
</h4> </h4>
<p className="mb-2"> <p className="mb-2">
@@ -57,7 +57,7 @@ const InstitutionLogistration = props => {
<tr key={provider} className="pgn__data-table-row"> <tr key={provider} className="pgn__data-table-row">
<td> <td>
<Hyperlink <Hyperlink
className="btn nav-item p-0 mb-1 secondary-provider-link" className="btn nav-item p-0 mb-1 institutions--provider-link"
destination={lmsBaseUrl + provider.loginUrl} destination={lmsBaseUrl + provider.loginUrl}
> >
{provider.name} {provider.name}

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { import {
@@ -9,33 +10,94 @@ import {
} from '@edx/paragon/icons'; } from '@edx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import messages from './messages'; import messages from './messages';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
import { validatePasswordField } from '../register/data/utils';
const PasswordField = (props) => { const PasswordField = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const dispatch = useDispatch();
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true); const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const handleBlur = (e) => { const handleBlur = (e) => {
if (props.handleBlur) { props.handleBlur(e); } const { name, value } = e.target;
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
return; // Do not run validations on password icon click
}
let passwordValue = value;
if (name === 'passwordIcon') {
// To validate actual password value when onBlur is triggered by focusing out the password icon
passwordValue = props.value;
}
if (props.handleBlur) {
props.handleBlur({
target: {
name: props.name,
value: passwordValue,
},
});
}
setShowTooltip(props.showRequirements && false); setShowTooltip(props.showRequirements && false);
if (props.handleErrorChange) { // If rendering from register page
const fieldError = validatePasswordField(passwordValue, formatMessage);
if (fieldError) {
props.handleErrorChange('password', fieldError);
} else if (!validationApiRateLimited) {
dispatch(fetchRealtimeValidations({ password: passwordValue }));
}
}
}; };
const handleFocus = (e) => { const handleFocus = (e) => {
if (e.target?.name === 'passwordIcon') {
return; // Do not clear error on password icon focus
}
if (props.handleFocus) { if (props.handleFocus) {
props.handleFocus(e); props.handleFocus(e);
} }
if (props.handleErrorChange) {
props.handleErrorChange('password', '');
dispatch(clearRegistrationBackendError('password'));
}
setTimeout(() => setShowTooltip(props.showRequirements && true), 150); setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
}; };
const HideButton = ( const HideButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} /> <IconButton
onFocus={handleFocus}
onBlur={handleBlur}
name="passwordIcon"
src={VisibilityOff}
iconAs={Icon}
onClick={setHiddenTrue}
size="sm"
variant="secondary"
alt={formatMessage(messages['hide.password'])}
/>
); );
const ShowButton = ( const ShowButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} /> <IconButton
onFocus={handleFocus}
onBlur={handleBlur}
name="passwordIcon"
src={Visibility}
iconAs={Icon}
onClick={setHiddenFalse}
size="sm"
variant="secondary"
alt={formatMessage(messages['show.password'])}
/>
); );
const placement = window.innerWidth < 768 ? 'top' : 'left'; const placement = window.innerWidth < 768 ? 'top' : 'left';
const tooltip = ( const tooltip = (
<Tooltip id={`password-requirement-${placement}`}> <Tooltip id={`password-requirement-${placement}`}>
@@ -59,7 +121,7 @@ const PasswordField = (props) => {
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}> <OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
<Form.Control <Form.Control
as="input" as="input"
className="form-field" className="form-group__form-field"
type={isPasswordHidden ? 'password' : 'text'} type={isPasswordHidden ? 'password' : 'text'}
name={props.name} name={props.name}
value={props.value} value={props.value}
@@ -89,6 +151,7 @@ PasswordField.defaultProps = {
handleBlur: null, handleBlur: null,
handleFocus: null, handleFocus: null,
handleChange: () => {}, handleChange: () => {},
handleErrorChange: null,
showRequirements: true, showRequirements: true,
autoComplete: null, autoComplete: null,
}; };
@@ -100,6 +163,7 @@ PasswordField.propTypes = {
handleBlur: PropTypes.func, handleBlur: PropTypes.func,
handleFocus: PropTypes.func, handleFocus: PropTypes.func,
handleChange: PropTypes.func, handleChange: PropTypes.func,
handleErrorChange: PropTypes.func,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
showRequirements: PropTypes.bool, showRequirements: PropTypes.bool,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS } from '../data/constants'; import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS,
} from '../data/constants';
import { setCookie } from '../data/utils'; import { setCookie } from '../data/utils';
const RedirectLogistration = (props) => { const RedirectLogistration = (props) => {
const { const {
authenticatedUser,
finishAuthUrl, finishAuthUrl,
redirectUrl, redirectUrl,
redirectToProgressiveProfilingPage, redirectToProgressiveProfilingPage,
@@ -35,15 +36,17 @@ const RedirectLogistration = (props) => {
if (redirectToProgressiveProfilingPage) { if (redirectToProgressiveProfilingPage) {
// TODO: Do we still need this cookie? // TODO: Do we still need this cookie?
setCookie('van-504-returning-user', true); setCookie('van-504-returning-user', true);
const registrationResult = { redirectUrl: finalRedirectUrl, success }; const registrationResult = { redirectUrl: finalRedirectUrl, success };
return ( return (
<Redirect to={{ <Navigate
pathname: AUTHN_PROGRESSIVE_PROFILING, to={AUTHN_PROGRESSIVE_PROFILING}
state: { state={{
registrationResult, registrationResult,
optionalFields, optionalFields,
}, authenticatedUser,
}} }}
replace
/> />
); );
} }
@@ -52,14 +55,14 @@ const RedirectLogistration = (props) => {
if (redirectToRecommendationsPage) { if (redirectToRecommendationsPage) {
const registrationResult = { redirectUrl: finalRedirectUrl, success }; const registrationResult = { redirectUrl: finalRedirectUrl, success };
return ( return (
<Redirect to={{ <Navigate
pathname: RECOMMENDATIONS, to={RECOMMENDATIONS}
state: { state={{
registrationResult, registrationResult,
educationLevel, educationLevel,
userId, userId,
}, }}
}} replace
/> />
); );
} }
@@ -71,6 +74,7 @@ const RedirectLogistration = (props) => {
}; };
RedirectLogistration.defaultProps = { RedirectLogistration.defaultProps = {
authenticatedUser: {},
educationLevel: null, educationLevel: null,
finishAuthUrl: null, finishAuthUrl: null,
success: false, success: false,
@@ -82,6 +86,7 @@ RedirectLogistration.defaultProps = {
}; };
RedirectLogistration.propTypes = { RedirectLogistration.propTypes = {
authenticatedUser: PropTypes.shape({}),
educationLevel: PropTypes.string, educationLevel: PropTypes.string,
finishAuthUrl: PropTypes.string, finishAuthUrl: PropTypes.string,
success: PropTypes.bool, success: PropTypes.bool,

View File

@@ -2,12 +2,13 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'; import { Icon } from '@edx/paragon';
import { Login } from '@edx/paragon/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import messages from './messages'; import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
const SocialAuthProviders = (props) => { const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -31,14 +32,16 @@ const SocialAuthProviders = (props) => {
> >
{provider.iconImage ? ( {provider.iconImage ? (
<div aria-hidden="true"> <div aria-hidden="true">
<img className="icon-image" src={provider.iconImage} alt={`icon ${provider.name}`} /> <img className="btn-tpa__image-icon" src={provider.iconImage} alt={`icon ${provider.name}`} />
</div> </div>
) )
: ( : (
<div className="font-container" aria-hidden="true"> <div className="btn-tpa__font-container" aria-hidden="true">
<FontAwesomeIcon {SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? (
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt} <FontAwesomeIcon icon={['fab', provider.iconClass]} />)
/> : (
<Icon className="h-75" src={Login} />
)}
</div> </div>
)} )}
<span id="provider-name" className="notranslate 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>

View File

@@ -5,8 +5,8 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon'; import { Alert } from '@edx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import messages from './messages'; import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
const ThirdPartyAuthAlert = (props) => { const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();

View File

@@ -1,16 +1,18 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { Route } from 'react-router-dom'; import PropTypes from 'prop-types';
import { DEFAULT_REDIRECT_URL } from '../data/constants'; import {
DEFAULT_REDIRECT_URL,
} from '../data/constants';
/** /**
* This wrapper redirects the requester to our default redirect url if they are * This wrapper redirects the requester to our default redirect url if they are
* already authenticated. * already authenticated.
*/ */
const UnAuthOnlyRoute = (props) => { const UnAuthOnlyRoute = ({ children }) => {
const [authUser, setAuthUser] = useState({}); const [authUser, setAuthUser] = useState({});
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
@@ -27,10 +29,14 @@ const UnAuthOnlyRoute = (props) => {
return null; return null;
} }
return <Route {...props} />; return children;
} }
return null; return null;
}; };
UnAuthOnlyRoute.propTypes = {
children: PropTypes.node.isRequired,
};
export default UnAuthOnlyRoute; export default UnAuthOnlyRoute;

View File

@@ -5,6 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import Zendesk from 'react-zendesk'; import Zendesk from 'react-zendesk';
import messages from './messages'; import messages from './messages';
import { REGISTER_EMBEDDED_PAGE } from '../data/constants';
const ZendeskHelp = () => { const ZendeskHelp = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -16,6 +17,9 @@ const ZendeskHelp = () => {
}, },
chat: { chat: {
suppress: false, suppress: false,
departments: {
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
},
}, },
contactForm: { contactForm: {
ticketForms: [ ticketForms: [
@@ -45,6 +49,10 @@ const ZendeskHelp = () => {
}, },
}; };
if (window.location.pathname === REGISTER_EMBEDDED_PAGE) {
return null;
}
return ( return (
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} /> <Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
); );

View File

@@ -1,11 +1,17 @@
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions'; import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
export const defaultState = { export const defaultState = {
fieldDescriptions: {}, fieldDescriptions: {
optionalFields: {}, fields: {},
},
optionalFields: {
fields: {},
extended_profile: [],
},
thirdPartyAuthApiStatus: null, thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: { thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null, currentProvider: null,
finishAuthUrl: null, finishAuthUrl: null,
countryCode: null, countryCode: null,
@@ -13,6 +19,7 @@ export const defaultState = {
secondaryProviders: [], secondaryProviders: [],
pipelineUserDetails: null, pipelineUserDetails: null,
errorMessage: null, errorMessage: null,
welcomePageRedirectUrl: null,
}, },
}; };
@@ -35,7 +42,11 @@ const reducer = (state = defaultState, action = {}) => {
case THIRD_PARTY_AUTH_CONTEXT.FAILURE: case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
return { return {
...state, ...state,
thirdPartyAuthApiStatus: COMPLETE_STATE, thirdPartyAuthApiStatus: FAILURE_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
}; };
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG: case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
return { return {

View File

@@ -1,7 +1,6 @@
import { logError } from '@edx/frontend-platform/logging'; import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects'; import { call, put, takeEvery } from 'redux-saga/effects';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
import { import {
getThirdPartyAuthContextBegin, getThirdPartyAuthContextBegin,
getThirdPartyAuthContextFailure, getThirdPartyAuthContextFailure,
@@ -11,6 +10,7 @@ import {
import { import {
getThirdPartyAuthContext, getThirdPartyAuthContext,
} from './service'; } from './service';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
export function* fetchThirdPartyAuthContext(action) { export function* fetchThirdPartyAuthContext(action) {
try { try {

View File

@@ -18,8 +18,8 @@ export async function getThirdPartyAuthContext(urlParams) {
throw (e); throw (e);
}); });
return { return {
fieldDescriptions: data.registrationFields || data.registration_fields, fieldDescriptions: data.registrationFields || {},
optionalFields: data.optionalFields || data.optional_fields, optionalFields: data.optionalFields || {},
thirdPartyAuthContext: data.contextData || data.context_data, thirdPartyAuthContext: data.contextData || {},
}; };
} }

View File

@@ -58,7 +58,7 @@ describe('common components reducer', () => {
providers: [], providers: [],
secondaryProviders: [], secondaryProviders: [],
pipelineUserDetails: null, pipelineUserDetails: null,
errorMessage: 'An error occured', errorMessage: 'An error occurred',
}, },
}; };

View File

@@ -1,5 +1,6 @@
export { default as RedirectLogistration } from './RedirectLogistration'; export { default as RedirectLogistration } from './RedirectLogistration';
export { default as registerIcons } from './RegisterFaIcons'; export { default as registerIcons } from './RegisterFaIcons';
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute'; export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
export { default as NotFoundPage } from './NotFoundPage'; export { default as NotFoundPage } from './NotFoundPage';
export { default as SocialAuthProviders } from './SocialAuthProviders'; export { default as SocialAuthProviders } from './SocialAuthProviders';
@@ -11,5 +12,4 @@ export { default as saga } from './data/sagas';
export { storeName } from './data/selectors'; export { storeName } from './data/selectors';
export { default as FormGroup } from './FormGroup'; export { default as FormGroup } from './FormGroup';
export { default as PasswordField } from './PasswordField'; export { default as PasswordField } from './PasswordField';
export { default as Logistration } from './Logistration';
export { default as Zendesk } from './Zendesk'; export { default as Zendesk } from './Zendesk';

View File

@@ -0,0 +1,72 @@
/* eslint-disable import/no-import-module-exports */
/* eslint-disable react/function-component-definition */
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { REGISTER_EMBEDDED_PAGE } from '../../data/constants';
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
const RRD = require('react-router-dom');
// Just render plain div with its children
// eslint-disable-next-line react/prop-types
RRD.BrowserRouter = ({ children }) => <div>{ children }</div>;
module.exports = RRD;
const TestApp = () => (
<Router>
<div>
<Routes>
<Route
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><span>Embedded Register Page</span></EmbeddedRegistrationRoute>}
/>
</Routes>
</div>
</Router>
);
describe('EmbeddedRegistrationRoute', () => {
const routerWrapper = () => (
<MemoryRouter initialEntries={[REGISTER_EMBEDDED_PAGE]}>
<TestApp />
</MemoryRouter>
);
afterEach(() => {
jest.clearAllMocks();
});
it('should not render embedded register page if host query param is not available in the url', async () => {
let embeddedRegistrationPage = null;
await act(async () => {
embeddedRegistrationPage = await mount(routerWrapper());
});
expect(embeddedRegistrationPage.find('span').exists()).toBeFalsy();
});
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
delete window.location;
window.location = {
href: getConfig().BASE_URL.concat(REGISTER_EMBEDDED_PAGE),
search: '?host=http://localhost/host-websit',
};
let embeddedRegistrationPage = null;
await act(async () => {
embeddedRegistrationPage = await mount(routerWrapper());
});
expect(embeddedRegistrationPage.find('span').exists()).toBeTruthy();
expect(embeddedRegistrationPage.find('span').text()).toBe('Embedded Register Page');
});
});

View File

@@ -1,9 +1,13 @@
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { fetchRealtimeValidations } from '../../register/data/actions';
import FormGroup from '../FormGroup'; import FormGroup from '../FormGroup';
import PasswordField from '../PasswordField'; import PasswordField from '../PasswordField';
@@ -26,10 +30,27 @@ describe('FormGroup', () => {
}); });
describe('PasswordField', () => { describe('PasswordField', () => {
const mockStore = configureStore();
const IntlPasswordField = injectIntl(PasswordField); const IntlPasswordField = injectIntl(PasswordField);
let props = {}; let props = {};
let store = {};
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
const initialState = {
register: {
validationApiRateLimited: false,
},
};
beforeEach(() => { beforeEach(() => {
store = mockStore(initialState);
props = { props = {
floatingLabel: 'Password', floatingLabel: 'Password',
name: 'password', name: 'password',
@@ -39,7 +60,7 @@ describe('PasswordField', () => {
}); });
it('should show/hide password on icon click', () => { it('should show/hide password on icon click', () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>); const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('click'); passwordField.find('button[aria-label="Show password"]').simulate('click');
expect(passwordField.find('input').prop('type')).toEqual('text'); expect(passwordField.find('input').prop('type')).toEqual('text');
@@ -49,7 +70,7 @@ describe('PasswordField', () => {
}); });
it('should show password requirement tooltip on focus', async () => { it('should show password requirement tooltip on focus', async () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>); const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
jest.useFakeTimers(); jest.useFakeTimers();
await act(async () => { await act(async () => {
passwordField.find('input').simulate('focus'); passwordField.find('input').simulate('focus');
@@ -67,7 +88,7 @@ describe('PasswordField', () => {
}; };
jest.useFakeTimers(); jest.useFakeTimers();
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>); const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
await act(async () => { await act(async () => {
passwordField.find('input').simulate('focus'); passwordField.find('input').simulate('focus');
jest.runAllTimers(); jest.runAllTimers();
@@ -80,7 +101,7 @@ describe('PasswordField', () => {
}); });
it('should update password requirement checks', async () => { it('should update password requirement checks', async () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>); const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
jest.useFakeTimers(); jest.useFakeTimers();
await act(async () => { await act(async () => {
passwordField.find('input').simulate('focus'); passwordField.find('input').simulate('focus');
@@ -92,4 +113,142 @@ describe('PasswordField', () => {
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1'); expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1'); expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
}); });
it('should not run validations when blur is fired on password icon click', () => {
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
target: {
name: 'password',
value: 'invalid',
},
relatedTarget: {
name: 'passwordIcon',
},
});
expect(passwordField.find('div[feedback-for="password"]').exists()).toBeFalsy();
});
it('should call props handle blur if available', () => {
props = {
...props,
handleBlur: jest.fn(),
};
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('input#password').simulate('blur', {
target: {
name: 'password',
value: '',
},
});
expect(props.handleBlur).toHaveBeenCalledTimes(1);
});
it('should run validations on blur event when rendered from register page', () => {
props = {
...props,
handleErrorChange: jest.fn(),
};
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('input#password').simulate('blur', {
target: {
name: 'password',
value: '',
},
});
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'password',
'Password criteria has not been met',
);
});
it('should not clear error when focus is fired on password icon click when rendered from register page', () => {
props = {
...props,
handleErrorChange: jest.fn(),
};
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
target: {
name: 'passwordIcon',
value: '',
},
});
expect(props.handleErrorChange).toHaveBeenCalledTimes(0);
});
it('should clear error when focus is fired on password icon click when rendered from register page', () => {
props = {
...props,
handleErrorChange: jest.fn(),
};
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
target: {
name: 'password',
value: 'invalid',
},
});
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'password',
'',
);
});
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
store.dispatch = jest.fn(store.dispatch);
props = {
...props,
handleErrorChange: jest.fn(),
};
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
target: {
name: 'password',
value: 'password123',
},
});
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ password: 'password123' }));
});
it('should use password value from prop when password icon is focused out (blur due to icon)', () => {
store.dispatch = jest.fn(store.dispatch);
props = {
...props,
value: 'testPassword',
handleErrorChange: jest.fn(),
handleBlur: jest.fn(),
};
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
target: {
name: 'passwordIcon',
value: undefined,
},
});
expect(props.handleBlur).toHaveBeenCalledTimes(1);
expect(props.handleBlur).toHaveBeenCalledWith({
target: {
name: 'password',
value: 'testPassword',
},
});
});
}); });

View File

@@ -2,15 +2,21 @@
/* eslint-disable react/function-component-definition */ /* eslint-disable react/function-component-definition */
import React from 'react'; import React from 'react';
import * as auth from '@edx/frontend-platform/auth'; import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { UnAuthOnlyRoute } from '..'; import { UnAuthOnlyRoute } from '..';
import { LOGIN_PAGE } from '../../data/constants'; import { REGISTER_PAGE } from '../../data/constants';
import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom'; import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
jest.mock('@edx/frontend-platform/auth'); jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
fetchAuthenticatedUser: jest.fn(),
}));
const RRD = require('react-router-dom'); const RRD = require('react-router-dom');
// Just render plain div with its children // Just render plain div with its children
@@ -21,16 +27,16 @@ module.exports = RRD;
const TestApp = () => ( const TestApp = () => (
<Router> <Router>
<div> <div>
<Switch> <Routes>
<UnAuthOnlyRoute path={LOGIN_PAGE} render={() => (<span>Login Page</span>)} /> <Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><span>Register Page</span></UnAuthOnlyRoute>} />
</Switch> </Routes>
</div> </div>
</Router> </Router>
); );
describe('UnAuthOnlyRoute', () => { describe('UnAuthOnlyRoute', () => {
const routerWrapper = () => ( const routerWrapper = () => (
<MemoryRouter initialEntries={[LOGIN_PAGE]}> <MemoryRouter initialEntries={[REGISTER_PAGE]}>
<TestApp /> <TestApp />
</MemoryRouter> </MemoryRouter>
); );
@@ -39,25 +45,30 @@ describe('UnAuthOnlyRoute', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it('should have called with forceRefresh true', () => { it('should have called with forceRefresh true', async () => {
const user = { const user = {
username: 'gonzo', username: 'gonzo',
other: 'data', other: 'data',
}; };
auth.getAuthenticatedUser = jest.fn(() => user);
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() }));
mount(routerWrapper()); getAuthenticatedUser.mockReturnValue(user);
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(user));
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true }); await act(async () => {
await mount(routerWrapper());
});
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
}); });
it('should have called with forceRefresh false', () => { it('should have called with forceRefresh false', async () => {
auth.getAuthenticatedUser = jest.fn(() => null); getAuthenticatedUser.mockReturnValue(null);
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() })); fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(null));
mount(routerWrapper()); await act(async () => {
await mount(routerWrapper());
});
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false }); expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
}); });
}); });

View File

@@ -10,25 +10,27 @@ exports[`SocialAuthProviders should match social auth provider with default icon
> >
<div <div
aria-hidden="true" aria-hidden="true"
className="font-container" className="btn-tpa__font-container"
> >
<svg <span
aria-hidden="true" className="pgn__icon h-75"
className="svg-inline--fa fa-right-to-bracket "
data-icon="right-to-bracket"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
> >
<path <svg
d="M352 96h64c17.7 0 32 14.3 32 32V384c0 17.7-14.3 32-32 32H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h64c53 0 96-43 96-96V128c0-53-43-96-96-96H352c-17.7 0-32 14.3-32 32s14.3 32 32 32zm-7.5 177.4c4.8-4.5 7.5-10.8 7.5-17.4s-2.7-12.9-7.5-17.4l-144-136c-7-6.6-17.2-8.4-26-4.6s-14.5 12.5-14.5 22v72H32c-17.7 0-32 14.3-32 32v64c0 17.7 14.3 32 32 32H160v72c0 9.6 5.7 18.2 14.5 22s19 2 26-4.6l144-136z" aria-hidden={true}
fill="currentColor" fill="none"
style={Object {}} focusable={false}
/> height={24}
</svg> role="img"
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7 9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h10V3H12v2h8v14z"
fill="currentColor"
/>
</svg>
</span>
</div> </div>
<span <span
aria-hidden="true" aria-hidden="true"
@@ -55,7 +57,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
> >
<div <div
aria-hidden="true" aria-hidden="true"
className="font-container" className="btn-tpa__font-container"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -104,7 +106,7 @@ Array [
> >
<img <img
alt="icon Apple" alt="icon Apple"
className="icon-image" className="btn-tpa__image-icon"
src="https://edx.devstack.lms/logo.png" src="https://edx.devstack.lms/logo.png"
/> />
</div> </div>
@@ -133,7 +135,7 @@ Array [
> >
<img <img
alt="icon Facebook" alt="icon Facebook"
className="icon-image" className="btn-tpa__image-icon"
src="https://edx.devstack.lms/facebook-logo.png" src="https://edx.devstack.lms/facebook-logo.png"
/> />
</div> </div>

View File

@@ -20,6 +20,17 @@ exports[`Zendesk Help should match login page third party auth alert message sna
}, },
}, },
"chat": Object { "chat": Object {
"departments": Object {
"enabled": Array [
"account settings",
"billing and payments",
"certificates",
"deadlines",
"errors and technical issues",
"other",
"proctoring",
],
},
"suppress": false, "suppress": false,
}, },
"contactForm": Object { "contactForm": Object {

View File

@@ -2,13 +2,11 @@ const configuration = {
// Cookies related configs // Cookies related configs
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN, SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null, REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME || null,
// Features // Features
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '', DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
ENABLE_COOKIE_POLICY_BANNER: process.env.ENABLE_COOKIE_POLICY_BANNER || false,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false, ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false, ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
ENABLE_PERSONALIZED_RECOMMENDATIONS: process.env.ENABLE_PERSONALIZED_RECOMMENDATIONS || false, ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '', MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false, SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
// Links // Links
@@ -16,14 +14,24 @@ const configuration = {
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null, AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null,
LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null, LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null,
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null, PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
POST_REGISTRATION_REDIRECT_URL: process.env.POST_REGISTRATION_REDIRECT_URL || '',
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null, PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null, TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
TOS_LINK: process.env.TOS_LINK || null, TOS_LINK: process.env.TOS_LINK || null,
// Miscellaneous // Base container images
BANNER_IMAGE_LARGE: process.env.BANNER_IMAGE_LARGE || '',
BANNER_IMAGE_MEDIUM: process.env.BANNER_IMAGE_MEDIUM || '',
BANNER_IMAGE_SMALL: process.env.BANNER_IMAGE_SMALL || '',
BANNER_IMAGE_EXTRA_SMALL: process.env.BANNER_IMAGE_EXTRA_SMALL || '',
// Recommendation constants
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]', GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
// Miscellaneous
INFO_EMAIL: process.env.INFO_EMAIL || '', INFO_EMAIL: process.env.INFO_EMAIL || '',
ZENDESK_KEY: process.env.ZENDESK_KEY, ZENDESK_KEY: process.env.ZENDESK_KEY,
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL, ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
}; };
export default configuration; export default configuration;

20
src/data/algolia.js Normal file
View File

@@ -0,0 +1,20 @@
import { getConfig } from '@edx/frontend-platform';
import algoliasearch from 'algoliasearch';
// initialize Algolia workers
const initializeSearchClient = () => algoliasearch(
getConfig().ALGOLIA_APP_ID,
getConfig().ALGOLIA_SEARCH_API_KEY,
);
const getLocationRestrictionFilter = (userCountry) => {
if (userCountry) {
return `NOT blocked_in:"${userCountry}" AND (allowed_in:"null" OR allowed_in:"${userCountry}")`;
}
return '';
};
export {
initializeSearchClient,
getLocationRestrictionFilter,
};

View File

@@ -1,6 +1,7 @@
// URL Paths // URL Paths
export const LOGIN_PAGE = '/login'; export const LOGIN_PAGE = '/login';
export const REGISTER_PAGE = '/register'; export const REGISTER_PAGE = '/register';
export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
export const RESET_PAGE = '/reset'; export const RESET_PAGE = '/reset';
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome'; export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
export const DEFAULT_REDIRECT_URL = '/dashboard'; export const DEFAULT_REDIRECT_URL = '/dashboard';
@@ -23,16 +24,16 @@ export const PENDING_STATE = 'pending';
export const COMPLETE_STATE = 'complete'; export const COMPLETE_STATE = 'complete';
export const FAILURE_STATE = 'failure'; export const FAILURE_STATE = 'failure';
export const FORBIDDEN_STATE = 'forbidden'; export const FORBIDDEN_STATE = 'forbidden';
export const EMBEDDED = 'embedded';
// Regex export const LETTER_REGEX = /[a-zA-Z]/;
export const NUMBER_REGEX = /\d/;
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*' export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"' + '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})' + ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$'; + '|\\[(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 INVALID_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 // Query string parameters that can be passed to LMS to manage
// things like auto-enrollment upon login and registration. // things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free', 'track', 'is_account_recovery']; export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
export const REDIRECT = 'redirect';

3
src/data/oneTrust.js Normal file
View File

@@ -0,0 +1,3 @@
const isOneTrustFunctionalCookieEnabled = () => !!window?.OnetrustActiveGroups?.includes('C0003');
export default isOneTrustFunctionalCookieEnabled;

View File

@@ -4,8 +4,14 @@ import {
const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY; const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY;
const optimizely = createInstance({ const getOptimizelyInstance = () => {
sdkKey: OPTIMIZELY_SDK_KEY, if (OPTIMIZELY_SDK_KEY) {
}); return createInstance({
sdkKey: OPTIMIZELY_SDK_KEY,
});
}
export default optimizely; return null;
};
export default getOptimizelyInstance();

View File

@@ -0,0 +1,16 @@
import { getLocationRestrictionFilter } from '../algolia';
describe('algoliaUtilsTests', () => {
it('test getLocationRestrictionFilter returns filter if country is passed', () => {
const countryCode = 'PK';
const filter = getLocationRestrictionFilter(countryCode);
const expectedFilter = `NOT blocked_in:"${countryCode}" AND (allowed_in:"null" OR allowed_in:"${countryCode}")`;
expect(filter).toEqual(expectedFilter);
});
it('test getLocationRestrictionFilter returns empty string if country is not passed', () => {
const countryCode = '';
const filter = getLocationRestrictionFilter(countryCode);
const expectedFilter = '';
expect(filter).toEqual(expectedFilter);
});
});

View File

@@ -1,5 +1,5 @@
import { LOGIN_PAGE } from '../constants'; import { LOGIN_PAGE } from '../constants';
import { updatePathWithQueryParams } from './dataUtils'; import { updatePathWithQueryParams } from '../utils/dataUtils';
describe('updatePathWithQueryParams', () => { describe('updatePathWithQueryParams', () => {
it('should append query params into the path', () => { it('should append query params into the path', () => {

View File

@@ -1,4 +1,4 @@
import AsyncActionType from './reduxUtils'; import AsyncActionType from '../utils/reduxUtils';
describe('AsyncActionType', () => { describe('AsyncActionType', () => {
it('should return well formatted action strings', () => { it('should return well formatted action strings', () => {

View File

@@ -1,7 +1,7 @@
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import Cookies from 'universal-cookie'; import Cookies from 'universal-cookie';
export function setCookie(cookieName, cookieValue, cookieExpiry) { export default function setCookie(cookieName, cookieValue, cookieExpiry) {
const cookies = new Cookies(); const cookies = new Cookies();
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' }; const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
if (cookieExpiry) { if (cookieExpiry) {
@@ -9,13 +9,3 @@ export function setCookie(cookieName, cookieValue, cookieExpiry) {
} }
cookies.set(cookieName, cookieValue, options); cookies.set(cookieName, cookieValue, options);
} }
export default function setSurveyCookie(surveyType) {
const cookieName = getConfig().USER_SURVEY_COOKIE_NAME;
if (cookieName) {
const signupTimestamp = (new Date()).getTime();
// set expiry to exactly 24 hours from now
const cookieExpiry = new Date(signupTimestamp + 1 * 864e5);
setCookie(cookieName, surveyType, cookieExpiry);
}
}

View File

@@ -76,3 +76,8 @@ export const windowScrollTo = (options) => {
return window.scrollTo(options.top, options.left); return window.scrollTo(options.top, options.left);
}; };
export const isHostAvailableInQueryParams = () => {
const queryParams = getAllPossibleQueryParams();
return 'host' in queryParams;
};

View File

@@ -1,10 +1,11 @@
export { export {
getTpaProvider, getTpaProvider,
getTpaHint, getTpaHint,
updatePathWithQueryParams,
getAllPossibleQueryParams, getAllPossibleQueryParams,
getActivationStatus, getActivationStatus,
isHostAvailableInQueryParams,
updatePathWithQueryParams,
windowScrollTo, windowScrollTo,
} from './dataUtils'; } from './dataUtils';
export { default as AsyncActionType } from './reduxUtils'; export { default as AsyncActionType } from './reduxUtils';
export { default as setSurveyCookie, setCookie } from './cookies'; export { default as setCookie } from './cookies';

View File

@@ -25,7 +25,7 @@ describe('FieldRendererTests', () => {
type: 'select', type: 'select',
label: 'Year of Birth', label: 'Year of Birth',
name: 'yob-field', name: 'yob-field',
options: [['1997', 1997], ['1998', 1998]], options: [['1997', '1997'], ['1998', '1998']],
}; };
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />); const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);

View File

@@ -6,11 +6,11 @@ import { Alert } from '@edx/paragon';
import { CheckCircle, Error } from '@edx/paragon/icons'; import { CheckCircle, Error } from '@edx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import messages from './messages';
import { import {
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR, COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
} from '../data/constants'; } from '../data/constants';
import { PASSWORD_RESET } from '../reset-password/data/constants'; import { PASSWORD_RESET } from '../reset-password/data/constants';
import messages from './messages';
const ForgotPasswordAlert = (props) => { const ForgotPasswordAlert = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();

View File

@@ -15,16 +15,16 @@ import {
import { ChevronLeft } from '@edx/paragon/icons'; import { ChevronLeft } from '@edx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Redirect } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { BaseComponent } from '../base-component';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import { forgotPassword, setForgotPasswordFormData } from './data/actions'; import { forgotPassword, setForgotPasswordFormData } from './data/actions';
import { forgotPasswordResultSelector } from './data/selectors'; import { forgotPasswordResultSelector } from './data/selectors';
import ForgotPasswordAlert from './ForgotPasswordAlert'; import ForgotPasswordAlert from './ForgotPasswordAlert';
import messages from './messages'; import messages from './messages';
import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
const ForgotPasswordPage = (props) => { const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME; const platformName = getConfig().SITE_NAME;
@@ -38,7 +38,7 @@ const ForgotPasswordPage = (props) => {
const [bannerEmail, setBannerEmail] = useState(''); const [bannerEmail, setBannerEmail] = useState('');
const [formErrors, setFormErrors] = useState(''); const [formErrors, setFormErrors] = useState('');
const [validationError, setValidationError] = useState(emailValidationError); const [validationError, setValidationError] = useState(emailValidationError);
const [key, setKey] = useState(''); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
sendPageEvent('login_and_registration', 'reset'); sendPageEvent('login_and_registration', 'reset');
@@ -95,19 +95,16 @@ const ForgotPasswordPage = (props) => {
); );
return ( return (
<BaseComponent> <BaseContainer>
<Helmet> <Helmet>
<title>{formatMessage(messages['forgot.password.page.title'], <title>{formatMessage(messages['forgot.password.page.title'],
{ siteName: getConfig().SITE_NAME })} { siteName: getConfig().SITE_NAME })}
</title> </title>
</Helmet> </Helmet>
<div> <div>
<Tabs activeKey="" id="controlled-tab" onSelect={(k) => setKey(k)}> <Tabs activeKey="" id="controlled-tab" onSelect={(key) => navigate(updatePathWithQueryParams(key))}>
<Tab title={tabTitle} eventKey={LOGIN_PAGE} /> <Tab title={tabTitle} eventKey={LOGIN_PAGE} />
</Tabs> </Tabs>
{ key && (
<Redirect to={updatePathWithQueryParams(key)} />
)}
<div id="main-content" className="main-content"> <div id="main-content" className="main-content">
<Form id="forget-password-form" name="forget-password-form" className="mw-xs"> <Form id="forget-password-form" name="forget-password-form" className="mw-xs">
<ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} /> <ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} />
@@ -133,7 +130,7 @@ const ForgotPasswordPage = (props) => {
name="submit-forget-password" name="submit-forget-password"
type="submit" type="submit"
variant="brand" variant="brand"
className="forgot-password-button-width" className="forgot-password--button"
state={submitState} state={submitState}
labels={{ labels={{
default: formatMessage(messages['forgot.password.page.submit.button']), default: formatMessage(messages['forgot.password.page.submit.button']),
@@ -163,7 +160,7 @@ const ForgotPasswordPage = (props) => {
</Form> </Form>
</div> </div>
</div> </div>
</BaseComponent> </BaseContainer>
); );
}; };

View File

@@ -1,6 +1,6 @@
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants'; import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions'; import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
export const defaultState = { export const defaultState = {
status: '', status: '',

View File

@@ -1,15 +1,11 @@
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { mergeConfig } from '@edx/frontend-platform'; import { mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { MemoryRouter, Router } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { INTERNAL_SERVER_ERROR, LOGIN_PAGE } from '../../data/constants'; import { INTERNAL_SERVER_ERROR, LOGIN_PAGE } from '../../data/constants';
@@ -17,14 +13,20 @@ import { PASSWORD_RESET } from '../../reset-password/data/constants';
import { setForgotPasswordFormData } from '../data/actions'; import { setForgotPasswordFormData } from '../data/actions';
import ForgotPasswordPage from '../ForgotPasswordPage'; import ForgotPasswordPage from '../ForgotPasswordPage';
jest.mock('@edx/frontend-platform/analytics'); const mockedNavigator = jest.fn();
jest.mock('@edx/frontend-platform/auth');
analytics.sendPageEvent = jest.fn(); jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),
useNavigate: () => mockedNavigator,
}));
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage); const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
const mockStore = configureStore(); const mockStore = configureStore();
const history = createMemoryHistory();
const initialState = { const initialState = {
forgotPassword: { forgotPassword: {
@@ -51,7 +53,12 @@ describe('ForgotPasswordPage', () => {
beforeEach(() => { beforeEach(() => {
store = mockStore(initialState); store = mockStore(initialState);
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'test-user' })); jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({
userId: 3,
username: 'test-user',
})),
}));
configure({ configure({
loggingService: { logError: jest.fn() }, loggingService: { logError: jest.fn() },
config: { config: {
@@ -191,11 +198,6 @@ describe('ForgotPasswordPage', () => {
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false); expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
}); });
it('check cookie rendered', () => {
const forgotPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
expect(forgotPage.find(<CookiePolicyBanner />)).toBeTruthy();
});
it('should display success message after email is sent', () => { it('should display success message after email is sent', () => {
store = mockStore({ store = mockStore({
...initialState, ...initialState,
@@ -227,15 +229,11 @@ describe('ForgotPasswordPage', () => {
}); });
it('should redirect onto login page', async () => { it('should redirect onto login page', async () => {
const forgotPasswordPage = mount(reduxWrapper( const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
<Router history={history}>
<IntlForgotPasswordPage {...props} />
</Router>,
));
await act(async () => { await forgotPasswordPage.find('nav').find('a').first().simulate('click'); }); await act(async () => { await forgotPasswordPage.find('nav').find('a').first().simulate('click'); });
forgotPasswordPage.update(); forgotPasswordPage.update();
expect(history.location.pathname).toEqual(LOGIN_PAGE); expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE);
}); });
}); });

View File

@@ -1,41 +1,41 @@
import arMessages from './messages/ar.json'; import { messages as paragonMessages } from '@edx/paragon';
import caMessages from './messages/ca.json';
// no need to import en messages-- they are in the defaultMessage field
import dedeMessages from './messages/de_DE.json';
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import heMessages from './messages/he.json';
import hiMessages from './messages/hi.json';
import idMessages from './messages/id.json';
import ititMessages from './messages/it_IT.json';
import kokrMessages from './messages/ko_kr.json';
import plMessages from './messages/pl.json';
import ptbrMessages from './messages/pt_br.json';
import ptptMessages from './messages/pt_PT.json';
import ruMessages from './messages/ru.json';
import thMessages from './messages/th.json';
import ukMessages from './messages/uk.json';
import zhcnMessages from './messages/zh_CN.json';
const messages = { import arMessages from './messages/ar.json';
import deMessages from './messages/de.json';
import deDEMessages from './messages/de_DE.json';
import es419Messages from './messages/es_419.json';
import faIRMessages from './messages/fa_IR.json';
import frMessages from './messages/fr.json';
import frCAMessages from './messages/fr_CA.json';
import hiMessages from './messages/hi.json';
import itMessages from './messages/it.json';
import itITMessages from './messages/it_IT.json';
import ptMessages from './messages/pt.json';
import ptPTMessages from './messages/pt_PT.json';
import ruMessages from './messages/ru.json';
import ukMessages from './messages/uk.json';
import zhCNMessages from './messages/zh_CN.json';
// no need to import en messages-- they are in the defaultMessage field
const appMessages = {
ar: arMessages, ar: arMessages,
es: es419Messages, // Prospectus uses es language code for spanish, added `es` option and pointed to es-419 strings. de: deMessages,
'de-de': deDEMessages,
'es-419': es419Messages, 'es-419': es419Messages,
'fa-ir': faIRMessages,
fr: frMessages, fr: frMessages,
'zh-cn': zhcnMessages, 'fr-ca': frCAMessages,
'it-it': ititMessages,
'pt-pt': ptptMessages,
'de-de': dedeMessages,
ca: caMessages,
he: heMessages,
id: idMessages,
'ko-kr': kokrMessages,
pl: plMessages,
'pt-br': ptbrMessages,
ru: ruMessages,
th: thMessages,
uk: ukMessages,
hi: hiMessages, hi: hiMessages,
it: itMessages,
'it-it': itITMessages,
pt: ptMessages,
'pt-pt': ptPTMessages,
ru: ruMessages,
uk: ukMessages,
'zh-cn': zhCNMessages,
}; };
export default messages; export default [
paragonMessages,
appMessages,
];

View File

@@ -1,15 +1,17 @@
{ {
"start.learning": "ابدأ التعلم ", "start.learning": "ابدأ التعلم ",
"with.site.name": "مع {siteName}", "with.site.name": "مع {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "أهلا بك {username} في {siteName}",
"complete.your.profile.1": "أكمل", "complete.your.profile.1": "أكمل",
"complete.your.profile.2": "ملفك الشخصي", "complete.your.profile.2": "ملفك الشخصي",
"welcome.to.platform": "أهلا بك {username} في {siteName}",
"institution.login.page.sub.heading": "اختر مؤسستك من القائمة أدناه", "institution.login.page.sub.heading": "اختر مؤسستك من القائمة أدناه",
"logistration.sign.in": "تسجيل الدخول", "logistration.sign.in": "تسجيل الدخول",
"logistration.register": "التسجيل", "logistration.register": "التسجيل",
"enterprisetpa.title.heading": "هل ترغب في تسجيل الدخول باستخدام بيانات {providerName} الخاصة بك؟", "enterprisetpa.title.heading": "هل ترغب في تسجيل الدخول باستخدام بيانات {providerName} الخاصة بك؟",
"enterprisetpa.login.button.text": "أرِني وسائل أخرى لتسجيل الدخول أو للتسجيل", "enterprisetpa.login.button.text": "أرِني وسائل أخرى لتسجيل الدخول أو للتسجيل",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in", "enterprisetpa.login.button.text.public.account.creation.disabled": "أرني طرقًا أخرى لتسجيل الدخول",
"sso.sign.in.with": "تسجيل الدخول باستخدام {providerName}", "sso.sign.in.with": "تسجيل الدخول باستخدام {providerName}",
"sso.create.account.using": "إنشاء حساب باستخدام {providerName}", "sso.create.account.using": "إنشاء حساب باستخدام {providerName}",
"show.password": "إظهار كلمة المرور", "show.password": "إظهار كلمة المرور",
@@ -23,7 +25,7 @@
"register.third.party.auth.account.not.linked": "لقد سجلت دخولك بنجاح إلى {currentProvider}! نحتاج فقط قليلاً بعدُ من المعلومات قبل أن تبدأ التعلم مع {platformName}.", "register.third.party.auth.account.not.linked": "لقد سجلت دخولك بنجاح إلى {currentProvider}! نحتاج فقط قليلاً بعدُ من المعلومات قبل أن تبدأ التعلم مع {platformName}.",
"registration.using.tpa.form.heading": "إتمام إنشاء حسابك", "registration.using.tpa.form.heading": "إتمام إنشاء حسابك",
"zendesk.supportTitle": "edX Support", "zendesk.supportTitle": "edX Support",
"zendesk.selectTicketForm": "Please choose your request type:", "zendesk.selectTicketForm": "الرجاء اختيار نوع الطلب الخاص بك:",
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.", "error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
"forgot.password.confirmation.message": "لقد أرسلنا بريدًا إلكترونيًا إلى {email} به إرشادات لإعادة ضبط كلمة المرور الخاصة بك. إن لم تستلم رسالة إعادة ضبط كلمة المرور بعد دقيقة واحدة، فتحقق من إدخال عنوان البريد الإلكتروني الصحيح، أو تفقد مجلد الرسائل غير المرغوب فيها. إن احتجت مزيدًا من المساعدة، {supportLink}.", "forgot.password.confirmation.message": "لقد أرسلنا بريدًا إلكترونيًا إلى {email} به إرشادات لإعادة ضبط كلمة المرور الخاصة بك. إن لم تستلم رسالة إعادة ضبط كلمة المرور بعد دقيقة واحدة، فتحقق من إدخال عنوان البريد الإلكتروني الصحيح، أو تفقد مجلد الرسائل غير المرغوب فيها. إن احتجت مزيدًا من المساعدة، {supportLink}.",
"forgot.password.page.title": "نسيت كلمة المرور | {siteName}", "forgot.password.page.title": "نسيت كلمة المرور | {siteName}",
@@ -96,20 +98,30 @@
"password.security.block.body": "اكتشف نظامنا أن كلمة مرورك صعيفة. غيّر كلمة مرورك حتى يظل حسابك آمنًا.", "password.security.block.body": "اكتشف نظامنا أن كلمة مرورك صعيفة. غيّر كلمة مرورك حتى يظل حسابك آمنًا.",
"password.security.close.button": "إغلاق", "password.security.close.button": "إغلاق",
"password.security.redirect.to.reset.password.button": "إعادة ضبط كلمة المرور", "password.security.redirect.to.reset.password.button": "إعادة ضبط كلمة المرور",
"progressive.profiling.page.title": "Welcome | {siteName}", "login.tpa.authentication.failure": "عذرًا ، غير مصرح لك بالوصول إلى {platform_name} عبر هذه القناة. يرجى الاتصال بمسؤول التعلم أو المدير من أجل الوصول إلى {platform_name}. {lineBreak} {lineBreak} تفاصيل الخطأ: {lineBreak} {errorMessage}",
"progressive.profiling.page.title": "مرحبا بكم | {siteName}",
"progressive.profiling.page.heading": "بعض الأسئلة الموجهة لك ستساعدنا كي نزداد ذكاءً.", "progressive.profiling.page.heading": "بعض الأسئلة الموجهة لك ستساعدنا كي نزداد ذكاءً.",
"optional.fields.information.link": "معرفة المزيد عن كيفية استخدامنا لهذه المعلومات.", "optional.fields.information.link": "معرفة المزيد عن كيفية استخدامنا لهذه المعلومات.",
"optional.fields.submit.button": "إرسال", "optional.fields.submit.button": "إرسال",
"optional.fields.skip.button": "التخطي مؤقتا", "optional.fields.skip.button": "التخطي مؤقتا",
"optional.fields.next.button": "Next", "optional.fields.next.button": "التالي",
"continue.to.platform": "المواصلة إلى {platformName}", "continue.to.platform": "المواصلة إلى {platformName}",
"modal.title": "شكرا لإعلامنا.", "modal.title": "شكرا لإعلامنا.",
"modal.description": "إن غيرت رأيك، قيمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.", "modal.description": "إن غيرت رأيك، قيمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
"welcome.page.error.heading": "لم نتمكن من تحديث ملفك الشخصي", "welcome.page.error.heading": "لم نتمكن من تحديث ملفك الشخصي",
"welcome.page.error.message": "حدث خطأ ما. يمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.", "welcome.page.error.message": "حدث خطأ ما. يمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
"recommendation.page.title": "Recommendations | {siteName}", "recommendation.page.title": "التوصيات | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.", "recommendation.page.heading": "لدينا بعض التوصيات لكي تبدأ.",
"recommendation.skip.button": "Skip for now", "recommendation.skip.button": "التخطي مؤقتا",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "التسجيل | {siteName}", "register.page.title": "التسجيل | {siteName}",
"registration.fullname.label": "الاسم الكامل", "registration.fullname.label": "الاسم الكامل",
"registration.email.label": "البريد الإلكتروني", "registration.email.label": "البريد الإلكتروني",
@@ -123,6 +135,7 @@
"help.text.email": "لتفعيل الحساب و التحديثات الهامة", "help.text.email": "لتفعيل الحساب و التحديثات الهامة",
"create.account.for.free.button": "إنشاء حساب مجانا", "create.account.for.free.button": "إنشاء حساب مجانا",
"registration.other.options.heading": "أو سجل باستخدام:", "registration.other.options.heading": "أو سجل باستخدام:",
"create.account.cta.button": "{label}",
"register.institution.login.button": "بيانات المؤسسة / الجامعة", "register.institution.login.button": "بيانات المؤسسة / الجامعة",
"register.institution.login.page.title": "التسجيل باستخدام بيانات المؤسسة / الجامعة", "register.institution.login.page.title": "التسجيل باستخدام بيانات المؤسسة / الجامعة",
"empty.name.field.error": "أدخل اسمك الكامل", "empty.name.field.error": "أدخل اسمك الكامل",
@@ -140,6 +153,7 @@
"registration.request.server.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.", "registration.request.server.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
"registration.rate.limit.error": "كثرت محاولات التسجيل الفاشلة. أعد المحاولة لاحقًا.", "registration.rate.limit.error": "كثرت محاولات التسجيل الفاشلة. أعد المحاولة لاحقًا.",
"registration.tpa.session.expired": "نفد وقت التسجيل باستخدام {provider}.", "registration.tpa.session.expired": "نفد وقت التسجيل باستخدام {provider}.",
"registration.tpa.authentication.failure": "عذرًا، غير مصرح لك بالوصول إلى {platform_name} عبر هذه القناة. يرجى الاتصال بمسؤول التعلم أو المدير من أجل الوصول إلى {platform_name}. {lineBreak} {lineBreak} تفاصيل الخطأ: {lineBreak} {errorMessage}",
"terms.of.service.and.honor.code": "شروط الخدمة وميثاق الشرف الأكاديمي", "terms.of.service.and.honor.code": "شروط الخدمة وميثاق الشرف الأكاديمي",
"privacy.policy": "سياسة الخصوصية", "privacy.policy": "سياسة الخصوصية",
"honor.code": "ميثاق الشرف الأكاديمي", "honor.code": "ميثاق الشرف الأكاديمي",
@@ -147,8 +161,8 @@
"registration.username.suggestion.label": "مقترح:", "registration.username.suggestion.label": "مقترح:",
"did.you.mean.alert.text": "هل تقصد", "did.you.mean.alert.text": "هل تقصد",
"register.page.terms.of.service.and.honor.code": "بإنشاءك حسابًا، فإنك توافق على {tosAndHonorCode} و تقر بأن {platformName} و كل عضو يعالج بياناتك الشخصية وفقًا لـ{privacyPolicy}.", "register.page.terms.of.service.and.honor.code": "بإنشاءك حسابًا، فإنك توافق على {tosAndHonorCode} و تقر بأن {platformName} و كل عضو يعالج بياناتك الشخصية وفقًا لـ{privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}", "register.page.honor.code": "اوافق على شروط {platformName} {tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}", "register.page.terms.of.service": "اوافق على {platformName} {termsOfService}",
"sign.in": "تسجيل الدخول", "sign.in": "تسجيل الدخول",
"reset.password.page.title": "إعادة ضبط كلمة المرور | {siteName}", "reset.password.page.title": "إعادة ضبط كلمة المرور | {siteName}",
"reset.password": "إعادة ضبط كلمة المرور", "reset.password": "إعادة ضبط كلمة المرور",

180
src/i18n/messages/de.json Normal file
View File

@@ -0,0 +1,180 @@
{
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
"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}.",
"registration.using.tpa.form.heading": "Finish creating your account",
"zendesk.supportTitle": "edX Support",
"zendesk.selectTicketForm": "Please choose your request type:",
"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.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 {platformName} 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.",
"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}.",
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
"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",
"forgot.password": "Forgot password",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Sign in with institution/campus credentials",
"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.",
"enterprise.login.btn.text": "Company or school credentials",
"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",
"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",
"tpa.account.link": "{provider} account",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
"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.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",
"login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"optional.fields.next.button": "Next",
"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.",
"recommendation.page.title": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"registration.other.options.heading": "Or register with:",
"create.account.cta.button": "{label}",
"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.do.not.match": "The email addresses do not match.",
"email.invalid.format.error": "Enter a valid email address",
"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",
"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.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.username.suggestion.label": "Suggested:",
"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}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Enter and confirm your new password.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"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.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}

View File

@@ -1,9 +1,11 @@
{ {
"start.learning": "Beginne zu lernen", "start.learning": "Beginne zu lernen",
"with.site.name": "mit {siteName}", "with.site.name": "mit {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Willkommen bei {siteName}, {username}!",
"complete.your.profile.1": "Vervollständige", "complete.your.profile.1": "Vervollständige",
"complete.your.profile.2": "dein Profil", "complete.your.profile.2": "dein Profil",
"welcome.to.platform": "Willkommen bei {siteName}, {username}!",
"institution.login.page.sub.heading": "Wählen Sie Ihre Institution aus der folgenden Liste aus", "institution.login.page.sub.heading": "Wählen Sie Ihre Institution aus der folgenden Liste aus",
"logistration.sign.in": "Anmelden", "logistration.sign.in": "Anmelden",
"logistration.register": "Registrieren", "logistration.register": "Registrieren",
@@ -96,6 +98,7 @@
"password.security.block.body": "Unser System hat festgestellt, dass Ihr Passwort angreifbar ist. Ändern Sie Ihr Passwort, damit Ihr Konto sicher bleibt.", "password.security.block.body": "Unser System hat festgestellt, dass Ihr Passwort angreifbar ist. Ändern Sie Ihr Passwort, damit Ihr Konto sicher bleibt.",
"password.security.close.button": "Schließen", "password.security.close.button": "Schließen",
"password.security.redirect.to.reset.password.button": "Setzen Sie Ihr Passwort zurück", "password.security.redirect.to.reset.password.button": "Setzen Sie Ihr Passwort zurück",
"login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Welcome | {siteName}", "progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "Ein paar Fragen an Sie helfen uns, schlauer zu werden.", "progressive.profiling.page.heading": "Ein paar Fragen an Sie helfen uns, schlauer zu werden.",
"optional.fields.information.link": "Erfahren Sie mehr darüber, wie wir diese Informationen verwenden.", "optional.fields.information.link": "Erfahren Sie mehr darüber, wie wir diese Informationen verwenden.",
@@ -110,6 +113,15 @@
"recommendation.page.title": "Empfehlungen | {siteName}", "recommendation.page.title": "Empfehlungen | {siteName}",
"recommendation.page.heading": "Wir haben ein paar Empfehlungen für den Einstieg.", "recommendation.page.heading": "Wir haben ein paar Empfehlungen für den Einstieg.",
"recommendation.skip.button": "Überspringen", "recommendation.skip.button": "Überspringen",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Registrieren | {siteName}", "register.page.title": "Registrieren | {siteName}",
"registration.fullname.label": "Vollständiger Name", "registration.fullname.label": "Vollständiger Name",
"registration.email.label": "E-Mail-Adresse", "registration.email.label": "E-Mail-Adresse",
@@ -123,6 +135,7 @@
"help.text.email": "Für die Kontoaktivierung und wichtige Updates", "help.text.email": "Für die Kontoaktivierung und wichtige Updates",
"create.account.for.free.button": "Erstellen Sie kostenlos ein Benutzerkonto", "create.account.for.free.button": "Erstellen Sie kostenlos ein Benutzerkonto",
"registration.other.options.heading": "Oder registrieren Sie sich bei:", "registration.other.options.heading": "Oder registrieren Sie sich bei:",
"create.account.cta.button": "{label}",
"register.institution.login.button": "Zeugnisse der Institution/des Campus", "register.institution.login.button": "Zeugnisse der Institution/des Campus",
"register.institution.login.page.title": "Registrieren Sie sich mit Institutions-/Campus-Anmeldeinformationen", "register.institution.login.page.title": "Registrieren Sie sich mit Institutions-/Campus-Anmeldeinformationen",
"empty.name.field.error": "Geben Sie Ihren vollständigen Namen ein", "empty.name.field.error": "Geben Sie Ihren vollständigen Namen ein",
@@ -140,6 +153,7 @@
"registration.request.server.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.", "registration.request.server.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
"registration.rate.limit.error": "Zu viele fehlgeschlagene Registrierungsversuche. Versuchen Sie es später noch einmal.", "registration.rate.limit.error": "Zu viele fehlgeschlagene Registrierungsversuche. Versuchen Sie es später noch einmal.",
"registration.tpa.session.expired": "Die Registrierung mit {provider} ist abgelaufen.", "registration.tpa.session.expired": "Die Registrierung mit {provider} ist abgelaufen.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Nutzungsbedingungen und Verhaltenskodex", "terms.of.service.and.honor.code": "Nutzungsbedingungen und Verhaltenskodex",
"privacy.policy": "Datenschutzbestimmungen", "privacy.policy": "Datenschutzbestimmungen",
"honor.code": "Verhaltenskodex", "honor.code": "Verhaltenskodex",

View File

@@ -1,15 +1,17 @@
{ {
"start.learning": "Empieza a aprender", "start.learning": "Empieza a aprender",
"with.site.name": "con {siteName}", "with.site.name": "con {siteName}",
"your.career.turning.point": "El punto de inflexión de tu carrera",
"is.here": "es aquí.",
"welcome.to.platform": "¡Bienvenido a {siteName}, {username}!",
"complete.your.profile.1": "Completado", "complete.your.profile.1": "Completado",
"complete.your.profile.2": "tu perfil ", "complete.your.profile.2": "tu perfil ",
"welcome.to.platform": "¡Bienvenido a {siteName}, {username}!",
"institution.login.page.sub.heading": "Selecciona tu institución de la lista siguiente", "institution.login.page.sub.heading": "Selecciona tu institución de la lista siguiente",
"logistration.sign.in": "Iniciar sesión", "logistration.sign.in": "Iniciar sesión",
"logistration.register": "Registrarse", "logistration.register": "Registrarse",
"enterprisetpa.title.heading": "¿Deseas iniciar sesión con tus credenciales de {providerName}?", "enterprisetpa.title.heading": "¿Deseas iniciar sesión con tus credenciales de {providerName}?",
"enterprisetpa.login.button.text": "Mostrar otras formas de iniciar sesión o de registrarme", "enterprisetpa.login.button.text": "Mostrar otras formas de iniciar sesión o de registrarme",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in", "enterprisetpa.login.button.text.public.account.creation.disabled": "Mostrar otras formas de iniciar sesión",
"sso.sign.in.with": "Inicio de sesión con {providerName}", "sso.sign.in.with": "Inicio de sesión con {providerName}",
"sso.create.account.using": "Crear una cuenta con {providerName}", "sso.create.account.using": "Crear una cuenta con {providerName}",
"show.password": "Mostrar contraseña", "show.password": "Mostrar contraseña",
@@ -22,8 +24,8 @@
"login.third.party.auth.account.not.linked": "Te has registrado correctamente en {currentProvider}, pero tu cuenta de {currentProvider} no tiene una cuenta de {platformName} asociada. Para asociar tus cuentas, inicia sesión ahora usando tu contraseña de {platformName}.", "login.third.party.auth.account.not.linked": "Te has registrado correctamente en {currentProvider}, pero tu cuenta de {currentProvider} no tiene una cuenta de {platformName} asociada. Para asociar tus cuentas, inicia sesión ahora usando tu contraseña de {platformName}.",
"register.third.party.auth.account.not.linked": "¡Has iniciado sesión con éxito en {currentProvider}! Sólo necesitamos un poco más de información antes de que empieces a aprender con {platformName}.", "register.third.party.auth.account.not.linked": "¡Has iniciado sesión con éxito en {currentProvider}! Sólo necesitamos un poco más de información antes de que empieces a aprender con {platformName}.",
"registration.using.tpa.form.heading": "Termina de crear tu cuenta", "registration.using.tpa.form.heading": "Termina de crear tu cuenta",
"zendesk.supportTitle": "edX Support", "zendesk.supportTitle": "Soporte edX",
"zendesk.selectTicketForm": "Please choose your request type:", "zendesk.selectTicketForm": "Elegir el tipo de solicitud:",
"error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, verifica la URL y vuelve a intentarlo.", "error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, verifica la URL y vuelve a intentarlo.",
"forgot.password.confirmation.message": "Hemos enviado un correo electrónico a {email} con instrucciones para restablecer tu contraseña.\n Si no recibes un mensaje de restablecimiento de contraseña después de 1 minuto, verifica que has introducido\n la dirección de correo electrónico correcta, o comprueba tu carpeta de correo no deseado. Si necesitas más ayuda, {supportLink}.", "forgot.password.confirmation.message": "Hemos enviado un correo electrónico a {email} con instrucciones para restablecer tu contraseña.\n Si no recibes un mensaje de restablecimiento de contraseña después de 1 minuto, verifica que has introducido\n la dirección de correo electrónico correcta, o comprueba tu carpeta de correo no deseado. Si necesitas más ayuda, {supportLink}.",
"forgot.password.page.title": "Olvidé la contraseña | {siteName}", "forgot.password.page.title": "Olvidé la contraseña | {siteName}",
@@ -96,7 +98,8 @@
"password.security.block.body": "Nuestro sistema detectó que su contraseña es vulnerable. Cambie su contraseña para que su cuenta permanezca segura.", "password.security.block.body": "Nuestro sistema detectó que su contraseña es vulnerable. Cambie su contraseña para que su cuenta permanezca segura.",
"password.security.close.button": "Cerrar", "password.security.close.button": "Cerrar",
"password.security.redirect.to.reset.password.button": "Restablece tu contraseña", "password.security.redirect.to.reset.password.button": "Restablece tu contraseña",
"progressive.profiling.page.title": "Welcome | {siteName}", "login.tpa.authentication.failure": "Lo sentimos, no está autorizado para acceder a {platform_name} a través de este canal. Comuníquese con su administrador o gerente de aprendizaje para acceder a {platform_name}.{lineBreak}{lineBreak}Detalles del error:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Bienvenido | {siteName}",
"progressive.profiling.page.heading": "Unas cuantas preguntas para ti nos ayudarán a mejorar.", "progressive.profiling.page.heading": "Unas cuantas preguntas para ti nos ayudarán a mejorar.",
"optional.fields.information.link": "Aprende más sobre cómo usamos esta información.", "optional.fields.information.link": "Aprende más sobre cómo usamos esta información.",
"optional.fields.submit.button": "Enviar", "optional.fields.submit.button": "Enviar",
@@ -110,6 +113,15 @@
"recommendation.page.title": "Recomendaciones | {siteName}", "recommendation.page.title": "Recomendaciones | {siteName}",
"recommendation.page.heading": "Tenemos algunas recomendaciones para empezar.", "recommendation.page.heading": "Tenemos algunas recomendaciones para empezar.",
"recommendation.skip.button": "Saltar por ahora ", "recommendation.skip.button": "Saltar por ahora ",
"recommendation.option.trending": "Tendencias",
"recommendation.option.popular": "Más popular",
"recommendation.product-card.pill-text.course": "Curso",
"recommendation.product-card.pill-text.professional-certificate": "Certificado profesional",
"recommendation.product-card.pill-text.emeritus": "Ofrecido en Emeritus",
"recommendation.product-card.pill-text.shorelight": "Ofrecido a través de Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Suscripción",
"recommendation.product-card.launch-icon.sr-text": "Abrir un enlace en una pestaña nueva",
"register.page.title": "Register | {siteName}", "register.page.title": "Register | {siteName}",
"registration.fullname.label": "Nombre completo", "registration.fullname.label": "Nombre completo",
"registration.email.label": "Correo electrónico", "registration.email.label": "Correo electrónico",
@@ -123,6 +135,7 @@
"help.text.email": "Para la activación de la cuenta y las actualizaciones importantes", "help.text.email": "Para la activación de la cuenta y las actualizaciones importantes",
"create.account.for.free.button": "Crea una cuenta gratis", "create.account.for.free.button": "Crea una cuenta gratis",
"registration.other.options.heading": "O regístrese con:", "registration.other.options.heading": "O regístrese con:",
"create.account.cta.button": "{label}",
"register.institution.login.button": "Credenciales de la institución/campus", "register.institution.login.button": "Credenciales de la institución/campus",
"register.institution.login.page.title": "Registro con 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.name.field.error": "Introduce tu nombre completo",
@@ -140,6 +153,7 @@
"registration.request.server.error": "Se ha producido un error. Intenta actualizar la página o comprueba tu conexión a Internet.", "registration.request.server.error": "Se ha producido un error. Intenta actualizar la página o comprueba tu conexión a Internet.",
"registration.rate.limit.error": "Demasiados intentos de registro fallidos. Vuelve a intentarlo más tarde.", "registration.rate.limit.error": "Demasiados intentos de registro fallidos. Vuelve a intentarlo más tarde.",
"registration.tpa.session.expired": "Inscripción usando {provider} ha expirado.", "registration.tpa.session.expired": "Inscripción usando {provider} ha expirado.",
"registration.tpa.authentication.failure": "Lo sentimos, no está autorizado para acceder a {platform_name} a través de este canal. Comuníquese con su administrador o gerente de aprendizaje para acceder a {platform_name}.{lineBreak}{lineBreak}Detalles del error:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Condiciones de servicio y código de honor", "terms.of.service.and.honor.code": "Condiciones de servicio y código de honor",
"privacy.policy": "Política de privacidad ", "privacy.policy": "Política de privacidad ",
"honor.code": "Código de Honor", "honor.code": "Código de Honor",

View File

@@ -0,0 +1,180 @@
{
"start.learning": "آغاز یادگیری",
"with.site.name": "با {siteName}",
"your.career.turning.point": "نقطه عطف حرفه ای شما",
"is.here": "اینجاست.",
"welcome.to.platform": "خوش آمدید به {siteName}, {username}!",
"complete.your.profile.1": "کامل",
"complete.your.profile.2": "پرونده کاربری شما",
"institution.login.page.sub.heading": "موسسه خود را از فهرست زیر برگزینید",
"logistration.sign.in": "ورود",
"logistration.register": "ثبت‌نام",
"enterprisetpa.title.heading": "آیا می‌خواهید با استفاده از اطلاعات کاربری {providerName} خود وارد سامانه شوید؟",
"enterprisetpa.login.button.text": "راه‌های دیگری برای ورود به سامانه یا ثبت‌نام به من نشان بده",
"enterprisetpa.login.button.text.public.account.creation.disabled": "راه های دیگری را برای ورود به سیستم به من نشان دهید",
"sso.sign.in.with": "با {providerName} وارد شوید",
"sso.create.account.using": "با استفاده از {providerName} حساب کاربری بسازید",
"show.password": "نمایش گذرواژه",
"hide.password": "پنهان‌سازی گذرواژه",
"one.letter": "1 نامه",
"one.number": "1 رقم",
"eight.characters": "8 نویسه",
"password.sr.only.helping.text": "گذرواژه باید حداقل 8 نویسه، حداقل یک حرف و حداقل یک عدد داشته باشد",
"tpa.alert.heading": "تقریبا تمام شد!",
"login.third.party.auth.account.not.linked": "شما با موفقیت به {currentProvider} وارد شدید، اما حساب کاربری {currentProvider} شما به حساب کاربری {platformName} پیوند ندارد. برای پیوند حساب‌های کاربری خود، اکنون با استفاده از گذرواژه {platformName} خود وارد شوید.",
"register.third.party.auth.account.not.linked": "شما با موفقیت به {currentProvider} وارد شدید! پیش از آغاز یادگیری با {platformName} فقط به کمی اطلاعات بیشتر نیاز داریم.",
"registration.using.tpa.form.heading": "ساخت حساب کاربری خود را به اتمام برسانید",
"zendesk.supportTitle": "پشتیبانی edX",
"zendesk.selectTicketForm": "لطفا نوع درخواست خود را انتخاب کنید:",
"error.notfound.message": "صفحه مورد نظر شما در دسترس نیست یا خطایی در نشانی اینترنتی وجود دارد. لطفاً نشانی اینترنتی را بررسی کرده و دوباره تلاش کنید.",
"forgot.password.confirmation.message": "ما رایانامه‌ای به‌همراه دستورالعمل بازنشانی گذرواژه به {email} ارسال کردیم. اگر پس از 1 دقیقه پیام بازنشانی را دریافت نکردید، بررسی کنید که نشانی رایانامه را صحیح وارد کرده‌اید یا پوشه هرزنامه خود را بررسی کنید. اگر به دریافت راهنمایی بیشتری، با {supportLink} تماس بگیرید.",
"forgot.password.page.title": "فراموش گذرواژه | {siteName}",
"forgot.password.page.heading": "بازتنظیم گذرواژه",
"forgot.password.page.instructions": "لطفا نشانی رایانامه خود را در قسمت زیر وارد کنید و ما رایانامه‌ای حاوی دستورالعمل نحوه بازتنظیم مجدد گذرواژه برای شما ارسال خواهیم کرد.",
"forgot.password.page.invalid.email.message": "نشانی رایانامه معتبری را وارد کنید",
"forgot.password.page.email.field.label": "رایانامه",
"forgot.password.page.submit.button": "ارسال",
"forgot.password.error.alert.title.": "ما نتوانستیم با شما تماس بگیریم.",
"forgot.password.error.message.title": "خطایی رخ داد.",
"forgot.password.request.in.progress.message": "درخواست قبلی شما در حال انجام است، لطفا چند لحظه دیگر دوباره تلاش کنید.",
"forgot.password.empty.email.field.error": "نشانی رایانامه خود را وارد کنید",
"forgot.password.email.help.text": "نشانی رایانامه‌ای که برای ثبت‌نام در {platformName} استفاده کردید",
"confirmation.message.title": "صندوق رایانامه خود را ببینید",
"confirmation.support.link": "با پشتیبانی فنی تماس بگیرید",
"need.help.sign.in.text": "برای ورود به سامانه نیاز به کمک دارید؟",
"additional.help.text": "برای دریافت راهنمایی بیشتر، با پشتیبانی {platformName} در این آدرس تماس بگیرید",
"sign.in.text": "ورود",
"extend.field.errors": "{emailError} زیر.",
"invalid.token.heading": "پیوند بازتنظیم گذرواژه معتبر نیست",
"invalid.token.error.message": "این پیوند برای بازتنظیم گذرواژه معتبر نیست. ممکن است قبلاً استفاده شده باشد. برای دریافت پیوند جدید نشانی رایانامه خود را در زیر وارد کنید.",
"token.validation.rate.limit.error.heading": "تعداد زیاد درخواست",
"token.validation.rate.limit.error": "به دلیل درخواست‌های زیاد، خطایی روی داده است. لطفا بعد از مدتی دوباره امتحان کنید.",
"token.validation.internal.sever.error.heading": "اعتبارسنجی رمز انجام نشد",
"token.validation.internal.sever.error": "خطایی رخ داده است. صفحه را دوباره بارگیری کنید یا اتصال اینترنت خود را بررسی کنید.",
"internal.server.error": "خطایی رخ داده است. صفحه را دوباره بارگیری کنید یا اتصال اینترنت خود را بررسی کنید.",
"account.activation.error.message": "اشتباهی رخ داد، لطفاً برای حل این مساله ، به این قسمت{supportLink} مراجعه کنید.",
"login.inactive.user.error": "برای ورود به سامانه، باید حساب کاربری خود را فعال کنید.{lineBreak} {lineBreak}هم اکنون پیوند فعالسازی را به {email} فرستادیم. اگر رایانامه‌ای دریافت نکردید، پوشه‌های هرزنامه یا {supportLink} را بررسی کنید.",
"allowed.domain.login.error": "به عنوان کاربر {allowedDomain}، باید با {allowedDomain} {tpaLink} خود وارد شوید.",
"login.incorrect.credentials.error.attempts.text.1": "نام کاربری، نشانی رایانامه یا گذرواژه‌ای که وارد کرده‌اید نادرست است. پیش از اینکه حساب کاربری شما موقتا قفل شود، {remainingAttempts} تلاش دیگر برای ورود به سامانه دارید.",
"login.incorrect.credentials.error.attempts.text.2": "اگر گذرواژه خود را فراموش کرده‌اید، {resetLink}",
"account.locked.out.message.2": "برای حفظ امنیت، می‌توانید پیش از تلاش مجدد، {resetLink} را انجام دهید.",
"login.incorrect.credentials.error.with.reset.link": "نام کاربری، نشانی رایانامه یا گذرواژه‌ای که وارد کردید درست نیست. لطفاً دوباره امتحان کنید یا {resetLink}.",
"login.page.title": "ورود به سامانه | {siteName}",
"login.user.identity.label": "نام کاربری یا نشانی رایانامه",
"login.password.label": "گذرواژه",
"sign.in.button": "ورود به سامانه",
"forgot.password": "فراموشی گذرواژه",
"institution.login.button": "اعتبارنامه‌های موسسه/پردیس",
"institution.login.page.title": "با اعتبار موسسه/پردیس وارد شوید",
"login.other.options.heading": "یا وارد شوید با:",
"non.compliant.password.title": "ما اخیراً الزامات گذرواژه خود را تغییر دادیم",
"non.compliant.password.message": "گذرواژه فعلی شما الزامات امنیتی جدید را برآورده نمی‌کند. ما فقط یک پیام بازتنظیم گذرواژه به نشانی رایانامه مرتبط با این حساب کاربری ارسال کردیم. از اینکه به ما کمک می‌کنید تا داده‌های شما را ایمن نگه دارید متشکریم.",
"account.locked.out.message.1": "حساب کاربری شما، به دلیل حفاظت، به‌طور موقت قفل شده است. 30 دقیقه دیگر دوباره امتحان کنید.",
"enterprise.login.btn.text": "اعتبار دانشکده یا شرکت",
"username.or.email.format.validation.less.chars.message": "نام کاربری یا نشانی رایانامه حداقل باید 3 نویسه داشته باشد",
"email.validation.message": "نام کاربری یا رایانامه خود را وارد کنید",
"password.validation.message": "معیارهای گذرواژه رعایت نشده است",
"account.activation.success.message.title": "موفق شدید! شما حساب کاربری خود را فعال کردید.",
"account.activation.success.message": "اکنون رایانامه‌های مربوط به روزآمدسازی‌ها و هشدارها درباره دوره‌های آموزشی را که در آن ثبت‌نام کرده‌اید از ما دریافت خواهید کرد. برای ادامه وارد شوید.",
"account.activation.info.message": "حساب کاربری مورد نظر شما قبلاً فعال شده است.",
"account.activation.error.message.title": "امکان فعالسازی حساب کاربری شما نبود",
"account.activation.support.link": "تماس با پشتیبانی ",
"account.confirmation.success.message.title": "موفق شدید! نشانی رایانامه خود را تایید کردید.",
"account.confirmation.success.message": "برای ادامه وارد سامانه شوید.",
"account.confirmation.info.message": "این نشانی رایانامه قبلا تایید شده‌است.",
"account.confirmation.error.message.title": "امکان تایید نشانی رایانامه شما وجود ندارد",
"tpa.account.link": "حساب {provider}",
"internal.server.error.message": "خطایی رخ داده است. صفحه را دوباره بارگیری کنید یا اتصال اینترنت خود را بررسی کنید.",
"login.rate.limit.reached.message": "شما برای ورود به حساب کاربری چند بار تلاش ناموفق داشتید. لطفا بعدا تلاش نمایید.",
"login.failure.header.title": "قادر نیستیم شما را به سامانه وارد کنیم.",
"contact.support.link": "با پشتیبانی {platformName} تماس بگیرید",
"login.incorrect.credentials.error": "نام کاربری، نشانی رایانامه یا گذرواژه‌ای که وارد کردید نادرست است. لطفا دوباره تلاش کنید.",
"login.form.invalid.error.message": "لطفا قسمت‌های زیر را پر کنید.",
"login.incorrect.credentials.error.reset.link.text": "گذرواژه را بازتنظیم کنید",
"login.incorrect.credentials.error.before.account.blocked.text": "برای بازتنظیم اینجا بزنید",
"password.security.nudge.title": "امنیت گذرواژه",
"password.security.block.title": "تغییر گذرواژه ضروری است",
"password.security.nudge.body": "سامانه ما تشخیص داده است که گذرواژه شما آسیب‌پذیر است. توصیه ما این است که آن را تغییر دهید تا حساب کاربری شما ایمن بماند.",
"password.security.block.body": "سامانه ما تشخیص داده است که گذرواژه شما ضعیف و آسیب‌پذیر است. گذرواژه خود را تغییر دهید تا حساب کاربری شما ایمن بماند.",
"password.security.close.button": "بستن",
"password.security.redirect.to.reset.password.button": "بازتنظیم گذرواژه",
"login.tpa.authentication.failure": "متأسفیم، شما مجاز به دسترسی به {platform_name} از طریق این کانال نیستید. لطفاً برای دسترسی به {platform_name} با سرپرست یا مدیر آموزشی خود تماس بگیرید.{lineBreak}{lineBreak}جزئیات خطا:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "خوش آمدید | {siteName}",
"progressive.profiling.page.heading": "چند سوال برای شما به ما کمک خواهد کرد تا باهوش‌تر شویم.",
"optional.fields.information.link": "درباره نحوه استفاده ما از این اطلاعات بیشتر بدانید.",
"optional.fields.submit.button": "ارسال",
"optional.fields.skip.button": "فعلا بگذرید",
"optional.fields.next.button": "بعدی",
"continue.to.platform": "ادامه در {platformName}",
"modal.title": "از اینکه اطلاع دادید تشکر می‌کنیم.",
"modal.description": "در صورت تغییر تصمیم‌تان، شما هر زمانی این امکان را دارید که پرونده کاربری خود را در قسمت تنظیمات تکمیل کنید. ",
"welcome.page.error.heading": "امکان روزآمدسازی پرونده کاربری شما نیست",
"welcome.page.error.message": "خطایی رخ داد. می‌توانید پرونده کاربری خود را هر زمان در قسمت تنظیمات تکمیل کنید.",
"recommendation.page.title": "توصیه ها | {siteName}",
"recommendation.page.heading": "ما چند توصیه برای شروع کار داریم.",
"recommendation.skip.button": "فعلا بگذرید",
"recommendation.option.trending": "پرطرفدار",
"recommendation.option.popular": "محبوبترین",
"recommendation.product-card.pill-text.course": "دوره آموزشی",
"recommendation.product-card.pill-text.professional-certificate": "گواهی حرفه‌ای",
"recommendation.product-card.pill-text.emeritus": "ارائه شده در Emeritus",
"recommendation.product-card.pill-text.shorelight": "از طریق Shorelight ارائه می شود",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "اشتراک",
"recommendation.product-card.launch-icon.sr-text": "پیوندی را در یک برگه جدید باز می کند",
"register.page.title": "ثبت‌نام | {siteName}",
"registration.fullname.label": "نام و نام خانوادگی",
"registration.email.label": "رایانامه",
"registration.username.label": "نام‌کاربری عمومی",
"registration.password.label": "گذرواژه",
"registration.country.label": "کشور/منطقه",
"registration.opt.in.label": "با ارسال پیام‌های بازرگانی از سوی {siteName} موافقم.",
"help.text.name": "این نام در هر گواهی که بدست آورید استفاده خواهد شد.",
"help.text.username.1": "نامی که شما را در دوره‌های آموزشی با آن شناخته خواهید شد.",
"help.text.username.2": "این مورد بعدا قابل تغییر نیست.",
"help.text.email": "برای فعال‌سازی حساب کاربری و روزآمدسازی‌های مهم",
"create.account.for.free.button": "یک حساب کاربری رایگان بسازید",
"registration.other.options.heading": "یا ثبت‌نام کنید با:",
"create.account.cta.button": "{label}",
"register.institution.login.button": "اعتبارنامه‌های موسسه/پردیس",
"register.institution.login.page.title": "با اعتبارنامه موسسه/پردیس ثبت‌نام کنید",
"empty.name.field.error": "نام و نام خانوادگی خود را وارد کنید",
"empty.email.field.error": "نشانی رایانامه خود را وارد کنید",
"empty.username.field.error": "نام کاربری باید بین 2 تا 30 نویسه داشته باشد",
"empty.password.field.error": "معیارهای گذرواژه رعایت نشده است",
"empty.country.field.error": "کشور یا منطقه محل سکونت خود را برگزینید",
"email.do.not.match": "نشانی‌های رایانامه همخوانی ندارند.",
"email.invalid.format.error": "نشانی رایانامه معتبر وارد کنید",
"username.validation.message": "نام کاربری باید بین 2 تا 30 نویسه داشته باشد",
"name.validation.message": "نامی معتبر وارد کنید",
"username.format.validation.message": "نام کاربری فقط می‌تواند شامل حروف (A-Z، a-z)، اعداد (0-9)، خط زیر (_) و خط فاصله (-) باشد. نام کاربری نمی‌تواند حاوی فاصله باشد",
"registration.request.failure.header": "موفق به ایجاد حساب کاربری شما نشدیم.",
"registration.empty.form.submission.error": "لطفاً پاسخ‌های خود را بررسی کرده و دوباره امتحان کنید.",
"registration.request.server.error": "خطایی رخ داده است. صفحه را دوباره بارگیری کنید یا اتصال اینترنت خود را بررسی کنید.",
"registration.rate.limit.error": "تعداد دفعات تلاش ناموفق برای ثبت‌نام بسیار بوده است. بعدا دوباره تلاش کنید.",
"registration.tpa.session.expired": "مهلت ثبت‌نام {provider} به پایان رسیده‌است.",
"registration.tpa.authentication.failure": "متأسفیم، شما مجاز به دسترسی به {platform_name} از طریق این کانال نیستید. لطفاً برای دسترسی به {platform_name} با سرپرست یا مدیر آموزشی خود تماس بگیرید.{lineBreak}{lineBreak}جزئیات خطا:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "شرایط استفاده از خدمات و اصول اخلاقی",
"privacy.policy": "قواعد حفظ حریم خصوصی",
"honor.code": "اصول اخلاقی",
"terms.of.service": "شرایط استفاده از خدمات",
"registration.username.suggestion.label": "پیشنهادشده:",
"did.you.mean.alert.text": "منظور شما این بود",
"register.page.terms.of.service.and.honor.code": "با ایجاد یک حساب کاربری، با {tosAndHonorCode} موافقت می‌کنید و تصدیق می‌کنید که {platformName} و هر عضو داده‌های شخصی شما را مطابق با {privacyPolicy} پردازش می‌کنند.",
"register.page.honor.code": "من با {platformName} {tosAndHonorCode} موافقم",
"register.page.terms.of.service": "من با {platformName} {termsOfService} موافقم",
"sign.in": "ورود",
"reset.password.page.title": "بازتنظیم گذرواژه | {siteName}",
"reset.password": "بازتنظیم گذرواژه",
"reset.password.page.instructions": "گذرواژه جدید را مجددا وارد کنید ",
"new.password.label": "گذرواژه جدید",
"confirm.password.label": "تایید گذرواژه",
"passwords.do.not.match": "گذرواژه‌ها مطابقت ندارند",
"confirm.your.password": "تایید گذرواژه ",
"reset.password.failure.heading": "امکان بازتنظیم گذرواژه شما نیست.",
"reset.password.form.submission.error": "لطفاً پاسخ‌های خود را بررسی کرده و دوباره امتحان کنید.",
"reset.server.rate.limit.error": "تعداد درخواست‌ها خیلی زیاد است.",
"reset.password.success.heading": "بازتنظیم گذرواژه  تکمیل شد.",
"reset.password.success": "گذرواژه شما بازتنظیم شد. وارد حساب کاربری خود شوید",
"rate.limit.error": "به دلیل درخواست‌های زیاد، خطایی روی داده است. لطفا بعد از مدتی دوباره امتحان کنید."
}

View File

@@ -1,9 +1,11 @@
{ {
"start.learning": "Démarrer l'apprentissage", "start.learning": "Démarrer l'apprentissage",
"with.site.name": "avec {siteName}", "with.site.name": "avec {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
"complete.your.profile.1": "Terminé", "complete.your.profile.1": "Terminé",
"complete.your.profile.2": "votre profil", "complete.your.profile.2": "votre profil",
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
"institution.login.page.sub.heading": "Sélectionner votre institution dans la liste ci-dessous", "institution.login.page.sub.heading": "Sélectionner votre institution dans la liste ci-dessous",
"logistration.sign.in": "Connectez-vous", "logistration.sign.in": "Connectez-vous",
"logistration.register": "S'inscrire", "logistration.register": "S'inscrire",
@@ -96,6 +98,7 @@
"password.security.block.body": "Notre système a détecté que votre mot de passe est vulnérable. Changez votre mot de passe afin que votre compte reste sécurisé.", "password.security.block.body": "Notre système a détecté que votre mot de passe est vulnérable. Changez votre mot de passe afin que votre compte reste sécurisé.",
"password.security.close.button": "Fermer", "password.security.close.button": "Fermer",
"password.security.redirect.to.reset.password.button": "Réinitialiser votre mot de passe", "password.security.redirect.to.reset.password.button": "Réinitialiser votre mot de passe",
"login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Welcome | {siteName}", "progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "Quelques questions pour vous nous aideront à devenir plus intelligents.", "progressive.profiling.page.heading": "Quelques questions pour vous nous aideront à devenir plus intelligents.",
"optional.fields.information.link": "En savoir plus sur la façon dont nous utilisons ces informations.", "optional.fields.information.link": "En savoir plus sur la façon dont nous utilisons ces informations.",
@@ -110,6 +113,15 @@
"recommendation.page.title": "Recommendations | {siteName}", "recommendation.page.title": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.", "recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now", "recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "S'inscrire | {siteName}", "register.page.title": "S'inscrire | {siteName}",
"registration.fullname.label": "Nom complet", "registration.fullname.label": "Nom complet",
"registration.email.label": "Email", "registration.email.label": "Email",
@@ -123,6 +135,7 @@
"help.text.email": "Pour l'activation du compte et les mises à jour importantes", "help.text.email": "Pour l'activation du compte et les mises à jour importantes",
"create.account.for.free.button": "Créer un compte gratuitement", "create.account.for.free.button": "Créer un compte gratuitement",
"registration.other.options.heading": "Ou inscrivez-vous avec :", "registration.other.options.heading": "Ou inscrivez-vous avec :",
"create.account.cta.button": "{label}",
"register.institution.login.button": "Identifiants de l'établissement/du campus", "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", "register.institution.login.page.title": "Inscription avec les crédentiels d'institution ou de campus",
"empty.name.field.error": "Saisissez votre nom complet", "empty.name.field.error": "Saisissez votre nom complet",
@@ -140,6 +153,7 @@
"registration.request.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.", "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.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.", "registration.tpa.session.expired": "L'inscription avec {provider} a échouée.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Conditions d'utilisation et Code d'honneur", "terms.of.service.and.honor.code": "Conditions d'utilisation et Code d'honneur",
"privacy.policy": "Politique de confidentialité", "privacy.policy": "Politique de confidentialité",
"honor.code": "Code d'honneur", "honor.code": "Code d'honneur",

View File

@@ -0,0 +1,180 @@
{
"start.learning": "Commencez à apprendre",
"with.site.name": "avec {siteName}",
"your.career.turning.point": "Votre tournant de carrière",
"is.here": "est là.",
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
"complete.your.profile.1": "Complet",
"complete.your.profile.2": "votre profil",
"institution.login.page.sub.heading": "Sélectionner votre institution dans la liste ci-dessous",
"logistration.sign.in": "Connexion",
"logistration.register": "Inscription",
"enterprisetpa.title.heading": "Souhaitez-vous vous connecter à l'aide de vos informations d'identification {providerName}?",
"enterprisetpa.login.button.text": "Affichez moi d'autres façons de se connecter ou de s'inscrire",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Montrez-moi d'autres façons de me connecter",
"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 numéro",
"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 terminé!",
"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}.",
"registration.using.tpa.form.heading": "Terminer la création de votre compte",
"zendesk.supportTitle": "Prise en charge d'edX",
"zendesk.selectTicketForm": "Veuillez choisir votre type de demande :",
"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 pourriels. 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": "Entrez une adresse de courriel valide",
"forgot.password.page.email.field.label": "Courriel",
"forgot.password.page.submit.button": "Soumettre",
"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": "Votre demande précédente est en cours, veuillez réessayer dans quelques instants.",
"forgot.password.empty.email.field.error": "Saisissez votre courriel",
"forgot.password.email.help.text": "L'adresse courriel que vous avez utilisée pour vous inscrire sur {platformName}",
"confirmation.message.title": "Vérifiez votre courriel",
"confirmation.support.link": "contacter le support technique",
"need.help.sign.in.text": "Besoin d'aide pour vous connecter?",
"additional.help.text": "Pour obtenir une aide supplémentaire, contactez l'assistance {platformName} à l'adresse ",
"sign.in.text": "Connexion",
"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.",
"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 pourriels ou {supportLink}.",
"allowed.domain.login.error": "En tant qu'utilisateur {allowedDomain}, vous devez vous connecter avec votre {allowedDomain} {tpaLink}.",
"login.incorrect.credentials.error.attempts.text.1": "Le nom d'utilisateur, le courriel ou le mot de passe que vous avez entré sont incorrects. 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": "Connexion",
"forgot.password": "Mot de passe oublié",
"institution.login.button": "Informations d'identification de l'établissement/du campus",
"institution.login.page.title": "Connectez vous avec les informations d'identification d'institution ou de campus",
"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.",
"enterprise.login.btn.text": "Informations d'identification de la compagnie ou de l'école",
"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",
"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": "Bravo! 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é",
"tpa.account.link": "compte {provider}",
"internal.server.error.message": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
"login.rate.limit.reached.message": "Trop de tentatives d'accès refusées. Essayer 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.form.invalid.error.message": "Veuillez remplir les champs ci-dessous.",
"login.incorrect.credentials.error.reset.link.text": "réinitialiser votre mot de passe",
"login.incorrect.credentials.error.before.account.blocked.text": "cliquez ici pour le réinitialiser.",
"password.security.nudge.title": "Sécurité du mot de passe",
"password.security.block.title": "Changement de mot de passe requis",
"password.security.nudge.body": "Notre système a détecté que votre mot de passe est vulnérable. Nous vous recommandons de le modifier afin que votre compte reste sécurisé.",
"password.security.block.body": "Notre système a détecté que votre mot de passe est vulnérable. Changez votre mot de passe afin que votre compte reste sécurisé.",
"password.security.close.button": "Fermer",
"password.security.redirect.to.reset.password.button": "Réinitialiser votre mot de passe",
"login.tpa.authentication.failure": "Nous sommes désolés, vous n'êtes pas autorisé à accéder à {platform_name} via ce canal. Veuillez contacter votre administrateur ou responsable de formation pour accéder à {platform_name}.{lineBreak}{lineBreak}Détails de l'erreur :{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Bienvenue | {siteName}",
"progressive.profiling.page.heading": "Quelques questions pour vous nous aideront à devenir plus intelligents.",
"optional.fields.information.link": "En savoir plus sur la façon dont nous utilisons ces informations.",
"optional.fields.submit.button": "Soumettre",
"optional.fields.skip.button": "Ignorer pour l'instant",
"optional.fields.next.button": "Suivant",
"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.",
"recommendation.page.title": "Recommandations | {siteName}",
"recommendation.page.heading": "Nous avons quelques recommandations pour vous aider à démarrer.",
"recommendation.skip.button": "Ignorer pour l'instant",
"recommendation.option.trending": "Tendance",
"recommendation.option.popular": "Le plus populaire",
"recommendation.product-card.pill-text.course": "Cours",
"recommendation.product-card.pill-text.professional-certificate": "Attestation professionnelle",
"recommendation.product-card.pill-text.emeritus": "Offert à titre émérite",
"recommendation.product-card.pill-text.shorelight": "Offert par Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Abonnement",
"recommendation.product-card.launch-icon.sr-text": "Ouvre un lien dans un nouvel onglet",
"register.page.title": "S'inscrire | {siteName}",
"registration.fullname.label": "Nom complet",
"registration.email.label": "Courriel",
"registration.username.label": "Nom d'utilisateur",
"registration.password.label": "Mot de passe",
"registration.country.label": "Pays/Région",
"registration.opt.in.label": "{siteName} peux m'envoyer des messages de marketing.",
"help.text.name": "Ce nom sera utilisé pour toutes les attestations que vous obtiendrez.",
"help.text.username.1": "Le nom qui vous identifiera dans vos cours.",
"help.text.username.2": "Cela ne peut pas être modifié ultérieurement.",
"help.text.email": "Pour l'activation du compte et les mises à jour importantes",
"create.account.for.free.button": "Créer un compte gratuitement",
"registration.other.options.heading": "Ou inscrivez-vous avec :",
"create.account.cta.button": "{label}",
"register.institution.login.button": "Informations d'identification de l'établissement/du campus",
"register.institution.login.page.title": "Inscription avec les informations d'identification 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.do.not.match": "Les adresses courriel ne correspondent pas.",
"email.invalid.format.error": "Entrez une adresse de courriel valide",
"username.validation.message": "Le nom d'utilisateur doit comporter entre 2 et 30 caractères",
"name.validation.message": "Entrez un nom valide",
"username.format.validation.message": "Les noms d'utilisateur ne peuvent contenir que des lettres (A-Z, a-z), des chiffres (0-9), des traits de soulignement (_) et des traits d'union (-). Les noms d'utilisateur ne peuvent pas contenir d'espaces",
"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 à l'aide de {provider} a expiré.",
"registration.tpa.authentication.failure": "Nous sommes désolés, vous n'êtes pas autorisé à accéder à {platform_name} via ce canal. Veuillez contacter votre administrateur ou responsable de formation pour accéder à {platform_name}.{lineBreak}{lineBreak}Détails de l'erreur :{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Conditions générales du service et code d'honneur",
"privacy.policy": "Politique de confidentialité",
"honor.code": "Code d'honneur",
"terms.of.service": "Conditions générales du service",
"registration.username.suggestion.label": "Suggéré :",
"did.you.mean.alert.text": "Vouliez-vous dire",
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque\n membre peut traiter vos données personnelles conformément à la {privacyPolicy}.",
"register.page.honor.code": "J'accepte le {tosAndHonorCode} {platformName}",
"register.page.terms.of.service": "J'accepte les {termsOfService} {platformName}",
"sign.in": "Connexion",
"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",
"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.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.",
"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."
}

View File

@@ -1,9 +1,11 @@
{ {
"start.learning": "Start learning", "start.learning": "Start learning",
"with.site.name": "with {siteName}", "with.site.name": "with {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"complete.your.profile.1": "Complete", "complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile", "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", "institution.login.page.sub.heading": "Choose your institution from the list below",
"logistration.sign.in": "Sign in", "logistration.sign.in": "Sign in",
"logistration.register": "Register", "logistration.register": "Register",
@@ -96,6 +98,7 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password 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.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password", "password.security.redirect.to.reset.password.button": "Reset your password",
"login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Welcome | {siteName}", "progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.", "progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"optional.fields.information.link": "Learn more about how we use this information.", "optional.fields.information.link": "Learn more about how we use this information.",
@@ -110,6 +113,15 @@
"recommendation.page.title": "Recommendations | {siteName}", "recommendation.page.title": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.", "recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now", "recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Register | {siteName}", "register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name", "registration.fullname.label": "Full name",
"registration.email.label": "Email", "registration.email.label": "Email",
@@ -123,6 +135,7 @@
"help.text.email": "For account activation and important updates", "help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free", "create.account.for.free.button": "Create an account for free",
"registration.other.options.heading": "Or register with:", "registration.other.options.heading": "Or register with:",
"create.account.cta.button": "{label}",
"register.institution.login.button": "Institution/campus credentials", "register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials", "register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name", "empty.name.field.error": "Enter your full name",
@@ -140,6 +153,7 @@
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.", "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.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.", "registration.tpa.session.expired": "Registration using {provider} has timed out.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code", "terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy", "privacy.policy": "Privacy Policy",
"honor.code": "Honor Code", "honor.code": "Honor Code",

180
src/i18n/messages/it.json Normal file
View File

@@ -0,0 +1,180 @@
{
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
"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}.",
"registration.using.tpa.form.heading": "Finish creating your account",
"zendesk.supportTitle": "edX Support",
"zendesk.selectTicketForm": "Please choose your request type:",
"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.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 {platformName} 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.",
"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}.",
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
"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",
"forgot.password": "Forgot password",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Sign in with institution/campus credentials",
"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.",
"enterprise.login.btn.text": "Company or school credentials",
"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",
"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",
"tpa.account.link": "{provider} account",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
"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.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",
"login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"optional.fields.next.button": "Next",
"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.",
"recommendation.page.title": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"registration.other.options.heading": "Or register with:",
"create.account.cta.button": "{label}",
"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.do.not.match": "The email addresses do not match.",
"email.invalid.format.error": "Enter a valid email address",
"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",
"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.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.username.suggestion.label": "Suggested:",
"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}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Enter and confirm your new password.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"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.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}

View File

@@ -1,9 +1,11 @@
{ {
"start.learning": "Inizia a imparare", "start.learning": "Inizia a imparare",
"with.site.name": "con {siteName}", "with.site.name": "con {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Benvenuto in {siteName}, {username}!",
"complete.your.profile.1": "Completata", "complete.your.profile.1": "Completata",
"complete.your.profile.2": "Il tuo profilo", "complete.your.profile.2": "Il tuo profilo",
"welcome.to.platform": "Benvenuto in {siteName}, {username}!",
"institution.login.page.sub.heading": "Scegli il tuo istituto dall&#39;elenco sottostante", "institution.login.page.sub.heading": "Scegli il tuo istituto dall&#39;elenco sottostante",
"logistration.sign.in": "Accedi", "logistration.sign.in": "Accedi",
"logistration.register": "Registrazione", "logistration.register": "Registrazione",
@@ -96,7 +98,8 @@
"password.security.block.body": "Il nostro sistema ha rilevato che la tua password è vulnerabile. Cambia la tua password in modo che il tuo account rimanga sicuro.", "password.security.block.body": "Il nostro sistema ha rilevato che la tua password è vulnerabile. Cambia la tua password in modo che il tuo account rimanga sicuro.",
"password.security.close.button": "Chiudi", "password.security.close.button": "Chiudi",
"password.security.redirect.to.reset.password.button": "Ripristina la tua password", "password.security.redirect.to.reset.password.button": "Ripristina la tua password",
"progressive.profiling.page.title": "Welcome | {siteName}", "login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Benvenuto | {siteName}",
"progressive.profiling.page.heading": "Alcune domande per te ci aiuteranno a diventare più intelligenti.", "progressive.profiling.page.heading": "Alcune domande per te ci aiuteranno a diventare più intelligenti.",
"optional.fields.information.link": "Ulteriori informazioni su come utilizziamo queste informazioni.", "optional.fields.information.link": "Ulteriori informazioni su come utilizziamo queste informazioni.",
"optional.fields.submit.button": "Invia", "optional.fields.submit.button": "Invia",
@@ -110,6 +113,15 @@
"recommendation.page.title": "Recommendations | {siteName}", "recommendation.page.title": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.", "recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now", "recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Registrazione | {siteName}", "register.page.title": "Registrazione | {siteName}",
"registration.fullname.label": "Nome e Cognome", "registration.fullname.label": "Nome e Cognome",
"registration.email.label": "Email", "registration.email.label": "Email",
@@ -123,6 +135,7 @@
"help.text.email": "Per l&#39;attivazione dell&#39;account e aggiornamenti importanti", "help.text.email": "Per l&#39;attivazione dell&#39;account e aggiornamenti importanti",
"create.account.for.free.button": "Crea un account gratis", "create.account.for.free.button": "Crea un account gratis",
"registration.other.options.heading": "Oppure registrati con:", "registration.other.options.heading": "Oppure registrati con:",
"create.account.cta.button": "{label}",
"register.institution.login.button": "Credenziali dell&#39;istituto/campus", "register.institution.login.button": "Credenziali dell&#39;istituto/campus",
"register.institution.login.page.title": "Registrati con le credenziali dell'istituzione/campus", "register.institution.login.page.title": "Registrati con le credenziali dell'istituzione/campus",
"empty.name.field.error": "Inserisci il tuo nome e cognome", "empty.name.field.error": "Inserisci il tuo nome e cognome",
@@ -140,6 +153,7 @@
"registration.request.server.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.", "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.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.", "registration.tpa.session.expired": "La registrazione mediante {provider} è andata in timeout.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Termini e Condizioni del Servizio", "terms.of.service.and.honor.code": "Termini e Condizioni del Servizio",
"privacy.policy": "Informativa sulla privacy", "privacy.policy": "Informativa sulla privacy",
"honor.code": "Codice d'Onore", "honor.code": "Codice d'Onore",

180
src/i18n/messages/pt.json Normal file
View File

@@ -0,0 +1,180 @@
{
"start.learning": "Start learning",
"with.site.name": "with {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"logistration.sign.in": "Sign in",
"logistration.register": "Register",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
"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}.",
"registration.using.tpa.form.heading": "Finish creating your account",
"zendesk.supportTitle": "edX Support",
"zendesk.selectTicketForm": "Please choose your request type:",
"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.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 {platformName} 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.",
"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}.",
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
"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",
"forgot.password": "Forgot password",
"institution.login.button": "Institution/campus credentials",
"institution.login.page.title": "Sign in with institution/campus credentials",
"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.",
"enterprise.login.btn.text": "Company or school credentials",
"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",
"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",
"tpa.account.link": "{provider} account",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
"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.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",
"login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"optional.fields.information.link": "Learn more about how we use this information.",
"optional.fields.submit.button": "Submit",
"optional.fields.skip.button": "Skip for now",
"optional.fields.next.button": "Next",
"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.",
"recommendation.page.title": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
"registration.username.label": "Public username",
"registration.password.label": "Password",
"registration.country.label": "Country/Region",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
"help.text.name": "This name will be used by any certificates that you earn.",
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free",
"registration.other.options.heading": "Or register with:",
"create.account.cta.button": "{label}",
"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.do.not.match": "The email addresses do not match.",
"email.invalid.format.error": "Enter a valid email address",
"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",
"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.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.username.suggestion.label": "Suggested:",
"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}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",
"reset.password.page.instructions": "Enter and confirm your new password.",
"new.password.label": "New password",
"confirm.password.label": "Confirm password",
"passwords.do.not.match": "Passwords do not match",
"confirm.your.password": "Confirm your password",
"reset.password.failure.heading": "We couldn't reset your password.",
"reset.password.form.submission.error": "Please check your responses and try again.",
"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.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}

View File

@@ -1,166 +1,180 @@
{ {
"start.learning": "Começar a aprender", "start.learning": "Começar a aprender",
"with.site.name": "with {siteName}", "with.site.name": "com {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Bem vindo a {siteName}, {username}!",
"complete.your.profile.1": "Concluído", "complete.your.profile.1": "Concluído",
"complete.your.profile.2": "o seu perfil", "complete.your.profile.2": "o seu perfil",
"welcome.to.platform": "Bem vindo a {siteName}, {username}!", "institution.login.page.sub.heading": "Escolha a sua instituição a partir da lista abaixo",
"institution.login.page.sub.heading": "Choose your institution from the list below",
"logistration.sign.in": "Iniciar sessão", "logistration.sign.in": "Iniciar sessão",
"logistration.register": "Registe-se", "logistration.register": "Registe-se",
"enterprisetpa.title.heading": "Gostaria de iniciar sessão usando as suas {providerName} credenciais?", "enterprisetpa.title.heading": "Gostaria de iniciar sessão usando as suas {providerName} credenciais?",
"enterprisetpa.login.button.text": "Mostre-me outras formas de iniciar sessão ou registar-se", "enterprisetpa.login.button.text": "Mostre-me outras formas de iniciar sessão ou registar-se",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in", "enterprisetpa.login.button.text.public.account.creation.disabled": "Mostre-me outras formas de iniciar sessão",
"sso.sign.in.with": "Inicie sessão com {providerName}", "sso.sign.in.with": "Inicie sessão com {providerName}",
"sso.create.account.using": "Criar conta usando {providerName}", "sso.create.account.using": "Criar conta usando {providerName}",
"show.password": "Show password", "show.password": "Mostrar palavra-passe",
"hide.password": "Hide password", "hide.password": "Ocultar palavra-passe",
"one.letter": "1 letter", "one.letter": "1 letra",
"one.number": "1 number", "one.number": "1 número",
"eight.characters": "8 characters", "eight.characters": "8 caracteres",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number", "password.sr.only.helping.text": "A palavra-passe deve conter pelo menos 8 carateres, pelo menos uma letra, e pelo menos um número",
"tpa.alert.heading": "Almost done!", "tpa.alert.heading": "Quase pronto!",
"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}.", "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}.", "register.third.party.auth.account.not.linked": "Entrou com sucesso em {currentProvider}! Só precisamos de um pouco mais de informação antes de começar a aprender com {platformName}.",
"registration.using.tpa.form.heading": "Finish creating your account", "registration.using.tpa.form.heading": "Acabe de criar a sua conta",
"zendesk.supportTitle": "edX Support", "zendesk.supportTitle": "Apoio edX",
"zendesk.selectTicketForm": "Please choose your request type:", "zendesk.selectTicketForm": "Por favor, escolha o seu tipo de pedido:",
"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.", "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.confirmation.message": "Enviámos um email para {email} com instruções para redefinir a sua palavra-passe.\n Se não receber uma mensagem de redefinição de palavra-passe após 1 minuto, verifique se introduziu\n o endereço de correio electrónico correto, ou verifique a sua pasta de spam. Se precisar de mais assistência, {supportLink}.",
"forgot.password.page.title": "Esqueceu a Senha | {siteName}", "forgot.password.page.title": "Esqueceu a Senha | {siteName}",
"forgot.password.page.heading": "Reset password", "forgot.password.page.heading": "Redefinir palavra-passe",
"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.instructions": "Por favor introduza o seu endereço de email abaixo e enviar-lhe-emos um email com instruções sobre como redefinir a sua palavra-passe.",
"forgot.password.page.invalid.email.message": "Introduzir um endereço de correio electrónico válido", "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.email.field.label": "Email",
"forgot.password.page.submit.button": "Submeter", "forgot.password.page.submit.button": "Submeter",
"forgot.password.error.alert.title.": "We were unable to contact you.", "forgot.password.error.alert.title.": "Não nos foi possível contactá-lo.",
"forgot.password.error.message.title": "Ocorreu um erro.", "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.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.empty.email.field.error": "Digite o seu email",
"forgot.password.email.help.text": "O endereço de e-mail que usou para se registar em {platformName}", "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.message.title": "Verifique o seu email",
"confirmation.support.link": "contact technical support", "confirmation.support.link": "contacto o suporte técnico",
"need.help.sign.in.text": "Need help signing in?", "need.help.sign.in.text": "Precisa de ajuda para entrar?",
"additional.help.text": "For additional help, contact {platformName} support at ", "additional.help.text": "Para ajuda adicional, contacte o apoio {platformName} em",
"sign.in.text": "Sign in", "sign.in.text": "Iniciar sessão",
"extend.field.errors": "{emailError} below.", "extend.field.errors": "{emailError} abaixo.",
"invalid.token.heading": "Invalid password reset link", "invalid.token.heading": "Link para Redefinir Palavra-passe inválido",
"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.", "invalid.token.error.message": "Este link de redefinição de palavra-passe é inválido. Pode já ter sido utilizada. Introduza o seu email abaixo para receber um novo link.",
"token.validation.rate.limit.error.heading": "Too many requests", "token.validation.rate.limit.error.heading": "Demasiados pedidos",
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.", "token.validation.rate.limit.error": "Ocorreu um erro devido a demasiados pedidos. Por favor, tente novamente após algum tempo.",
"token.validation.internal.sever.error.heading": "Token validation failure", "token.validation.internal.sever.error.heading": "Falha na validação do Token",
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.", "token.validation.internal.sever.error": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.", "internal.server.error": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
"account.activation.error.message": "Alguma coisa correu mal, siga {supportLink} para resolver esta questão.", "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.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}.",
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.", "allowed.domain.login.error": "Como utilizador do {allowedDomain}, deve iniciar sessão com o seu {allowedDomain} {tpaLink}.",
"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.1": "O nome de utilizador, email ou palavra-passe que introduziu está incorreto. Tem {remainingAttempts} mais tentativas\n de login antes da sua conta ser temporariamente bloqueada.",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}", "login.incorrect.credentials.error.attempts.text.2": "Se esqueceu a sua palavra-passe, {resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.", "account.locked.out.message.2": "Por precaução, pode {resetLink} antes de tentar novamente.",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.", "login.incorrect.credentials.error.with.reset.link": "O nome de utilizador, email ou senha que introduziu está incorreto. Por favor, tente novamente ou {resetLink}.",
"login.page.title": "Iniciar sessão | {siteName}", "login.page.title": "Iniciar sessão | {siteName}",
"login.user.identity.label": "Username or email", "login.user.identity.label": "Nome de utilizador ou e-mail",
"login.password.label": "Password", "login.password.label": "Palavra-passe",
"sign.in.button": "Iniciar Sessão", "sign.in.button": "Iniciar Sessão",
"forgot.password": "Forgot password", "forgot.password": "Esqueci-me da palavra-passe",
"institution.login.button": "Institution/campus credentials", "institution.login.button": "Credenciais da instituição/campus",
"institution.login.page.title": "Inicie sessão com credenciais de instituição/campus", "institution.login.page.title": "Inicie sessão com credenciais de instituição/campus",
"login.other.options.heading": "Or sign in with:", "login.other.options.heading": "Ou faça login com:",
"non.compliant.password.title": "Recentemente mudámos os nossos requisitos de palavra-passe", "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.", "non.compliant.password.message": "A sua palavra-passe atual não satisfaz os novos requisitos de segurança. Acabámos de enviar uma mensagem de redefinição da palavra-passe para o endereço de email associado a esta conta. Obrigado por nos ajudar a manter os seus dados em segurança.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.", "account.locked.out.message.1": "Para proteger sua conta, esta foi temporariamente bloqueada. Tente novamente dentro de 30 minutos.",
"enterprise.login.btn.text": "Company or school credentials", "enterprise.login.btn.text": "Credenciais da empresa ou escola",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.", "username.or.email.format.validation.less.chars.message": "O nome de utilizador ou email deve ter pelo menos 3 carateres.",
"email.validation.message": "Enter your username or email", "email.validation.message": "Insira o seu nome de utilizador ou email",
"password.validation.message": "Password criteria has not been met", "password.validation.message": "Os critérios de palavra-passe não foram cumpridos",
"account.activation.success.message.title": "Sucesso! Você ativou a sua conta.", "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.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.info.message": "Esta conta já foi ativada.",
"account.activation.error.message.title": "A sua conta não pôde ser ativada", "account.activation.error.message.title": "A sua conta não pôde ser ativada",
"account.activation.support.link": "contato de suporte", "account.activation.support.link": "contato de suporte",
"account.confirmation.success.message.title": "Success! You have confirmed your email.", "account.confirmation.success.message.title": "Sucesso! Confirmou o seu email.",
"account.confirmation.success.message": "Sign in to continue.", "account.confirmation.success.message": "Inicie sessão para continuar.",
"account.confirmation.info.message": "This email has already been confirmed.", "account.confirmation.info.message": "Este email já foi confirmado.",
"account.confirmation.error.message.title": "Your email could not be confirmed", "account.confirmation.error.message.title": "Não foi possível confirmar o seu email",
"tpa.account.link": "{provider} account", "tpa.account.link": "Conta {provider}",
"internal.server.error.message": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.", "internal.server.error.message": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
"login.rate.limit.reached.message": "Muitas tentativas de login sem sucesso. Tente novamente mais tarde.", "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.", "login.failure.header.title": "O seu acesso não foi possível.",
"contact.support.link": "contactar o suporte {platformName}", "contact.support.link": "contactar o suporte {platformName}",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.", "login.incorrect.credentials.error": "O nome de utilizador, email, ou palavra-passe que introduziu está incorreto. Por favor, tente novamente.",
"login.form.invalid.error.message": "Please fill in the fields below.", "login.form.invalid.error.message": "Por favor, preencha os campos abaixo.",
"login.incorrect.credentials.error.reset.link.text": "reset your password", "login.incorrect.credentials.error.reset.link.text": "redefinir a sua palavra-passe",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.", "login.incorrect.credentials.error.before.account.blocked.text": "clique aqui para a redefinir.",
"password.security.nudge.title": "Password security", "password.security.nudge.title": "Segurança da palavra-passe",
"password.security.block.title": "Password change required", "password.security.block.title": "Alteração de palavra-passe necessária",
"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.nudge.body": "O nosso sistema detectou que a sua palavra-passe é vulnerável. Recomendamos que a altere para que a sua conta permaneça segura.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.", "password.security.block.body": "O nosso sistema detectou que a sua palavra-passe é vulnerável. Altere a sua palavra-passe para que a sua conta permaneça segura.",
"password.security.close.button": "Close", "password.security.close.button": "Fechar",
"password.security.redirect.to.reset.password.button": "Reset your password", "password.security.redirect.to.reset.password.button": "Redefinir a sua palavra-passe",
"login.tpa.authentication.failure": "Lamentamos, mas não tem autorização para aceder à plataforma {platform_name} através deste canal. Pedimos-lhe que entre em contacto com o administrador ou gestor do curso para aceder à plataforma {platform_name}. {line_break}{line_break}Detalhes do erro: {line_break}{error_message}",
"progressive.profiling.page.title": "Welcome | {siteName}", "progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.", "progressive.profiling.page.heading": "Algumas perguntas para si vão ajudar a tornar-nos mais espertos.",
"optional.fields.information.link": "Learn more about how we use this information.", "optional.fields.information.link": "Saiba mais sobre a forma como utilizamos esta informação.",
"optional.fields.submit.button": "Submit", "optional.fields.submit.button": "Submeter",
"optional.fields.skip.button": "Skip for now", "optional.fields.skip.button": "Saltar por enquanto",
"optional.fields.next.button": "Next", "optional.fields.next.button": "Seguinte",
"continue.to.platform": "Continue to {platformName}", "continue.to.platform": "Continue para {platformName}",
"modal.title": "Thanks for letting us know.", "modal.title": "Obrigado por nos informar.",
"modal.description": "You can complete your profile in settings at any time if you change your mind.", "modal.description": "Pode completar o seu perfil nas configurações em qualquer altura se mudar de ideias.",
"welcome.page.error.heading": "We couldn't update your profile", "welcome.page.error.heading": "Não foi possível atualizar o seu perfil",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time.", "welcome.page.error.message": "Ocorreu um erro. Pode completar o seu perfil nas configurações em qualquer altura.",
"recommendation.page.title": "Recommendations | {siteName}", "recommendation.page.title": "Recomendações | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.", "recommendation.page.heading": "Temos algumas recomendações para o ajudar a começar.",
"recommendation.skip.button": "Skip for now", "recommendation.skip.button": "Saltar por enquanto",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Registar | {siteName}", "register.page.title": "Registar | {siteName}",
"registration.fullname.label": "Full name", "registration.fullname.label": "Nome completo",
"registration.email.label": "Email", "registration.email.label": "E-mail",
"registration.username.label": "Public username", "registration.username.label": "Nome de utilizador público",
"registration.password.label": "Password", "registration.password.label": "Palavra-passe",
"registration.country.label": "Country/Region", "registration.country.label": "País/Região",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.", "registration.opt.in.label": "Concordo que {siteName} pode enviar-me mensagens de marketing.",
"help.text.name": "This name will be used by any certificates that you earn.", "help.text.name": "Este nome será usado por quaisquer certificados que conseguir obter.",
"help.text.username.1": "The name that will identify you in your courses.", "help.text.username.1": "O nome que o irá identificar nos seus cursos.",
"help.text.username.2": "This can not be changed later.", "help.text.username.2": "Isto não pode ser alterado mais tarde.",
"help.text.email": "For account activation and important updates", "help.text.email": "Para ativação de contas e atualizações importantes",
"create.account.for.free.button": "Create an account for free", "create.account.for.free.button": "Criar uma conta gratuitamente",
"registration.other.options.heading": "Or register with:", "registration.other.options.heading": "Ou registe-se com:",
"register.institution.login.button": "Institution/campus credentials", "create.account.cta.button": "{label}",
"register.institution.login.button": "Credenciais da instituição/campus",
"register.institution.login.page.title": "Registo com credenciais da instituição/campus", "register.institution.login.page.title": "Registo com credenciais da instituição/campus",
"empty.name.field.error": "Enter your full name", "empty.name.field.error": "Insira o seu nome completo",
"empty.email.field.error": "Enter your email", "empty.email.field.error": "Insira o seu email",
"empty.username.field.error": "Username must be between 2 and 30 characters", "empty.username.field.error": "O nome de utilizador deve ter entre 2 e 30 carateres",
"empty.password.field.error": "Password criteria has not been met", "empty.password.field.error": "Os critérios de palavra-passe não foram cumpridos",
"empty.country.field.error": "Select your country or region of residence", "empty.country.field.error": "Selecione o seu país ou região de residência",
"email.do.not.match": "The email addresses do not match.", "email.do.not.match": "Os endereços de email não correspondem.",
"email.invalid.format.error": "Enter a valid email address", "email.invalid.format.error": "Introduzir um endereço de correio electrónico válido",
"username.validation.message": "Username must be between 2 and 30 characters", "username.validation.message": "O nome de utilizador deve ter entre 2 e 30 carateres",
"name.validation.message": "Enter a valid name", "name.validation.message": "Introduza um nome válido",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces", "username.format.validation.message": "Os nomes de utilizador só podem conter letras (A-Z, a-z), numerais (0-9), sublinhados (_), e hífenes (-). Os nomes de utilizador não podem conter espaços",
"registration.request.failure.header": "Não foi possível criar a sua conta.", "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.empty.form.submission.error": "Verifique as suas respostas e tente novamente.",
"registration.request.server.error": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.", "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.rate.limit.error": "Demasiadas tentativas de registo falhadas. Tente novamente mais tarde.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.", "registration.tpa.session.expired": "O tempo para registo utilizando {provider} expirou.",
"registration.tpa.authentication.failure": "Lamentamos, mas não tem autorização para aceder à plataforma {platform_name} através deste canal. Pedimos-lhe que entre em contacto com o administrador ou gestor do curso para aceder à plataforma {platform_name}. {line_break}{line_break}Detalhes do erro: {line_break}{error_message}",
"terms.of.service.and.honor.code": "Termos de Serviço e Código de Honra", "terms.of.service.and.honor.code": "Termos de Serviço e Código de Honra",
"privacy.policy": "Política de Privacidade", "privacy.policy": "Política de Privacidade",
"honor.code": "Honor Code", "honor.code": "Código de Honra",
"terms.of.service": "Terms of Service", "terms.of.service": "Termos do Serviço",
"registration.username.suggestion.label": "Suggested:", "registration.username.suggestion.label": "Sugerido:",
"did.you.mean.alert.text": "Did you mean", "did.you.mean.alert.text": "Quis dizer",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.", "register.page.terms.of.service.and.honor.code": "Ao criar uma conta, concorda com o {tosAndHonorCode} e reconhece que {platformName} e cada\n Membro processa os seus dados pessoais em conformidade com a {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}", "register.page.honor.code": "Concordo com a {tosAndHonorCode} {platformName}",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}", "register.page.terms.of.service": "Concordo com os {termsOfService} {platformName}",
"sign.in": "Sign in", "sign.in": "Iniciar sessão",
"reset.password.page.title": "Redefinir Palavra-passe | {siteName}", "reset.password.page.title": "Redefinir Palavra-passe | {siteName}",
"reset.password": "Reset password", "reset.password": "Redefinir palavra-passe",
"reset.password.page.instructions": "Insira e confirme a sua nova palavra-passe.", "reset.password.page.instructions": "Insira e confirme a sua nova palavra-passe.",
"new.password.label": "New password", "new.password.label": "Nova palavra-passe",
"confirm.password.label": "Confirm password", "confirm.password.label": "Confirmar palavra-passe",
"passwords.do.not.match": "Passwords do not match", "passwords.do.not.match": "As palavras-passe não coincidem",
"confirm.your.password": "Confirm your password", "confirm.your.password": "Confirme a sua palavra-passe",
"reset.password.failure.heading": "We couldn't reset your password.", "reset.password.failure.heading": "Não conseguimos redefinir sua palavra-passe.",
"reset.password.form.submission.error": "Please check your responses and try again.", "reset.password.form.submission.error": "Por favor, verifique as suas respostas e tente novamente.",
"reset.server.rate.limit.error": "Too many requests.", "reset.server.rate.limit.error": "Demasiados pedidos.",
"reset.password.success.heading": "Password reset complete.", "reset.password.success.heading": "Redefinição de palavra-passe concluída",
"reset.password.success": "Your password has been reset. Sign in to your account.", "reset.password.success": "A sua palavra-passe foi redefinida. Inicie sessão na sua conta.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time." "rate.limit.error": "Ocorreu um erro devido a demasiados pedidos. Por favor, tente novamente após algum tempo."
} }

View File

@@ -1,9 +1,11 @@
{ {
"start.learning": "Start learning", "start.learning": "Start learning",
"with.site.name": "with {siteName}", "with.site.name": "with {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"complete.your.profile.1": "Complete", "complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile", "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", "institution.login.page.sub.heading": "Choose your institution from the list below",
"logistration.sign.in": "Sign in", "logistration.sign.in": "Sign in",
"logistration.register": "Register", "logistration.register": "Register",
@@ -96,6 +98,7 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password 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.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password", "password.security.redirect.to.reset.password.button": "Reset your password",
"login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Welcome | {siteName}", "progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.", "progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"optional.fields.information.link": "Learn more about how we use this information.", "optional.fields.information.link": "Learn more about how we use this information.",
@@ -110,6 +113,15 @@
"recommendation.page.title": "Recommendations | {siteName}", "recommendation.page.title": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.", "recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now", "recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Register | {siteName}", "register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name", "registration.fullname.label": "Full name",
"registration.email.label": "Email", "registration.email.label": "Email",
@@ -123,6 +135,7 @@
"help.text.email": "For account activation and important updates", "help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free", "create.account.for.free.button": "Create an account for free",
"registration.other.options.heading": "Or register with:", "registration.other.options.heading": "Or register with:",
"create.account.cta.button": "{label}",
"register.institution.login.button": "Institution/campus credentials", "register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials", "register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name", "empty.name.field.error": "Enter your full name",
@@ -140,6 +153,7 @@
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.", "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.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.", "registration.tpa.session.expired": "Registration using {provider} has timed out.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code", "terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy", "privacy.policy": "Privacy Policy",
"honor.code": "Honor Code", "honor.code": "Honor Code",

View File

@@ -1,9 +1,11 @@
{ {
"start.learning": "Start learning", "start.learning": "Start learning",
"with.site.name": "with {siteName}", "with.site.name": "with {siteName}",
"your.career.turning.point": "Your career turning point",
"is.here": "is here.",
"welcome.to.platform": "Welcome to {siteName}, {username}!",
"complete.your.profile.1": "Complete", "complete.your.profile.1": "Complete",
"complete.your.profile.2": "your profile", "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", "institution.login.page.sub.heading": "Choose your institution from the list below",
"logistration.sign.in": "Sign in", "logistration.sign.in": "Sign in",
"logistration.register": "Register", "logistration.register": "Register",
@@ -96,6 +98,7 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password 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.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password", "password.security.redirect.to.reset.password.button": "Reset your password",
"login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.title": "Welcome | {siteName}", "progressive.profiling.page.title": "Welcome | {siteName}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.", "progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
"optional.fields.information.link": "Learn more about how we use this information.", "optional.fields.information.link": "Learn more about how we use this information.",
@@ -110,6 +113,15 @@
"recommendation.page.title": "Recommendations | {siteName}", "recommendation.page.title": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.", "recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now", "recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending",
"recommendation.option.popular": "Most Popular",
"recommendation.product-card.pill-text.course": "Course",
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"recommendation.product-card.footer-text.subscription": "Subscription",
"recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"register.page.title": "Register | {siteName}", "register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name", "registration.fullname.label": "Full name",
"registration.email.label": "Email", "registration.email.label": "Email",
@@ -123,6 +135,7 @@
"help.text.email": "For account activation and important updates", "help.text.email": "For account activation and important updates",
"create.account.for.free.button": "Create an account for free", "create.account.for.free.button": "Create an account for free",
"registration.other.options.heading": "Or register with:", "registration.other.options.heading": "Or register with:",
"create.account.cta.button": "{label}",
"register.institution.login.button": "Institution/campus credentials", "register.institution.login.button": "Institution/campus credentials",
"register.institution.login.page.title": "Register with institution/campus credentials", "register.institution.login.page.title": "Register with institution/campus credentials",
"empty.name.field.error": "Enter your full name", "empty.name.field.error": "Enter your full name",
@@ -140,6 +153,7 @@
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.", "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.rate.limit.error": "Too many failed registration attempts. Try again later.",
"registration.tpa.session.expired": "Registration using {provider} has timed out.", "registration.tpa.session.expired": "Registration using {provider} has timed out.",
"registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code", "terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy", "privacy.policy": "Privacy Policy",
"honor.code": "Honor Code", "honor.code": "Honor Code",

View File

@@ -1,166 +1,180 @@
{ {
"start.learning": "Start learning", "start.learning": "开始学习",
"with.site.name": "with {siteName}", "with.site.name": "{siteName}",
"complete.your.profile.1": "Complete", "your.career.turning.point": "Your career turning point",
"complete.your.profile.2": "your profile", "is.here": "is here.",
"welcome.to.platform": "Welcome to {siteName}, {username}!", "welcome.to.platform": "欢迎来到{siteName}{username}",
"institution.login.page.sub.heading": "Choose your institution from the list below", "complete.your.profile.1": "完成",
"logistration.sign.in": "Sign in", "complete.your.profile.2": "个人资料",
"logistration.register": "Register", "institution.login.page.sub.heading": "从下面的列表中选择您的机构",
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?", "logistration.sign.in": "登录",
"enterprisetpa.login.button.text": "Show me other ways to sign in or register", "logistration.register": "注册",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in", "enterprisetpa.title.heading": "您要使用 {providerName} 登录吗?",
"sso.sign.in.with": "Sign in with {providerName}", "enterprisetpa.login.button.text": "为我显示其他登录或注册方式",
"sso.create.account.using": "Create account using {providerName}", "enterprisetpa.login.button.text.public.account.creation.disabled": "显示其他登录方式",
"show.password": "Show password", "sso.sign.in.with": "使用 {providerName} 登录",
"hide.password": "Hide password", "sso.create.account.using": "使用 {providerName} 创建帐户",
"one.letter": "1 letter", "show.password": "显示密码",
"one.number": "1 number", "hide.password": "隐藏密码",
"eight.characters": "8 characters", "one.letter": "1 个字母",
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number", "one.number": "1个数字",
"tpa.alert.heading": "Almost done!", "eight.characters": "8个字符",
"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.", "password.sr.only.helping.text": "密码必须包含至少 8 个字符、至少一个字母和至少一个数字",
"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}.", "tpa.alert.heading": "快完成了!",
"registration.using.tpa.form.heading": "Finish creating your account", "login.third.party.auth.account.not.linked": "您已成功登录 {currentProvider},但您的 {currentProvider} 帐户没有关联的 {platformName} 帐户。要链接您的帐户,请立即使用您的 {platformName} 密码登录。",
"register.third.party.auth.account.not.linked": "您已成功登录 {currentProvider}!在您开始学习 {platformName} 之前,我们只需要更多信息。",
"registration.using.tpa.form.heading": "完成创建您的帐户",
"zendesk.supportTitle": "edX Support", "zendesk.supportTitle": "edX Support",
"zendesk.selectTicketForm": "Please choose your request type:", "zendesk.selectTicketForm": "Please choose your request type:",
"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.", "error.notfound.message": "您访问的地址不存在或有误。请检查URL后重新尝试访问。",
"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.confirmation.message": "我们向 {email} 发送了一封电子邮件,其中包含重置密码的说明。如果您在 1 分钟后没有收到密码重置消息,请确认您输入了正确的电子邮件地址,或者检查您的垃圾邮件文件夹。如果您需要进一步的帮助,点击{supportLink}",
"forgot.password.page.title": "Forgot Password | {siteName}", "forgot.password.page.title": "忘记密码 | {siteName}",
"forgot.password.page.heading": "Reset password", "forgot.password.page.heading": "重置密码",
"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.instructions": "请在下面输入您的电子邮件地址,我们将向您发送一封电子邮件,其中包含有关如何重置密码的说明。",
"forgot.password.page.invalid.email.message": "Enter a valid email address", "forgot.password.page.invalid.email.message": "输入一个有效的电子邮件地址",
"forgot.password.page.email.field.label": "Email", "forgot.password.page.email.field.label": "邮箱",
"forgot.password.page.submit.button": "Submit", "forgot.password.page.submit.button": "提交",
"forgot.password.error.alert.title.": "We were unable to contact you.", "forgot.password.error.alert.title.": "我们无法联系到您。",
"forgot.password.error.message.title": "An error occurred.", "forgot.password.error.message.title": "发生了一个错误。",
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.", "forgot.password.request.in.progress.message": "你的前一个请求正在处理中,请稍后再尝试。",
"forgot.password.empty.email.field.error": "Enter your email", "forgot.password.empty.email.field.error": "输入你的电子邮箱",
"forgot.password.email.help.text": "The email address you used to register with {platformName}", "forgot.password.email.help.text": "您用于注册 {platformName} 的电子邮件地址",
"confirmation.message.title": "Check your email", "confirmation.message.title": "查收您的邮件",
"confirmation.support.link": "contact technical support", "confirmation.support.link": "联系技术支持",
"need.help.sign.in.text": "Need help signing in?", "need.help.sign.in.text": "需要帮助登录?",
"additional.help.text": "For additional help, contact {platformName} support at ", "additional.help.text": "如需更多帮助,请通过以下方式联系 {platformName} 支持",
"sign.in.text": "Sign in", "sign.in.text": "登录",
"extend.field.errors": "{emailError} below.", "extend.field.errors": "{emailError} 如下。",
"invalid.token.heading": "Invalid password reset link", "invalid.token.heading": "密码重置链接无效",
"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.", "invalid.token.error.message": "此密码重置链接无效。它可能已经被使用过。在下面输入您的电子邮件以接收新链接。",
"token.validation.rate.limit.error.heading": "Too many requests", "token.validation.rate.limit.error.heading": "请求过多",
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.", "token.validation.rate.limit.error": "由于请求过多而发生错误。请稍后重试。",
"token.validation.internal.sever.error.heading": "Token validation failure", "token.validation.internal.sever.error.heading": "验证失败",
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.", "token.validation.internal.sever.error": "发生了错误。尝试刷新页面,或检查您的互联网连接。",
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.", "internal.server.error": "发生了错误。尝试刷新页面,或检查您的互联网连接。",
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.", "account.activation.error.message": "出了点问题,请联系{supportLink}解决这个问题。",
"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.inactive.user.error": "若要登录,您需要激活您的帐户。{lineBreak} {lineBreak}我们刚刚向 {email} 发送了一个激活链接。如果您没有收到电子邮件,请检查您的垃圾邮件文件夹或 {supportLink}",
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.", "allowed.domain.login.error": "作为 {allowedDomain} 用户,您必须使用 {allowedDomain} {tpaLink} 登录。",
"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.1": "您输入的用户名、电子邮件或密码不正确。在您的帐户被暂时锁定之前,您还有 {remainingAttempts} 次登录尝试。",
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}", "login.incorrect.credentials.error.attempts.text.2": "如果您忘记了密码,{resetLink}",
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.", "account.locked.out.message.2": "为了安全起见,您可以先{resetLink}再试一次。",
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.", "login.incorrect.credentials.error.with.reset.link": "您输入的用户名、电子邮件或密码不正确。请重试或 {resetLink}",
"login.page.title": "Login | {siteName}", "login.page.title": "登入 | {siteName}",
"login.user.identity.label": "Username or email", "login.user.identity.label": "用户名或电子邮件",
"login.password.label": "Password", "login.password.label": "密码",
"sign.in.button": "Sign in", "sign.in.button": "登录",
"forgot.password": "Forgot password", "forgot.password": "忘记密码",
"institution.login.button": "Institution/campus credentials", "institution.login.button": "机构/校园凭证",
"institution.login.page.title": "Sign in with institution/campus credentials", "institution.login.page.title": "使用机构/校园凭据登录",
"login.other.options.heading": "Or sign in with:", "login.other.options.heading": "或登录:",
"non.compliant.password.title": "We recently changed our password requirements", "non.compliant.password.title": "我们最近更改了密码要求",
"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.", "non.compliant.password.message": "您当前的密码不符合新的安全要求。我们刚刚向与此帐户关联的电子邮件地址发送了密码重置邮件。感谢您帮助我们保护您的数据安全。",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.", "account.locked.out.message.1": "为了保护您的帐户,它已被暂时锁定。请在 30 分钟后重试。",
"enterprise.login.btn.text": "Company or school credentials", "enterprise.login.btn.text": "单位或学校证书",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.", "username.or.email.format.validation.less.chars.message": "用户名或电子邮件必须至少包含 3 个字符。",
"email.validation.message": "Enter your username or email", "email.validation.message": "输入您的用户名或电子邮件",
"password.validation.message": "Password criteria has not been met", "password.validation.message": "未满足密码条件",
"account.activation.success.message.title": "Success! You have activated your account.", "account.activation.success.message.title": "成功!您已激活您的帐户。",
"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.success.message": "您现在将收到我们发送的与您注册的课程相关的电子邮件更新和提醒。登录以继续。",
"account.activation.info.message": "This account has already been activated.", "account.activation.info.message": "本账号已经被激活。",
"account.activation.error.message.title": "Your account could not be activated", "account.activation.error.message.title": "您的帐户无法激活",
"account.activation.support.link": "contact support", "account.activation.support.link": "请联系技术支持",
"account.confirmation.success.message.title": "Success! You have confirmed your email.", "account.confirmation.success.message.title": "成功!您已确认您的电子邮件。",
"account.confirmation.success.message": "Sign in to continue.", "account.confirmation.success.message": "登录并继续。",
"account.confirmation.info.message": "This email has already been confirmed.", "account.confirmation.info.message": "此电子邮件已被确认。",
"account.confirmation.error.message.title": "Your email could not be confirmed", "account.confirmation.error.message.title": "无法确认您的电子邮件",
"tpa.account.link": "{provider} account", "tpa.account.link": "{provider} 帐户",
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.", "internal.server.error.message": "发生了错误。尝试刷新页面,或检查您的互联网连接。",
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.", "login.rate.limit.reached.message": "失败次数超过限制,请稍后再试!",
"login.failure.header.title": "We couldn't sign you in.", "login.failure.header.title": "登录失败。",
"contact.support.link": "contact {platformName} support", "contact.support.link": "联系 {platformName} 支持",
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.", "login.incorrect.credentials.error": "您输入的用户名、电子邮件或密码不正确。请再试一次。",
"login.form.invalid.error.message": "Please fill in the fields below.", "login.form.invalid.error.message": "请填写以下字段。",
"login.incorrect.credentials.error.reset.link.text": "reset your password", "login.incorrect.credentials.error.reset.link.text": "重置你的密码",
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.", "login.incorrect.credentials.error.before.account.blocked.text": "点击此处来重置它。",
"password.security.nudge.title": "Password security", "password.security.nudge.title": "密码安全",
"password.security.block.title": "Password change required", "password.security.block.title": "需要更改密码",
"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.nudge.body": "系统检测到您的密码存在漏洞。我们建议您更改它,以便您的帐户保持安全。",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.", "password.security.block.body": "系统检测到您的密码存在漏洞。更改您的密码,以确保您的帐户安全。",
"password.security.close.button": "Close", "password.security.close.button": "关闭",
"password.security.redirect.to.reset.password.button": "Reset your password", "password.security.redirect.to.reset.password.button": "重置你的密码",
"progressive.profiling.page.title": "Welcome | {siteName}", "login.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.", "progressive.profiling.page.title": "欢迎来的 | {siteName}",
"optional.fields.information.link": "Learn more about how we use this information.", "progressive.profiling.page.heading": "你的问题将会帮助我们服务做的更好。",
"optional.fields.submit.button": "Submit", "optional.fields.information.link": "详细了解我们如何使用这些信息。",
"optional.fields.skip.button": "Skip for now", "optional.fields.submit.button": "提交",
"optional.fields.next.button": "Next", "optional.fields.skip.button": "暂时跳过",
"continue.to.platform": "Continue to {platformName}", "optional.fields.next.button": "下一节",
"modal.title": "Thanks for letting us know.", "continue.to.platform": "继续{platformName}",
"modal.description": "You can complete your profile in settings at any time if you change your mind.", "modal.title": "感谢您指点。",
"welcome.page.error.heading": "We couldn't update your profile", "modal.description": "如果您改变主意,可以随时在设置中完成您的个人资料。",
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time.", "welcome.page.error.heading": "我们无法更新您的个人资料",
"recommendation.page.title": "Recommendations | {siteName}", "welcome.page.error.message": "发生错误。您可以随时在设置中完成您的个人资料。",
"recommendation.page.heading": "We have a few recommendations to get you started.", "recommendation.page.title": "建议 | {siteName}",
"recommendation.skip.button": "Skip for now", "recommendation.page.heading": "我们有一些建议可以帮助您入门。",
"register.page.title": "Register | {siteName}", "recommendation.skip.button": "暂时跳过",
"registration.fullname.label": "Full name", "recommendation.option.trending": "Trending",
"registration.email.label": "Email", "recommendation.option.popular": "Most Popular",
"registration.username.label": "Public username", "recommendation.product-card.pill-text.course": "Course",
"registration.password.label": "Password", "recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
"registration.country.label": "Country/Region", "recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.", "recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
"help.text.name": "This name will be used by any certificates that you earn.", "recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
"help.text.username.1": "The name that will identify you in your courses.", "recommendation.product-card.footer-text.subscription": "Subscription",
"help.text.username.2": "This can not be changed later.", "recommendation.product-card.launch-icon.sr-text": "Opens a link in a new tab",
"help.text.email": "For account activation and important updates", "register.page.title": "注册 | {siteName}",
"create.account.for.free.button": "Create an account for free", "registration.fullname.label": "全名",
"registration.other.options.heading": "Or register with:", "registration.email.label": "邮箱",
"register.institution.login.button": "Institution/campus credentials", "registration.username.label": "公开用户名",
"register.institution.login.page.title": "Register with institution/campus credentials", "registration.password.label": "密码",
"empty.name.field.error": "Enter your full name", "registration.country.label": "国家/地区",
"empty.email.field.error": "Enter your email", "registration.opt.in.label": "我同意 {siteName} 可以向我发送课程相关推广信息。",
"empty.username.field.error": "Username must be between 2 and 30 characters", "help.text.name": "您获得的任何证书都将使用此名称。",
"empty.password.field.error": "Password criteria has not been met", "help.text.username.1": "此名称将会用于您在课程中的身份识别。",
"empty.country.field.error": "Select your country or region of residence", "help.text.username.2": "过后无法更改。",
"email.do.not.match": "The email addresses do not match.", "help.text.email": "用于帐户激活和重要更新",
"email.invalid.format.error": "Enter a valid email address", "create.account.for.free.button": "免费创建一个帐户",
"username.validation.message": "Username must be between 2 and 30 characters", "registration.other.options.heading": "或注册:",
"name.validation.message": "Enter a valid name", "create.account.cta.button": "{label}",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces", "register.institution.login.button": "机构/院系验证",
"registration.request.failure.header": "We couldn't create your account.", "register.institution.login.page.title": "使用机构/院系账户注册",
"registration.empty.form.submission.error": "Please check your responses and try again.", "empty.name.field.error": "输入您的全名",
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.", "empty.email.field.error": "输入你的电子邮箱",
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.", "empty.username.field.error": "用户名必须介于 2 到 30 个字符之间",
"registration.tpa.session.expired": "Registration using {provider} has timed out.", "empty.password.field.error": "未满足密码条件",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code", "empty.country.field.error": "选择您居住的国家或地区",
"privacy.policy": "Privacy Policy", "email.do.not.match": "邮箱不一致。",
"honor.code": "Honor Code", "email.invalid.format.error": "输入一个有效的电子邮件地址",
"terms.of.service": "Terms of Service", "username.validation.message": "用户名必须介于 2 到 30 个字符之间",
"registration.username.suggestion.label": "Suggested:", "name.validation.message": "输入有效名称",
"did.you.mean.alert.text": "Did you mean", "username.format.validation.message": "用户名只能包含字母AZ、az、数字0-9、下划线_和连字符-)。用户名不能包含空格",
"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}.", "registration.request.failure.header": "无法创建账号。",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}", "registration.empty.form.submission.error": "请检查您的回复并重试。",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}", "registration.request.server.error": "发生了错误。尝试刷新页面,或检查您的互联网连接。",
"sign.in": "Sign in", "registration.rate.limit.error": "注册尝试失败次数过多。稍后再试。",
"reset.password.page.title": "Reset Password | {siteName}", "registration.tpa.session.expired": "使用{provider}注册已超时。",
"reset.password": "Reset password", "registration.tpa.authentication.failure": "We are sorry, you are not authorized to access {platform_name} via this channel. Please contact your learning administrator or manager in order to access {platform_name}.{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}",
"reset.password.page.instructions": "Enter and confirm your new password.", "terms.of.service.and.honor.code": "服务条款和诚信准则",
"new.password.label": "New password", "privacy.policy": "隐私政策",
"confirm.password.label": "Confirm password", "honor.code": "规则",
"passwords.do.not.match": "Passwords do not match", "terms.of.service": "服务条款",
"confirm.your.password": "Confirm your password", "registration.username.suggestion.label": "建议:",
"reset.password.failure.heading": "We couldn't reset your password.", "did.you.mean.alert.text": "你的意思是",
"reset.password.form.submission.error": "Please check your responses and try again.", "register.page.terms.of.service.and.honor.code": "创建帐户即表示您同意 {tosAndHonorCode} 并承认 {platformName} 和每位会员根据 {privacyPolicy} 处理您的个人数据。",
"reset.server.rate.limit.error": "Too many requests.", "register.page.honor.code": "我同意 {platformName} {tosAndHonorCode}",
"reset.password.success.heading": "Password reset complete.", "register.page.terms.of.service": "我同意接受 {platformName} {termsOfService}",
"reset.password.success": "Your password has been reset. Sign in to your account.", "sign.in": "登录",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time." "reset.password.page.title": "重设密码 | {siteName}",
"reset.password": "重置密码",
"reset.password.page.instructions": "输入并确认你的新密码。",
"new.password.label": "新密码",
"confirm.password.label": "确认密码",
"passwords.do.not.match": "密码不匹配",
"confirm.your.password": "确认你的密码",
"reset.password.failure.heading": "我们无法重置您的密码。",
"reset.password.form.submission.error": "请检查您的回复并重试。",
"reset.server.rate.limit.error": "请求过多。",
"reset.password.success.heading": "密码重置完成。",
"reset.password.success": "您的密码已重置。登录到您的帐户。",
"rate.limit.error": "由于请求过多而发生错误。请稍后重试。"
} }

View File

@@ -8,10 +8,9 @@ import {
APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe, APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe,
} from '@edx/frontend-platform'; } from '@edx/frontend-platform';
import { ErrorPage } from '@edx/frontend-platform/react'; import { ErrorPage } from '@edx/frontend-platform/react';
import { messages as paragonMessages } from '@edx/paragon';
import configuration from './config'; import configuration from './config';
import appMessages from './i18n'; import messages from './i18n';
import MainApp from './MainApp'; import MainApp from './MainApp';
subscribe(APP_READY, () => { subscribe(APP_READY, () => {
@@ -31,8 +30,5 @@ initialize({
mergeConfig(configuration); mergeConfig(configuration);
}, },
}, },
messages: [ messages,
appMessages,
paragonMessages,
],
}); });

View File

@@ -3,5 +3,4 @@
@import "~@edx/paragon/scss/core/core"; @import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides"; @import "~@edx/brand/paragon/overrides";
@import "@edx/frontend-component-cookie-policy-banner/build/_cookie-policy-banner";
@import "sass/style"; @import "sass/style";

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
@@ -7,12 +7,12 @@ import {
} from '@edx/paragon'; } from '@edx/paragon';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link, Redirect } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import messages from './messages';
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants'; import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils'; import { updatePathWithQueryParams } from '../data/utils';
import useMobileResponsive from '../data/utils/useMobileResponsive'; import useMobileResponsive from '../data/utils/useMobileResponsive';
import messages from './messages';
const ChangePasswordPrompt = ({ variant, redirectUrl }) => { const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
const isMobileView = useMobileResponsive(); const isMobileView = useMobileResponsive();
@@ -29,10 +29,14 @@ const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const [isOpen, open, close] = useToggle(true, handlers); const [isOpen, open, close] = useToggle(true, handlers);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const navigate = useNavigate();
useEffect(() => {
if (redirectToResetPasswordPage) {
navigate(updatePathWithQueryParams(RESET_PAGE));
}
}, [redirectToResetPasswordPage, navigate]);
if (redirectToResetPasswordPage) {
return <Redirect to={updatePathWithQueryParams(RESET_PAGE)} />;
}
return ( return (
<ModalDialog <ModalDialog
title="Password security" title="Password security"

View File

@@ -13,6 +13,14 @@ import { Helmet } from 'react-helmet';
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import AccountActivationMessage from './AccountActivationMessage';
import {
loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData,
} from './data/actions';
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import { loginErrorSelector, loginFormDataSelector, loginRequestSelector } from './data/selectors';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
import { import {
FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration, FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration,
RenderInstitutionButton, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton, SocialAuthProviders, ThirdPartyAuthAlert,
@@ -28,19 +36,10 @@ import {
getAllPossibleQueryParams, getAllPossibleQueryParams,
getTpaHint, getTpaHint,
getTpaProvider, getTpaProvider,
setSurveyCookie,
updatePathWithQueryParams, updatePathWithQueryParams,
windowScrollTo, windowScrollTo,
} from '../data/utils'; } from '../data/utils';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess'; import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import AccountActivationMessage from './AccountActivationMessage';
import {
loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData,
} from './data/actions';
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import { loginErrorSelector, loginFormDataSelector, loginRequestSelector } from './data/selectors';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
class LoginPage extends React.Component { class LoginPage extends React.Component {
constructor(props, context) { constructor(props, context) {
@@ -230,16 +229,6 @@ class LoginPage extends React.Component {
}; };
tpaAuthenticationError.errorCode = TPA_AUTHENTICATION_FAILURE; tpaAuthenticationError.errorCode = TPA_AUTHENTICATION_FAILURE;
} }
if (this.props.loginResult.success) {
setSurveyCookie('login');
// Fire optimizely events
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'event',
eventName: 'authn-login-coversion',
});
}
return ( return (
<> <>

View File

@@ -1,6 +1,6 @@
import { LOGIN_PERSIST_FORM_DATA, LOGIN_REMOVE_PASSWORD_RESET_BANNER, LOGIN_REQUEST } from './actions';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants'; import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
import { RESET_PASSWORD } from '../../reset-password'; import { RESET_PASSWORD } from '../../reset-password';
import { LOGIN_PERSIST_FORM_DATA, LOGIN_REMOVE_PASSWORD_RESET_BANNER, LOGIN_REQUEST } from './actions';
export const defaultState = { export const defaultState = {
loginError: null, loginError: null,

View File

@@ -3,15 +3,19 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { MemoryRouter, Router } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { RESET_PAGE } from '../../data/constants'; import { RESET_PAGE } from '../../data/constants';
import ChangePasswordPrompt from '../ChangePasswordPrompt'; import ChangePasswordPrompt from '../ChangePasswordPrompt';
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt); const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
const history = createMemoryHistory(); const mockedNavigator = jest.fn();
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),
useNavigate: () => mockedNavigator,
}));
describe('ChangePasswordPromptTests', () => { describe('ChangePasswordPromptTests', () => {
let props = {}; let props = {};
@@ -55,9 +59,7 @@ describe('ChangePasswordPromptTests', () => {
const changePasswordPrompt = mount( const changePasswordPrompt = mount(
<IntlProvider locale="en"> <IntlProvider locale="en">
<MemoryRouter> <MemoryRouter>
<Router history={history}> <IntlChangePasswordPrompt {...props} />
<IntlChangePasswordPrompt {...props} />
</Router>
</MemoryRouter> </MemoryRouter>
</IntlProvider>, </IntlProvider>,
); );
@@ -67,6 +69,6 @@ describe('ChangePasswordPromptTests', () => {
}); });
changePasswordPrompt.update(); changePasswordPrompt.update();
expect(history.location.pathname).toEqual(RESET_PAGE); expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE);
}); });
}); });

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import * as auth from '@edx/frontend-platform/auth';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
@@ -20,8 +19,9 @@ import {
} from '../data/constants'; } from '../data/constants';
import LoginFailureMessage from '../LoginFailure'; import LoginFailureMessage from '../LoginFailure';
jest.mock('@edx/frontend-platform/auth'); jest.mock('@edx/frontend-platform/auth', () => ({
auth.getAuthService = jest.fn(); getAuthService: jest.fn(),
}));
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage); const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
@@ -225,7 +225,7 @@ describe('LoginFailureMessage', () => {
loginError: { loginError: {
errorCode: TPA_AUTHENTICATION_FAILURE, errorCode: TPA_AUTHENTICATION_FAILURE,
context: { context: {
errorMessage: 'An error occured', errorMessage: 'An error occurred',
}, },
}, },
}; };
@@ -239,7 +239,7 @@ describe('LoginFailureMessage', () => {
const expectedMessageSubstring = 'We are sorry, you are not authorized to access'; const expectedMessageSubstring = 'We are sorry, you are not authorized to access';
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toContain(expectedMessageSubstring); expect(loginFailureMessage.find('#login-failure-alert').first().text()).toContain(expectedMessageSubstring);
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toContain('An error occured'); expect(loginFailureMessage.find('#login-failure-alert').first().text()).toContain('An error occurred');
}); });
it('should show modal that nudges users to change password', () => { it('should show modal that nudges users to change password', () => {

View File

@@ -1,17 +1,15 @@
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics'; import { sendPageEvent } from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants'; import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
import { import {
loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData, loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData,
} from '../data/actions'; } from '../data/actions';
@@ -19,21 +17,19 @@ import { INTERNAL_SERVER_ERROR } from '../data/constants';
import LoginFailureMessage from '../LoginFailure'; import LoginFailureMessage from '../LoginFailure';
import LoginPage from '../LoginPage'; import LoginPage from '../LoginPage';
jest.mock('@edx/frontend-platform/analytics'); jest.mock('@edx/frontend-platform/analytics', () => ({
jest.mock('@edx/frontend-platform/auth'); sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
analytics.sendTrackEvent = jest.fn(); }));
analytics.sendPageEvent = jest.fn(); jest.mock('@edx/frontend-platform/auth', () => ({
auth.getAuthService = jest.fn(); getAuthService: jest.fn(),
}));
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage); const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
const IntlLoginPage = injectIntl(LoginPage); const IntlLoginPage = injectIntl(LoginPage);
const mockStore = configureStore(); const mockStore = configureStore();
describe('LoginPage', () => { describe('LoginPage', () => {
mergeConfig({
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME,
});
let props = {}; let props = {};
let store = {}; let store = {};
let loginFormData = {}; let loginFormData = {};
@@ -59,6 +55,9 @@ describe('LoginPage', () => {
secondaryProviders: [], secondaryProviders: [],
}, },
}, },
register: {
validationApiRateLimited: false,
},
}; };
const secondaryProviders = { const secondaryProviders = {
@@ -72,7 +71,7 @@ describe('LoginPage', () => {
const ssoProvider = { const ssoProvider = {
id: 'oa2-apple-id', id: 'oa2-apple-id',
name: 'Apple', name: 'Apple',
iconClass: null, iconClass: 'apple',
iconImage: 'https://openedx.devstack.lms/logo.png', iconImage: 'https://openedx.devstack.lms/logo.png',
loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard', loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard',
}; };
@@ -414,7 +413,7 @@ describe('LoginPage', () => {
+ 'to the courses you are enrolled in. Sign in to continue.'; + 'to the courses you are enrolled in. Sign in to continue.';
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?account_activation_status=success' }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?account_activation_status=success' };
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />)); const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find('div#account-activation-message').text()).toEqual(activationMessage); expect(loginPage.find('div#account-activation-message').text()).toEqual(activationMessage);
@@ -449,13 +448,13 @@ describe('LoginPage', () => {
thirdPartyAuthContext: { thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext, ...initialState.commonComponents.thirdPartyAuthContext,
currentProvider: null, currentProvider: null,
errorMessage: 'An error occured', errorMessage: 'An error occurred',
}, },
}, },
}); });
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />)); const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find('#login-failure-alert').find('p').text()).toContain('An error occured'); expect(loginPage.find('#login-failure-alert').find('p').text()).toContain('An error occurred');
}); });
it('should match invalid login form error message', () => { it('should match invalid login form error message', () => {
@@ -569,8 +568,7 @@ describe('LoginPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
ssoProvider.iconImage = null;
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />)); const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(ssoProvider.name); expect(loginPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(ssoProvider.name);
@@ -592,7 +590,7 @@ describe('LoginPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
secondaryProviders.iconImage = null; secondaryProviders.iconImage = null;
mount(reduxWrapper(<IntlLoginPage {...props} />)); mount(reduxWrapper(<IntlLoginPage {...props} />));
@@ -617,8 +615,7 @@ describe('LoginPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?next=/dashboard&tpa_hint=invalid' }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
ssoProvider.iconImage = null;
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />)); const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(`${ssoProvider.name}`); expect(loginPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(`${ssoProvider.name}`);
@@ -642,8 +639,7 @@ describe('LoginPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
ssoProvider.iconImage = null;
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />)); const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find('button#other-ways-to-sign-in').text()).toEqual('Show me other ways to sign in or register'); expect(loginPage.find('button#other-ways-to-sign-in').text()).toEqual('Show me other ways to sign in or register');
@@ -666,8 +662,7 @@ describe('LoginPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
ssoProvider.iconImage = null;
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />)); const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find('button#other-ways-to-sign-in').text()).toEqual('Show me other ways to sign in'); expect(loginPage.find('button#other-ways-to-sign-in').text()).toEqual('Show me other ways to sign in');
@@ -675,14 +670,9 @@ describe('LoginPage', () => {
// ******** miscellaneous tests ******** // ******** miscellaneous tests ********
it('should render cookie banner', () => {
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find(<CookiePolicyBanner />)).toBeTruthy();
});
it('should send page event when login page is rendered', () => { it('should send page event when login page is rendered', () => {
mount(reduxWrapper(<IntlLoginPage {...props} />)); mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login'); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
}); });
it('tests that form is only scrollable on form submission', () => { it('tests that form is only scrollable on form submission', () => {
@@ -695,21 +685,6 @@ describe('LoginPage', () => {
expect(loginPage.find('LoginPage').state('isSubmitted')).toEqual(true); expect(loginPage.find('LoginPage').state('isSubmitted')).toEqual(true);
}); });
it('should set login survey cookie', () => {
store = mockStore({
...initialState,
login: {
...initialState.login,
loginResult: {
success: true,
},
},
});
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
expect(document.cookie).toMatch(`${getConfig().USER_SURVEY_COOKIE_NAME}=login`);
});
it('should reset login form errors', () => { it('should reset login form errors', () => {
const errorState = { emailOrUsername: '', password: '' }; const errorState = { emailOrUsername: '', password: '' };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);

View File

@@ -12,19 +12,21 @@ import {
} from '@edx/paragon'; } from '@edx/paragon';
import { ChevronLeft } from '@edx/paragon/icons'; import { ChevronLeft } from '@edx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom'; import { Navigate, useNavigate } from 'react-router-dom';
import { BaseComponent } from '../base-component'; import BaseContainer from '../base-container';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import {
tpaProvidersSelector,
} from '../common-components/data/selectors';
import messages from '../common-components/messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import { getTpaHint, getTpaProvider, updatePathWithQueryParams } from '../data/utils'; import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
import { LoginPage } from '../login'; import { LoginPage } from '../login';
import { RegistrationPage } from '../register'; import { RegistrationPage } from '../register';
import { backupRegistrationForm } from '../register/data/actions'; import { backupRegistrationForm } from '../register/data/actions';
import { clearThirdPartyAuthContextErrorMessage } from './data/actions';
import {
tpaProvidersSelector,
} from './data/selectors';
import messages from './messages';
const Logistration = (props) => { const Logistration = (props) => {
const { selectedPage, tpaProviders } = props; const { selectedPage, tpaProviders } = props;
@@ -35,6 +37,7 @@ const Logistration = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [institutionLogin, setInstitutionLogin] = useState(false); const [institutionLogin, setInstitutionLogin] = useState(false);
const [key, setKey] = useState(''); const [key, setKey] = useState('');
const navigate = useNavigate();
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false; const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
useEffect(() => { useEffect(() => {
@@ -44,6 +47,12 @@ const Logistration = (props) => {
} }
}); });
useEffect(() => {
if (disablePublicAccountCreation) {
navigate(updatePathWithQueryParams(LOGIN_PAGE));
}
}, [navigate, disablePublicAccountCreation]);
const handleInstitutionLogin = (e) => { const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
if (typeof e === 'string') { if (typeof e === 'string') {
@@ -81,12 +90,11 @@ const Logistration = (props) => {
}; };
return ( return (
<BaseComponent> <BaseContainer>
<div> <div>
{disablePublicAccountCreation {disablePublicAccountCreation
? ( ? (
<> <>
<Redirect to={updatePathWithQueryParams(LOGIN_PAGE)} />
{institutionLogin && ( {institutionLogin && (
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}> <Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
<Tab title={tabTitle} eventKey={LOGIN_PAGE} /> <Tab title={tabTitle} eventKey={LOGIN_PAGE} />
@@ -115,7 +123,7 @@ const Logistration = (props) => {
</Tabs> </Tabs>
))} ))}
{ key && ( { key && (
<Redirect to={updatePathWithQueryParams(key)} /> <Navigate to={updatePathWithQueryParams(key)} replace />
)} )}
<div id="main-content" className="main-content"> <div id="main-content" className="main-content">
{selectedPage === LOGIN_PAGE {selectedPage === LOGIN_PAGE
@@ -130,7 +138,7 @@ const Logistration = (props) => {
</div> </div>
)} )}
</div> </div>
</BaseComponent> </BaseContainer>
); );
}; };

View File

@@ -2,22 +2,25 @@ import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants'; import Logistration from './Logistration';
import { backupRegistrationForm } from '../../register/data/actions'; import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import { clearThirdPartyAuthContextErrorMessage } from '../data/actions'; import { RenderInstitutionButton } from '../common-components/InstitutionLogistration';
import { RenderInstitutionButton } from '../InstitutionLogistration'; import {
import Logistration from '../Logistration'; COMPLETE_STATE, LOGIN_PAGE,
} from '../data/constants';
import { backupRegistrationForm } from '../register/data/actions';
jest.mock('@edx/frontend-platform/analytics'); jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth'); jest.mock('@edx/frontend-platform/auth');
analytics.sendPageEvent = jest.fn();
const mockStore = configureStore(); const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration); const IntlLogistration = injectIntl(Logistration);
@@ -40,8 +43,47 @@ describe('Logistration', () => {
</IntlProvider> </IntlProvider>
); );
const initialState = {
register: {
registrationFormData: {
configurableFormFields: {
marketingEmailsOptIn: true,
},
formFields: {
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
name: '', email: '', username: '', password: '',
},
},
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
},
login: {
loginResult: { success: false, redirectUrl: '' },
},
};
beforeEach(() => { beforeEach(() => {
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'test-user' })); store = mockStore(initialState);
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({
userId: 3,
username: 'test-user',
})),
}));
configure({ configure({
loggingService: { logError: jest.fn() }, loggingService: { logError: jest.fn() },
config: { config: {
@@ -56,36 +98,13 @@ describe('Logistration', () => {
mergeConfig({ mergeConfig({
ALLOW_PUBLIC_ACCOUNT_CREATION: true, ALLOW_PUBLIC_ACCOUNT_CREATION: true,
}); });
store = mockStore({
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
},
});
const logistration = mount(reduxWrapper(<IntlLogistration />)); const logistration = mount(reduxWrapper(<IntlLogistration />));
expect(logistration.find('#main-content').find('RegistrationPage').exists()).toBeTruthy(); expect(logistration.find('#main-content').find('RegistrationPage').exists()).toBeTruthy();
}); });
it('should render login page', () => { it('should render login page', () => {
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
},
});
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />)); const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
@@ -99,9 +118,7 @@ describe('Logistration', () => {
}); });
store = mockStore({ store = mockStore({
login: { ...initialState,
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: { commonComponents: {
thirdPartyAuthContext: { thirdPartyAuthContext: {
currentProvider: null, currentProvider: null,
@@ -131,9 +148,7 @@ describe('Logistration', () => {
}); });
store = mockStore({ store = mockStore({
login: { ...initialState,
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: { commonComponents: {
thirdPartyAuthContext: { thirdPartyAuthContext: {
currentProvider: null, currentProvider: null,
@@ -164,9 +179,7 @@ describe('Logistration', () => {
}); });
store = mockStore({ store = mockStore({
login: { ...initialState,
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: { commonComponents: {
thirdPartyAuthContext: { thirdPartyAuthContext: {
currentProvider: null, currentProvider: null,
@@ -182,8 +195,8 @@ describe('Logistration', () => {
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />)); const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true }); logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login'); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
mergeConfig({ mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '', DISABLE_ENTERPRISE_LOGIN: '',
@@ -196,10 +209,7 @@ describe('Logistration', () => {
}); });
store = mockStore({ store = mockStore({
register: { ...initialState,
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: { commonComponents: {
thirdPartyAuthContext: { thirdPartyAuthContext: {
currentProvider: null, currentProvider: null,
@@ -224,22 +234,6 @@ describe('Logistration', () => {
}); });
it('should fire action to backup registration form on tab click', () => { it('should fire action to backup registration form on tab click', () => {
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
},
});
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const logistration = mount(reduxWrapper(<IntlLogistration />)); const logistration = mount(reduxWrapper(<IntlLogistration />));
logistration.find('a[data-rb-event-key="/login"]').simulate('click'); logistration.find('a[data-rb-event-key="/login"]').simulate('click');
@@ -247,22 +241,6 @@ describe('Logistration', () => {
}); });
it('should clear tpa context errorMessage tab click', () => { it('should clear tpa context errorMessage tab click', () => {
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
},
});
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const logistration = mount(reduxWrapper(<IntlLogistration />)); const logistration = mount(reduxWrapper(<IntlLogistration />));
logistration.find('a[data-rb-event-key="/login"]').simulate('click'); logistration.find('a[data-rb-event-key="/login"]').simulate('click');

View File

@@ -6,9 +6,7 @@ import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/f
import { import {
AxiosJwtAuthService, AxiosJwtAuthService,
configure as configureAuth, configure as configureAuth,
ensureAuthenticatedUser,
getAuthenticatedUser, getAuthenticatedUser,
hydrateAuthenticatedUser,
} from '@edx/frontend-platform/auth'; } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { getLoggingService } from '@edx/frontend-platform/logging'; import { getLoggingService } from '@edx/frontend-platform/logging';
@@ -21,97 +19,128 @@ import {
import { Error } from '@edx/paragon/icons'; import { Error } from '@edx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useLocation } from 'react-router-dom';
import { BaseComponent } from '../base-component';
import { RedirectLogistration } from '../common-components';
import {
DEFAULT_REDIRECT_URL, DEFAULT_STATE, FAILURE_STATE,
} from '../data/constants';
import { getAllPossibleQueryParams } from '../data/utils';
import { FormFieldRenderer } from '../field-renderer';
import {
activateRecommendationsExperiment, RECOMMENDATIONS_EXP_VARIATION, trackRecommendationViewedOptimizely,
} from '../recommendations/optimizelyExperiment';
import { trackRecommendationsGroup, trackRecommendationsViewed } from '../recommendations/track';
import { saveUserProfile } from './data/actions'; import { saveUserProfile } from './data/actions';
import { welcomePageSelector } from './data/selectors'; import { welcomePageContextSelector } from './data/selectors';
import messages from './messages'; import messages from './messages';
import ProgressiveProfilingPageModal from './ProgressiveProfilingPageModal'; import ProgressiveProfilingPageModal from './ProgressiveProfilingPageModal';
import BaseContainer from '../base-container';
import { RedirectLogistration } from '../common-components';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import {
COMPLETE_STATE,
DEFAULT_REDIRECT_URL,
DEFAULT_STATE,
FAILURE_STATE,
PENDING_STATE,
} from '../data/constants';
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
import { FormFieldRenderer } from '../field-renderer';
const ProgressiveProfiling = (props) => { const ProgressiveProfiling = (props) => {
const {
formRenderState, submitState, showError, location,
} = props;
const enablePersonalizedRecommendations = getConfig().ENABLE_PERSONALIZED_RECOMMENDATIONS;
const registrationResponse = location.state?.registrationResult;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [ready, setReady] = useState(false); const {
getFieldDataFromBackend,
submitState,
showError,
welcomePageContext,
welcomePageContextApiStatus,
} = props;
const location = useLocation();
const registrationEmbedded = isHostAvailableInQueryParams();
const queryParams = getAllPossibleQueryParams();
const authenticatedUser = getAuthenticatedUser() || location.state?.authenticatedUser;
const functionalCookiesConsent = isOneTrustFunctionalCookieEnabled();
const enablePostRegistrationRecommendations = (
getConfig().ENABLE_POST_REGISTRATION_RECOMMENDATIONS && functionalCookiesConsent
);
const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' }); const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' });
const [formFieldData, setFormFieldData] = useState({ fields: {}, extendedProfile: [] });
const [values, setValues] = useState({}); const [values, setValues] = useState({});
const [openDialog, setOpenDialog] = useState(false); const [showModal, setShowModal] = useState(false);
const [showRecommendationsPage, setShowRecommendationsPage] = useState(false); const [showRecommendationsPage, setShowRecommendationsPage] = useState(false);
const authenticatedUser = getAuthenticatedUser();
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
useEffect(() => { useEffect(() => {
configureAuth(AxiosJwtAuthService, { loggingService: getLoggingService(), config: getConfig() }); if (registrationEmbedded) {
ensureAuthenticatedUser(DASHBOARD_URL) getFieldDataFromBackend({ is_welcome_page: true, next: queryParams?.next });
.then(() => { } else {
hydrateAuthenticatedUser().then(() => { configureAuth(AxiosJwtAuthService, { loggingService: getLoggingService(), config: getConfig() });
setReady(true); }
}); }, [registrationEmbedded, getFieldDataFromBackend, queryParams?.next]);
})
.catch(() => {});
useEffect(() => {
const registrationResponse = location.state?.registrationResult;
if (registrationResponse) { if (registrationResponse) {
setRegistrationResult(registrationResponse); setRegistrationResult(registrationResponse);
setFormFieldData({
fields: location.state?.optionalFields.fields,
extendedProfile: location.state?.optionalFields.extended_profile,
});
} }
}, [DASHBOARD_URL, registrationResponse]); }, [location.state]);
useEffect(() => { useEffect(() => {
if (ready && authenticatedUser?.userId) { if (registrationEmbedded && Object.keys(welcomePageContext).includes('fields')) {
setFormFieldData({
fields: welcomePageContext.fields,
extendedProfile: welcomePageContext.extended_profile,
});
const nextUrl = welcomePageContext.nextUrl ? welcomePageContext.nextUrl : getConfig().SEARCH_CATALOG_URL;
setRegistrationResult({ redirectUrl: nextUrl });
}
}, [registrationEmbedded, welcomePageContext]);
useEffect(() => {
if (authenticatedUser?.userId) {
identifyAuthenticatedUser(authenticatedUser.userId); identifyAuthenticatedUser(authenticatedUser.userId);
sendPageEvent('login_and_registration', 'welcome'); sendPageEvent('login_and_registration', 'welcome');
} }
}, [authenticatedUser, ready]); }, [authenticatedUser]);
useEffect(() => { useEffect(() => {
if (registrationResponse && authenticatedUser?.userId) { if (!enablePostRegistrationRecommendations) {
const queryParams = getAllPossibleQueryParams(registrationResponse.redirectUrl); sendTrackEvent(
if (enablePersonalizedRecommendations && !('enrollment_action' in queryParams)) { 'edx.bi.user.recommendations.not.enabled',
const userIdStr = authenticatedUser.userId.toString(); { functionalCookiesConsent, page: 'authn_recommendations' },
const variation = activateRecommendationsExperiment(userIdStr); );
const showRecommendations = variation === RECOMMENDATIONS_EXP_VARIATION; return;
}
trackRecommendationsGroup(variation, authenticatedUser.userId); if (registrationResult.redirectUrl && authenticatedUser?.userId) {
trackRecommendationViewedOptimizely(userIdStr); const redirectQueryParams = getAllPossibleQueryParams(registrationResult.redirectUrl);
setShowRecommendationsPage(showRecommendations); if (!('enrollment_action' in redirectQueryParams || queryParams?.next)) {
if (!showRecommendations) { setShowRecommendationsPage(true);
trackRecommendationsViewed([], true, authenticatedUser.userId);
}
} }
} }
}, [authenticatedUser, enablePersonalizedRecommendations, registrationResponse]); }, [
authenticatedUser,
enablePostRegistrationRecommendations,
functionalCookiesConsent,
registrationResult.redirectUrl,
queryParams?.next,
]);
if (!location.state || !location.state.registrationResult || formRenderState === FAILURE_STATE) { if (
!authenticatedUser
|| !(location.state?.registrationResult || registrationEmbedded)
|| welcomePageContextApiStatus === FAILURE_STATE
|| (welcomePageContextApiStatus === COMPLETE_STATE && !Object.keys(welcomePageContext).includes('fields'))
) {
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
global.location.assign(DASHBOARD_URL); global.location.assign(DASHBOARD_URL);
return null; return null;
} }
if (!ready) {
return null;
}
const optionalFields = location.state.optionalFields.fields;
const extendedProfile = location.state.optionalFields.extended_profile;
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
window.history.replaceState(location.state, null, ''); window.history.replaceState(location.state, null, '');
const payload = { ...values, extendedProfile: [] }; const payload = { ...values, extendedProfile: [] };
if (Object.keys(extendedProfile).length > 0) { if (Object.keys(formFieldData.extendedProfile).length > 0) {
extendedProfile.forEach(fieldName => { formFieldData.extendedProfile.forEach(fieldName => {
if (values[fieldName]) { if (values[fieldName]) {
payload.extendedProfile.push({ fieldName, fieldValue: values[fieldName] }); payload.extendedProfile.push({ fieldName, fieldValue: values[fieldName] });
} }
@@ -126,15 +155,21 @@ const ProgressiveProfiling = (props) => {
isGenderSelected: !!values.gender, isGenderSelected: !!values.gender,
isYearOfBirthSelected: !!values.year_of_birth, isYearOfBirthSelected: !!values.year_of_birth,
isLevelOfEducationSelected: !!values.level_of_education, isLevelOfEducationSelected: !!values.level_of_education,
host: queryParams?.host || '',
}, },
); );
}; };
const handleSkip = (e) => { const handleSkip = (e) => {
e.preventDefault(); e.preventDefault();
window.history.replaceState(props.location.state, null, ''); window.history.replaceState(location.state, null, '');
setOpenDialog(true); setShowModal(true);
sendTrackEvent('edx.bi.welcome.page.skip.link.clicked'); sendTrackEvent(
'edx.bi.welcome.page.skip.link.clicked',
{
host: queryParams?.host || '',
},
);
}; };
const onChangeHandler = (e) => { const onChangeHandler = (e) => {
@@ -145,8 +180,8 @@ const ProgressiveProfiling = (props) => {
} }
}; };
const formFields = Object.keys(optionalFields).map((fieldName) => { const formFields = Object.keys(formFieldData.fields).map((fieldName) => {
const fieldData = optionalFields[fieldName]; const fieldData = formFieldData.fields[fieldName];
return ( return (
<span key={fieldData.name}> <span key={fieldData.name}>
<FormFieldRenderer <FormFieldRenderer
@@ -159,14 +194,20 @@ const ProgressiveProfiling = (props) => {
}); });
return ( return (
<BaseComponent showWelcomeBanner> <BaseContainer showWelcomeBanner username={authenticatedUser?.username}>
<Helmet> <Helmet>
<title>{formatMessage(messages['progressive.profiling.page.title'], <title>{formatMessage(messages['progressive.profiling.page.title'],
{ siteName: getConfig().SITE_NAME })} { siteName: getConfig().SITE_NAME })}
</title> </title>
</Helmet> </Helmet>
<ProgressiveProfilingPageModal isOpen={openDialog} redirectUrl={registrationResult.redirectUrl} /> <ProgressiveProfilingPageModal isOpen={showModal} redirectUrl={registrationResult.redirectUrl} />
{props.shouldRedirect ? ( {(props.shouldRedirect && welcomePageContext.nextUrl) && (
<RedirectLogistration
success
redirectUrl={registrationResult.redirectUrl}
/>
)}
{props.shouldRedirect && (
<RedirectLogistration <RedirectLogistration
success success
redirectUrl={registrationResult.redirectUrl} redirectUrl={registrationResult.redirectUrl}
@@ -174,10 +215,10 @@ const ProgressiveProfiling = (props) => {
educationLevel={values?.level_of_education} educationLevel={values?.level_of_education}
userId={authenticatedUser?.userId} userId={authenticatedUser?.userId}
/> />
) : null} )}
<div className="mw-xs m-4 pp-page-content"> <div className="mw-xs m-4 pp-page-content">
<div> <div>
<h2 className="pp-page-heading text-primary">{formatMessage(messages['progressive.profiling.page.heading'])}</h2> <h2 className="pp-page__heading text-primary">{formatMessage(messages['progressive.profiling.page.heading'])}</h2>
</div> </div>
<hr className="border-light-700 mb-4" /> <hr className="border-light-700 mb-4" />
{showError ? ( {showError ? (
@@ -189,7 +230,7 @@ const ProgressiveProfiling = (props) => {
<Form> <Form>
{formFields} {formFields}
{(getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK) && ( {(getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK) && (
<span className="progressive-profiling-support"> <span className="pp-page__support-link">
<Hyperlink <Hyperlink
isInline isInline
variant="muted" variant="muted"
@@ -206,7 +247,7 @@ const ProgressiveProfiling = (props) => {
<StatefulButton <StatefulButton
type="submit" type="submit"
variant="brand" variant="brand"
className="login-button-width" className="pp-page__button-width"
state={submitState} state={submitState}
labels={{ labels={{
default: showRecommendationsPage ? formatMessage(messages['optional.fields.next.button']) : formatMessage(messages['optional.fields.submit.button']), default: showRecommendationsPage ? formatMessage(messages['optional.fields.next.button']) : formatMessage(messages['optional.fields.submit.button']),
@@ -228,46 +269,54 @@ const ProgressiveProfiling = (props) => {
</div> </div>
</Form> </Form>
</div> </div>
</BaseComponent> </BaseContainer>
); );
}; };
ProgressiveProfiling.propTypes = { ProgressiveProfiling.propTypes = {
formRenderState: PropTypes.string.isRequired, authenticatedUser: PropTypes.shape({
location: PropTypes.shape({ username: PropTypes.string,
state: PropTypes.shape({ userId: PropTypes.number,
registrationResult: PropTypes.shape({
redirectUrl: PropTypes.string,
}),
optionalFields: PropTypes.shape({
extended_profile: PropTypes.arrayOf(PropTypes.string),
fields: PropTypes.shape({}),
}),
}),
}), }),
saveUserProfile: PropTypes.func.isRequired,
showError: PropTypes.bool, showError: PropTypes.bool,
shouldRedirect: PropTypes.bool, shouldRedirect: PropTypes.bool,
submitState: PropTypes.string, submitState: PropTypes.string,
welcomePageContext: PropTypes.shape({
extended_profile: PropTypes.arrayOf(PropTypes.string),
fields: PropTypes.shape({}),
nextUrl: PropTypes.string,
}),
welcomePageContextApiStatus: PropTypes.string,
// Actions
getFieldDataFromBackend: PropTypes.func.isRequired,
saveUserProfile: PropTypes.func.isRequired,
}; };
ProgressiveProfiling.defaultProps = { ProgressiveProfiling.defaultProps = {
location: { state: {} }, authenticatedUser: {},
shouldRedirect: false, shouldRedirect: false,
showError: false, showError: false,
submitState: DEFAULT_STATE, submitState: DEFAULT_STATE,
welcomePageContext: {},
welcomePageContextApiStatus: PENDING_STATE,
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => {
formRenderState: welcomePageSelector(state).formRenderState, const welcomePageStore = state.welcomePage;
shouldRedirect: welcomePageSelector(state).success,
submitState: welcomePageSelector(state).submitState, return {
showError: welcomePageSelector(state).showError, shouldRedirect: welcomePageStore.success,
}); showError: welcomePageStore.showError,
submitState: welcomePageStore.submitState,
welcomePageContext: welcomePageContextSelector(state),
welcomePageContextApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
};
};
export default connect( export default connect(
mapStateToProps, mapStateToProps,
{ {
saveUserProfile, saveUserProfile,
getFieldDataFromBackend: getThirdPartyAuthContext,
}, },
)(ProgressiveProfiling); )(ProgressiveProfiling);

View File

@@ -1,12 +1,11 @@
import { SAVE_USER_PROFILE } from './actions';
import { import {
DEFAULT_STATE, PENDING_STATE, DEFAULT_STATE, PENDING_STATE,
} from '../../data/constants'; } from '../../data/constants';
import { SAVE_USER_PROFILE } from './actions';
export const defaultState = { export const defaultState = {
extendedProfile: [], extendedProfile: [],
fieldDescriptions: {}, fieldDescriptions: {},
formRenderState: DEFAULT_STATE,
success: false, success: false,
submitState: DEFAULT_STATE, submitState: DEFAULT_STATE,
showError: false, showError: false,

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