Compare commits

..

2 Commits

Author SHA1 Message Date
Attiya Ishaque
e29a461a34 fix: change for testing sandbox purpose 2024-03-26 19:00:40 +05:00
Attiya Ishaque
9276f25200 feat: remove username from the registration from (#1201) 2024-03-20 15:21:25 +05:00
220 changed files with 21628 additions and 27755 deletions

2
.env
View File

@@ -41,5 +41,3 @@ BANNER_IMAGE_EXTRA_SMALL=''
# ***** Miscellaneous *****
APP_ID=''
MFE_CONFIG_API_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -41,5 +41,3 @@ APP_ID=''
MFE_CONFIG_API_URL=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -18,4 +18,3 @@ SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
APP_ID=''
MFE_CONFIG_API_URL=''
PARAGON_THEME_URLS={}

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @openedx/2U-infinity

View File

@@ -1,7 +0,0 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -25,5 +25,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
#### Post-merge Checklist
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-vanguards** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -10,7 +10,7 @@ on:
jobs:
autoupdate:
name: autoupdate
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: docker://chinthakagodawita/autoupdate-action:v1
env:

View File

@@ -10,15 +10,17 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v6
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
node-version: ${{ env.NODE_VER }}
- name: Install Dependencies
run: npm ci
@@ -39,7 +41,4 @@ jobs:
run: npm run build
- name: Run Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
uses: codecov/codecov-action@v4

View File

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

1
.gitignore vendored
View File

@@ -18,4 +18,3 @@ temp/babel-plugin-react-intl
*~
/temp
/.vscode
src/i18n/messages

2
.nvmrc
View File

@@ -1 +1 @@
24
18

9
.tx/config Normal file
View File

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

2
CODEOWNERS Normal file
View File

@@ -0,0 +1,2 @@
# The following users are the owners of all frontend-app-authn files
* @openedx/2u-vanguards

View File

@@ -1,3 +1,6 @@
export TRANSIFEX_RESOURCE = frontend-app-authn
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
i18n = ./src/i18n
@@ -29,6 +32,23 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
@@ -39,6 +59,7 @@ pull_translations:
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
$(intl_imports) paragon frontend-platform frontend-app-authn
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -26,53 +26,32 @@ This is a micro-frontend application responsible for the login, registration and
Getting Started
***************
Prerequisites
=============
Installation
============
`Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
Cloning and Startup
===================
2. Start up LMS, if it's not already started.
1. Clone your new repo:
4. Within this project (frontend-app-authn), install requirements and start the development server:
.. code-block:: bash
.. code-block::
git clone https://github.com/edx/frontend-app-authn.git
npm install
npm start # The server will run on port 1999
2. Use the version of Node specified in the ``.nvmrc`` file.
5. Once the dev server is up, visit http://localhost:1999 to access the MFE
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes a ``.nvmrc`` file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
3. Install npm dependencies:
.. code-block:: bash
cd frontend-app-authn && npm install
4. Update the application port to use for local development:
The default port is 1999. If this does not work for you, update the line
``PORT=1999`` to your port in all ``.env.*`` files
5. Start the devserver. The app will be running at ``localhost:1999``, or whatever port you change it too.
.. code-block:: bash
npm run dev
.. image:: ./docs/images/frontend-app-authn-localhost-preview.png
**Note:** Follow `Enable social auth locally <docs/how_tos/enable_social_auth.rst>`_ for enabling Social Sign-on Buttons (SSO) locally
Environment Variables/Setup Notes
=================================
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
The authentication micro-frontend also requires the following additional variable:
@@ -130,7 +109,7 @@ The authentication micro-frontend also requires the following additional variabl
* - ``MFE_CONFIG_API_URL``
- Link of the API to get runtime mfe configuration variables from the site configuration or django settings.
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
* - ``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.
@@ -160,16 +139,16 @@ Furthermore, there are several edX-specific environment variables that enable in
* - ``SHOW_CONFIGURABLE_EDX_FIELDS``
- For edX, country and honor code fields are required by default. This flag enables edX specific required fields.
- ``true`` | ``''`` (empty strings are falsy)
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
How To Contribute
=================
Contributions are very welcome, and strongly encouraged! We've
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
@@ -208,7 +187,7 @@ All community members are expected to follow the `Open edX Code of Conduct <http
People
======
The assigned maintainers for this component and other project details may be
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-infinity>`_. Backstage pulls this data from the ``catalog-info.yaml``
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
Reporting Security Issues

View File

@@ -12,8 +12,7 @@ metadata:
icon: 'Article'
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:2u-infinity
owner: group:2u-vanguards
type: 'service'
lifecycle: 'production'

View File

@@ -3,7 +3,7 @@ Enable Social Auth Locally
Please follow the steps below to enable social auth (SSO) locally.
1. Follow `Enabling Third Party Authentication <https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration.
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.

8
openedx.yaml Normal file
View File

@@ -0,0 +1,8 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: Authn MFE
oeps: {}
owner: openedx/2u-vanguards
openedx-release:
ref: master

32466
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,15 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/authn/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
@@ -30,47 +33,53 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "^8.3.1",
"@edx/frontend-platform": "7.1.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.2",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-brands-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/paragon": "^22.1.1",
"@optimizely/react-sdk": "^2.9.1",
"@tanstack/react-query": "^5.90.19",
"@testing-library/react": "^16.2.0",
"@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.26.0",
"algoliasearch-helper": "^3.14.0",
"classnames": "2.5.1",
"core-js": "3.43.0",
"core-js": "3.36.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.6",
"form-urlencoded": "6.1.4",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-loading-skeleton": "3.4.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.30.3",
"react-router-dom": "6.30.3",
"react-router": "6.22.3",
"react-router-dom": "6.22.3",
"react-zendesk": "^0.1.13",
"redux": "4.2.0",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.4",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"universal-cookie": "7.2.2"
"reselect": "4.1.8",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/typescript-config": "^1.1.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.9.1",
"babel-plugin-formatjs": "10.5.41",
"eslint-plugin-import": "2.32.0",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "13.0.28",
"babel-plugin-formatjs": "10.5.13",
"eslint-plugin-import": "2.29.1",
"glob": "7.2.3",
"history": "5.3.0",
"jest": "30.3.0",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0"
"husky": "7.0.4",
"jest": "29.7.0",
"react-test-renderer": "^17.0.2"
}
}

View File

@@ -5,8 +5,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.9/iframeResizer.contentWindow.min.js"
integrity="sha512-mdT/HQRzoRP4laVz49Mndx6rcCGA3IhuyhP3gaY0E9sZPkwbtDk9ttQIq9o8qGCf5VvJv1Xsy3k2yTjfUoczqw=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>

View File

@@ -1,12 +1,14 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Helmet } from 'react-helmet';
import { Navigate, Route, Routes } from 'react-router-dom';
import {
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
} from './common-components';
import configureStore from './data/configureStore';
import {
AUTHN_PROGRESSIVE_PROFILING,
LOGIN_PAGE,
@@ -29,48 +31,33 @@ import './index.scss';
registerIcons();
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
retry: false,
},
},
});
const MainApp = () => (
<QueryClientProvider client={queryClient}>
<AppProvider>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Routes>
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
/>
<Route
path={LOGIN_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route
path={REGISTER_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={REGISTER_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
<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>
</QueryClientProvider>
<AppProvider store={configureStore()}>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Routes>
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
/>
<Route
path={LOGIN_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
<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>
);
export default MainApp;

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { breakpoints } from '@openedx/paragon';
import classNames from 'classnames';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { mergeConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react';
@@ -14,9 +16,7 @@ describe('Base component tests', () => {
it('should show default layout', () => {
const { container } = render(
<IntlProvider locale="en">
<BaseContainer>
<div>Test Content</div>
</BaseContainer>
<BaseContainer />
</IntlProvider>,
LargeScreen,
);
@@ -32,9 +32,7 @@ describe('Base component tests', () => {
const { container } = render(
<IntlProvider locale="en">
<BaseContainer showWelcomeBanner={false}>
<div>Test Content</div>
</BaseContainer>
<BaseContainer showWelcomeBanner={false} />
</IntlProvider>,
LargeScreen,
);

View File

@@ -1,3 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import React, { useState } from 'react';
import {
Form, TransitionReplace,

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink, Icon } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const NotFoundPage = () => (

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
@@ -11,31 +12,17 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import { useRegisterContext } from '../register/components/RegisterContext';
import { useFieldValidations } from '../register/data/apiHook';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
import { validatePasswordField } from '../register/data/utils';
const PasswordField = (props) => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
const [showTooltip, setShowTooltip] = useState(false);
const {
setValidationsSuccess,
setValidationsFailure,
validationApiRateLimited,
clearRegistrationBackendError,
} = useRegisterContext();
const fieldValidationsMutation = useFieldValidations({
onSuccess: (data) => {
setValidationsSuccess(data);
},
onError: () => {
setValidationsFailure();
},
});
const handleBlur = (e) => {
const { name, value } = e.target;
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
@@ -63,7 +50,7 @@ const PasswordField = (props) => {
if (fieldError) {
props.handleErrorChange('password', fieldError);
} else if (!validationApiRateLimited) {
fieldValidationsMutation.mutate({ password: passwordValue });
dispatch(fetchRealtimeValidations({ password: passwordValue }));
}
}
};
@@ -78,7 +65,7 @@ const PasswordField = (props) => {
}
if (props.handleErrorChange) {
props.handleErrorChange('password', '');
clearRegistrationBackendError('password');
dispatch(clearRegistrationBackendError('password'));
}
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
};

View File

@@ -22,6 +22,7 @@ const RedirectLogistration = (props) => {
host,
} = props;
let finalRedirectUrl = '';
if (success) {
// If we're in a third party auth pipeline, we must complete the pipeline
// once user has successfully logged in. Otherwise, redirect to the specified redirect url.

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
@@ -35,7 +37,6 @@ const ThirdPartyAuth = (props) => {
const isSocialAuthActive = !!providers.length && !currentProvider;
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
return (
<>
@@ -60,7 +61,7 @@ const ThirdPartyAuth = (props) => {
</Hyperlink>
)}
{thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? (
{thirdPartyAuthApiStatus === PENDING_STATE ? (
<div className="mt-4">
<Skeleton className="tpa-skeleton" height={36} count={2} />
</div>

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import Zendesk from 'react-zendesk';

View File

@@ -1,61 +0,0 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ThirdPartyAuthProvider, useThirdPartyAuthContext } from './ThirdPartyAuthContext';
const TestComponent = () => {
const {
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
} = useThirdPartyAuthContext();
return (
<div>
<div>{fieldDescriptions ? 'FieldDescriptions Available' : 'FieldDescriptions Not Available'}</div>
<div>{optionalFields ? 'OptionalFields Available' : 'OptionalFields Not Available'}</div>
<div>{thirdPartyAuthApiStatus !== null ? 'AuthApiStatus Available' : 'AuthApiStatus Not Available'}</div>
<div>{thirdPartyAuthContext ? 'AuthContext Available' : 'AuthContext Not Available'}</div>
</div>
);
};
describe('ThirdPartyAuthContext', () => {
it('should render children', () => {
render(
<ThirdPartyAuthProvider>
<div>Test Child</div>
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('Test Child')).toBeInTheDocument();
});
it('should provide all context values to children', () => {
render(
<ThirdPartyAuthProvider>
<TestComponent />
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('FieldDescriptions Available')).toBeInTheDocument();
expect(screen.getByText('OptionalFields Available')).toBeInTheDocument();
expect(screen.getByText('AuthApiStatus Not Available')).toBeInTheDocument(); // Initially null
expect(screen.getByText('AuthContext Available')).toBeInTheDocument();
});
it('should render multiple children', () => {
render(
<ThirdPartyAuthProvider>
<div>First Child</div>
<div>Second Child</div>
<div>Third Child</div>
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('First Child')).toBeInTheDocument();
expect(screen.getByText('Second Child')).toBeInTheDocument();
expect(screen.getByText('Third Child')).toBeInTheDocument();
});
});

View File

@@ -1,133 +0,0 @@
import {
createContext, FC, ReactNode, useCallback, useContext, useMemo, useState,
} from 'react';
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
interface ThirdPartyAuthContextType {
fieldDescriptions: any;
optionalFields: {
fields: any;
extended_profile: any[];
};
thirdPartyAuthApiStatus: string | null;
thirdPartyAuthContext: {
platformName: string | null;
autoSubmitRegForm: boolean;
currentProvider: string | null;
finishAuthUrl: string | null;
countryCode: string | null;
providers: any[];
secondaryProviders: any[];
pipelineUserDetails: any | null;
errorMessage: string | null;
welcomePageRedirectUrl: string | null;
};
setThirdPartyAuthContextBegin: () => void;
setThirdPartyAuthContextSuccess: (fieldDescData: any, optionalFieldsData: any, contextData: any) => void;
setThirdPartyAuthContextFailure: () => void;
clearThirdPartyAuthErrorMessage: () => void;
}
const ThirdPartyAuthContext = createContext<ThirdPartyAuthContextType | undefined>(undefined);
interface ThirdPartyAuthProviderProps {
children: ReactNode;
}
export const ThirdPartyAuthProvider: FC<ThirdPartyAuthProviderProps> = ({ children }) => {
const [fieldDescriptions, setFieldDescriptions] = useState({});
const [optionalFields, setOptionalFields] = useState({
fields: {},
extended_profile: [],
});
const [thirdPartyAuthApiStatus, setThirdPartyAuthApiStatus] = useState<string | null>(null);
const [thirdPartyAuthContext, setThirdPartyAuthContext] = useState({
platformName: null,
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
});
// Function to handle begin state - mirrors THIRD_PARTY_AUTH_CONTEXT.BEGIN
const setThirdPartyAuthContextBegin = useCallback(() => {
setThirdPartyAuthApiStatus(PENDING_STATE);
}, []);
// Function to handle success - mirrors THIRD_PARTY_AUTH_CONTEXT.SUCCESS
const setThirdPartyAuthContextSuccess = useCallback((fieldDescData, optionalFieldsData, contextData) => {
setFieldDescriptions(fieldDescData?.fields || {});
setOptionalFields(optionalFieldsData || { fields: {}, extended_profile: [] });
setThirdPartyAuthContext(contextData || {
platformName: null,
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
});
setThirdPartyAuthApiStatus(COMPLETE_STATE);
}, []);
// Function to handle failure - mirrors THIRD_PARTY_AUTH_CONTEXT.FAILURE
const setThirdPartyAuthContextFailure = useCallback(() => {
setThirdPartyAuthApiStatus(FAILURE_STATE);
setThirdPartyAuthContext(prev => ({
...prev,
errorMessage: null,
}));
}, []);
// Function to clear error message - mirrors THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG
const clearThirdPartyAuthErrorMessage = useCallback(() => {
setThirdPartyAuthApiStatus(PENDING_STATE);
setThirdPartyAuthContext(prev => ({
...prev,
errorMessage: null,
}));
}, []);
const value = useMemo(() => ({
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
setThirdPartyAuthContextBegin,
setThirdPartyAuthContextSuccess,
setThirdPartyAuthContextFailure,
clearThirdPartyAuthErrorMessage,
}), [
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
setThirdPartyAuthContextBegin,
setThirdPartyAuthContextSuccess,
setThirdPartyAuthContextFailure,
clearThirdPartyAuthErrorMessage,
]);
return (
<ThirdPartyAuthContext.Provider value={value}>
{children}
</ThirdPartyAuthContext.Provider>
);
};
export const useThirdPartyAuthContext = (): ThirdPartyAuthContextType => {
const context = useContext(ThirdPartyAuthContext);
if (context === undefined) {
throw new Error('useThirdPartyAuthContext must be used within a ThirdPartyAuthProvider');
}
return context;
};

View File

@@ -0,0 +1,27 @@
import { AsyncActionType } from '../../data/utils';
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
export const THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG = 'THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG';
// Third party auth context
export const getThirdPartyAuthContext = (urlParams) => ({
type: THIRD_PARTY_AUTH_CONTEXT.BASE,
payload: { urlParams },
});
export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
});
export const clearThirdPartyAuthContextErrorMessage = () => ({
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
});

View File

@@ -1,17 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { getThirdPartyAuthContext } from './api';
import { ThirdPartyAuthQueryKeys } from './queryKeys';
// Error constants
export const THIRD_PARTY_AUTH_ERROR = 'third-party-auth-error';
const useThirdPartyAuthHook = (pageId, payload) => useQuery({
queryKey: ThirdPartyAuthQueryKeys.byPage(pageId),
queryFn: () => getThirdPartyAuthContext(payload),
retry: false,
});
export {
useThirdPartyAuthHook,
};

View File

@@ -1,6 +0,0 @@
import { appId } from '../../constants';
export const ThirdPartyAuthQueryKeys = {
all: [appId, 'ThirdPartyAuth'] as const,
byPage: (pageId: string) => [appId, 'ThirdPartyAuth', pageId] as const,
};

View File

@@ -0,0 +1,63 @@
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 = {
fieldDescriptions: {},
optionalFields: {
fields: {},
extended_profile: [],
},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
},
};
const reducer = (state = defaultState, action = {}) => {
switch (action.type) {
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
return {
...state,
fieldDescriptions: action.payload.fieldDescriptions?.fields,
optionalFields: action.payload.optionalFields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
}
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
return {
...state,
thirdPartyAuthApiStatus: FAILURE_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
};
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
};
default:
return state;
}
};
export default reducer;

View File

@@ -0,0 +1,32 @@
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextFailure,
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
import {
getThirdPartyAuthContext,
} from './service';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const {
fieldDescriptions, optionalFields, thirdPartyAuthContext,
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
}
}
export default function* saga() {
yield takeEvery(THIRD_PARTY_AUTH_CONTEXT.BASE, fetchThirdPartyAuthContext);
}

View File

@@ -0,0 +1,28 @@
import { createSelector } from 'reselect';
export const storeName = 'commonComponents';
export const commonComponentsSelector = state => ({ ...state[storeName] });
export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);
export const fieldDescriptionSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.fieldDescriptions,
);
export const optionalFieldsSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.optionalFields,
);
export const tpaProvidersSelector = createSelector(
commonComponentsSelector,
commonComponents => ({
providers: commonComponents.thirdPartyAuthContext.providers,
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
}),
);

View File

@@ -1,7 +1,8 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getThirdPartyAuthContext = async (urlParams : string) => {
// eslint-disable-next-line import/prefer-default-export
export async function getThirdPartyAuthContext(urlParams) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: urlParams,
@@ -12,14 +13,13 @@ const getThirdPartyAuthContext = async (urlParams : string) => {
.get(
`${getConfig().LMS_BASE_URL}/api/mfe_context`,
requestConfig,
);
)
.catch((e) => {
throw (e);
});
return {
fieldDescriptions: data.registrationFields || {},
optionalFields: data.optionalFields || {},
thirdPartyAuthContext: data.contextData || {},
};
};
export {
getThirdPartyAuthContext,
};
}

View File

@@ -0,0 +1,82 @@
import { PENDING_STATE } from '../../../data/constants';
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from '../actions';
import reducer from '../reducers';
describe('common components reducer', () => {
it('test mfe context response', () => {
const state = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
},
};
const fieldDescriptions = {
fields: [],
};
const optionalFields = {
fields: [],
extended_profile: {},
};
const thirdPartyAuthContext = { ...state.thirdPartyAuthContext };
const action = {
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
};
expect(
reducer(state, action),
).toEqual(
{
...state,
fieldDescriptions: [],
optionalFields: {
fields: [],
extended_profile: {},
},
thirdPartyAuthApiStatus: 'complete',
},
);
});
it('should clear tpa context error message', () => {
const state = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: 'An error occurred',
},
};
const action = {
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
};
expect(
reducer(state, action),
).toEqual(
{
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
},
);
});
});

View File

@@ -0,0 +1,71 @@
import { runSaga } from 'redux-saga';
import { setCountryFromThirdPartyAuthContext } from '../../../register/data/actions';
import initializeMockLogging from '../../../setupTest';
import * as actions from '../actions';
import { fetchThirdPartyAuthContext } from '../sagas';
import * as api from '../service';
const { loggingService } = initializeMockLogging();
describe('fetchThirdPartyAuthContext', () => {
const params = {
payload: { urlParams: {} },
};
const data = {
currentProvider: null,
providers: [],
secondaryProviders: [],
finishAuthUrl: null,
pipelineUserDetails: {},
};
beforeEach(() => {
loggingService.logError.mockReset();
});
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({
thirdPartyAuthContext: data,
fieldDescriptions: {},
optionalFields: {},
}));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
setCountryFromThirdPartyAuthContext(),
actions.getThirdPartyAuthContextSuccess({}, {}, data),
]);
getThirdPartyAuthContext.mockClear();
});
it('should call service and dispatch error action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.reject(new Error('something went wrong')));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextFailure(),
]);
getThirdPartyAuthContext.mockClear();
});
});

View File

@@ -7,6 +7,9 @@ export { default as SocialAuthProviders } from './SocialAuthProviders';
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
export { default as InstitutionLogistration } from './InstitutionLogistration';
export { RenderInstitutionButton } from './InstitutionLogistration';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';
export { default as FormGroup } from './FormGroup';
export { default as PasswordField } from './PasswordField';
export { default as Zendesk } from './Zendesk';

View File

@@ -5,13 +5,14 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { render } from '@testing-library/react';
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';
import { PAGE_NOT_FOUND, REGISTER_EMBEDDED_PAGE } from '../../data/constants';
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
const RRD = require('react-router-dom');
// Just render plain div with its children
// eslint-disable-next-line react/prop-types
@@ -26,10 +27,6 @@ const TestApp = () => (
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><span>Embedded Register Page</span></EmbeddedRegistrationRoute>}
/>
<Route
path={PAGE_NOT_FOUND}
element={<span>Page not found</span>}
/>
</Routes>
</div>
</Router>
@@ -48,13 +45,15 @@ describe('EmbeddedRegistrationRoute', () => {
it('should not render embedded register page if host query param is not available in the url', async () => {
let embeddedRegistrationPage = null;
await act(async () => {
const { container } = await render(routerWrapper());
embeddedRegistrationPage = container;
});
const renderedPage = embeddedRegistrationPage.querySelector('span');
expect(renderedPage.textContent).toBe('Page not found');
const spanElement = embeddedRegistrationPage.querySelector('span');
expect(spanElement).toBeNull();
});
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
@@ -65,13 +64,15 @@ describe('EmbeddedRegistrationRoute', () => {
};
let embeddedRegistrationPage = null;
await act(async () => {
const { container } = await render(routerWrapper());
embeddedRegistrationPage = container;
});
const renderedPage = embeddedRegistrationPage.querySelector('span');
expect(renderedPage).toBeTruthy();
expect(renderedPage.textContent).toBe('Embedded Register Page');
const spanElement = embeddedRegistrationPage.querySelector('span');
expect(spanElement).toBeTruthy();
expect(spanElement.textContent).toBe('Embedded Register Page');
});
});

View File

@@ -1,21 +1,16 @@
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { RegisterProvider } from '../../register/components/RegisterContext';
import { useFieldValidations } from '../../register/data/apiHook';
import { fetchRealtimeValidations } from '../../register/data/actions';
import FormGroup from '../FormGroup';
import PasswordField from '../PasswordField';
// Mock the useFieldValidations hook
jest.mock('../../register/data/apiHook', () => ({
useFieldValidations: jest.fn(),
}));
describe('FormGroup', () => {
const props = {
floatingLabel: 'Email',
@@ -41,52 +36,37 @@ describe('FormGroup', () => {
});
describe('PasswordField', () => {
const mockStore = configureStore();
const IntlPasswordField = injectIntl(PasswordField);
let props = {};
let queryClient;
let mockMutate;
let store = {};
const renderWrapper = (children) => (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
<RegisterProvider>
{children}
</RegisterProvider>
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
const initialState = {
register: {
validationApiRateLimited: false,
},
};
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
mockMutate = jest.fn();
useFieldValidations.mockReturnValue({
mutate: mockMutate,
isPending: false,
});
store = mockStore(initialState);
props = {
floatingLabel: 'Password',
name: 'password',
value: 'password123',
handleFocus: jest.fn(),
};
jest.clearAllMocks();
});
it('should show/hide password on icon click', () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = getByLabelText('Password');
const showPasswordButton = getByLabelText('Show password');
@@ -99,7 +79,7 @@ describe('PasswordField', () => {
});
it('should show password requirement tooltip on focus', async () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -116,7 +96,7 @@ describe('PasswordField', () => {
...props,
value: '',
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -139,7 +119,7 @@ describe('PasswordField', () => {
});
it('should update password requirement checks', async () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -162,7 +142,7 @@ describe('PasswordField', () => {
});
it('should not run validations when blur is fired on password icon click', () => {
const { container, getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { container, getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
const passwordIcon = getByLabelText('Show password');
@@ -183,7 +163,7 @@ describe('PasswordField', () => {
...props,
handleBlur: jest.fn(),
};
const { container } = render(renderWrapper(<PasswordField {...props} />));
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -201,7 +181,7 @@ describe('PasswordField', () => {
...props,
handleErrorChange: jest.fn(),
};
const { container } = render(renderWrapper(<PasswordField {...props} />));
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -224,7 +204,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -244,7 +224,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -263,11 +243,12 @@ describe('PasswordField', () => {
});
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 { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordField = getByLabelText('Password');
fireEvent.blur(passwordField, {
target: {
@@ -276,17 +257,18 @@ describe('PasswordField', () => {
},
});
expect(mockMutate).toHaveBeenCalledWith({ password: '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 { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import { PENDING_STATE, REGISTER_PAGE } from '../../data/constants';
import { REGISTER_PAGE } from '../../data/constants';
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
describe('ThirdPartyAuthAlert', () => {
@@ -36,19 +38,4 @@ describe('ThirdPartyAuthAlert', () => {
).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders skeleton for pending third-party auth', () => {
props = {
...props,
thirdPartyAuthApiStatus: PENDING_STATE,
isThirdPartyAuthActive: true,
};
const tree = renderer.create(
<IntlProvider locale="en">
<ThirdPartyAuthAlert {...props} />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,15 +1,18 @@
/* eslint-disable import/no-import-module-exports */
/* eslint-disable react/function-component-definition */
import React from 'react';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
import { UnAuthOnlyRoute } from '..';
import { REGISTER_PAGE } from '../../data/constants';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
fetchAuthenticatedUser: jest.fn(),

View File

@@ -66,14 +66,14 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
data-prefix="fab"
focusable="false"
role="img"
style={{}}
style={Object {}}
viewBox="0 0 488 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
fill="currentColor"
style={{}}
style={Object {}}
/>
</svg>
</div>
@@ -93,7 +93,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
`;
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
[
Array [
<button
className="btn-social btn-oa2-apple-id mr-3"
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"

View File

@@ -1,25 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThirdPartyAuthAlert renders skeleton for pending third-party auth 1`] = `
<div
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
id="tpa-alert"
role="alert"
>
<div
className="pgn__alert-message-wrapper"
>
<div
className="alert-message-content"
>
<p>
You have successfully signed into Google, but your Google account does not have a linked Your Platform Name Here account. To link your accounts, sign in now using your Your Platform Name Here password.
</p>
</div>
</div>
</div>
`;
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
<div
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
@@ -41,7 +21,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
`;
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
[
Array [
<div
className="fade alert-content alert-success mt-n2 mb-5 alert show"
id="tpa-alert"

View File

@@ -5,23 +5,23 @@ exports[`Zendesk Help should match login page third party auth alert message sna
cookies={true}
defer={true}
webWidget={
{
"answerBot": {
"avatar": {
"name": {
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
"*": "edX Support",
},
"url": undefined,
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": {
"title": Object {
"*": "edX Support",
},
},
"chat": {
"departments": {
"enabled": [
"chat": Object {
"departments": Object {
"enabled": Array [
"account settings",
"billing and payments",
"certificates",
@@ -33,17 +33,17 @@ exports[`Zendesk Help should match login page third party auth alert message sna
},
"suppress": false,
},
"contactForm": {
"contactForm": Object {
"attachments": true,
"selectTicketForm": {
"selectTicketForm": Object {
"*": "Please choose your request type:",
},
"ticketForms": [
{
"fields": [
{
"ticketForms": Array [
Object {
"fields": Array [
Object {
"id": "description",
"prefill": {
"prefill": Object {
"*": "",
},
},
@@ -53,10 +53,10 @@ exports[`Zendesk Help should match login page third party auth alert message sna
},
],
},
"contactOptions": {
"contactOptions": Object {
"enabled": false,
},
"helpCenter": {
"helpCenter": Object {
"originalArticleButton": true,
},
}

View File

@@ -4,7 +4,7 @@ const configuration = {
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
// Features
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || false,
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || true,
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_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,

View File

@@ -1 +0,0 @@
export const appId = 'org.openedx.frontend.app.authn';

View File

@@ -0,0 +1,33 @@
import { getConfig } from '@edx/frontend-platform';
import { composeWithDevTools } from '@redux-devtools/extension';
import { applyMiddleware, compose, createStore } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import createRootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
function composeMiddleware() {
if (getConfig().ENVIRONMENT === 'development') {
const loggerMiddleware = createLogger({
collapsed: true,
});
return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware));
}
return compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
}
export default function configureStore(initialState = {}) {
const store = createStore(
createRootReducer(),
initialState,
composeMiddleware(),
);
sagaMiddleware.run(rootSaga);
return store;
}

36
src/data/reducers.js Executable file
View File

@@ -0,0 +1,36 @@
import { combineReducers } from 'redux';
import {
reducer as commonComponentsReducer,
storeName as commonComponentsStoreName,
} from '../common-components';
import {
reducer as forgotPasswordReducer,
storeName as forgotPasswordStoreName,
} from '../forgot-password';
import {
reducer as loginReducer,
storeName as loginStoreName,
} from '../login';
import {
reducer as authnProgressiveProfilingReducers,
storeName as authnProgressiveProfilingStoreName,
} from '../progressive-profiling';
import {
reducer as registerReducer,
storeName as registerStoreName,
} from '../register';
import {
reducer as resetPasswordReducer,
storeName as resetPasswordStoreName,
} from '../reset-password';
const createRootReducer = () => combineReducers({
[loginStoreName]: loginReducer,
[registerStoreName]: registerReducer,
[commonComponentsStoreName]: commonComponentsReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
});
export default createRootReducer;

19
src/data/sagas.js Normal file
View File

@@ -0,0 +1,19 @@
import { all } from 'redux-saga/effects';
import { saga as commonComponentsSaga } from '../common-components';
import { saga as forgotPasswordSaga } from '../forgot-password';
import { saga as loginSaga } from '../login';
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
import { saga as registrationSaga } from '../register';
import { saga as resetPasswordSaga } from '../reset-password';
export default function* rootSaga() {
yield all([
loginSaga(),
registrationSaga(),
commonComponentsSaga(),
forgotPasswordSaga(),
resetPasswordSaga(),
authnProgressiveProfilingSaga(),
]);
}

View File

@@ -0,0 +1,14 @@
import AsyncActionType from '../utils/reduxUtils';
describe('AsyncActionType', () => {
it('should return well formatted action strings', () => {
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
});
});

View File

@@ -7,4 +7,5 @@ export {
updatePathWithQueryParams,
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
export { default as setCookie } from './cookies';

View File

@@ -0,0 +1,34 @@
/**
* Helper class to save time when writing out action types for asynchronous methods. Also helps
* ensure that actions are namespaced.
*/
export default class AsyncActionType {
constructor(topic, name) {
this.topic = topic;
this.name = name;
}
get BASE() {
return `${this.topic}__${this.name}`;
}
get BEGIN() {
return `${this.topic}__${this.name}__BEGIN`;
}
get SUCCESS() {
return `${this.topic}__${this.name}__SUCCESS`;
}
get FAILURE() {
return `${this.topic}__${this.name}__FAILURE`;
}
get RESET() {
return `${this.topic}__${this.name}__RESET`;
}
get FORBIDDEN() {
return `${this.topic}__${this.name}__FORBIDDEN`;
}
}

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { Form, Icon } from '@openedx/paragon';
import { ExpandMore } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { fireEvent, render } from '@testing-library/react';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';
@@ -41,7 +43,7 @@ const ForgotPasswordAlert = (props) => {
}}
/>
);
break;
break;
case INTERNAL_SERVER_ERROR:
message = formatMessage(messages['internal.server.error']);
break;

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -12,39 +13,42 @@ import {
Tabs,
} from '@openedx/paragon';
import { ChevronLeft } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useLocation, useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useForgotPassword } from './data/apiHook';
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
import { forgotPasswordResultSelector } from './data/selectors';
import ForgotPasswordAlert from './ForgotPasswordAlert';
import messages from './messages';
import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
const ForgotPasswordPage = () => {
const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME;
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
const {
status, submitState, emailValidationError,
} = props;
const { formatMessage } = useIntl();
const navigate = useNavigate();
const location = useLocation();
const [email, setEmail] = useState('');
const [email, setEmail] = useState(props.email);
const [bannerEmail, setBannerEmail] = useState('');
const [formErrors, setFormErrors] = useState('');
const [validationError, setValidationError] = useState('');
const [status, setStatus] = useState(location.state?.status || null);
// React Query hook for forgot password
const { mutate: sendForgotPassword, isPending: isSending } = useForgotPassword();
const submitState = isSending ? 'pending' : 'default';
const [validationError, setValidationError] = useState(emailValidationError);
const navigate = useNavigate();
useEffect(() => {
sendPageEvent('login_and_registration', 'reset');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
}, []);
useEffect(() => {
setValidationError(emailValidationError);
}, [emailValidationError]);
useEffect(() => {
if (status === 'complete') {
setEmail('');
@@ -64,38 +68,22 @@ const ForgotPasswordPage = () => {
};
const handleBlur = () => {
setValidationError(getValidationMessage(email));
props.setForgotPasswordFormData({ email, emailValidationError: getValidationMessage(email) });
};
const handleFocus = () => {
setValidationError('');
};
const handleFocus = () => props.setForgotPasswordFormData({ emailValidationError: '' });
const handleSubmit = (e) => {
e.preventDefault();
setBannerEmail(email);
const validateError = getValidationMessage(email);
if (validateError) {
setFormErrors(validateError);
setValidationError(validateError);
const error = getValidationMessage(email);
if (error) {
setFormErrors(error);
props.setForgotPasswordFormData({ email, emailValidationError: error });
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
} else {
setFormErrors('');
sendForgotPassword(email, {
onSuccess: (data, emailUsed) => {
setStatus('complete');
setBannerEmail(emailUsed);
setFormErrors('');
},
onError: (error) => {
if (error.response && error.response.status === 403) {
setStatus('forbidden');
} else {
setStatus('server-error');
}
},
});
props.forgotPassword(email);
}
};
@@ -165,7 +153,7 @@ const ForgotPasswordPage = () => {
)}
<p className="mt-5.5 small text-gray-700">
{formatMessage(messages['additional.help.text'], { platformName })}
<span className="mx-1">
<span>
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
</span>
</p>
@@ -176,4 +164,26 @@ const ForgotPasswordPage = () => {
);
};
export default ForgotPasswordPage;
ForgotPasswordPage.propTypes = {
email: PropTypes.string,
emailValidationError: PropTypes.string,
forgotPassword: PropTypes.func.isRequired,
setForgotPasswordFormData: PropTypes.func.isRequired,
status: PropTypes.string,
submitState: PropTypes.string,
};
ForgotPasswordPage.defaultProps = {
email: '',
emailValidationError: '',
status: null,
submitState: DEFAULT_STATE,
};
export default connect(
forgotPasswordResultSelector,
{
forgotPassword,
setForgotPasswordFormData,
},
)(ForgotPasswordPage);

View File

@@ -0,0 +1,32 @@
import { AsyncActionType } from '../../data/utils';
export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
export const FORGOT_PASSWORD_PERSIST_FORM_DATA = 'FORGOT_PASSWORD_PERSIST_FORM_DATA';
// Forgot Password
export const forgotPassword = email => ({
type: FORGOT_PASSWORD.BASE,
payload: { email },
});
export const forgotPasswordBegin = () => ({
type: FORGOT_PASSWORD.BEGIN,
});
export const forgotPasswordSuccess = email => ({
type: FORGOT_PASSWORD.SUCCESS,
payload: { email },
});
export const forgotPasswordForbidden = () => ({
type: FORGOT_PASSWORD.FORBIDDEN,
});
export const forgotPasswordServerError = () => ({
type: FORGOT_PASSWORD.FAILURE,
});
export const setForgotPasswordFormData = (forgotPasswordFormData) => ({
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
payload: { forgotPasswordFormData },
});

View File

@@ -1,144 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded';
import { forgotPassword } from './api';
// Mock the platform dependencies
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
jest.mock('form-urlencoded', () => jest.fn());
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>;
const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as
jest.MockedFunction<typeof getAuthenticatedHttpClient>;
const mockFormurlencoded = formurlencoded as jest.MockedFunction<typeof formurlencoded>;
describe('forgot-password api', () => {
const mockHttpClient = {
post: jest.fn(),
};
const mockConfig = {
LMS_BASE_URL: 'http://localhost:18000',
};
beforeEach(() => {
jest.clearAllMocks();
mockGetConfig.mockReturnValue(mockConfig);
mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
mockFormurlencoded.mockImplementation((data) => `encoded=${JSON.stringify(data)}`);
});
describe('forgotPassword', () => {
const testEmail = 'test@example.com';
const expectedUrl = `${mockConfig.LMS_BASE_URL}/account/password`;
const expectedConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
it('should send forgot password request successfully', async () => {
const mockResponse = {
data: {
message: 'Password reset email sent successfully',
success: true,
},
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(mockGetAuthenticatedHttpClient).toHaveBeenCalled();
expect(mockFormurlencoded).toHaveBeenCalledWith({ email: testEmail });
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`encoded=${JSON.stringify({ email: testEmail })}`,
expectedConfig
);
expect(result).toEqual(mockResponse.data);
});
it('should handle empty email address', async () => {
const emptyEmail = '';
const mockResponse = {
data: {
message: 'Email is required',
success: false,
}
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(emptyEmail);
expect(mockFormurlencoded).toHaveBeenCalledWith({ email: emptyEmail });
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`encoded=${JSON.stringify({ email: emptyEmail })}`,
expectedConfig,
);
expect(result).toEqual(mockResponse.data);
});
it('should handle network errors without response', async () => {
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockHttpClient.post.mockRejectedValueOnce(networkError);
await expect(forgotPassword(testEmail)).rejects.toThrow('Network Error');
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
expect.any(String),
expectedConfig
);
});
it('should handle timeout errors', async () => {
const timeoutError = new Error('Request timeout');
timeoutError.name = 'TimeoutError';
mockHttpClient.post.mockRejectedValueOnce(timeoutError);
await expect(forgotPassword(testEmail)).rejects.toThrow('Request timeout');
});
it('should handle response with no data field', async () => {
const mockResponse = {
// No data field
status: 200,
statusText: 'OK',
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(result).toBeUndefined();
});
it('should return exactly the data field from response', async () => {
const expectedData = {
message: 'Password reset email sent successfully',
success: true,
timestamp: '2026-02-05T10:00:00Z',
};
const mockResponse = {
data: expectedData,
status: 200,
headers: {},
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(result).toEqual(expectedData);
expect(result).not.toHaveProperty('status');
expect(result).not.toHaveProperty('headers');
});
});
});

View File

@@ -1,175 +0,0 @@
import React from 'react';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import * as api from './api';
import { useForgotPassword } from './apiHook';
// Mock the logging functions
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
logInfo: jest.fn(),
}));
// Mock the API function
jest.mock('./api', () => ({
forgotPassword: jest.fn(),
}));
const mockForgotPassword = api.forgotPassword as jest.MockedFunction<typeof api.forgotPassword>;
const mockLogError = logError as jest.MockedFunction<typeof logError>;
const mockLogInfo = logInfo as jest.MockedFunction<typeof logInfo>;
// Test wrapper component
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function TestWrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('useForgotPassword', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
expect(result.current.isPending).toBe(false);
expect(result.current.isError).toBe(false);
expect(result.current.isSuccess).toBe(false);
expect(result.current.error).toBe(null);
});
it('should send forgot password email successfully and log success', async () => {
const testEmail = 'test@example.com';
const mockResponse = {
message: 'Password reset email sent successfully',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(result.current.data).toEqual(mockResponse);
});
it('should handle 403 forbidden error and log as info', async () => {
const testEmail = 'blocked@example.com';
const mockError = {
response: {
status: 403,
data: {
detail: 'Too many password reset attempts',
},
},
message: 'Forbidden',
};
mockForgotPassword.mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(mockLogInfo).toHaveBeenCalledWith(mockError);
expect(mockLogError).not.toHaveBeenCalled();
expect(result.current.error).toEqual(mockError);
});
it('should handle network errors without response and log as error', async () => {
const testEmail = 'test@example.com';
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockForgotPassword.mockRejectedValueOnce(networkError);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(mockLogError).toHaveBeenCalledWith(networkError);
expect(mockLogInfo).not.toHaveBeenCalled();
expect(result.current.error).toEqual(networkError);
});
it('should handle empty email address', async () => {
const testEmail = '';
const mockResponse = {
message: 'Email sent',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith('');
});
it('should handle email with special characters', async () => {
const testEmail = 'user+test@example-domain.co.uk';
const mockResponse = {
message: 'Password reset email sent',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(result.current.data).toEqual(mockResponse);
});
});

View File

@@ -1,47 +0,0 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { useMutation } from '@tanstack/react-query';
import { forgotPassword } from './api';
interface ForgotPasswordResult {
success: boolean;
message?: string;
}
interface UseForgotPasswordOptions {
onSuccess?: (data: ForgotPasswordResult, email: string) => void;
onError?: (error: Error) => void;
}
interface ApiError {
response?: {
status: number;
data: Record<string, unknown>;
};
}
const useForgotPassword = (options: UseForgotPasswordOptions = {}) => useMutation({
mutationFn: (email: string) => (
forgotPassword(email)
),
onSuccess: (data: ForgotPasswordResult, email: string) => {
if (options.onSuccess) {
options.onSuccess(data, email);
}
},
onError: (error: ApiError) => {
// Handle different error types like the saga did
if (error.response && error.response.status === 403) {
logInfo(error);
} else {
logError(error);
}
if (options.onError) {
options.onError(error as Error);
}
},
});
export {
useForgotPassword,
};

View File

@@ -0,0 +1,58 @@
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
export const defaultState = {
status: '',
submitState: '',
email: '',
emailValidationError: '',
};
const reducer = (state = defaultState, action = null) => {
if (action !== null) {
switch (action.type) {
case FORGOT_PASSWORD.BEGIN:
return {
email: state.email,
status: 'pending',
submitState: PENDING_STATE,
};
case FORGOT_PASSWORD.SUCCESS:
return {
...defaultState,
status: 'complete',
};
case FORGOT_PASSWORD.FORBIDDEN:
return {
email: state.email,
status: 'forbidden',
};
case FORGOT_PASSWORD.FAILURE:
return {
email: state.email,
status: INTERNAL_SERVER_ERROR,
};
case PASSWORD_RESET_FAILURE:
return {
status: action.payload.errorCode,
};
case FORGOT_PASSWORD_PERSIST_FORM_DATA: {
const { forgotPasswordFormData } = action.payload;
return {
...state,
...forgotPasswordFormData,
};
}
default:
return {
...defaultState,
email: state.email,
emailValidationError: state.emailValidationError,
};
}
}
return state;
};
export default reducer;

View File

@@ -0,0 +1,35 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
// Actions
import {
FORGOT_PASSWORD,
forgotPasswordBegin,
forgotPasswordForbidden,
forgotPasswordServerError,
forgotPasswordSuccess,
} from './actions';
import { forgotPassword } from './service';
// Services
export function* handleForgotPassword(action) {
try {
yield put(forgotPasswordBegin());
yield call(forgotPassword, action.payload.email);
yield put(forgotPasswordSuccess(action.payload.email));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(forgotPasswordForbidden());
logInfo(e);
} else {
yield put(forgotPasswordServerError());
logError(e);
}
}
}
export default function* saga() {
yield takeEvery(FORGOT_PASSWORD.BASE, handleForgotPassword);
}

View File

@@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
export const storeName = 'forgotPassword';
export const forgotPasswordSelector = state => ({ ...state[storeName] });
export const forgotPasswordResultSelector = createSelector(
forgotPasswordSelector,
forgotPassword => forgotPassword,
);

View File

@@ -2,7 +2,8 @@ import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded';
const forgotPassword = async (email: string) => {
// eslint-disable-next-line import/prefer-default-export
export async function forgotPassword(email) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
@@ -19,8 +20,4 @@ const forgotPassword = async (email: string) => {
});
return data;
};
export {
forgotPassword,
};
}

View File

@@ -0,0 +1,34 @@
import {
FORGOT_PASSWORD_PERSIST_FORM_DATA,
} from '../actions';
import reducer from '../reducers';
describe('forgot password reducer', () => {
it('should set email and emailValidationError', () => {
const state = {
status: '',
submitState: '',
email: '',
emailValidationError: '',
};
const forgotPasswordFormData = {
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
const action = {
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
payload: { forgotPasswordFormData },
};
expect(
reducer(state, action),
).toEqual(
{
status: '',
submitState: '',
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
},
);
});
});

View File

@@ -0,0 +1,67 @@
import { runSaga } from 'redux-saga';
import initializeMockLogging from '../../../setupTest';
import * as actions from '../actions';
import { handleForgotPassword } from '../sagas';
import * as api from '../service';
const { loggingService } = initializeMockLogging();
describe('handleForgotPassword', () => {
const params = {
payload: {
forgotPasswordFormData: {
email: 'test@test.com',
},
},
};
beforeEach(() => {
loggingService.logError.mockReset();
loggingService.logInfo.mockReset();
});
it('should handle 500 error code', async () => {
const passwordErrorResponse = { response: { status: 500 } };
const forgotPasswordRequest = jest.spyOn(api, 'forgotPassword').mockImplementation(
() => Promise.reject(passwordErrorResponse),
);
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
handleForgotPassword,
params,
);
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.forgotPasswordBegin(),
actions.forgotPasswordServerError(),
]);
forgotPasswordRequest.mockClear();
});
it('should handle rate limit error', async () => {
const forbiddenErrorResponse = { response: { status: 403 } };
const forbiddenPasswordRequest = jest.spyOn(api, 'forgotPassword').mockImplementation(
() => Promise.reject(forbiddenErrorResponse),
);
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
handleForgotPassword,
params,
);
expect(loggingService.logInfo).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.forgotPasswordBegin(),
actions.forgotPasswordForbidden(null),
]);
forbiddenPasswordRequest.mockClear();
});
});

View File

@@ -1 +1,5 @@
export { default as ForgotPasswordPage } from './ForgotPasswordPage';
export { default as reducer } from './data/reducers';
export { FORGOT_PASSWORD } from './data/actions';
export { default as saga } from './data/sagas';
export { storeName, forgotPasswordResultSelector } from './data/selectors';

View File

@@ -74,7 +74,7 @@ const messages = defineMessages({
},
'additional.help.text': {
id: 'additional.help.text',
defaultMessage: 'For additional help, contact {platformName} support at',
defaultMessage: 'For additional help, contact {platformName} support at ',
description: 'additional help text on forgot password page',
},
'sign.in.text': {

View File

@@ -1,17 +1,17 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen, waitFor,
fireEvent, render, screen,
} from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR, LOGIN_PAGE,
} from '../../data/constants';
import { INTERNAL_SERVER_ERROR, LOGIN_PAGE } from '../../data/constants';
import { PASSWORD_RESET } from '../../reset-password/data/constants';
import { useForgotPassword } from '../data/apiHook';
import ForgotPasswordAlert from '../ForgotPasswordAlert';
import { setForgotPasswordFormData } from '../data/actions';
import ForgotPasswordPage from '../ForgotPasswordPage';
const mockedNavigator = jest.fn();
@@ -26,9 +26,14 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedNavigator,
}));
jest.mock('../data/apiHook', () => ({
useForgotPassword: jest.fn(),
}));
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
const mockStore = configureStore();
const initialState = {
forgotPassword: {
status: '',
},
};
describe('ForgotPasswordPage', () => {
mergeConfig({
@@ -36,55 +41,19 @@ describe('ForgotPasswordPage', () => {
INFO_EMAIL: '',
});
let queryClient;
let mockMutate;
let mockIsPending;
let props = {};
let store = {};
const renderWrapper = (component, options = {}) => {
const {
status = null,
isPending = false,
mutateImplementation = jest.fn(),
} = options;
mockMutate = jest.fn((email, callbacks) => {
if (mutateImplementation && typeof mutateImplementation === 'function') {
mutateImplementation(email, callbacks);
}
});
mockIsPending = isPending;
useForgotPassword.mockReturnValue({
mutate: mockMutate,
isPending: mockIsPending,
isError: status === 'error' || status === 'server-error',
isSuccess: status === 'complete',
});
return (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
{component}
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>
);
};
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
beforeEach(() => {
// Create a fresh QueryClient for each test
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
store = mockStore(initialState);
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({
userId: 3,
@@ -99,13 +68,17 @@ describe('ForgotPasswordPage', () => {
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
// Clear mock calls between tests
jest.clearAllMocks();
props = {
forgotPassword: jest.fn(),
status: null,
};
});
const findByTextContent = (container, text) => Array.from(container.querySelectorAll('*')).find(
element => element.textContent === text,
);
it('not should display need other help signing in button', () => {
const { queryByTestId } = render(renderWrapper(<ForgotPasswordPage />));
const { queryByTestId } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const forgotPasswordButton = queryByTestId('forgot-password');
expect(forgotPasswordButton).toBeNull();
});
@@ -114,14 +87,14 @@ describe('ForgotPasswordPage', () => {
mergeConfig({
LOGIN_ISSUE_SUPPORT_LINK: '/support',
});
render(renderWrapper(<ForgotPasswordPage />));
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const forgotPasswordButton = screen.findByText('Need help signing in?');
expect(forgotPasswordButton).toBeDefined();
});
it('should display email validation error message', async () => {
const validationMessage = 'We were unable to contact you.Enter a valid email address below.';
const { container } = render(renderWrapper(<ForgotPasswordPage />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
@@ -135,28 +108,23 @@ describe('ForgotPasswordPage', () => {
expect(validationErrors).toBe(validationMessage);
});
it('should show alert on server error', async () => {
it('should show alert on server error', () => {
store = mockStore({
forgotPassword: { status: INTERNAL_SERVER_ERROR },
});
const expectedMessage = 'We were unable to contact you.'
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
// Create a component with server-error status to simulate the error state
const { container } = render(renderWrapper(<ForgotPasswordPage />, {
status: 'server-error',
}));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
// The ForgotPasswordAlert should render with server error status
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const validationErrors = alertElements[0].textContent;
expect(validationErrors).toBe(expectedMessage);
}
});
const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent;
expect(validationErrors).toBe(expectedMessage);
});
it('should display empty email validation message', () => {
it('should display empty email validation message', async () => {
const validationMessage = 'We were unable to contact you.Enter your email below.';
const { container } = render(renderWrapper(<ForgotPasswordPage />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
@@ -167,25 +135,21 @@ describe('ForgotPasswordPage', () => {
expect(validationErrors).toBe(validationMessage);
});
it('should display request in progress error message', async () => {
it('should display request in progress error message', () => {
const rateLimitMessage = 'An error occurred.Your previous request is in progress, please try again in a few moments.';
// Create component with forbidden status to simulate rate limit error
const { container } = render(renderWrapper(<ForgotPasswordPage />, {
status: 'forbidden',
}));
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const validationErrors = alertElements[0].textContent;
expect(validationErrors).toBe(rateLimitMessage);
}
store = mockStore({
forgotPassword: { status: 'forbidden' },
});
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent;
expect(validationErrors).toBe(rateLimitMessage);
});
it('should not display any error message on change event', () => {
render(renderWrapper(<ForgotPasswordPage />));
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
@@ -195,248 +159,115 @@ describe('ForgotPasswordPage', () => {
expect(errorElement).toBeNull();
});
it('should not cause errors when blur event occurs', () => {
render(renderWrapper(<ForgotPasswordPage />));
it('should set error in redux store on onBlur', () => {
const forgotPasswordFormData = {
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
props = {
...props,
email: 'test@gmail',
emailValidationError: '',
};
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
// Simply test that blur event doesn't cause errors
fireEvent.blur(emailInput);
// No error assertions needed as we're just testing stability
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
});
it('should display validation error message when invalid email is submitted', () => {
it('should display error message if available in props', async () => {
const validationMessage = 'Enter your email';
const { container } = render(renderWrapper(<ForgotPasswordPage />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
props = {
...props,
emailValidationError: validationMessage,
email: '',
};
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const validationElement = container.querySelector('.pgn__form-text-invalid');
expect(validationElement.textContent).toEqual(validationMessage);
});
it('should not cause errors when focus event occurs', () => {
render(renderWrapper(<ForgotPasswordPage />));
it('should clear error in redux store on onFocus', () => {
const forgotPasswordFormData = {
emailValidationError: '',
};
props = {
...props,
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
fireEvent.focus(emailInput);
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
});
it('should not display error message initially', async () => {
render(renderWrapper(<ForgotPasswordPage />));
it('should clear error message when cleared in props on focus', async () => {
props = {
...props,
emailValidationError: '',
email: '',
};
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const errorElement = screen.queryByTestId('email-invalid-feedback');
expect(errorElement).toBeNull();
});
it('should display success message after email is sent', async () => {
const testEmail = 'test@example.com';
const { container } = render(renderWrapper(<ForgotPasswordPage />, {
status: 'complete',
}));
const emailInput = screen.getByLabelText('Email');
const submitButton = screen.getByText('Submit');
fireEvent.change(emailInput, { target: { value: testEmail } });
fireEvent.click(submitButton);
await waitFor(() => {
const successElements = container.querySelectorAll('.alert-success');
if (successElements.length > 0) {
const successMessage = successElements[0].textContent;
expect(successMessage).toContain('Check your email');
expect(successMessage).toContain('We sent an email');
}
it('should display success message after email is sent', () => {
store = mockStore({
...initialState,
forgotPassword: {
status: 'complete',
},
});
const successMessage = 'Check your emailWe sent an email to with instructions to reset your password. If you do not '
+ 'receive a password reset message after 1 minute, verify that you entered the correct email address,'
+ ' or check your spam folder. If you need further assistance, contact technical support.';
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const successElement = findByTextContent(container, successMessage);
expect(successElement).toBeDefined();
expect(successElement.textContent).toEqual(successMessage);
});
it('should call mutation on form submission with valid email', async () => {
render(renderWrapper(<ForgotPasswordPage />));
const emailInput = screen.getByLabelText('Email');
const submitButton = screen.getByText('Submit');
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.click(submitButton);
// Verify the mutation was called with the correct email and callbacks
await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith('test@example.com', expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}));
it('should display invalid password reset link error', () => {
store = mockStore({
...initialState,
forgotPassword: {
status: PASSWORD_RESET.INVALID_TOKEN,
},
});
});
const successMessage = 'Invalid password reset link'
+ 'This password reset link is invalid. It may have been used already. '
+ 'Enter your email below to receive a new link.';
it('should call mutation with success callback', async () => {
const successMutation = (email, { onSuccess }) => {
onSuccess({}, email);
};
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const successElement = findByTextContent(container, successMessage);
render(renderWrapper(<ForgotPasswordPage />, {
mutateImplementation: successMutation,
}));
const emailInput = screen.getByLabelText('Email');
const submitButton = screen.getByText('Submit');
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith('test@example.com', expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}));
});
expect(successElement).toBeDefined();
expect(successElement.textContent).toEqual(successMessage);
});
it('should redirect onto login page', async () => {
const { container } = render(renderWrapper(<ForgotPasswordPage />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const navElement = container.querySelector('nav');
const anchorElement = navElement.querySelector('a');
fireEvent.click(anchorElement);
expect(mockedNavigator).toHaveBeenCalledWith(expect.stringContaining(LOGIN_PAGE));
});
it('should display token validation rate limit error message', async () => {
const expectedHeading = 'Too many requests';
const expectedMessage = 'An error has occurred because of too many requests. Please try again after some time.';
const { container } = render(renderWrapper(<ForgotPasswordPage />, {
status: PASSWORD_RESET.FORBIDDEN_REQUEST,
}));
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const alertContent = alertElements[0].textContent;
expect(alertContent).toContain(expectedHeading);
expect(alertContent).toContain(expectedMessage);
}
});
});
it('should display invalid token error message', async () => {
const expectedHeading = 'Invalid password reset link';
const expectedMessage = 'This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.';
const { container } = render(renderWrapper(<ForgotPasswordAlert />, {
status: PASSWORD_RESET.INVALID_TOKEN,
}));
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const alertContent = alertElements[0].textContent;
expect(alertContent).toContain(expectedHeading);
expect(alertContent).toContain(expectedMessage);
}
});
});
it('should display token validation internal server error message', async () => {
const expectedHeading = 'Token validation failure';
const expectedMessage = 'An error has occurred. Try refreshing the page, or check your internet connection.';
const { container } = render(renderWrapper(<ForgotPasswordAlert />, {
status: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
}));
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const alertContent = alertElements[0].textContent;
expect(alertContent).toContain(expectedHeading);
expect(alertContent).toContain(expectedMessage);
}
});
});
});
describe('ForgotPasswordAlert', () => {
const renderAlertWrapper = (props) => {
const queryClient = new QueryClient();
return render(
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
<ForgotPasswordAlert {...props} />
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>,
);
};
it('should display internal server error message', () => {
const { container } = renderAlertWrapper({
status: INTERNAL_SERVER_ERROR,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('We were unable to contact you.');
expect(alertElement.textContent).toContain('An error has occurred. Try refreshing the page, or check your internet connection.');
});
it('should display forbidden state error message', () => {
const { container } = renderAlertWrapper({
status: FORBIDDEN_STATE,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('An error occurred.');
expect(alertElement.textContent).toContain('Your previous request is in progress, please try again in a few moments.');
});
it('should display form submission error message', () => {
const emailError = 'Enter a valid email address';
const { container } = renderAlertWrapper({
status: FORM_SUBMISSION_ERROR,
email: 'test@example.com',
emailError,
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('We were unable to contact you.');
expect(alertElement.textContent).toContain(`${emailError} below.`);
});
it('should display password reset invalid token error message', () => {
const { container } = renderAlertWrapper({
status: PASSWORD_RESET.INVALID_TOKEN,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('Invalid password reset link');
expect(alertElement.textContent).toContain('This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.');
});
it('should display password reset forbidden request error message', () => {
const { container } = renderAlertWrapper({
status: PASSWORD_RESET.FORBIDDEN_REQUEST,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('Too many requests');
expect(alertElement.textContent).toContain('An error has occurred because of too many requests. Please try again after some time.');
});
it('should display password reset internal server error message', () => {
const { container } = renderAlertWrapper({
status: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('Token validation failure');
expect(alertElement.textContent).toContain('An error has occurred. Try refreshing the page, or check your internet connection.');
expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE);
});
});

View File

@@ -1 +0,0 @@
export default [];

41
src/i18n/index.jsx Normal file
View File

@@ -0,0 +1,41 @@
import { messages as paragonMessages } from '@openedx/paragon';
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,
de: deMessages,
'de-de': deDEMessages,
'es-419': es419Messages,
'fa-ir': faIRMessages,
fr: frMessages,
'fr-ca': frCAMessages,
hi: hiMessages,
it: itMessages,
'it-it': itITMessages,
pt: ptMessages,
'pt-pt': ptPTMessages,
ru: ruMessages,
uk: ukMessages,
'zh-cn': zhCNMessages,
};
export default [
paragonMessages,
appMessages,
];

181
src/i18n/messages/ar.json Normal file
View File

@@ -0,0 +1,181 @@
{
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
"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": "حرف واحد",
"one.number": "رقم واحد",
"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 Support",
"zendesk.selectTicketForm": "الرجاء اختيار نوع الطلب الخاص بك:",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {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": "For additional help, contact {platformName} support at",
"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": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
"allowed.domain.login.error": "كونك مستخدمًا على {allowedDomain}، فإن عليك تسجيل الدخول باستخدام {tpaLink} الخاص بـ {allowedDomain} .",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
"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": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 2 أحرف على الأقل.",
"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": "Trending Now",
"recommendation.option.popular": "Most Popular",
"recommendation.option.recommended.for.you": "Recommended For You",
"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}",
"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": "هل تقصد",
"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": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت.",
"start.learning": "ابدأ التعلم ",
"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.2": "ملفك الشخصي",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}

View File

@@ -0,0 +1 @@
{}

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

@@ -0,0 +1,181 @@
{
"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.",
"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:",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. 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} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, 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 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 2 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 Now",
"recommendation.option.popular": "Most Popular",
"recommendation.option.recommended.for.you": "Recommended For You",
"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",
"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.",
"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",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}

View File

@@ -0,0 +1,181 @@
{
"error.notfound.message": "Die gesuchte Seite ist nicht verfügbar oder es liegt ein Fehler in der URL vor. Bitte überprüfen Sie die URL und versuchen Sie es erneut.",
"institution.login.page.sub.heading": "Wählen Sie Ihre Institution aus der folgenden Liste aus",
"logistration.sign.in": "Anmelden",
"logistration.register": "Registrieren",
"enterprisetpa.title.heading": "Möchten Sie sich mit Ihren {providerName}-Anmeldedaten anmelden?",
"enterprisetpa.login.button.text": "Andere Möglichkeiten für die Anmeldung oder Registrierung",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
"sso.sign.in.with": "Melden Sie sich mit {providerName} an",
"sso.create.account.using": "Erstellen Sie ein Konto mit {providerName}",
"show.password": "Passwort anzeigen",
"hide.password": "Passwort verbergen",
"one.letter": "1 Buchstabe",
"one.number": "1 Nummer",
"eight.characters": "8 Charaktere",
"password.sr.only.helping.text": "Das Passwort muss mindestens 8 Zeichen, mindestens einen Buchstaben und mindestens eine Zahl enthalten",
"tpa.alert.heading": "Fast fertig!",
"login.third.party.auth.account.not.linked": "Sie haben sich erfolgreich bei {currentProvider} angemeldet, aber Ihr {currentProvider}-Konto hat kein verknüpftes {platformName}-Konto. Um Ihre Konten zu verknüpfen, melden Sie sich jetzt mit Ihrem {platformName}-Passwort an.",
"register.third.party.auth.account.not.linked": "Sie haben sich erfolgreich bei {currentProvider} angemeldet! Wir brauchen nur ein paar mehr Informationen, bevor Sie anfangen, mit {platformName} zu lernen.",
"registration.using.tpa.form.heading": "Beenden Sie die Erstellung Ihres Kontos",
"zendesk.supportTitle": "edX Support",
"zendesk.selectTicketForm": "Please choose your request type:",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": "Passwort vergessen | {siteName}",
"forgot.password.page.heading": "Passwort zurücksetzen",
"forgot.password.page.instructions": "Bitte geben Sie unten Ihre E-Mail-Adresse ein und wir senden Ihnen eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts.",
"forgot.password.page.invalid.email.message": "Geben sie eine gültige E-Mail-Adresse an",
"forgot.password.page.email.field.label": "E-Mail Adresse",
"forgot.password.page.submit.button": "Einreichen",
"forgot.password.error.alert.title.": "Wir konnten Sie nicht kontaktieren.",
"forgot.password.error.message.title": "Ein Fehler ist aufgetreten.",
"forgot.password.request.in.progress.message": "Ihre vorherige Anfrage ist in Bearbeitung, bitte versuchen Sie es in wenigen Augenblicken erneut.",
"forgot.password.empty.email.field.error": "Geben sie ihre E-Mail Adresse ein",
"forgot.password.email.help.text": "Die E-Mail-Adresse, mit der Sie sich bei {platformName} registriert haben",
"confirmation.message.title": "Prüfen Sie Ihr E-Mail-Postfach",
"confirmation.support.link": "wenden Sie sich an den technischen Support",
"need.help.sign.in.text": "Brauchen Sie Hilfe bei der Anmeldung?",
"additional.help.text": "For additional help, contact {platformName} support at",
"sign.in.text": "Anmelden",
"extend.field.errors": "{emailError} unten.",
"invalid.token.heading": "Ungültiger Link zum Zurücksetzen des Passworts",
"invalid.token.error.message": "Dieser Link zum Zurücksetzen des Passwortes ist ungültig. Möglicherweise wurde es bereits verwendet. Geben Sie unten Ihre E-Mail-Adresse ein, um einen neuen Link zu erhalten.",
"token.validation.rate.limit.error.heading": "Zu viele Anfragen",
"token.validation.rate.limit.error": "Aufgrund von zu vieler Anfragen ist ein Fehler aufgetreten. Bitte versuchen Sie es nach einiger Zeit erneut.",
"token.validation.internal.sever.error.heading": "Token-Validierungsfehler",
"token.validation.internal.sever.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
"internal.server.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
"account.activation.error.message": "Etwas ist schief gelaufen, bitte {supportLink} um dieses Problem zu lösen.",
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
"allowed.domain.login.error": "Als {allowedDomain}-Benutzer müssen Sie sich mit Ihrem {allowedDomain} {tpaLink} anmelden.",
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "Wenn Sie Ihr Passwort vergessen haben, {resetLink}",
"account.locked.out.message.2": "Um auf der sicheren Seite zu sein, können Sie {resetLink} tun, bevor Sie es erneut versuchen.",
"login.incorrect.credentials.error.with.reset.link": "Der eingegebene Benutzername, die E-Mail-Adresse oder das Passwort ist falsch. Bitte versuchen Sie es erneut oder {resetLink}.",
"login.page.title": "Anmelden | {siteName}",
"login.user.identity.label": "Benutzername oder E-Mail-Adresse",
"login.password.label": "Passwort",
"sign.in.button": "Anmelden",
"forgot.password": "Passwort vergessen",
"institution.login.button": "Zeugnisse der Institution/des Campus",
"institution.login.page.title": "Melden Sie sich mit Institutions-/Campus-Anmeldeinformationen an",
"login.other.options.heading": "Oder melden Sie sich an mit:",
"non.compliant.password.title": "Wir haben kürzlich unsere Passwortanforderungen geändert",
"non.compliant.password.message": "Ihr aktuelles Passwort entspricht nicht den neuen Sicherheitsanforderungen. Wir haben gerade eine Nachricht zum Zurücksetzen des Passworts an die mit diesem Konto verknüpfte E-Mail-Adresse gesendet. Vielen Dank, dass Sie uns helfen, Ihre Daten zu schützen.",
"account.locked.out.message.1": "Um Ihr Konto zu schützen, wurde es vorübergehend gesperrt. Versuchen Sie es in 30 Minuten erneut.",
"enterprise.login.btn.text": "Arbeits- oder Schulzeugnisse",
"username.or.email.format.validation.less.chars.message": "Benutzername oder E-Mail müssen mindestens 2 Zeichen lang sein.",
"email.validation.message": "Geben Sie Ihren Benutzernamen oder Ihre E-Mail-Adresse ein",
"password.validation.message": "Die Passwortkriterien wurden nicht erfüllt",
"account.activation.success.message.title": "Super! Sie haben Ihr Konto aktiviert.",
"account.activation.success.message": "Sie erhalten jetzt E-Mail-Updates und Benachrichtigungen von uns in Bezug auf die Kurse, für die Sie eingeschrieben sind. Melden Sie sich an, um fortzufahren.",
"account.activation.info.message": "Dieses Konto wurde bereits aktiviert.",
"account.activation.error.message.title": "Ihr Konto konnte nicht aktiviert werden",
"account.activation.support.link": "kontaktieren Sie den Support",
"account.confirmation.success.message.title": "Super! Sie haben Ihre E-Mail bestätigt.",
"account.confirmation.success.message": "Melden Sie sich an, um fortzufahren.",
"account.confirmation.info.message": "Diese E-Mail-Adresse wurde bereits bestätigt.",
"account.confirmation.error.message.title": "Ihre E-Mail-Adresse konnte nicht bestätigt werden",
"tpa.account.link": "{provider}-Konto",
"internal.server.error.message": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
"login.rate.limit.reached.message": "Zu viele fehlgeschlagene Anmeldeversuche. Bitte versuche es später noch einmal.",
"login.failure.header.title": "Wir konnten Sie leider nicht einloggen.",
"contact.support.link": "Wenden Sie sich an den Support der {platformName}",
"login.incorrect.credentials.error": "Der eingegebene Benutzername, die E-Mail-Adresse oder das Passwort ist falsch. Bitte versuche es erneut.",
"login.form.invalid.error.message": "Bitte füllen Sie die unten stehenden Felder aus.",
"login.incorrect.credentials.error.reset.link.text": "Setzen Sie Ihr Passwort zurück",
"login.incorrect.credentials.error.before.account.blocked.text": "Klicken Sie hier, um es zurückzusetzen.",
"password.security.nudge.title": "Passwortsicherheit",
"password.security.block.title": "Passwortänderung erforderlich",
"password.security.nudge.body": "Unser System hat festgestellt, dass Ihr Passwort angreifbar ist. Wir empfehlen Ihnen, es zu ändern, 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.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.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.submit.button": "Einreichen",
"optional.fields.skip.button": "Überspringen",
"optional.fields.next.button": "Weiter",
"continue.to.platform": "Weiter zu {platformName}",
"modal.title": "Danke, dass Sie uns das mitteilen.",
"modal.description": "Sie können Ihr Profil jederzeit in den Einstellungen vervollständigen, wenn Sie Ihre Meinung ändern.",
"welcome.page.error.heading": "Wir konnten Ihr Profil nicht aktualisieren",
"welcome.page.error.message": "Ein Fehler ist aufgetreten. Sie können Ihr Profil jederzeit in den Einstellungen vervollständigen.",
"recommendation.page.title": "Empfehlungen | {siteName}",
"recommendation.page.heading": "Wir haben ein paar Empfehlungen für den Einstieg.",
"recommendation.skip.button": "Überspringen",
"recommendation.option.trending": "Trending Now",
"recommendation.option.popular": "Most Popular",
"recommendation.option.recommended.for.you": "Recommended For You",
"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}",
"registration.fullname.label": "Vollständiger Name",
"registration.email.label": "E-Mail-Adresse",
"registration.username.label": "Öffentlicher Benutzername",
"registration.password.label": "Passwort",
"registration.country.label": "Land/Region",
"registration.opt.in.label": "Ich stimme zu, dass {siteName} mir Marketingmitteilungen senden darf.",
"help.text.name": "Dieser Name wird von allen Zertifikaten verwendet, die Sie erwerben.",
"help.text.username.1": "Der Name, der Sie in Ihren Kursen identifiziert.",
"help.text.username.2": "Dies kann später nicht mehr geändert werden.",
"help.text.email": "Für die Kontoaktivierung und wichtige Updates",
"create.account.for.free.button": "Erstellen Sie kostenlos ein Benutzerkonto",
"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.page.title": "Registrieren Sie sich mit Institutions-/Campus-Anmeldeinformationen",
"empty.name.field.error": "Geben Sie Ihren vollständigen Namen ein",
"empty.email.field.error": "Geben Sie Ihre E-Mail-Adresse ein",
"empty.username.field.error": "Der Benutzername muss zwischen 2 und 30 Zeichen lang sein",
"empty.password.field.error": "Kennwortkriterien wurden nicht erfüllt",
"empty.country.field.error": "Wählen Sie das Land oder die Region Ihres Wohnsitzes aus",
"email.do.not.match": "Die E-Mail-Adressen stimmen nicht überein.",
"email.invalid.format.error": "Geben sie eine gültige E-Mail-Adresse an",
"username.validation.message": "Der Benutzername muss zwischen 2 und 30 Zeichen lang sein",
"name.validation.message": "Geben Sie einen gültigen Namen ein",
"username.format.validation.message": "Benutzernamen dürfen nur Buchstaben (AZ, az), Ziffern (0-9), Unterstriche (_) und Bindestriche (-) enthalten. Benutzernamen dürfen keine Leerzeichen enthalten",
"registration.request.failure.header": "Wir konnten Ihr Konto leider nicht erstellen.",
"registration.empty.form.submission.error": "Bitte überprüfen Sie Ihre Antworten und versuchen Sie es erneut.",
"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.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",
"privacy.policy": "Datenschutzbestimmungen",
"honor.code": "Verhaltenskodex",
"terms.of.service": "Nutzungsbedingungen",
"registration.username.suggestion.label": "Empfohlen:",
"did.you.mean.alert.text": "Meinten Sie",
"sign.in": "Anmelden",
"reset.password.page.title": "Passwort zurücksetzen | {siteName}",
"reset.password": "Passwort zurücksetzen",
"reset.password.page.instructions": "Neues Passwort eingeben und bestätigen",
"new.password.label": "Neues Passwort",
"confirm.password.label": "Kennwort bestätigen",
"passwords.do.not.match": "Passwörter stimmen nicht überein",
"confirm.your.password": "Bestätigen Sie Ihr Passwort",
"reset.password.failure.heading": "Wir konnten Ihr Passwort nicht zurücksetzen.",
"reset.password.form.submission.error": "Bitte überprüfen Sie Ihre Antworten und versuchen Sie es erneut.",
"reset.server.rate.limit.error": "Zu viele Anfragen.",
"reset.password.success.heading": "Zurücksetzen des Passworts abgeschlossen.",
"reset.password.success": "Ihr Passwort wurde zurückgesetzt. Melden Sie sich bei Ihrem Konto an.",
"rate.limit.error": "Aufgrund zu vieler Anfragen ist ein Fehler aufgetreten. Bitte versuchen Sie es nach einiger Zeit erneut.",
"start.learning": "Beginne zu lernen",
"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.2": "dein Profil",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}

View File

@@ -0,0 +1,181 @@
{
"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.",
"institution.login.page.sub.heading": "Selecciona tu institución de la lista siguiente",
"logistration.sign.in": "Iniciar sesión",
"logistration.register": "Registrarse",
"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.public.account.creation.disabled": "Mostrar otras formas de iniciar sesión",
"sso.sign.in.with": "Inicio de sesión con {providerName}",
"sso.create.account.using": "Crear una cuenta con {providerName}",
"show.password": "Mostrar contraseña",
"hide.password": "Ocultar contraseña",
"one.letter": "1 letra",
"one.number": "1 número",
"eight.characters": "8 caracteres",
"password.sr.only.helping.text": "La contraseña debe contener al menos 8 caracteres, al menos una letra y al menos un número",
"tpa.alert.heading": "¡Ya casi has terminado!",
"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}.",
"registration.using.tpa.form.heading": "Termina de crear tu cuenta",
"zendesk.supportTitle": "Soporte edX",
"zendesk.selectTicketForm": "Elegir el tipo de solicitud:",
"forgot.password.confirmation.message": "Enviamos un correo electrónico a {email} con instrucciones para restablecer su contraseña. Si no recibe un mensaje de restablecimiento de contraseña después de 1 minuto, verifique que ingresó la dirección de correo electrónico correcta o verifique su carpeta de correo no deseado. Si necesita más ayuda, {supportLink}.",
"forgot.password.page.title": "Olvidé la contraseña | {siteName}",
"forgot.password.page.heading": "Restablecer mi contraseña",
"forgot.password.page.instructions": "Por favor, introduce tu dirección de correo electrónico y te enviaremos un correo electrónico con instrucciones sobre cómo restablecer tu contraseña.",
"forgot.password.page.invalid.email.message": "Introduce una dirección de correo electrónico válida",
"forgot.password.page.email.field.label": "Correo electrónico",
"forgot.password.page.submit.button": "Enviar",
"forgot.password.error.alert.title.": "No hemos podido entrar en contacto contigo.",
"forgot.password.error.message.title": "Ha ocurrido un error.",
"forgot.password.request.in.progress.message": "Su solicitud anterior está en progreso, por favor inténtalo de nuevo en unos minutos.",
"forgot.password.empty.email.field.error": "Introduce tu email",
"forgot.password.email.help.text": "El correo electrónico que utilizaste para registrarte en {platformName}",
"confirmation.message.title": "Verifica tu correo electrónico",
"confirmation.support.link": "entra en contacto con el equipo de soporte técnico",
"need.help.sign.in.text": "¿Necesitas ayuda para iniciar sesión?",
"additional.help.text": "Para obtener ayuda adicional, comuníquese con el soporte {platformName} en",
"sign.in.text": "Iniciar sesión",
"extend.field.errors": "{emailError} a continuación.",
"invalid.token.heading": "Enlace de restablecimiento de contraseña inválido",
"invalid.token.error.message": "Este enlace para restablecer la contraseña no es válido. Es posible que ya haya sido utilizado. Introduce tu correo electrónico para recibir un nuevo enlace.",
"token.validation.rate.limit.error.heading": "Demasiadas solicitudes",
"token.validation.rate.limit.error": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo.",
"token.validation.internal.sever.error.heading": "Fallo de validación del token",
"token.validation.internal.sever.error": "Se ha producido un error. Intenta actualizar la página o verifica tu conexión a Internet.",
"internal.server.error": "Se ha producido un error. Intenta actualizar la página o verifica tu conexión a Internet.",
"account.activation.error.message": "Algo no funcionó correctamente, por favor {supportLink} para resolver este problema.",
"login.inactive.user.error": "Para iniciar sesión, debe activar su cuenta.{lineBreak} {lineBreak}Acabamos de enviar un enlace de activación a {email}. Si no recibe un correo electrónico, revise sus carpetas de spam o {supportLink}.",
"allowed.domain.login.error": "Como usuario {allowedDomain}, debe iniciar sesión con su {allowedDomain} {tpaLink}.",
"login.incorrect.credentials.error.attempts.text.1": "El nombre de usuario, correo electrónico o contraseña que ingresó es incorrecto. Tiene {remainingAttempts} más intentos de inicio de sesión antes de que su cuenta se bloquee temporalmente.",
"login.incorrect.credentials.error.attempts.text.2": "Si has olvidado tu contraseña, {resetLink}",
"account.locked.out.message.2": "Para estar seguro, puedes {resetLink} antes de volver a intentarlo.",
"login.incorrect.credentials.error.with.reset.link": "El nombre de usuario, el correo electrónico o la contraseña que has introducido son incorrectos. Por favor, inténtalo de nuevo o {resetLink}.",
"login.page.title": "Login | {siteName}",
"login.user.identity.label": "Nombre de usuario o correo electrónico",
"login.password.label": "Contraseña",
"sign.in.button": "Iniciar sesión",
"forgot.password": "Olvidé mi contraseña",
"institution.login.button": "Credenciales de la institución/campus",
"institution.login.page.title": "Iniciar sesión con las credenciales de la institución/campus",
"login.other.options.heading": "O bien, inicia sesión con:",
"non.compliant.password.title": "Recientemente hemos cambiado los requisitos de las contraseñas",
"non.compliant.password.message": "Tu contraseña actual no cumple con los nuevos requisitos de seguridad. Acabamos de enviar un mensaje de restablecimiento de contraseña a la dirección de correo electrónico asociada a esta cuenta. Gracias por ayudarnos a mantener tus datos seguros.",
"account.locked.out.message.1": "Para proteger tu cuenta, se ha bloqueado temporalmente. Inténtalo de nuevo en 30 minutos.",
"enterprise.login.btn.text": "Credenciales de la empresa o de la institución ",
"username.or.email.format.validation.less.chars.message": "El nombre de usuario o el correo electrónico deben tener al menos 2 caracteres.",
"email.validation.message": "Introduce tu nombre de usuario o correo electrónico",
"password.validation.message": "No se han cumplido los criterios de la contraseña",
"account.activation.success.message.title": "Ha sido un éxito. Has activado tu cuenta.",
"account.activation.success.message": "Ahora recibirás por correo electrónico actualizaciones y alertas relacionadas con los cursos en los que estás inscrito. Inicia sesión para continuar.",
"account.activation.info.message": "La cuenta ya ha sido activada.",
"account.activation.error.message.title": "Tu cuenta no ha podido ser activada",
"account.activation.support.link": "contacta al equipo de soporte de edX",
"account.confirmation.success.message.title": "¡Éxito! Has confirmado tu correo electrónico.",
"account.confirmation.success.message": "Inicia sesión para continuar.",
"account.confirmation.info.message": "Este correo electrónico ya ha sido confirmado.",
"account.confirmation.error.message.title": "Tu correo electrónico no pudo ser confirmado",
"tpa.account.link": "{provider} cuenta",
"internal.server.error.message": "Se ha producido un error. Intenta actualizar la página o comprueba tu conexión a Internet.",
"login.rate.limit.reached.message": "Demasiados intentos fallidos de inicio de sesión. Inténtelo de nuevo más tarde.",
"login.failure.header.title": "No se ha podido iniciar tu sesión.",
"contact.support.link": "entrar en contacto con el soporte de {platformName}",
"login.incorrect.credentials.error": "El nombre de usuario, el correo electrónico o la contraseña que has introducido son incorrectos. Por favor, inténtalo de nuevo.",
"login.form.invalid.error.message": "Por favor, complete los siguientes campos.",
"login.incorrect.credentials.error.reset.link.text": "restablecer la contraseña",
"login.incorrect.credentials.error.before.account.blocked.text": "Pulse aquí para restablecerla.",
"password.security.nudge.title": "Seguridad de contraseña",
"password.security.block.title": "Cambio de contraseña requerido",
"password.security.nudge.body": "Nuestro sistema detectó que su contraseña es vulnerable. Le recomendamos que lo cambie para que su cuenta se mantenga 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.redirect.to.reset.password.button": "Restablece tu contraseña",
"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.",
"optional.fields.information.link": "Aprende más sobre cómo usamos esta información.",
"optional.fields.submit.button": "Enviar",
"optional.fields.skip.button": "Saltar por ahora ",
"optional.fields.next.button": "Siguiente",
"continue.to.platform": "Continuar a {platformName}",
"modal.title": "Gracias por informarnos.",
"modal.description": "Puedes completar tu perfil en los ajustes en cualquier momento si cambias de opinión.",
"welcome.page.error.heading": "No hemos podido actualizar tu perfil",
"welcome.page.error.message": "Se ha producido un error. Puedes completar tu perfil en los ajustes en cualquier momento.",
"recommendation.page.title": "Recomendaciones | {siteName}",
"recommendation.page.heading": "Tenemos algunas recomendaciones para empezar.",
"recommendation.skip.button": "Saltar por ahora ",
"recommendation.option.trending": "Tendencias",
"recommendation.option.popular": "Más popular",
"recommendation.option.recommended.for.you": "Recomendado para usted",
"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}",
"registration.fullname.label": "Nombre completo",
"registration.email.label": "Correo electrónico",
"registration.username.label": "Nombre de usuario público",
"registration.password.label": "Contraseña",
"registration.country.label": "País/Región",
"registration.opt.in.label": "Acepto que {siteName} pueda enviarme mensajes de marketing.",
"help.text.name": "Este nombre será utilizado por los certificados que obtengas.",
"help.text.username.1": "El nombre que te identificará en tus cursos.",
"help.text.username.2": "Esto no puede modificarse posteriormente.",
"help.text.email": "Para la activación de la cuenta y las actualizaciones importantes",
"create.account.for.free.button": "Crea una cuenta gratis",
"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.page.title": "Registro con credenciales de la institución/campus",
"empty.name.field.error": "Introduce tu nombre completo",
"empty.email.field.error": "Introduce tu email",
"empty.username.field.error": "El nombre de usuario debe tener entre 2 y 30 caracteres",
"empty.password.field.error": "No se han cumplido los criterios de la contraseña",
"empty.country.field.error": "Selecciona tu país o región de residencia",
"email.do.not.match": "Los correos electrónicos no son iguales.",
"email.invalid.format.error": "Introduce una dirección de correo electrónico válida",
"username.validation.message": "El nombre de usuario debe tener entre 2 y 30 caracteres",
"name.validation.message": "Introduce un nombre válido",
"username.format.validation.message": "Los nombres de usuario solo pueden contener letras (A-Z, a-z), números (0-9), guiones bajos (_) y guiones (-). Los nombres de usuario no pueden contener espacios",
"registration.request.failure.header": "No pudimos crear tu cuenta.",
"registration.empty.form.submission.error": "Por favor, verifica tus respuestas y vuelve a intentarlo.",
"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.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",
"privacy.policy": "Política de privacidad ",
"honor.code": "Código de Honor",
"terms.of.service": "Términos de servicio",
"registration.username.suggestion.label": "Se recomienda:",
"did.you.mean.alert.text": "¿Quieres decir",
"sign.in": "Iniciar sesión",
"reset.password.page.title": "Restablecer contraseña | {siteName}",
"reset.password": "Restablecer mi contraseña",
"reset.password.page.instructions": "Ingresa y confirma tu nueva contraseña.",
"new.password.label": "Nueva contraseña",
"confirm.password.label": "Confirmar contraseña",
"passwords.do.not.match": "Las contraseñas no coinciden",
"confirm.your.password": "Confirma tu contraseña",
"reset.password.failure.heading": "No hemos podido restablecer tu contraseña.",
"reset.password.form.submission.error": "Por favor, verifica tus respuestas y vuelve a intentarlo.",
"reset.server.rate.limit.error": "Demasiadas solicitudes.",
"reset.password.success.heading": "Restablecimiento de la contraseña completado.",
"reset.password.success": "Tu contraseña ha sido restablecida. Acceda a tu cuenta.",
"rate.limit.error": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo.",
"start.learning": "Empieza a aprender",
"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.2": "tu perfil ",
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, acepta {tosAndHonorCode} y reconoce que {platformName} y cada miembro procesan sus datos personales de acuerdo con {privacyPolicy}.",
"register.page.honor.code": "Acepto las {platformName} {tosAndHonorCode}",
"register.page.terms.of.service": "Acepto las {platformName} {termsOfService}"
}

View File

@@ -0,0 +1,181 @@
{
"error.notfound.message": "صفحه مورد نظر شما در دسترس نیست یا خطایی در نشانی اینترنتی وجود دارد. لطفاً نشانی اینترنتی را بررسی کرده و دوباره تلاش کنید.",
"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": "لطفا نوع درخواست خود را انتخاب کنید:",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {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": "For additional help, contact {platformName} support at",
"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": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
"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 attempts before your account is temporarily locked.",
"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": "نام کاربری یا نشانی رایانامه حداقل باید 2 نویسه داشته باشد",
"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": "Trending Now",
"recommendation.option.popular": "محبوبترین",
"recommendation.option.recommended.for.you": "Recommended For You",
"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": "منظور شما این بود",
"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": "به دلیل درخواست‌های زیاد، خطایی روی داده است. لطفا بعد از مدتی دوباره امتحان کنید.",
"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": "پرونده کاربری شما",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}

181
src/i18n/messages/fr.json Normal file
View File

@@ -0,0 +1,181 @@
{
"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.",
"institution.login.page.sub.heading": "Sélectionner votre institution dans la liste ci-dessous",
"logistration.sign.in": "Connectez-vous",
"logistration.register": "S'inscrire",
"enterprisetpa.title.heading": "Souhaitez-vous vous connecter à l'aide de vos identifiants {providerName} ?",
"enterprisetpa.login.button.text": "Montrez-moi d'autres méthodes pour me connecter ou m'inscrire",
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
"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 letter",
"one.number": "1 number",
"eight.characters": "8 characters",
"password.sr.only.helping.text": "Le mot de passe doit contenir au moins 8 caractères, au moins une lettre et au moins un chiffre",
"tpa.alert.heading": "Presque fini !",
"login.third.party.auth.account.not.linked": "Vous vous êtes connecté avec succès à {currentProvider}, mais votre compte {currentProvider} n'a pas de compte relié à {platformName}. Pour lier vos comptes, connectez-vous en utilisant votre mot de passe {platformName}.",
"register.third.party.auth.account.not.linked": "Vous vous êtes connecté avec succès à {currentProvider} ! Nous avons juste besoin d'un peu plus d'informations avant que vous commenciez à apprendre avec {platformName}.",
"registration.using.tpa.form.heading": "Terminer la création de votre compte",
"zendesk.supportTitle": "edX Support",
"zendesk.selectTicketForm": "Please choose your request type:",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
"forgot.password.page.title": " Mot de passe oublié | {siteName}",
"forgot.password.page.heading": "Réinitialiser le mot de passe",
"forgot.password.page.instructions": "Veuillez entrer votre adresse courriel ci-dessous et nous vous enverrons un courriel avec les instructions pour réinitialiser votre mot de passe.",
"forgot.password.page.invalid.email.message": "Enter a valid email address",
"forgot.password.page.email.field.label": "Email",
"forgot.password.page.submit.button": "Envoyez",
"forgot.password.error.alert.title.": "Nous n'avons pas pu vous contacter.",
"forgot.password.error.message.title": "Une erreur est survenue.",
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
"forgot.password.empty.email.field.error": "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 email",
"confirmation.support.link": "contacter le support technique",
"need.help.sign.in.text": "Besoin d'aide pour vous enregistrer?",
"additional.help.text": "For additional help, contact {platformName} support at",
"sign.in.text": "Connectez-vous",
"extend.field.errors": "{emailError} ci-dessous.",
"invalid.token.heading": "Lien de réinitialisation du mot de passe non valide",
"invalid.token.error.message": "Ce lien de réinitialisation de mot de passe n'est pas valide. Il a peut-être déjà été utilisé. Entrez votre courriel ci-dessous pour recevoir un nouveau lien.",
"token.validation.rate.limit.error.heading": "Trop de demandes",
"token.validation.rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps.",
"token.validation.internal.sever.error.heading": "Échec de la validation du jeton",
"token.validation.internal.sever.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
"internal.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
"account.activation.error.message": "Une erreur s'est produite, veuillez {supportLink} pour résoudre ce problème.",
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, 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 attempts before your account is temporarily locked.",
"login.incorrect.credentials.error.attempts.text.2": "Si vous avez oublié votre mot de passe, {resetLink}",
"account.locked.out.message.2": "Par mesure de sécurité, vous pouvez {resetLink} avant de réessayer.",
"login.incorrect.credentials.error.with.reset.link": "Le nom d'utilisateur, l'adresse courriel ou le mot de passe que vous avez saisis sont incorrects. Veuillez réessayer ou {resetLink}.",
"login.page.title": "Connexion | {siteName}",
"login.user.identity.label": "Nom d'utilisateur ou courriel",
"login.password.label": "Mot de passe",
"sign.in.button": "Connectez-vous",
"forgot.password": "Mot de passe oublié",
"institution.login.button": "Identifiants de l'établissement/du campus",
"institution.login.page.title": "Connectez vous avec les crédentiels 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": "Identifiants 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 2 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": "Succès ! Vous avez confirmé votre courriel.",
"account.confirmation.success.message": "Se connecter pour continuer.",
"account.confirmation.info.message": "Ce courriel a déjà été confirmé.",
"account.confirmation.error.message.title": "Votre courriel ne peut pas être confirmé.",
"tpa.account.link": "{provider} account",
"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 de connexion échouées. Réessayez plus tard.",
"login.failure.header.title": "Nous n'avons pas pu vous connecter.",
"contact.support.link": "veuillez contacter le support {platformName}",
"login.incorrect.credentials.error": "Le nom d'utilisateur, l'adresse courriel ou le mot de passe que vous avez saisis sont incorrects. Veuillez réessayer.",
"login.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": "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": "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": "Envoyez",
"optional.fields.skip.button": "Ignorer pour l'instant",
"optional.fields.next.button": "Next",
"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": "Recommendations | {siteName}",
"recommendation.page.heading": "We have a few recommendations to get you started.",
"recommendation.skip.button": "Skip for now",
"recommendation.option.trending": "Trending Now",
"recommendation.option.popular": "Most Popular",
"recommendation.option.recommended.for.you": "Recommended For You",
"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}",
"registration.fullname.label": "Nom complet",
"registration.email.label": "Email",
"registration.username.label": "Nom d'utilisateur public",
"registration.password.label": "Mot de passe",
"registration.country.label": "Pays/Région",
"registration.opt.in.label": "{siteName} peux m'envoyer des messages de marketing.",
"help.text.name": "Ce nom sera utilisé pour toutes les attestations que vous obtiendrez.",
"help.text.username.1": "Le nom qui vous identifiera dans vos cours.",
"help.text.username.2": "Cela ne peut pas être modifié ultérieurement.",
"help.text.email": "Pour l'activation du compte et les mises à jour importantes",
"create.account.for.free.button": "Créer un compte gratuitement",
"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.page.title": "Inscription avec les crédentiels d'institution ou de campus",
"empty.name.field.error": "Saisissez votre nom complet",
"empty.email.field.error": "Saisissez votre courriel",
"empty.username.field.error": "Le nom d'utilisateur doit comporter entre 2 et 30 caractères",
"empty.password.field.error": "Les critères de mot de passe n'ont pas été remplis",
"empty.country.field.error": "Sélectionnez votre pays ou région de résidence",
"email.do.not.match": "The email addresses do not match.",
"email.invalid.format.error": "Enter a valid email address",
"username.validation.message": "Le nom d'utilisateur doit comporter entre 2 et 30 caractères",
"name.validation.message": "Enter a valid name",
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces",
"registration.request.failure.header": "Nous n'avons pas pu créer votre compte.",
"registration.empty.form.submission.error": "Veuillez vérifier vos réponses et réessayer.",
"registration.request.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
"registration.rate.limit.error": "Trop de tentatives d'inscriptions ont échoué. Réessayez plus tard.",
"registration.tpa.session.expired": "L'inscription avec {provider} a échouée.",
"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",
"privacy.policy": "Politique de confidentialité",
"honor.code": "Code d'honneur",
"terms.of.service": " Conditions d'utilisation",
"registration.username.suggestion.label": "Suggéré :",
"did.you.mean.alert.text": "Vouliez-vous dire",
"sign.in": "Connectez-vous",
"reset.password.page.title": "Réinitialiser le mot de passe | {siteName}",
"reset.password": "Réinitialiser le mot de passe",
"reset.password.page.instructions": "Saisir et confirmer votre nouveau mot de passe.",
"new.password.label": "Nouveau mot de passe",
"confirm.password.label": "Confirmer le mot de passe",
"passwords.do.not.match": "Les mots de passe ne correspondent pas",
"confirm.your.password": "Confirmer votre mot de passe",
"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.",
"start.learning": "Démarrer l'apprentissage",
"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.2": "votre profil",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}

View File

@@ -0,0 +1,181 @@
{
"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.",
"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 :",
"forgot.password.confirmation.message": "Nous avons envoyé un courriel à {email} avec des instructions pour réinitialiser votre mot de passe. Si vous ne recevez pas de message de réinitialisation de mot de passe après 1 minute, vérifiez que vous avez saisi l'adresse courriel correctement, ou vérifiez votre dossier de pourriels. Si vous avez besoin d'aide supplémentaire, contactez {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 de l'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}{lineBreak}Nous venons d'envoyer un lien d'activation à {email}. Si vous ne recevez pas de courriel, 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 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 2 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": "Tendances actuelles",
"recommendation.option.popular": "Le plus populaire",
"recommendation.option.recommended.for.you": "Recommandé pour vous",
"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 via 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",
"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.",
"start.learning": "Commencez à apprendre",
"with.site.name": "avec {siteName}",
"your.career.turning.point": "Un tournant dans votre carrière",
"is.here": "est là.",
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
"complete.your.profile.1": "Complet",
"complete.your.profile.2": "votre profil",
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque membre traitent vos données personnelles conformément au {privacyPolicy}.",
"register.page.honor.code": "J'accepte le {tosAndHonorCode} {platformName}",
"register.page.terms.of.service": "J'accepte les {termsOfService} {platformName}"
}

View File

@@ -0,0 +1 @@
{}

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

@@ -0,0 +1,181 @@
{
"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.",
"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:",
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. 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} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, 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 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 2 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 Now",
"recommendation.option.popular": "Most Popular",
"recommendation.option.recommended.for.you": "Recommended For You",
"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",
"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.",
"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",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}

View File

@@ -0,0 +1 @@
{}

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