Compare commits

...

33 Commits

Author SHA1 Message Date
Omar Al-Ithawi
398330fa07 feat: include paragon in atlas pull (#1145)
This pull request is part of the [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) which is sparked by the [Translation Infrastructure update OEP-58](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0058-arch-translations-management.html#specification).
2023-07-25 11:21:54 -04:00
renovate[bot]
f92fc8c3a5 fix(deps): update dependency @popperjs/core to v2.11.8 2023-07-25 14:01:38 +00:00
renovate[bot]
5e072949d6 chore(deps): update dependency @edx/frontend-build to v12.9.3 2023-07-25 11:25:32 +00:00
Rebecca Graber
2d132f114c feat: upgrade pact (#1141)
Upgrade pact to 11
2023-07-17 12:47:29 -04:00
alangsto
c73ef26d8e feat: add segment event for lti modal launch (#1140) 2023-07-13 13:56:25 -04:00
ayesha waris
97ca7fe6aa fix: sidebar state remains open for all users (#1139) 2023-07-13 15:30:16 +05:00
Ben Warzeski
e95a59c6c8 fix: modal hook name for unit iframe modal signal (#1138) 2023-07-10 15:26:56 -04:00
Peter Kulko
5f9c441cd2 fix: added Paragon translations (#1136) 2023-07-10 14:25:56 -04:00
Ben Warzeski
2e641ac6c9 Bw/unit splitup (#1134)
* refactor: break Unit component into smaller unit-tested parts

* feat: save scroll position on video fullscreen exit

* chore: remove swap file
2023-07-10 10:29:56 -04:00
alangsto
22937918ab feat: add component to iframe LTI launch (#1135) 2023-07-06 14:57:39 -04:00
ihor-romaniuk
714f5d452c fix: save scroll position on exit from video xblock fullscreen mode 2023-07-06 08:43:01 -04:00
ayesha waris
8ac9745261 fix: modifies sidebar state such that it remains open (#1131)
* fix: modifies sidebar state such that it remains open

* refactor: removed localstorage for discussions sideba
2023-06-27 14:05:46 +05:00
Zachary Hancock
340580cb41 chore: update exams lib (#1130) 2023-06-22 09:19:43 -04:00
Leangseu Kim
5a99ca5c91 fix: breadcrumb jump nav styling 2023-06-08 09:19:45 -04:00
Ben Warzeski
9943df49e4 feat: allow clipboard write to xblock iframes (#1117) 2023-06-06 10:09:33 -04:00
Jenkins
855474d406 chore(i18n): update translations 2023-06-04 17:09:52 -04:00
Ghassan Maslamani
a78496a3f6 fix: sync LMS_BASE_URL for bookmark API if changed
This change makes it possible to use the latest  LMS_BASE_API
  if it was changed because of dynamic config API, which is the
  default case of tutor.

  This changes closes openedx/wg-build-test-release/issues/270

   Fixes that are simlar to this
  - gradebook openedx/frontend-app-gradebook/pull/290
  - course authoring openedx/frontend-app-course-authoring/pull/389
2023-05-31 15:11:34 +01:00
Jansen Kantor
79b65dadca fix: gracefully handle 403 responses in tab loading (#1111) 2023-05-24 11:38:40 -04:00
Bilal Qamar
fc8f5d43e8 feat: upgraded to node v18, added .nvmrc and updated workflows (#1084)
* feat: upgraded to node v18, added .nvmrc and updated workflows

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* fix: add workaround in json file

* build: remove jestEnv line from json

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* fix: add workaround in json file

* build: remove jestEnv line from json

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated jest & fixed failing tests

* refactor: updated lmsPact failing test cases

* refactor: updated frontend-build version

* Merge branch master of github.com:edx/frontend-app-learning into bilalqamar95/node-v18-upgrade

---------

Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
2023-05-23 18:39:34 +05:00
Zachary Hancock
6232f40a74 feat: update special-exams-lib (#1113) 2023-05-16 15:08:46 -04:00
David Joy
bc0ff1ce65 chore: bumping frontend-platform version to get userId logging
frontend-platform has a new feature to include the userId (if it exists) when logging an error to the logging service.  We want that.
2023-05-16 09:44:29 -04:00
David Joy
5997b29cee fix: logging an error when unit iframe fails to load
Right now we log nothing to the logging service when a unit iframe fails to load.  The ErrorPage that’s shown isn’t using the ErrorBoundary, so we had no indication that something went wrong.  This solves the problem closer to the source where the error originates.
2023-05-16 09:44:29 -04:00
Omar Al-Ithawi
d2de0632cd feat: add experimental atlas to pull_translations (#1093)
This is an experimental off-by-defualt feature for moving the translation files ouside the repos.

Run `OPENEDX_ATLAS_PULL=true make translations` to use atlas to pull translations instead of transifex.

Refs: FC-12 OEP-58
2023-05-09 10:03:42 -04:00
Zachary Hancock
922cc2187a fix: update exams lib to fix download click bug (#1110) 2023-05-09 09:23:40 -04:00
Sagirov Eugeniy
d9539796b5 chore: update frontend-platform version to v4.2.0 2023-05-02 14:34:30 -03:00
alangsto
e0acb501eb chore: upgrade frontend-lib-special-exams version (#1107) 2023-04-26 08:52:03 -04:00
Asad Ali
a03ffe2724 fix: fix links under contenttools (#1096) 2023-04-26 13:42:16 +05:00
Jenkins
cbdf7ce064 chore(i18n): update translations 2023-04-23 17:09:46 -04:00
Zachary Hancock
7184e85b2b feat: update exams library (#1103) 2023-04-21 12:09:29 -04:00
Emad Rad
b5321d01e4 feat: Persian Language added to messages (#989)
feat: fa_IR added to transifex_langs

feat: Persian translations added

Co-authored-by: Leangseu Kim <lkim@edx.org>
Co-authored-by: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com>
2023-04-18 14:03:06 -04:00
Yoiber
6c8ab1a4c9 chore(i18n): add more languages (#1063)
* chore(i18n): add more languages

* chore(i18n): Pylint fixed
2023-04-18 12:41:30 -04:00
Varsha
01f9d8f50b feat: fetch exam access token (#1083)
* feat: fetch exam access token

* build: update frontend lib special exams version
2023-04-17 14:59:05 -04:00
Zachary Hancock
764befd4bd feat: exams svc should not be enabled by default (#1100) 2023-04-12 10:36:21 -04:00
65 changed files with 16311 additions and 32136 deletions

View File

@@ -15,7 +15,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
EXAMS_BASE_URL='http://localhost:18740'
EXAMS_BASE_URL=''
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=''
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'

View File

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

View File

@@ -9,14 +9,13 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
steps:
- uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
node-version: ${{ env.NODE_VER }}
- run: make validate.ci
- name: Upload coverage
uses: codecov/codecov-action@v3

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.DS_Store
.eslintcache
.idea
*.swp
*.swo
node_modules
npm-debug.log
coverage

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18

View File

@@ -1,6 +1,7 @@
export TRANSIFEX_RESOURCE=frontend-app-learning
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fa_IR,fr_CA,it_IT,pt_PT,de_DE"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
@@ -42,9 +43,24 @@ push_translations:
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-learning
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

42777
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,17 +30,18 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "11.6.3",
"@edx/frontend-component-header": "3.6.4",
"@edx/frontend-lib-special-exams": "2.10.0",
"@edx/frontend-platform": "3.4.1",
"@edx/frontend-component-footer": "12.0.0",
"@edx/frontend-component-header": "4.0.0",
"@edx/frontend-lib-special-exams": "2.19.1",
"@edx/frontend-platform": "4.3.0",
"@edx/paragon": "20.28.4",
"@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.5.3",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.1.18",
"@popperjs/core": "2.11.6",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.8.1",
"classnames": "2.3.2",
"core-js": "3.22.2",
@@ -48,7 +49,7 @@
"js-cookie": "3.0.1",
"lodash.camelcase": "4.3.0",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"query-string": "^7.1.3",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "6.1.0",
@@ -64,9 +65,9 @@
},
"devDependencies": {
"@edx/browserslist-config": "1.1.1",
"@edx/frontend-build": "^12.4.15",
"@edx/frontend-build": "^12.8.27",
"@edx/reactifex": "2.1.1",
"@pact-foundation/pact": "9.17.3",
"@pact-foundation/pact": "^11.0.2",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "13.5.0",
@@ -74,7 +75,7 @@
"copy-webpack-plugin": "^11.0.0",
"es-check": "6.2.1",
"husky": "7.0.4",
"jest": "27.5.1",
"jest": "29.5.0",
"rosie": "2.1.0"
}
}

View File

@@ -28,6 +28,7 @@ Factory.define('outlineTabData')
upgrade_url: `${host}/dashboard`,
}))
.attrs({
course_access_redirect: false,
has_scheduled_content: null,
access_expiration: null,
can_show_upgrade_sock: false,

View File

@@ -204,12 +204,18 @@ export async function getDatesTabData(courseId) {
const { data } = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(data);
} catch (error) {
const { httpErrorStatus } = error && error.customAttributes;
const httpErrorStatus = error?.response?.status;
if (httpErrorStatus === 401) {
// The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
// courseAccess in the metadata call, so just ignore this status for now.
return {};
}
if (httpErrorStatus === 403) {
// The backend sends this if there is a course access error and the user should be redirected. The redirect
// info is included in the course metadata request and will be handled there as long as this call returns
// without an error
return {};
}
throw error;
}
}
@@ -259,7 +265,7 @@ export async function getProgressTabData(courseId, targetUserId) {
return camelCasedData;
} catch (error) {
const { httpErrorStatus } = error && error.customAttributes;
const httpErrorStatus = error?.response?.status;
if (httpErrorStatus === 404) {
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/progress`);
return {};
@@ -269,6 +275,12 @@ export async function getProgressTabData(courseId, targetUserId) {
// courseAccess in the metadata call, so just ignore this status for now.
return {};
}
if (httpErrorStatus === 403) {
// The backend sends this if there is a course access error and the user should be redirected. The redirect
// info is included in the course metadata request and will be handled there as long as this call returns
// without an error
return {};
}
throw error;
}
}
@@ -322,7 +334,20 @@ export function getTimeOffsetMillis(headerDate, requestTime, responseTime) {
export async function getOutlineTabData(courseId) {
const url = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
const requestTime = Date.now();
const tabData = await getAuthenticatedHttpClient().get(url);
let tabData;
try {
tabData = await getAuthenticatedHttpClient().get(url);
} catch (error) {
const httpErrorStatus = error?.response?.status;
if (httpErrorStatus === 403) {
// The backend sends this if there is a course access error and the user should be redirected. The redirect
// info is included in the course metadata request and will be handled there as long as this call returns
// without an error
return {};
}
throw error;
}
const responseTime = Date.now();
const {

View File

@@ -1,6 +1,6 @@
import { Pact, Matchers } from '@pact-foundation/pact';
import path from 'path';
import { mergeConfig, getConfig } from '@edx/frontend-platform';
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import {
getCourseHomeCourseMetadata,
@@ -14,8 +14,8 @@ import {
const {
somethingLike: like, term, boolean, string, eachLike,
} = Matchers;
const provider = new Pact({
} = MatchersV3;
const provider = new PactV3({
consumer: 'frontend-app-learning',
provider: 'lms',
log: path.resolve(process.cwd(), 'src/course-home/data/pact-tests/logs', 'pact.log'),
@@ -28,194 +28,193 @@ const provider = new Pact({
describe('Course Home Service', () => {
beforeAll(async () => {
initializeMockApp();
await provider
.setup()
.then((options) => mergeConfig({
LMS_BASE_URL: `http://localhost:${options.port}`,
}, 'Custom app config for pact tests'));
mergeConfig({
LMS_BASE_URL: 'http://localhost:8081',
}, 'Custom app config for pact tests');
});
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
describe('When a request to fetch tab is made', () => {
it('returns tab data for a course_id', async () => {
await provider.addInteraction({
state: `Tab data exists for course_id ${courseId}`,
uponReceiving: 'a request to fetch tab',
withRequest: {
method: 'GET',
path: `/api/course_home/course_metadata/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
can_show_upgrade_sock: boolean(false),
verified_mode: like({
access_expiration_date: null,
currency: 'USD',
currency_symbol: '$',
price: 149,
sku: '8CF08E5',
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
}),
celebrations: like({
first_section: false,
streak_length_to_celebrate: null,
streak_discount_enabled: false,
}),
course_access: {
has_access: boolean(true),
error_code: null,
developer_message: null,
user_message: null,
additional_context_user_message: null,
user_fragment: null,
setTimeout(() => {
provider.addInteraction({
state: `Tab data exists for course_id ${courseId}`,
uponReceiving: 'a request to fetch tab',
withRequest: {
method: 'GET',
path: `/api/course_home/course_metadata/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
can_show_upgrade_sock: boolean(false),
verified_mode: like({
access_expiration_date: null,
currency: 'USD',
currency_symbol: '$',
price: 149,
sku: '8CF08E5',
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
}),
celebrations: like({
first_section: false,
streak_length_to_celebrate: null,
streak_discount_enabled: false,
}),
course_access: {
has_access: boolean(true),
error_code: null,
developer_message: null,
user_message: null,
additional_context_user_message: null,
user_fragment: null,
},
course_id: term({
generate: 'course-v1:edX+DemoX+Demo_Course',
matcher: opaqueKeysRegex,
}),
is_enrolled: boolean(true),
is_self_paced: boolean(false),
is_staff: boolean(true),
number: string('DemoX'),
org: string('edX'),
original_user_is_staff: boolean(true),
start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
tabs: eachLike({
tab_id: 'courseware',
title: 'Course',
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
}),
title: string('Demonstration Course'),
username: string('edx'),
},
course_id: term({
generate: 'course-v1:edX+DemoX+Demo_Course',
matcher: opaqueKeysRegex,
}),
is_enrolled: boolean(true),
is_self_paced: boolean(false),
is_staff: boolean(true),
number: string('DemoX'),
org: string('edX'),
original_user_is_staff: boolean(true),
start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
tabs: eachLike({
tab_id: 'courseware',
},
});
const normalizedTabData = {
canShowUpgradeSock: false,
verifiedMode: {
accessExpirationDate: null,
currency: 'USD',
currencySymbol: '$',
price: 149,
sku: '8CF08E5',
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
},
celebrations: {
firstSection: false,
streakLengthToCelebrate: null,
streakDiscountEnabled: false,
},
courseAccess: {
hasAccess: true,
errorCode: null,
developerMessage: null,
userMessage: null,
additionalContextUserMessage: null,
userFragment: null,
},
courseId: 'course-v1:edX+DemoX+Demo_Course',
isEnrolled: true,
isMasquerading: false,
isSelfPaced: false,
isStaff: true,
number: 'DemoX',
org: 'edX',
originalUserIsStaff: true,
start: '2013-02-05T05:00:00Z',
tabs: [
{
slug: 'outline',
title: 'Course',
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
}),
title: string('Demonstration Course'),
username: string('edx'),
},
},
});
const normalizedTabData = {
canShowUpgradeSock: false,
verifiedMode: {
accessExpirationDate: null,
currency: 'USD',
currencySymbol: '$',
price: 149,
sku: '8CF08E5',
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
},
celebrations: {
firstSection: false,
streakLengthToCelebrate: null,
streakDiscountEnabled: false,
},
courseAccess: {
hasAccess: true,
errorCode: null,
developerMessage: null,
userMessage: null,
additionalContextUserMessage: null,
userFragment: null,
},
courseId: 'course-v1:edX+DemoX+Demo_Course',
isEnrolled: true,
isMasquerading: false,
isSelfPaced: false,
isStaff: true,
number: 'DemoX',
org: 'edX',
originalUserIsStaff: true,
start: '2013-02-05T05:00:00Z',
tabs: [
{
slug: 'outline',
title: 'Course',
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
},
],
title: 'Demonstration Course',
username: 'edx',
};
const response = await getCourseHomeCourseMetadata(courseId, 'outline');
expect(response).toBeTruthy();
expect(response).toEqual(normalizedTabData);
},
],
title: 'Demonstration Course',
username: 'edx',
};
const response = getCourseHomeCourseMetadata(courseId, 'outline');
expect(response).toBeTruthy();
expect(response).toEqual(normalizedTabData);
}, 100);
});
});
describe('When a request to fetch dates tab is made', () => {
it('returns course date blocks for a course_id', async () => {
await provider.addInteraction({
state: `course date blocks exist for course_id ${courseId}`,
uponReceiving: 'a request to fetch dates tab',
withRequest: {
method: 'GET',
path: `/api/course_home/dates/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
dates_banner_info: like({
missed_deadlines: false,
content_type_gating_enabled: false,
missed_gated_content: false,
verified_upgrade_link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
}),
course_date_blocks: eachLike({
assignment_type: null,
setTimeout(() => {
provider.addInteraction({
state: `course date blocks exist for course_id ${courseId}`,
uponReceiving: 'a request to fetch dates tab',
withRequest: {
method: 'GET',
path: `/api/course_home/dates/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
dates_banner_info: like({
missed_deadlines: false,
content_type_gating_enabled: false,
missed_gated_content: false,
verified_upgrade_link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
}),
course_date_blocks: eachLike({
assignment_type: null,
complete: null,
date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
date_type: term({
generate: 'verified-upgrade-deadline',
matcher: dateTypeRegex,
}),
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
learner_has_access: true,
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
link_text: 'Upgrade to Verified Certificate',
title: 'Verification Upgrade Deadline',
extra_info: null,
first_component_block_id: '',
}),
has_ended: boolean(false),
learner_is_full_access: boolean(true),
user_timezone: null,
},
},
});
const camelCaseResponse = {
datesBannerInfo: {
missedDeadlines: false,
contentTypeGatingEnabled: false,
missedGatedContent: false,
verifiedUpgradeLink: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
},
courseDateBlocks: [
{
assignmentType: null,
complete: null,
date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
date_type: term({
generate: 'verified-upgrade-deadline',
matcher: dateTypeRegex,
}),
date: '2013-02-05T05:00:00Z',
dateType: 'verified-upgrade-deadline',
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
learner_has_access: true,
learnerHasAccess: true,
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
link_text: 'Upgrade to Verified Certificate',
linkText: 'Upgrade to Verified Certificate',
title: 'Verification Upgrade Deadline',
extra_info: null,
first_component_block_id: '',
}),
has_ended: boolean(false),
learner_is_full_access: boolean(true),
user_timezone: null,
},
},
});
const camelCaseResponse = {
datesBannerInfo: {
missedDeadlines: false,
contentTypeGatingEnabled: false,
missedGatedContent: false,
verifiedUpgradeLink: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
},
courseDateBlocks: [
{
assignmentType: null,
complete: null,
date: '2013-02-05T05:00:00Z',
dateType: 'verified-upgrade-deadline',
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
learnerHasAccess: true,
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
linkText: 'Upgrade to Verified Certificate',
title: 'Verification Upgrade Deadline',
extraInfo: null,
firstComponentBlockId: '',
},
],
hasEnded: false,
learnerIsFullAccess: true,
userTimezone: null,
};
const response = await getDatesTabData(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse);
extraInfo: null,
firstComponentBlockId: '',
},
],
hasEnded: false,
learnerIsFullAccess: true,
userTimezone: null,
};
const response = getDatesTabData(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse);
}, 100);
});
});
});

View File

@@ -21,6 +21,18 @@ describe('Data layer integration tests', () => {
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
const courseHomeAccessDeniedMetadata = Factory.build(
'courseHomeMetadata',
{
id: courseId,
course_access: {
has_access: false,
error_code: 'bad codes',
additional_context_user_message: 'your Codes Are BAD',
},
},
);
let store;
beforeEach(() => {
@@ -57,14 +69,31 @@ describe('Data layer integration tests', () => {
expect(state.courseHome.courseStatus).toEqual('loaded');
expect(state).toMatchSnapshot();
});
it.each([401, 403, 404])(
'should result in fetch denied for expected errors and failed for all others',
async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(`${datesBaseUrl}/${courseId}`).reply(errorStatus, {});
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
let expectedState = 'failed';
if (errorStatus === 401 || errorStatus === 403) {
expectedState = 'denied';
}
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
},
);
});
describe('Test fetchOutlineTab', () => {
const outlineBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline`;
const outlineUrl = `${outlineBaseUrl}/${courseId}`;
it('Should result in fetch failure if error occurs', async () => {
axiosMock.onGet(courseMetadataUrl).networkError();
axiosMock.onGet(`${outlineBaseUrl}/${courseId}`).networkError();
axiosMock.onGet(outlineUrl).networkError();
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
@@ -75,8 +104,6 @@ describe('Data layer integration tests', () => {
it('Should fetch, normalize, and save metadata', async () => {
const outlineTabData = Factory.build('outlineTabData', { courseId });
const outlineUrl = `${outlineBaseUrl}/${courseId}`;
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
@@ -86,6 +113,22 @@ describe('Data layer integration tests', () => {
expect(state.courseHome.courseStatus).toEqual('loaded');
expect(state).toMatchSnapshot();
});
it.each([401, 403, 404])(
'should result in fetch denied for expected errors and failed for all others',
async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(outlineUrl).reply(errorStatus, {});
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
let expectedState = 'failed';
if (errorStatus === 403) {
expectedState = 'denied';
}
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
},
);
});
describe('Test fetchProgressTab', () => {
@@ -129,6 +172,19 @@ describe('Data layer integration tests', () => {
const state = store.getState();
expect(state.courseHome.targetUserId).toEqual(2);
});
it.each([401, 403, 404])(
'should result in fetch denied for expected errors and failed for all others',
async (errorStatus) => {
const progressUrl = `${progressBaseUrl}/${courseId}`;
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(progressUrl).reply(errorStatus, {});
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
expect(store.getState().courseHome.courseStatus).toEqual('denied');
},
);
});
describe('Test saveCourseGoal', () => {

View File

@@ -119,11 +119,11 @@ describe('Outline Tab', () => {
// Click to expand section
userEvent.click(expandButton);
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true');
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
// Click to collapse section
userEvent.click(expandButton);
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
});
it('displays correct icon for complete assignment', async () => {

View File

@@ -11,7 +11,7 @@ import MockAdapter from 'axios-mock-adapter';
import { UserMessagesProvider } from '../generic/user-messages';
import tabMessages from '../tab-page/messages';
import { initializeMockApp } from '../setupTest';
import { initializeMockApp, waitFor } from '../setupTest';
import CoursewareContainer from './CoursewareContainer';
import { buildSimpleCourseBlocks, buildBinaryCourseBlocks } from '../shared/data/__factories__/courseBlocks.factory';
@@ -211,7 +211,7 @@ describe('CoursewareContainer', () => {
});
history.push(`/course/${courseId}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -234,7 +234,7 @@ describe('CoursewareContainer', () => {
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {});
history.push(`/course/${courseId}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -284,7 +284,7 @@ describe('CoursewareContainer', () => {
describe('when the URL does not contain a unit ID', () => {
it('should choose a unit within the section\'s first sequence', async () => {
setUrl(sectionTree[1].id);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container, 2);
assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id);
@@ -359,7 +359,7 @@ describe('CoursewareContainer', () => {
it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => {
history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -378,7 +378,7 @@ describe('CoursewareContainer', () => {
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -395,7 +395,7 @@ describe('CoursewareContainer', () => {
it('should load the specified unit', async () => {
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -411,7 +411,7 @@ describe('CoursewareContainer', () => {
});
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation button');
const sequenceNextButton = sequenceNavButtons[4];

View File

@@ -14,6 +14,7 @@ import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SidebarProvider from './sidebar/SidebarContextProvider';
import SidebarTriggers from './sidebar/SidebarTriggers';
import ChatTrigger from './lti-modal/ChatTrigger';
import { useModel } from '../../generic/model-store';
import { getSessionStorage, setSessionStorage } from '../../data/sessionStorage';
@@ -91,7 +92,15 @@ const Course = ({
unitId={unitId}
/>
{shouldDisplayTriggers && (
<SidebarTriggers />
<>
<ChatTrigger
enrollmentMode={course.enrollmentMode}
isStaff={isStaff}
launchUrl={course.learningAssistantLaunchUrl}
courseId={courseId}
/>
<SidebarTriggers />
</>
)}
</div>

View File

@@ -49,8 +49,7 @@ describe('Course', () => {
setItemSpy.mockRestore();
});
const setupDiscussionSidebar = async (storageValue = false) => {
localStorage.clear();
const setupDiscussionSidebar = async () => {
const testStore = await initializeTestStore({ provider: 'openedx' });
const state = testStore.getState();
const { courseware: { courseId } } = state;
@@ -65,9 +64,7 @@ describe('Course', () => {
mockData.unitId = firstUnitId;
const [firstSequenceId] = Object.keys(state.models.sequences);
mockData.sequenceId = firstSequenceId;
if (storageValue !== null) {
localStorage.setItem('showDiscussionSidebar', storageValue);
}
await render(<Course {...mockData} />, { store: testStore });
};
@@ -131,26 +128,66 @@ describe('Course', () => {
});
it('displays notification trigger and toggles active class on click', async () => {
localStorage.setItem('showDiscussionSidebar', false);
render(<Course {...mockData} />);
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
expect(notificationTrigger).toBeInTheDocument();
expect(notificationTrigger.parentNode).toHaveClass('border-primary-700');
expect(notificationTrigger.parentNode).toHaveClass('mt-3');
fireEvent.click(notificationTrigger);
expect(notificationTrigger.parentNode).not.toHaveClass('border-primary-700');
expect(notificationTrigger.parentNode).not.toHaveClass('mt-3', { exact: true });
});
it('handles click to open/close discussions sidebar', async () => {
await setupDiscussionSidebar();
const discussionsTrigger = await screen.getByRole('button', { name: /Show discussions tray/i });
const discussionsSideBar = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS'));
expect(discussionsSideBar).not.toHaveClass('d-none');
await act(async () => {
fireEvent.click(discussionsTrigger);
});
await expect(discussionsSideBar).toHaveClass('d-none');
await act(async () => {
fireEvent.click(discussionsTrigger);
});
await expect(discussionsSideBar).not.toHaveClass('d-none');
});
it('displays discussions sidebar when unit changes', async () => {
const testStore = await initializeTestStore();
const { courseware, models } = testStore.getState();
const { courseId, sequenceId } = courseware;
const testData = {
...mockData,
courseId,
sequenceId,
unitId: Object.values(models.units)[0].id,
};
await setupDiscussionSidebar();
const { rerender } = render(<Course {...testData} />, { store: testStore });
loadUnit();
await waitFor(() => {
expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
});
rerender(null);
});
it('handles click to open/close notification tray', async () => {
sessionStorage.clear();
localStorage.setItem('showDiscussionSidebar', false);
render(<Course {...mockData} />);
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"');
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
fireEvent.click(notificationShowButton);
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"');
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
});
it('handles reload persisting notification tray status', async () => {
@@ -174,7 +211,6 @@ describe('Course', () => {
it('handles sessionStorage from a different course for the notification tray', async () => {
sessionStorage.clear();
localStorage.setItem('showDiscussionSidebar', false);
const courseMetadataSecondCourse = Factory.build('courseMetadata', { id: 'second_course' });
// set sessionStorage for a different course before rendering Course
@@ -217,34 +253,6 @@ describe('Course', () => {
expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
});
[
{ value: true, visible: true },
{ value: false, visible: false },
{ value: null, visible: true },
].forEach(async ({ value, visible }) => (
it(`discussion sidebar is ${visible ? 'shown' : 'hidden'} when localstorage value is ${value}`, async () => {
await setupDiscussionSidebar(value);
const element = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS'));
if (visible) {
expect(element).not.toHaveClass('d-none');
} else {
expect(element).toHaveClass('d-none');
}
})));
[
{ value: true, result: 'false' },
{ value: false, result: 'true' },
].forEach(async ({ value, result }) => (
it(`Discussion sidebar storage value is ${!value} when sidebar is ${value ? 'closed' : 'open'}`, async () => {
await setupDiscussionSidebar(value);
await act(async () => {
const button = await screen.queryByRole('button', { name: /Show discussions tray/i });
button.click();
});
expect(localStorage.getItem('showDiscussionSidebar')).toBe(result);
})));
it('passes handlers to the sequence', async () => {
const nextSequenceHandler = jest.fn();
const previousSequenceHandler = jest.fn();

View File

@@ -1,57 +1,80 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHome } from '@fortawesome/free-solid-svg-icons';
import { useSelector } from 'react-redux';
import { SelectMenu } from '@edx/paragon';
import { useToggle, ModalPopup, Menu } from '@edx/paragon';
import { Link } from 'react-router-dom';
import { useModel, useModels } from '../../generic/model-store';
import JumpNavMenuItem from './JumpNavMenuItem';
const CourseBreadcrumb = ({
content, withSeparator, courseId, sequenceId, unitId, isStaff,
content,
withSeparator,
courseId,
sequenceId,
unitId,
isStaff,
}) => {
const defaultContent = content.filter(destination => destination.default)[0] || { id: courseId, label: '', sequences: [] };
const defaultContent = content.filter(
(destination) => destination.default,
)[0] || { id: courseId, label: '', sequences: [] };
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);
return (
<>
{withSeparator && (
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>/</li>
)}
<li style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
<li
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
data-testid="breadcrumb-item"
>
{ getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff
? (
<Link
className="text-primary-500"
to={defaultContent.sequences.length
{showRegularLink ? (
<Link
className="text-primary-500"
to={
defaultContent.sequences.length
? `/course/${courseId}/${defaultContent.sequences[0].id}`
: `/course/${courseId}/${defaultContent.id}`}
>
{defaultContent.label}
</Link>
)
: (
<SelectMenu isLink defaultMessage={defaultContent.label}>
{content.map(item => (
<JumpNavMenuItem
isDefault={item.default}
sequences={item.sequences}
courseId={courseId}
title={item.label}
currentSequence={sequenceId}
currentUnit={unitId}
/>
))}
</SelectMenu>
)}
: `/course/${courseId}/${defaultContent.id}`
}
>
{defaultContent.label}
</Link>
) : (
<>
{
// eslint-disable-next-line
<a className="text-primary-500" onClick={open} ref={setTarget}>
{defaultContent.label}
</a>
}
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
<Menu>
{content.map((item) => (
<JumpNavMenuItem
isDefault={item.default}
sequences={item.sequences}
courseId={courseId}
title={item.label}
currentSequence={sequenceId}
currentUnit={unitId}
onClick={close}
/>
))}
</Menu>
</ModalPopup>
</>
)}
</li>
</>
);
@@ -87,14 +110,21 @@ const CourseBreadcrumbs = ({
isStaff,
}) => {
const course = useModel('coursewareMeta', courseId);
const courseStatus = useSelector(state => state.courseware.courseStatus);
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
const courseStatus = useSelector((state) => state.courseware.courseStatus);
const sequenceStatus = useSelector(
(state) => state.courseware.sequenceStatus,
);
const allSequencesInSections = Object.fromEntries(useModels('sections', course.sectionIds).map(section => [section.id, {
default: section.id === sectionId,
title: section.title,
sequences: useModels('sequences', section.sequenceIds),
}]));
const allSequencesInSections = Object.fromEntries(
useModels('sections', course.sectionIds).map((section) => [
section.id,
{
default: section.id === sectionId,
title: section.title,
sequences: useModels('sequences', section.sequenceIds),
},
]),
);
const links = useMemo(() => {
const chapters = [];
@@ -108,7 +138,7 @@ const CourseBreadcrumbs = ({
sequences: section.sequences,
});
if (section.default) {
section.sequences.forEach(sequence => {
section.sequences.forEach((sequence) => {
sequentials.push({
id: sequence.id,
label: sequence.title,
@@ -124,7 +154,7 @@ const CourseBreadcrumbs = ({
return (
<nav aria-label="breadcrumb" className="my-4 d-inline-block col-sm-10">
<ol className="list-unstyled d-flex flex-nowrap align-items-center m-0">
<ol className="list-unstyled d-flex flex-nowrap align-items-center m-0">
<li className="list-unstyled col-auto m-0 p-0">
<Link
className="flex-shrink-0 text-primary"
@@ -138,7 +168,7 @@ const CourseBreadcrumbs = ({
/>
</Link>
</li>
{links.map(content => (
{links.map((content) => (
<CourseBreadcrumb
courseId={courseId}
sequenceId={sequenceId}

View File

@@ -123,6 +123,6 @@ describe('CourseBreadcrumbs', () => {
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByRole('button')).toHaveLength(2);
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
});
});

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { history } from '@edx/frontend-platform';
import { MenuItem } from '@edx/paragon';
import { Dropdown } from '@edx/paragon';
import {
sendTrackingLogEvent,
@@ -15,6 +15,7 @@ const JumpNavMenuItem = ({
currentUnit,
sequences,
isDefault,
onClick,
}) => {
function logEvent(targetUrl) {
const eventName = 'edx.ui.lms.jump_nav.selected';
@@ -34,19 +35,20 @@ const JumpNavMenuItem = ({
}
return `/course/${courseId}/${sequences[0].id}`;
}
function handleClick() {
function handleClick(e) {
const url = destinationUrl();
logEvent(url);
history.push(url);
if (onClick) { onClick(e); }
}
return (
<MenuItem
defaultSelected={isDefault}
onClick={() => handleClick()}
<Dropdown.Item
active={isDefault}
onClick={e => handleClick(e)}
>
{title}
</MenuItem>
</Dropdown.Item>
);
};
@@ -54,6 +56,10 @@ const sequenceShape = PropTypes.shape({
id: PropTypes.string.isRequired,
});
JumpNavMenuItem.defaultProps = {
onClick: null,
};
JumpNavMenuItem.propTypes = {
title: PropTypes.string.isRequired,
sequences: PropTypes.arrayOf(sequenceShape).isRequired,
@@ -61,6 +67,7 @@ JumpNavMenuItem.propTypes = {
courseId: PropTypes.string.isRequired,
currentSequence: PropTypes.string.isRequired,
currentUnit: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
export default JumpNavMenuItem;

View File

@@ -22,6 +22,7 @@ const mockData = {
},
],
isDefault: false,
onClick: jest.fn().mockName('onClick'),
};
describe('JumpNavMenuItem', () => {
render(

View File

@@ -1,12 +1,12 @@
import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { Factory } from 'rosie';
import {
render, screen, fireEvent, initializeTestStore, waitFor, authenticatedUser, logUnhandledRequests,
} from '../../../setupTest';
import { BookmarkButton } from './index';
import { getBookmarksBaseUrl } from './data/api';
describe('Bookmark Button', () => {
let axiosMock;
@@ -32,7 +32,8 @@ describe('Bookmark Button', () => {
mockData.unitId = nonBookmarkedUnitBlock.id;
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const bookmarkUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
const bookmarkUrl = getBookmarksBaseUrl();
axiosMock.onPost(bookmarkUrl).reply(200, { });
const bookmarkDeleteUrlRegExp = new RegExp(`${bookmarkUrl}*,*`);

View File

@@ -1,13 +1,13 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
const bookmarksBaseUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
export const getBookmarksBaseUrl = () => `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
export async function createBookmark(usageId) {
return getAuthenticatedHttpClient().post(bookmarksBaseUrl, { usage_id: usageId });
return getAuthenticatedHttpClient().post(getBookmarksBaseUrl(), { usage_id: usageId });
}
export async function deleteBookmark(usageId) {
const { username } = getAuthenticatedUser();
return getAuthenticatedHttpClient().delete(`${bookmarksBaseUrl}${username},${usageId}/`);
return getAuthenticatedHttpClient().delete(`${getBookmarksBaseUrl()}${username},${usageId}/`);
}

View File

@@ -1,6 +1,5 @@
.content-tools {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;

View File

@@ -0,0 +1,133 @@
import React, { useState } from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
ModalDialog,
Icon,
useToggle,
OverlayTrigger,
Popover,
} from '@edx/paragon';
import { ChatBubbleOutline } from '@edx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import messages from './messages';
const ChatTrigger = ({
intl,
enrollmentMode,
isStaff,
launchUrl,
courseId,
}) => {
const [isOpen, open, close] = useToggle(false);
const [hasOpenedChat, setHasOpenedChat] = useState(false);
const { userId } = getAuthenticatedUser();
const VERIFIED_MODES = [
'professional',
'verified',
'no-id-professional',
'credit',
'masters',
'executive-education',
];
const isVerifiedEnrollmentMode = (
enrollmentMode !== null
&& enrollmentMode !== undefined
&& VERIFIED_MODES.some(mode => mode === enrollmentMode)
);
const shouldDisplayChat = (
launchUrl
&& (isVerifiedEnrollmentMode || isStaff) // display only to non-audit or staff
);
const handleOpen = () => {
if (!hasOpenedChat) {
setHasOpenedChat(true);
}
open();
sendTrackEvent('edx.ui.lms.lti_modal.opened', {
course_id: courseId,
user_id: userId,
is_staff: isStaff,
});
};
return (
<>
{shouldDisplayChat && (
<div
className={classNames('mt-3', 'd-flex', 'ml-auto')}
>
<OverlayTrigger
trigger="click"
key="top"
show={!hasOpenedChat}
overlay={(
<Popover id="popover-chat-information">
<Popover.Title as="h3">{intl.formatMessage(messages.popoverTitle)}</Popover.Title>
<Popover.Content>
{intl.formatMessage(messages.popoverContent)}
</Popover.Content>
</Popover>
)}
>
<button
className="border border-light-400 bg-transparent align-items-center align-content-center d-flex"
type="button"
onClick={handleOpen}
aria-label={intl.formatMessage(messages.openChatModalTrigger)}
>
<div className="icon-container d-flex position-relative align-items-center">
<Icon src={ChatBubbleOutline} className="m-0 m-auto" />
</div>
</button>
</OverlayTrigger>
<ModalDialog
onClose={close}
isOpen={isOpen}
title={intl.formatMessage(messages.modalTitle)}
size="xl"
hasCloseButton
>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages.modalTitle)}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<iframe
src={launchUrl}
allowFullScreen
style={{
width: '100%',
height: '60vh',
}}
title={intl.formatMessage(messages.modalTitle)}
/>
</ModalDialog.Body>
</ModalDialog>
</div>
)}
</>
);
};
ChatTrigger.propTypes = {
intl: intlShape.isRequired,
isStaff: PropTypes.bool.isRequired,
enrollmentMode: PropTypes.string,
launchUrl: PropTypes.string,
courseId: PropTypes.string.isRequired,
};
ChatTrigger.defaultProps = {
launchUrl: null,
enrollmentMode: null,
};
export default injectIntl(ChatTrigger);

View File

@@ -0,0 +1,88 @@
import { render } from '@testing-library/react';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { BrowserRouter } from 'react-router-dom';
import React from 'react';
import ChatTrigger from './ChatTrigger';
import { act, fireEvent, screen } from '../../../setupTest';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({ userId: 1 })),
}));
jest.mock('@edx/frontend-platform/analytics');
describe('ChatTrigger', () => {
it('handles click to open/close chat modal', async () => {
sendTrackEvent.mockClear();
render(
<IntlProvider>
<BrowserRouter>
<ChatTrigger
enrollmentMode={null}
isStaff
launchUrl="https://testurl.org"
courseId="course-edX"
/>
</BrowserRouter>,
</IntlProvider>,
);
const chatTrigger = screen.getByRole('button', { name: /Show chat modal/i });
expect(chatTrigger).toBeInTheDocument();
expect(screen.queryByText('Need help understanding course content?')).toBeInTheDocument();
await act(async () => {
fireEvent.click(chatTrigger);
});
const modalCloseButton = screen.getByRole('button', { name: /Close/i });
await expect(modalCloseButton).toBeInTheDocument();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.lti_modal.opened', {
course_id: 'course-edX',
user_id: 1,
is_staff: true,
});
await act(async () => {
fireEvent.click(modalCloseButton);
});
await expect(modalCloseButton).not.toBeInTheDocument();
expect(screen.queryByText('Need help understanding course content?')).not.toBeInTheDocument();
});
const testCases = [
{ enrollmentMode: null, isVisible: false },
{ enrollmentMode: undefined, isVisible: false },
{ enrollmentMode: 'audit', isVisible: false },
{ enrollmentMode: 'xyz', isVisible: false },
{ enrollmentMode: 'professional', isVisible: true },
{ enrollmentMode: 'verified', isVisible: true },
{ enrollmentMode: 'no-id-professional', isVisible: true },
{ enrollmentMode: 'credit', isVisible: true },
{ enrollmentMode: 'masters', isVisible: true },
{ enrollmentMode: 'executive-education', isVisible: true },
];
testCases.forEach(test => {
it(`does chat to be visible based on enrollment mode of ${test.enrollmentMode}`, async () => {
render(
<IntlProvider>
<BrowserRouter>
<ChatTrigger
enrollmentMode={test.enrollmentMode}
isStaff={false}
launchUrl="https://testurl.org"
/>
</BrowserRouter>,
</IntlProvider>,
);
const chatTrigger = screen.queryByRole('button', { name: /Show chat modal/i });
if (test.isVisible) {
expect(chatTrigger).toBeInTheDocument();
} else {
expect(chatTrigger).not.toBeInTheDocument();
}
});
});
});

View File

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

View File

@@ -0,0 +1,26 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
popoverTitle: {
id: 'popover.title',
defaultMessage: 'Need help understanding course content?',
description: 'Title for popover alerting user of chat modal',
},
popoverContent: {
id: 'popover.content',
defaultMessage: 'Click here for your Xpert Learning Assistant.',
description: 'Content of the popover message',
},
openChatModalTrigger: {
id: 'chat.model.trigger',
defaultMessage: 'Show chat modal',
description: 'Alt text for button that opens the chat modal',
},
modalTitle: {
id: 'chat.model.title',
defaultMessage: 'Xpert Learning Assistant',
description: 'Title for chat modal header',
},
});
export default messages;

View File

@@ -1,257 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { AppContext, ErrorPage } from '@edx/frontend-platform/react';
import { Modal } from '@edx/paragon';
import PropTypes from 'prop-types';
import React, {
Suspense, useCallback, useContext, useEffect, useLayoutEffect, useState,
} from 'react';
import { useDispatch } from 'react-redux';
import { processEvent } from '../../../course-home/data/thunks';
import { useEventListener } from '../../../generic/hooks';
import { useModel } from '../../../generic/model-store';
import PageLoading from '../../../generic/PageLoading';
import { fetchCourse } from '../../data';
import BookmarkButton from '../bookmark/BookmarkButton';
import ShareButton from '../share/ShareButton';
import messages from './messages';
const HonorCode = React.lazy(() => import('./honor-code'));
const LockPaywall = React.lazy(() => import('./lock-paywall'));
/**
* Feature policy for iframe, allowing access to certain courseware-related media.
*
* We must use the wildcard (*) origin for each feature, as courseware content
* may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
* block that iframes external course content.
* This policy was selected in conference with the edX Security Working Group.
* Changes to it should be vetted by them (security@edx.org).
*/
const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *'
);
/**
* We discovered an error in Firefox where - upon iframe load - React would cease to call any
* useEffect hooks until the user interacts with the page again. This is particularly confusing
* when navigating between sequences, as the UI partially updates leaving the user in a nebulous
* state.
*
* We were able to solve this error by using a layout effect to update some component state, which
* executes synchronously on render. Somehow this forces React to continue it's lifecycle
* immediately, rather than waiting for user interaction. This layout effect could be anywhere in
* the parent tree, as far as we can tell - we chose to add a conspicuously 'load bearing' (that's
* a joke) one here so it wouldn't be accidentally removed elsewhere.
*
* If we remove this hook when one of these happens:
* 1. React figures out that there's an issue here and fixes a bug.
* 2. We cease to use an iframe for unit rendering.
* 3. Firefox figures out that there's an issue in their iframe loading and fixes a bug.
* 4. We stop supporting Firefox.
* 5. An enterprising engineer decides to create a repo that reproduces the problem, submits it to
* Firefox/React for review, and they kindly help us figure out what in the world is happening
* so we can fix it.
*
* This hook depends on the unit id just to make sure it re-evaluates whenever the ID changes. If
* we change whether or not the Unit component is re-mounted when the unit ID changes, this may
* become important, as this hook will otherwise only evaluate the useLayoutEffect once.
*/
function useLoadBearingHook(id) {
const setValue = useState(0)[1];
useLayoutEffect(() => {
setValue(currentValue => currentValue + 1);
}, [id]);
}
export function sendUrlHashToFrame(frame) {
const { hash } = window.location;
if (hash) {
// The url hash will be sent to LMS-served iframe in order to find the location of the
// hash within the iframe.
frame.contentWindow.postMessage({ hashName: hash }, `${getConfig().LMS_BASE_URL}`);
}
}
const Unit = ({
courseId,
format,
onLoaded,
id,
intl,
}) => {
const { authenticatedUser } = useContext(AppContext);
const view = authenticatedUser ? 'student_view' : 'public_view';
let iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=${view}`;
if (format) {
iframeUrl += `&format=${format}`;
}
const [iframeHeight, setIframeHeight] = useState(0);
const [hasLoaded, setHasLoaded] = useState(false);
const [showError, setShowError] = useState(false);
const [modalOptions, setModalOptions] = useState({ open: false });
const [shouldDisplayHonorCode, setShouldDisplayHonorCode] = useState(false);
const unit = useModel('units', id);
const course = useModel('coursewareMeta', courseId);
const {
contentTypeGatingEnabled,
userNeedsIntegritySignature,
} = course;
const dispatch = useDispatch();
// Do not remove this hook. See function description.
useLoadBearingHook(id);
useEffect(() => {
if (userNeedsIntegritySignature && unit.graded) {
setShouldDisplayHonorCode(true);
} else {
setShouldDisplayHonorCode(false);
}
}, [userNeedsIntegritySignature]);
const receiveMessage = useCallback(({ data }) => {
const {
type,
payload,
} = data;
if (type === 'plugin.resize') {
setIframeHeight(payload.height);
if (!hasLoaded && iframeHeight === 0 && payload.height > 0) {
setHasLoaded(true);
if (onLoaded) {
onLoaded();
}
}
} else if (type === 'plugin.modal') {
payload.open = true;
setModalOptions(payload);
} else if (data.offset) {
// We listen for this message from LMS to know when the page needs to
// be scrolled to another location on the page.
window.scrollTo(0, data.offset + document.getElementById('unit-iframe').offsetTop);
}
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded]);
useEventListener('message', receiveMessage);
useEffect(() => {
sendUrlHashToFrame(document.getElementById('unit-iframe'));
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded]);
return (
<div className="unit">
<h1 className="mb-0 h3">{unit.title}</h1>
<h2 className="sr-only">{intl.formatMessage(messages.headerPlaceholder)}</h2>
<BookmarkButton
unitId={unit.id}
isBookmarked={unit.bookmarked}
isProcessing={unit.bookmarkedUpdateState === 'loading'}
/>
{/* TODO: social share exp. Need to remove later */}
{(window.expSocialShareAboutUrls && window.expSocialShareAboutUrls[unit.id] !== undefined) && (
<ShareButton url={window.expSocialShareAboutUrls[unit.id]} />
)}
{contentTypeGatingEnabled && unit.containsContentTypeGatedContent && (
<Suspense
fallback={(
<PageLoading
srMessage={intl.formatMessage(messages.loadingLockedContent)}
/>
)}
>
<LockPaywall courseId={courseId} />
</Suspense>
)}
{shouldDisplayHonorCode && (
<Suspense
fallback={(
<PageLoading
srMessage={intl.formatMessage(messages.loadingHonorCode)}
/>
)}
>
<HonorCode courseId={courseId} />
</Suspense>
)}
{!shouldDisplayHonorCode && !hasLoaded && !showError && (
<PageLoading
srMessage={intl.formatMessage(messages.loadingSequence)}
/>
)}
{!shouldDisplayHonorCode && !hasLoaded && showError && (
<ErrorPage />
)}
{modalOptions.open && (
<Modal
body={(
<>
{modalOptions.body
? <div className="unit-modal">{ modalOptions.body }</div>
: (
<iframe
title={modalOptions.title}
allow={IFRAME_FEATURE_POLICY}
frameBorder="0"
src={modalOptions.url}
style={{
width: '100%',
height: '100vh',
}}
/>
)}
</>
)}
onClose={() => { setModalOptions({ open: false }); }}
open
dialogClassName="modal-lti"
/>
)}
{!shouldDisplayHonorCode && (
<div className="unit-iframe-wrapper">
<iframe
id="unit-iframe"
title={unit.title}
src={iframeUrl}
allow={IFRAME_FEATURE_POLICY}
allowFullScreen
height={iframeHeight}
scrolling="no"
referrerPolicy="origin"
onLoad={() => {
// onLoad *should* only fire after everything in the iframe has finished its own load events.
// Which means that the plugin.resize message (which calls setHasLoaded above) will have fired already
// for a successful load. If it *has not fired*, we are in an error state. For example, the backend
// could have given us a 4xx or 5xx response.
if (!hasLoaded) {
setShowError(true);
}
window.onmessage = (e) => {
if (e.data.event_name) {
dispatch(processEvent(e.data, fetchCourse));
}
};
}}
/>
</div>
)}
</div>
);
};
Unit.propTypes = {
courseId: PropTypes.string.isRequired,
format: PropTypes.string,
id: PropTypes.string.isRequired,
intl: intlShape.isRequired,
onLoaded: PropTypes.func,
};
Unit.defaultProps = {
format: null,
onLoaded: undefined,
};
export default injectIntl(Unit);

View File

@@ -1,177 +0,0 @@
import React from 'react';
import { Factory } from 'rosie';
import {
initializeTestStore, loadUnit, messageEvent, render, screen, waitFor,
} from '../../../setupTest';
import Unit, { sendUrlHashToFrame } from './Unit';
describe('Unit', () => {
let mockData;
const courseMetadata = Factory.build(
'courseMetadata',
{ content_type_gating_enabled: true },
);
const courseMetadataNeedsSignature = Factory.build(
'courseMetadata',
{ user_needs_integrity_signature: true },
);
const unitBlocks = [
Factory.build(
'block',
{ type: 'vertical', graded: 'true' },
{ courseId: courseMetadata.id },
), Factory.build(
'block',
{
type: 'vertical',
contains_content_type_gated_content: true,
bookmarked: true,
graded: true,
},
{ courseId: courseMetadata.id },
),
Factory.build(
'block',
{ type: 'vertical', graded: false },
{ courseId: courseMetadata.id },
),
];
const [unit, unitThatContainsGatedContent, ungradedUnit] = unitBlocks;
beforeAll(async () => {
await initializeTestStore({ courseMetadata, unitBlocks });
mockData = {
id: unit.id,
courseId: courseMetadata.id,
format: 'Homework',
};
});
it('renders correctly', () => {
render(<Unit {...mockData} />);
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
const renderedUnit = screen.getByTitle(unit.display_name);
expect(renderedUnit).toHaveAttribute('height', String(0));
expect(renderedUnit).toHaveAttribute('src', `http://localhost:18000/xblock/${mockData.id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view&format=${mockData.format}`);
});
it('renders proper message for gated content', () => {
render(<Unit {...mockData} id={unitThatContainsGatedContent.id} />);
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
expect(screen.getByText('Loading locked content messaging...')).toBeInTheDocument();
});
it('does not display HonorCode for ungraded units', async () => {
const signatureStore = await initializeTestStore(
{ courseMetadata: courseMetadataNeedsSignature, unitBlocks },
false,
);
const signatureData = {
id: ungradedUnit.id,
courseId: courseMetadataNeedsSignature.id,
format: 'Homework',
};
render(<Unit {...signatureData} />, { store: signatureStore });
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
});
it('displays HonorCode for graded units if user needs integrity signature', async () => {
const signatureStore = await initializeTestStore(
{ courseMetadata: courseMetadataNeedsSignature, unitBlocks },
false,
);
const signatureData = {
id: unit.id,
courseId: courseMetadataNeedsSignature.id,
format: 'Homework',
};
render(<Unit {...signatureData} />, { store: signatureStore });
expect(screen.getByText('Loading honor code messaging...')).toBeInTheDocument();
});
it('handles receiving MessageEvent', async () => {
render(<Unit {...mockData} />);
loadUnit();
// Loading message is gone now.
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
// Iframe's height is set via message.
expect(screen.getByTitle(unit.display_name)).toHaveAttribute('height', String(messageEvent.payload.height));
});
it('calls onLoaded after receiving MessageEvent', async () => {
const onLoaded = jest.fn();
render(<Unit {...mockData} {...{ onLoaded }} />);
loadUnit();
await waitFor(() => expect(onLoaded).toHaveBeenCalledTimes(1));
});
it('resizes iframe on second MessageEvent, does not call onLoaded again', async () => {
const onLoaded = jest.fn();
// Clone message and set different height.
const testMessageWithOtherHeight = { ...messageEvent, payload: { height: 200 } };
render(<Unit {...mockData} {...{ onLoaded }} />);
loadUnit();
await waitFor(() => expect(screen.getByTitle(unit.display_name)).toHaveAttribute('height', String(messageEvent.payload.height)));
window.postMessage(testMessageWithOtherHeight, '*');
await waitFor(() => expect(screen.getByTitle(unit.display_name)).toHaveAttribute('height', String(testMessageWithOtherHeight.payload.height)));
expect(onLoaded).toHaveBeenCalledTimes(1);
});
it('scrolls page on MessagaeEvent when receiving offset', async () => {
// Set message to constain offset data.
const testMessageWithOffset = { offset: 1500 };
render(<Unit {...mockData} />);
window.postMessage(testMessageWithOffset, '*');
await expect(waitFor(() => expect(window.scrollTo()).toHaveBeenCalled()));
expect(window.scrollY === testMessageWithOffset.offset);
});
it('ignores MessageEvent with unhandled type', async () => {
// Clone message and set different type.
const testMessageWithUnhandledType = { ...messageEvent, type: 'wrong type' };
render(<Unit {...mockData} />);
window.postMessage(testMessageWithUnhandledType, '*');
// HACK: We don't have a function we could reliably await here, so this test relies on the timeout of `waitFor`.
await expect(waitFor(
() => expect(screen.getByTitle(unit.display_name)).toHaveAttribute('height', String(testMessageWithUnhandledType.payload.height)),
{ timeout: 100 },
)).rejects.toThrowError(/Expected the element to have attribute/);
});
it('scrolls to correct place onLoad', () => {
document.body.innerHTML = "<iframe id='unit-iframe' />";
const mockHashCheck = jest.fn(frameVar => sendUrlHashToFrame(frameVar));
const frame = document.getElementById('unit-iframe');
const originalWindow = { ...window };
const windowSpy = jest.spyOn(global, 'window', 'get');
windowSpy.mockImplementation(() => ({
...originalWindow,
location: {
...originalWindow.location,
hash: '#test',
},
}));
const messageSpy = jest.spyOn(frame.contentWindow, 'postMessage');
messageSpy.mockImplementation(() => ({ hashName: originalWindow.location.hash }));
mockHashCheck(frame);
expect(mockHashCheck).toHaveBeenCalled();
expect(messageSpy).toHaveBeenCalled();
windowSpy.mockRestore();
});
it('calls useEffect and checkForHash', () => {
const mockHashCheck = jest.fn(() => sendUrlHashToFrame());
const effectSpy = jest.spyOn(React, 'useEffect');
effectSpy.mockImplementation(() => mockHashCheck());
render(<Unit {...mockData} />);
expect(React.useEffect).toHaveBeenCalled();
expect(mockHashCheck).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,114 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ErrorPage } from '@edx/frontend-platform/react';
import { StrictDict } from '@edx/react-unit-test-utils';
import { Modal } from '@edx/paragon';
import PageLoading from '../../../../generic/PageLoading';
import * as hooks from './hooks';
/**
* Feature policy for iframe, allowing access to certain courseware-related media.
*
* We must use the wildcard (*) origin for each feature, as courseware content
* may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
* block that iframes external course content.
* This policy was selected in conference with the edX Security Working Group.
* Changes to it should be vetted by them (security@edx.org).
*/
export const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *, clipboard-write *'
);
export const testIDs = StrictDict({
contentIFrame: 'content-iframe-test-id',
modalIFrame: 'modal-iframe-test-id',
});
const ContentIFrame = ({
iframeUrl,
shouldShowContent,
loadingMessage,
id,
elementId,
onLoaded,
title,
}) => {
const {
handleIFrameLoad,
hasLoaded,
iframeHeight,
showError,
} = hooks.useIFrameBehavior({
elementId,
id,
iframeUrl,
onLoaded,
});
const {
modalOptions,
handleModalClose,
} = hooks.useModalIFrameData();
const contentIFrameProps = {
id: elementId,
src: iframeUrl,
allow: IFRAME_FEATURE_POLICY,
allowFullScreen: true,
height: iframeHeight,
scrolling: 'no',
referrerPolicy: 'origin',
onLoad: handleIFrameLoad,
};
return (
<>
{(shouldShowContent && !hasLoaded) && (
showError ? <ErrorPage /> : <PageLoading srMessage={loadingMessage} />
)}
{shouldShowContent && (
<div className="unit-iframe-wrapper">
<iframe title={title} {...contentIFrameProps} data-testid={testIDs.contentIFrame} />
</div>
)}
{modalOptions.open && (
<Modal
body={modalOptions.body
? <div className="unit-modal">{ modalOptions.body }</div>
: (
<iframe
title={modalOptions.title}
allow={IFRAME_FEATURE_POLICY}
frameBorder="0"
src={modalOptions.url}
style={{ width: '100%', height: '100vh' }}
/>
)}
dialogClassName="modal-lti"
onClose={handleModalClose}
open
/>
)}
</>
);
};
ContentIFrame.propTypes = {
iframeUrl: PropTypes.string,
id: PropTypes.string.isRequired,
shouldShowContent: PropTypes.bool.isRequired,
loadingMessage: PropTypes.node.isRequired,
elementId: PropTypes.string.isRequired,
onLoaded: PropTypes.func,
title: PropTypes.node.isRequired,
};
ContentIFrame.defaultProps = {
iframeUrl: null,
onLoaded: () => ({}),
};
export default ContentIFrame;

View File

@@ -0,0 +1,174 @@
import React from 'react';
import { ErrorPage } from '@edx/frontend-platform/react';
import { Modal } from '@edx/paragon';
import { shallow } from '@edx/react-unit-test-utils';
import PageLoading from '../../../../generic/PageLoading';
import * as hooks from './hooks';
import ContentIFrame, { IFRAME_FEATURE_POLICY, testIDs } from './ContentIFrame';
jest.mock('@edx/frontend-platform/react', () => ({ ErrorPage: 'ErrorPage' }));
jest.mock('@edx/paragon', () => ({ Modal: 'Modal' }));
jest.mock('../../../../generic/PageLoading', () => 'PageLoading');
jest.mock('./hooks', () => ({
useIFrameBehavior: jest.fn(),
useModalIFrameData: jest.fn(),
}));
const iframeBehavior = {
handleIFrameLoad: jest.fn().mockName('IFrameBehavior.handleIFrameLoad'),
hasLoaded: false,
iframeHeight: 20,
showError: false,
};
const modalOptions = {
closed: {
open: false,
},
withBody: {
body: 'test-body',
open: true,
},
withUrl: {
open: true,
title: 'test-modal-title',
url: 'test-modal-url',
},
};
const modalIFrameData = {
modalOptions: modalOptions.closed,
handleModalClose: jest.fn().mockName('modalIFrameOptions.handleModalClose'),
};
hooks.useIFrameBehavior.mockReturnValue(iframeBehavior);
hooks.useModalIFrameData.mockReturnValue(modalIFrameData);
const props = {
iframeUrl: 'test-iframe-url',
shouldShowContent: true,
loadingMessage: 'test-loading-message',
id: 'test-id',
elementId: 'test-element-id',
onLoaded: jest.fn().mockName('props.onLoaded'),
title: 'test-title',
};
let el;
describe('ContentIFrame Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
beforeEach(() => {
el = shallow(<ContentIFrame {...props} />);
});
it('initializes iframe behavior hook', () => {
expect(hooks.useIFrameBehavior).toHaveBeenCalledWith({
elementId: props.elementId,
id: props.id,
iframeUrl: props.iframeUrl,
onLoaded: props.onLoaded,
});
});
it('initializes modal iframe data', () => {
expect(hooks.useModalIFrameData).toHaveBeenCalledWith();
});
});
describe('output', () => {
let component;
describe('shouldShowContent', () => {
describe('if not hasLoaded', () => {
it('displays errorPage if showError', () => {
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, showError: true });
el = shallow(<ContentIFrame {...props} />);
expect(el.instance.findByType(ErrorPage).length).toEqual(1);
});
it('displays PageLoading component if not showError', () => {
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(PageLoading);
expect(component.props.srMessage).toEqual(props.loadingMessage);
});
});
describe('hasLoaded', () => {
it('does not display PageLoading or ErrorPage', () => {
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, hasLoaded: true });
el = shallow(<ContentIFrame {...props} />);
expect(el.instance.findByType(PageLoading).length).toEqual(0);
expect(el.instance.findByType(ErrorPage).length).toEqual(0);
});
});
it('display iframe with props from hooks', () => {
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByTestId(testIDs.contentIFrame);
expect(component.props).toEqual({
allow: IFRAME_FEATURE_POLICY,
allowFullScreen: true,
scrolling: 'no',
referrerPolicy: 'origin',
title: props.title,
id: props.elementId,
src: props.iframeUrl,
height: iframeBehavior.iframeHeight,
onLoad: iframeBehavior.handleIFrameLoad,
'data-testid': testIDs.contentIFrame,
});
});
});
describe('not shouldShowContent', () => {
it('does not show PageLoading, ErrorPage, or unit-iframe-wrapper', () => {
el = shallow(<ContentIFrame {...{ ...props, shouldShowContent: false }} />);
expect(el.instance.findByType(PageLoading).length).toEqual(0);
expect(el.instance.findByType(ErrorPage).length).toEqual(0);
expect(el.instance.findByTestId(testIDs.contentIFrame).length).toEqual(0);
});
});
it('does not display modal if modalOptions returns open: false', () => {
el = shallow(<ContentIFrame {...props} />);
expect(el.instance.findByType(Modal).length).toEqual(0);
});
describe('if modalOptions.open', () => {
const testModalOpenAndHandleClose = () => {
test('Modal component is open, with handleModalClose from hook', () => {
expect(component.props.onClose).toEqual(modalIFrameData.handleModalClose);
});
};
describe('body modal', () => {
beforeEach(() => {
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withBody });
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(Modal);
});
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
expect(component.props.body).toEqual(<div className="unit-modal">{modalOptions.withBody.body}</div>);
});
testModalOpenAndHandleClose();
});
describe('url modal', () => {
beforeEach(() => {
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withUrl });
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(Modal);
});
testModalOpenAndHandleClose();
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
expect(component.props.body).toEqual(
<iframe
title={modalOptions.withUrl.title}
allow={IFRAME_FEATURE_POLICY}
frameBorder="0"
src={modalOptions.withUrl.url}
style={{ width: '100%', height: '100vh' }}
/>,
);
});
});
});
});
});

View File

@@ -0,0 +1,50 @@
import React, { Suspense } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useModel } from '../../../../generic/model-store';
import PageLoading from '../../../../generic/PageLoading';
import messages from '../messages';
import HonorCode from '../honor-code';
import LockPaywall from '../lock-paywall';
import * as hooks from './hooks';
import { modelKeys } from './constants';
const UnitSuspense = ({
courseId,
id,
}) => {
const { formatMessage } = useIntl();
const shouldDisplayHonorCode = hooks.useShouldDisplayHonorCode({ courseId, id });
const unit = useModel(modelKeys.units, id);
const meta = useModel(modelKeys.coursewareMeta, courseId);
const shouldDisplayContentGating = (
meta.contentTypeGatingEnabled && unit.containsContentTypeGatedContent
);
const suspenseComponent = (message, Component) => (
<Suspense fallback={<PageLoading srMessage={formatMessage(message)} />}>
<Component courseId={courseId} />
</Suspense>
);
return (
<>
{shouldDisplayContentGating && (
suspenseComponent(messages.loadingLockedContent, LockPaywall)
)}
{shouldDisplayHonorCode && (
suspenseComponent(messages.loadingHonorCode, HonorCode)
)}
</>
);
};
UnitSuspense.propTypes = {
courseId: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
};
export default UnitSuspense;

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
import { useModel } from '../../../../generic/model-store';
import PageLoading from '../../../../generic/PageLoading';
import messages from '../messages';
import HonorCode from '../honor-code';
import LockPaywall from '../lock-paywall';
import hooks from './hooks';
import { modelKeys } from './constants';
import UnitSuspense from './UnitSuspense';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
useIntl: () => ({ formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage }),
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
Suspense: 'Suspense',
}));
jest.mock('../honor-code', () => 'HonorCode');
jest.mock('../lock-paywall', () => 'LockPaywall');
jest.mock('../../../../generic/model-store', () => ({ useModel: jest.fn() }));
jest.mock('../../../../generic/PageLoading', () => 'PageLoading');
jest.mock('./hooks', () => ({
useShouldDisplayHonorCode: jest.fn(() => false),
}));
const mockModels = (enabled, containsContent) => {
useModel.mockImplementation((key) => (
key === modelKeys.units
? { containsContentTypeGatedContent: containsContent }
: { contentTypeGatingEnabled: enabled }
));
};
const props = {
courseId: 'test-course-id',
id: 'test-id',
};
let el;
describe('UnitSuspense component', () => {
beforeEach(() => {
jest.clearAllMocks();
mockModels(false, false);
});
describe('behavior', () => {
it('initializes models', () => {
el = shallow(<UnitSuspense {...props} />);
const { calls } = useModel.mock;
const [unitCall] = calls.filter(call => call[0] === modelKeys.units);
const [metaCall] = calls.filter(call => call[0] === modelKeys.coursewareMeta);
expect(unitCall[1]).toEqual(props.id);
expect(metaCall[1]).toEqual(props.courseId);
});
});
describe('output', () => {
describe('LockPaywall', () => {
const testNoPaywall = () => {
it('does not display LockPaywal', () => {
el = shallow(<UnitSuspense {...props} />);
expect(el.instance.findByType(LockPaywall).length).toEqual(0);
});
};
describe('gating not enabled', () => { testNoPaywall(); });
describe('gating enabled, but no gated content included', () => {
beforeEach(() => { mockModels(true, false); });
testNoPaywall();
});
describe('gating enabled, gated content included', () => {
beforeEach(() => { mockModels(true, true); });
it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => {
el = shallow(<UnitSuspense {...props} />);
const [component] = el.instance.findByType(LockPaywall);
expect(component.parent.type).toEqual('Suspense');
expect(component.parent.props.fallback)
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />);
expect(component.props.courseId).toEqual(props.courseId);
});
});
});
describe('HonorCode', () => {
it('does not display HonorCode if useShouldDisplayHonorCode => false', () => {
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
el = shallow(<UnitSuspense {...props} />);
expect(el.instance.findByType(HonorCode).length).toEqual(0);
});
it('displays HonorCode component in Suspense wrapper with PageLoading fallback if shouldDisplayHonorCode', () => {
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(true);
el = shallow(<UnitSuspense {...props} />);
const [component] = el.instance.findByType(HonorCode);
expect(component.parent.type).toEqual('Suspense');
expect(component.parent.props.fallback)
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingHonorCode)} />);
expect(component.props.courseId).toEqual(props.courseId);
});
});
});
});

View File

@@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Unit component output BookmarkButton props bookmarked, bookmark update pending snapshot 1`] = `
<BookmarkButton
isBookmarked={true}
isProcessing={false}
unitId="unit-id"
/>
`;
exports[`Unit component output BookmarkButton props not bookmarked, bookmark update loading snapshot 1`] = `
<BookmarkButton
isBookmarked={false}
isProcessing={true}
unitId="unit-id"
/>
`;
exports[`Unit component output snapshot: not bookmarked, do not show content 1`] = `
<div
className="unit"
>
<h1
className="mb-0 h3"
>
unit-title
</h1>
<h2
className="sr-only"
>
Level 2 headings may be created by course providers in the future.
</h2>
<BookmarkButton
isBookmarked={false}
isProcessing={false}
unitId="unit-id"
/>
<UnitSuspense
courseId="test-course-id"
id="test-props-id"
/>
<ContentIFrame
elementId="unit-iframe"
id="test-props-id"
loadingMessage="Loading learning sequence..."
onLoaded={[MockFunction props.onLoaded]}
shouldShowContent={true}
title="unit-title"
/>
</div>
`;

View File

@@ -0,0 +1,26 @@
import { StrictDict } from '@edx/react-unit-test-utils/dist';
export const modelKeys = StrictDict({
units: 'units',
coursewareMeta: 'coursewareMeta',
});
export const views = StrictDict({
student: 'student_view',
public: 'public_view',
});
export const loadingState = 'loading';
export const messageTypes = StrictDict({
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
});
export default StrictDict({
modelKeys,
views,
loadingState,
messageTypes,
});

View File

@@ -0,0 +1,5 @@
export { default as useExamAccess } from './useExamAccess';
export { default as useIFrameBehavior } from './useIFrameBehavior';
export { default as useLoadBearingHook } from './useLoadBearingHook';
export { default as useModalIFrameData } from './useModalIFrameData';
export { default as useShouldDisplayHonorCode } from './useShouldDisplayHonorCode';

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { logError } from '@edx/frontend-platform/logging';
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
import { getExamAccess, fetchExamAccess, isExam } from '@edx/frontend-lib-special-exams';
export const stateKeys = StrictDict({
accessToken: 'accessToken',
blockAccess: 'blockAccess',
});
const useExamAccess = ({
id,
}) => {
const [accessToken, setAccessToken] = useKeyedState(stateKeys.accessToken, '');
const [blockAccess, setBlockAccess] = useKeyedState(stateKeys.blockAccess, isExam());
React.useEffect(() => {
if (isExam()) {
return fetchExamAccess()
.finally(() => {
const examAccess = getExamAccess();
setAccessToken(examAccess);
setBlockAccess(false);
})
.catch((error) => {
logError(error);
});
}
return undefined;
}, [id]);
return {
blockAccess,
accessToken,
};
};
export default useExamAccess;

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { logError } from '@edx/frontend-platform/logging';
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
import { getExamAccess, fetchExamAccess, isExam } from '@edx/frontend-lib-special-exams';
import { isEqual } from 'lodash';
import useExamAccess, { stateKeys } from './useExamAccess';
const getEffect = (prereqs) => {
const { calls } = React.useEffect.mock;
const match = calls.filter(call => isEqual(call[1], prereqs));
return match.length ? match[0][0] : null;
};
jest.mock('react', () => ({
...jest.requireActual('react'),
useEffect: jest.fn(),
}));
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));
jest.mock('@edx/frontend-lib-special-exams', () => ({
getExamAccess: jest.fn(),
fetchExamAccess: jest.fn(),
isExam: jest.fn(() => false),
}));
const state = mockUseKeyedState(stateKeys);
const id = 'test-id';
const mockFetchExamAccess = Promise.resolve();
fetchExamAccess.mockReturnValue(mockFetchExamAccess);
const testAccessToken = 'test-access-token';
getExamAccess.mockReturnValue(testAccessToken);
describe('useExamAccess hook', () => {
beforeEach(() => {
jest.clearAllMocks();
state.mock();
});
describe('behavior', () => {
it('initializes access token to empty string', () => {
useExamAccess({ id });
state.expectInitializedWith(stateKeys.accessToken, '');
});
it('initializes blockAccess to true if is an exam', () => {
useExamAccess({ id });
state.expectInitializedWith(stateKeys.blockAccess, false);
});
it('initializes blockAccess to false if is not an exam', () => {
isExam.mockReturnValueOnce(true);
useExamAccess({ id });
state.expectInitializedWith(stateKeys.blockAccess, true);
});
describe('effects - on id change', () => {
let cb;
beforeEach(() => {
useExamAccess({ id });
cb = getEffect([id], React);
});
it('does not call fetchExamAccess if not an exam', () => {
cb();
expect(fetchExamAccess).not.toHaveBeenCalled();
});
it('fetches and sets exam access if isExam', async () => {
isExam.mockReturnValueOnce(true);
await cb();
state.expectSetStateCalledWith(stateKeys.accessToken, testAccessToken);
state.expectSetStateCalledWith(stateKeys.blockAccess, false);
});
const testError = 'test-error';
it('logs error if fetchExamAccess fails', async () => {
isExam.mockReturnValueOnce(true);
fetchExamAccess.mockReturnValueOnce(Promise.reject(testError));
await cb();
expect(logError).toHaveBeenCalledWith(testError);
});
});
});
describe('output', () => {
it('forwards blockAccess and accessToken from state fields', () => {
const testBlockAccess = 'test-block-access';
state.mockVals({
blockAccess: testBlockAccess,
accessToken: testAccessToken,
});
const out = useExamAccess({ id });
expect(out.blockAccess).toEqual(testBlockAccess);
expect(out.accessToken).toEqual(testAccessToken);
state.resetVals();
});
});
});

View File

@@ -0,0 +1,116 @@
import { getConfig } from '@edx/frontend-platform';
import React from 'react';
import { useDispatch } from 'react-redux';
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
import { logError } from '@edx/frontend-platform/logging';
import { fetchCourse } from '../../../../data';
import { processEvent } from '../../../../../course-home/data/thunks';
import { useEventListener } from '../../../../../generic/hooks';
import { messageTypes } from '../constants';
import useLoadBearingHook from './useLoadBearingHook';
export const stateKeys = StrictDict({
iframeHeight: 'iframeHeight',
hasLoaded: 'hasLoaded',
showError: 'showError',
windowTopOffset: 'windowTopOffset',
});
const useIFrameBehavior = ({
elementId,
id,
iframeUrl,
onLoaded,
}) => {
// Do not remove this hook. See function description.
useLoadBearingHook(id);
const dispatch = useDispatch();
const [iframeHeight, setIframeHeight] = useKeyedState(stateKeys.iframeHeight, 0);
const [hasLoaded, setHasLoaded] = useKeyedState(stateKeys.hasLoaded, false);
const [showError, setShowError] = useKeyedState(stateKeys.showError, false);
const [windowTopOffset, setWindowTopOffset] = useKeyedState(stateKeys.windowTopOffset, null);
React.useEffect(() => {
const frame = document.getElementById(elementId);
const { hash } = window.location;
if (hash) {
// The url hash will be sent to LMS-served iframe in order to find the location of the
// hash within the iframe.
frame.contentWindow.postMessage({ hashName: hash }, `${getConfig().LMS_BASE_URL}`);
}
}, [id, onLoaded, iframeHeight, hasLoaded]);
const receiveMessage = React.useCallback(({ data }) => {
const { type, payload } = data;
if (type === messageTypes.resize) {
setIframeHeight(payload.height);
// We observe exit from the video xblock fullscreen mode
// and scroll to the previously saved scroll position
if (windowTopOffset !== null) {
window.scrollTo(0, Number(windowTopOffset));
}
if (!hasLoaded && iframeHeight === 0 && payload.height > 0) {
setHasLoaded(true);
if (onLoaded) {
onLoaded();
}
}
} else if (type === messageTypes.videoFullScreen) {
// We listen for this message from LMS to know when we need to
// save or reset scroll position on toggle video xblock fullscreen mode
setWindowTopOffset(payload.open ? window.scrollY : null);
} else if (data.offset) {
// We listen for this message from LMS to know when the page needs to
// be scrolled to another location on the page.
window.scrollTo(0, data.offset + document.getElementById('unit-iframe').offsetTop);
}
}, [
id,
onLoaded,
hasLoaded,
setHasLoaded,
iframeHeight,
setIframeHeight,
windowTopOffset,
setWindowTopOffset,
]);
useEventListener('message', receiveMessage);
/**
* onLoad *should* only fire after everything in the iframe has finished its own load events.
* Which means that the plugin.resize message (which calls setHasLoaded above) will have fired already
* for a successful load. If it *has not fired*, we are in an error state. For example, the backend
* could have given us a 4xx or 5xx response.
*/
const handleIFrameLoad = () => {
if (!hasLoaded) {
setShowError(true);
logError('Unit iframe failed to load. Server possibly returned 4xx or 5xx response.', {
iframeUrl,
});
}
window.onmessage = (e) => {
if (e.data.event_name) {
dispatch(processEvent(e.data, fetchCourse));
}
};
};
return {
iframeHeight,
handleIFrameLoad,
showError,
hasLoaded,
};
};
export default useIFrameBehavior;

View File

@@ -0,0 +1,295 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { getEffects, mockUseKeyedState } from '@edx/react-unit-test-utils';
import { logError } from '@edx/frontend-platform/logging';
import { getConfig } from '@edx/frontend-platform';
import { fetchCourse } from '../../../../data';
import { processEvent } from '../../../../../course-home/data/thunks';
import { useEventListener } from '../../../../../generic/hooks';
import { messageTypes } from '../constants';
import useIFrameBehavior, { stateKeys } from './useIFrameBehavior';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useEffect: jest.fn(),
useCallback: jest.fn((cb, prereqs) => ({ cb, prereqs })),
}));
jest.mock('react-redux', () => ({
useDispatch: jest.fn(),
}));
jest.mock('./useLoadBearingHook', () => jest.fn());
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));
jest.mock('../../../../data', () => ({
fetchCourse: jest.fn(),
}));
jest.mock('../../../../../course-home/data/thunks', () => ({
processEvent: jest.fn((...args) => ({ processEvent: args })),
}));
jest.mock('../../../../../generic/hooks', () => ({
useEventListener: jest.fn(),
}));
const state = mockUseKeyedState(stateKeys);
const props = {
elementId: 'test-element-id',
id: 'test-id',
iframeUrl: 'test-iframe-url',
onLoaded: jest.fn(),
};
const testIFrameHeight = 42;
const config = { LMS_BASE_URL: 'test-base-url' };
getConfig.mockReturnValue(config);
const dispatch = jest.fn();
useDispatch.mockReturnValue(dispatch);
const postMessage = jest.fn();
const frame = { contentWindow: { postMessage } };
const mockGetElementById = jest.fn(() => frame);
const testHash = '#test-hash';
const defaultStateVals = {
iframeHeight: 0,
hasLoaded: false,
showError: false,
windowTopOffset: null,
};
const stateVals = {
iframeHeight: testIFrameHeight,
hasLoaded: true,
showError: true,
windowTopOffset: 32,
};
describe('useIFrameBehavior hook', () => {
let hook;
beforeEach(() => {
jest.clearAllMocks();
state.mock();
});
afterEach(() => {
state.resetVals();
});
describe('behavior', () => {
it('initializes iframe height to 0 and error/loaded values to false', () => {
hook = useIFrameBehavior(props);
state.expectInitializedWith(stateKeys.iframeHeight, 0);
state.expectInitializedWith(stateKeys.hasLoaded, false);
state.expectInitializedWith(stateKeys.showError, false);
state.expectInitializedWith(stateKeys.windowTopOffset, null);
});
describe('effects - on frame change', () => {
let oldGetElement;
beforeEach(() => {
global.window ??= Object.create(window);
Object.defineProperty(window, 'location', { value: {}, writable: true });
state.mockVals(stateVals);
oldGetElement = document.getElementById;
document.getElementById = mockGetElementById;
});
afterEach(() => {
state.resetVals();
document.getElementById = oldGetElement;
});
it('does not post url hash if the window does not have one', () => {
hook = useIFrameBehavior(props);
const cb = getEffects([
props.id,
props.onLoaded,
testIFrameHeight,
true,
], React)[0];
cb();
expect(postMessage).not.toHaveBeenCalled();
});
it('posts url hash if the window has one', () => {
window.location.hash = testHash;
hook = useIFrameBehavior(props);
const cb = getEffects([
props.id,
props.onLoaded,
testIFrameHeight,
true,
], React)[0];
cb();
expect(postMessage).toHaveBeenCalledWith({ hashName: testHash }, config.LMS_BASE_URL);
});
});
describe('event listener', () => {
it('calls eventListener with prepared callback', () => {
state.mockVals(stateVals);
hook = useIFrameBehavior(props);
const [call] = useEventListener.mock.calls;
expect(call[0]).toEqual('message');
expect(call[1].prereqs).toEqual([
props.id,
props.onLoaded,
state.values.hasLoaded,
state.setState.hasLoaded,
state.values.iframeHeight,
state.setState.iframeHeight,
state.values.windowTopOffset,
state.setState.windowTopOffset,
]);
});
describe('resize message', () => {
const resizeMessage = (height = 23) => ({
data: { type: messageTypes.resize, payload: { height } },
});
const testSetIFrameHeight = (height = 23) => {
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage(height));
expect(state.setState.iframeHeight).toHaveBeenCalledWith(height);
};
const testOnlySetsHeight = () => {
it('sets iframe height with payload height', () => {
testSetIFrameHeight();
});
it('does not set hasLoaded', () => {
expect(state.setState.hasLoaded).not.toHaveBeenCalled();
});
};
describe('hasLoaded', () => {
beforeEach(() => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
hook = useIFrameBehavior(props);
});
testOnlySetsHeight();
});
describe('iframeHeight is not 0', () => {
beforeEach(() => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
hook = useIFrameBehavior(props);
});
testOnlySetsHeight();
});
describe('payload height is 0', () => {
beforeEach(() => { hook = useIFrameBehavior(props); });
testOnlySetsHeight(0);
});
describe('payload is present but uninitialized', () => {
it('sets iframe height with payload height', () => {
hook = useIFrameBehavior(props);
testSetIFrameHeight();
});
it('sets hasLoaded and calls onLoaded', () => {
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage());
expect(state.setState.hasLoaded).toHaveBeenCalledWith(true);
expect(props.onLoaded).toHaveBeenCalled();
});
test('onLoaded is optional', () => {
hook = useIFrameBehavior({ ...props, onLoaded: undefined });
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage());
expect(state.setState.hasLoaded).toHaveBeenCalledWith(true);
});
});
it('scrolls to current window vertical offset if one is set', () => {
const windowTopOffset = 32;
state.mockVals({ ...defaultStateVals, windowTopOffset });
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage());
expect(window.scrollTo).toHaveBeenCalledWith(0, windowTopOffset);
});
it('does not scroll if towverticalp offset is not set', () => {
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage());
expect(window.scrollTo).not.toHaveBeenCalled();
});
});
describe('video fullscreen message', () => {
let cb;
const scrollY = 23;
const fullScreenMessage = (open) => ({
data: { type: messageTypes.videoFullScreen, payload: { open } },
});
beforeEach(() => {
window.scrollY = scrollY;
hook = useIFrameBehavior(props);
[[, { cb }]] = useEventListener.mock.calls;
});
it('sets window top offset based on window.scrollY if opening the video', () => {
cb(fullScreenMessage(true));
expect(state.setState.windowTopOffset).toHaveBeenCalledWith(scrollY);
});
it('sets window top offset to null if closing the video', () => {
cb(fullScreenMessage(false));
expect(state.setState.windowTopOffset).toHaveBeenCalledWith(null);
});
});
describe('offset message', () => {
it('scrolls to data offset', () => {
const offsetTop = 44;
const mockGetEl = jest.fn(() => ({ offsetTop }));
const oldGetElement = document.getElementById;
document.getElementById = mockGetEl;
const oldScrollTo = window.scrollTo;
window.scrollTo = jest.fn();
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
const offset = 99;
cb({ data: { offset } });
expect(window.scrollTo).toHaveBeenCalledWith(0, offset + offsetTop);
expect(mockGetEl).toHaveBeenCalledWith('unit-iframe');
document.getElementById = oldGetElement;
window.scrollTo = oldScrollTo;
});
});
});
});
describe('output', () => {
describe('handleIFrameLoad', () => {
it('sets and logs error if has not loaded', () => {
hook = useIFrameBehavior(props);
hook.handleIFrameLoad();
expect(state.setState.showError).toHaveBeenCalledWith(true);
expect(logError).toHaveBeenCalled();
});
it('does not set/log errors if loaded', () => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
hook = useIFrameBehavior(props);
hook.handleIFrameLoad();
expect(state.setState.showError).not.toHaveBeenCalled();
expect(logError).not.toHaveBeenCalled();
});
it('registers an event handler to process fetchCourse events.', () => {
hook = useIFrameBehavior(props);
hook.handleIFrameLoad();
const eventName = 'test-event-name';
const event = { data: { event_name: eventName } };
window.onmessage(event);
expect(dispatch).toHaveBeenCalledWith(processEvent(event.data, fetchCourse));
});
});
it('forwards handleIframeLoad, showError, and hasLoaded from state fields', () => {
state.mockVals(stateVals);
hook = useIFrameBehavior(props);
expect(hook.iframeHeight).toEqual(stateVals.iframeHeight);
expect(hook.showError).toEqual(stateVals.showError);
expect(hook.hasLoaded).toEqual(stateVals.hasLoaded);
});
});
});

View File

@@ -0,0 +1,35 @@
import React from 'react';
/**
* We discovered an error in Firefox where - upon iframe load - React would cease to call any
* useEffect hooks until the user interacts with the page again. This is particularly confusing
* when navigating between sequences, as the UI partially updates leaving the user in a nebulous
* state.
*
* We were able to solve this error by using a layout effect to update some component state, which
* executes synchronously on render. Somehow this forces React to continue it's lifecycle
* immediately, rather than waiting for user interaction. This layout effect could be anywhere in
* the parent tree, as far as we can tell - we chose to add a conspicuously 'load bearing' (that's
* a joke) one here so it wouldn't be accidentally removed elsewhere.
*
* If we remove this hook when one of these happens:
* 1. React figures out that there's an issue here and fixes a bug.
* 2. We cease to use an iframe for unit rendering.
* 3. Firefox figures out that there's an issue in their iframe loading and fixes a bug.
* 4. We stop supporting Firefox.
* 5. An enterprising engineer decides to create a repo that reproduces the problem, submits it to
* Firefox/React for review, and they kindly help us figure out what in the world is happening
* so we can fix it.
*
* This hook depends on the unit id just to make sure it re-evaluates whenever the ID changes. If
* we change whether or not the Unit component is re-mounted when the unit ID changes, this may
* become important, as this hook will otherwise only evaluate the useLayoutEffect once.
*/
const useLoadBearingHook = (id) => {
const setValue = React.useState(0)[1];
React.useLayoutEffect(() => {
setValue(currentValue => currentValue + 1);
}, [id]);
};
export default useLoadBearingHook;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import useLoadBearingHook from './useLoadBearingHook';
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
useLayoutEffect: jest.fn(),
}));
const setState = jest.fn();
React.useState.mockImplementation((val) => [val, setState]);
const id = 'test-id';
describe('useLoadBearingHook', () => {
it('increments a simple value w/ useLayoutEffect', () => {
useLoadBearingHook(id);
expect(React.useState).toHaveBeenCalledWith(0);
const [[layoutCb, prereqs]] = React.useLayoutEffect.mock.calls;
expect(prereqs).toEqual([id]);
layoutCb();
const [[setValueCb]] = setState.mock.calls;
expect(setValueCb(1)).toEqual(2);
});
});

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils/dist';
import { useEventListener } from '../../../../../generic/hooks';
export const stateKeys = StrictDict({
modalOptions: 'modalOptions',
});
const useModalIFrameBehavior = () => {
const [modalOptions, setModalOptions] = useKeyedState(stateKeys.modalOptions, ({ open: false }));
const receiveMessage = React.useCallback(({ data }) => {
const { type, payload } = data;
if (type === 'plugin.modal') {
payload.open = true;
setModalOptions(payload);
}
}, []);
useEventListener('message', receiveMessage);
const handleModalClose = () => {
setModalOptions({ open: false });
};
return {
handleModalClose,
modalOptions,
};
};
export default useModalIFrameBehavior;

View File

@@ -0,0 +1,50 @@
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
import { useEventListener } from '../../../../../generic/hooks';
import { messageTypes } from '../constants';
import useModalIFrameBehavior, { stateKeys } from './useModalIFrameData';
jest.mock('react', () => ({
...jest.requireActual('react'),
useCallback: jest.fn((cb, prereqs) => ({ cb, prereqs })),
}));
jest.mock('../../../../../generic/hooks', () => ({
useEventListener: jest.fn(),
}));
const state = mockUseKeyedState(stateKeys);
describe('useModalIFrameBehavior', () => {
beforeEach(() => {
jest.clearAllMocks();
state.mock();
});
describe('behavior', () => {
it('initializes modalOptions to closed', () => {
useModalIFrameBehavior();
state.expectInitializedWith(stateKeys.modalOptions, { open: false });
});
describe('eventListener', () => {
it('consumes modal events and opens sets modal options with open: true', () => {
useModalIFrameBehavior();
expect(useEventListener).toHaveBeenCalled();
const { cb, prereqs } = useEventListener.mock.calls[0][1];
expect(prereqs).toEqual([]);
const payload = { test: 'values' };
cb({ data: { type: messageTypes.modal, payload } });
expect(state.setState.modalOptions).toHaveBeenCalledWith({ ...payload, open: true });
});
});
});
describe('output', () => {
test('handleModalClose sets modal options to closed', () => {
useModalIFrameBehavior().handleModalClose();
state.expectSetStateCalledWith(stateKeys.modalOptions, { open: false });
});
it('forwards modalOptions from state value', () => {
const modalOptions = { test: 'options' };
state.mockVal(stateKeys.modalOptions, modalOptions);
expect(useModalIFrameBehavior().modalOptions).toEqual(modalOptions);
});
});
});

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils/dist';
import { useModel } from '../../../../../generic/model-store';
import { modelKeys } from '../constants';
export const stateKeys = StrictDict({
shouldDisplay: 'shouldDisplay',
});
/**
* @return {bool} should the honor code be displayed?
*/
const useShouldDisplayHonorCode = ({ id, courseId }) => {
const [shouldDisplay, setShouldDisplay] = useKeyedState(stateKeys.shouldDisplay, false);
const { graded } = useModel(modelKeys.units, id);
const { userNeedsIntegritySignature } = useModel(modelKeys.coursewareMeta, courseId);
React.useEffect(() => {
setShouldDisplay(userNeedsIntegritySignature && graded);
}, [setShouldDisplay, userNeedsIntegritySignature]);
return shouldDisplay;
};
export default useShouldDisplayHonorCode;

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { getEffects, mockUseKeyedState } from '@edx/react-unit-test-utils';
import { useModel } from '../../../../../generic/model-store';
import { modelKeys } from '../constants';
import useShouldDisplayHonorCode, { stateKeys } from './useShouldDisplayHonorCode';
jest.mock('react', () => ({
...jest.requireActual('react'),
useEffect: jest.fn(),
}));
jest.mock('../../../../../generic/model-store', () => ({
useModel: jest.fn(),
}));
const state = mockUseKeyedState(stateKeys);
const props = {
id: 'test-id',
courseId: 'test-course-id',
};
const mockModels = (graded, userNeedsIntegritySignature) => {
useModel.mockImplementation((key) => (
(key === modelKeys.units) ? { graded } : { userNeedsIntegritySignature }
));
};
describe('useShouldDisplayHonorCode hook', () => {
beforeEach(() => {
jest.clearAllMocks();
mockModels(false, false);
state.mock();
});
describe('behavior', () => {
it('initializes shouldDisplay to false', () => {
useShouldDisplayHonorCode(props);
state.expectInitializedWith(stateKeys.shouldDisplay, false);
});
describe('effect - on userNeedsIntegritySignature', () => {
describe('graded and needs integrity signature', () => {
it('sets shouldDisplay(true)', () => {
mockModels(true, true);
useShouldDisplayHonorCode(props);
const cb = getEffects([state.setState.shouldDisplay, true], React)[0];
cb();
expect(state.setState.shouldDisplay).toHaveBeenCalledWith(true);
});
});
describe('not graded', () => {
it('sets should not display', () => {
mockModels(true, false);
useShouldDisplayHonorCode(props);
const cb = getEffects([state.setState.shouldDisplay, false], React)[0];
cb();
expect(state.setState.shouldDisplay).toHaveBeenCalledWith(false);
});
});
describe('does not need integrity signature', () => {
it('sets should not display', () => {
mockModels(false, true);
useShouldDisplayHonorCode(props);
const cb = getEffects([state.setState.shouldDisplay, true], React)[0];
cb();
expect(state.setState.shouldDisplay).toHaveBeenCalledWith(false);
});
});
});
});
describe('output', () => {
it('returns shouldDisplay value from state', () => {
const testValue = 'test-value';
state.mockVal(stateKeys.shouldDisplay, testValue);
expect(useShouldDisplayHonorCode(props)).toEqual(testValue);
});
});
});

View File

@@ -0,0 +1,73 @@
import PropTypes from 'prop-types';
import React from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useModel } from '../../../../generic/model-store';
import BookmarkButton from '../../bookmark/BookmarkButton';
import messages from '../messages';
import ContentIFrame from './ContentIFrame';
import UnitSuspense from './UnitSuspense';
import { modelKeys, views } from './constants';
import { useExamAccess, useShouldDisplayHonorCode } from './hooks';
import { getIFrameUrl } from './urls';
const Unit = ({
courseId,
format,
onLoaded,
id,
}) => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const examAccess = useExamAccess({ id });
const shouldDisplayHonorCode = useShouldDisplayHonorCode({ courseId, id });
const unit = useModel(modelKeys.units, id);
const isProcessing = unit.bookmarkedUpdateState === 'loading';
const view = authenticatedUser ? views.student : views.public;
const iframeUrl = getIFrameUrl({
id,
view,
format,
examAccess,
});
return (
<div className="unit">
<h1 className="mb-0 h3">{unit.title}</h1>
<h2 className="sr-only">{formatMessage(messages.headerPlaceholder)}</h2>
<BookmarkButton
unitId={unit.id}
isBookmarked={unit.bookmarked}
isProcessing={isProcessing}
/>
<UnitSuspense {...{ courseId, id }} />
<ContentIFrame
elementId="unit-iframe"
id={id}
iframeUrl={iframeUrl}
loadingMessage={formatMessage(messages.loadingSequence)}
onLoaded={onLoaded}
shouldShowContent={!shouldDisplayHonorCode && !examAccess.blockAccess}
title={unit.title}
/>
</div>
);
};
Unit.propTypes = {
courseId: PropTypes.string.isRequired,
format: PropTypes.string,
id: PropTypes.string.isRequired,
onLoaded: PropTypes.func,
};
Unit.defaultProps = {
format: null,
onLoaded: undefined,
};
export default Unit;

View File

@@ -0,0 +1,191 @@
import React from 'react';
import { formatMessage, shallow } from '@edx/react-unit-test-utils/dist';
import { useModel } from '../../../../generic/model-store';
import BookmarkButton from '../../bookmark/BookmarkButton';
import UnitSuspense from './UnitSuspense';
import ContentIFrame from './ContentIFrame';
import Unit from '.';
import messages from '../messages';
import { getIFrameUrl } from './urls';
import { views } from './constants';
import * as hooks from './hooks';
jest.mock('./hooks', () => ({ useUnitData: jest.fn() }));
jest.mock('@edx/frontend-platform/i18n', () => {
const utils = jest.requireActual('@edx/react-unit-test-utils/dist');
return {
useIntl: () => ({ formatMessage: utils.formatMessage }),
defineMessages: m => m,
};
});
jest.mock('../../../../generic/PageLoading', () => 'PageLoading');
jest.mock('../../bookmark/BookmarkButton', () => 'BookmarkButton');
jest.mock('./ContentIFrame', () => 'ContentIFrame');
jest.mock('./UnitSuspense', () => 'UnitSuspense');
jest.mock('../honor-code', () => 'HonorCode');
jest.mock('../lock-paywall', () => 'LockPaywall');
jest.mock('../../../../generic/model-store', () => ({
useModel: jest.fn(),
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(v => v),
}));
jest.mock('./hooks', () => ({
useExamAccess: jest.fn(),
useShouldDisplayHonorCode: jest.fn(),
}));
jest.mock('./urls', () => ({
getIFrameUrl: jest.fn(),
}));
const props = {
courseId: 'test-course-id',
format: 'test-format',
onLoaded: jest.fn().mockName('props.onLoaded'),
id: 'test-props-id',
};
const context = { authenticatedUser: { test: 'user' } };
React.useContext.mockReturnValue(context);
const examAccess = {
accessToken: 'test-token',
blockAccess: false,
};
hooks.useExamAccess.mockReturnValue(examAccess);
hooks.useShouldDisplayHonorCode.mockReturnValue(false);
const unit = {
id: 'unit-id',
title: 'unit-title',
bookmarked: false,
bookmarkedUpdateState: 'pending',
};
useModel.mockReturnValue(unit);
let el;
describe('Unit component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<Unit {...props} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(hooks.useShouldDisplayHonorCode).toHaveBeenCalledWith({
courseId: props.courseId,
id: props.id,
});
});
});
describe('output', () => {
let component;
test('snapshot: not bookmarked, do not show content', () => {
el = shallow(<Unit {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
describe('BookmarkButton props', () => {
const renderComponent = () => {
el = shallow(<Unit {...props} />);
[component] = el.instance.findByType(BookmarkButton);
};
describe('not bookmarked, bookmark update loading', () => {
beforeEach(() => {
useModel.mockReturnValueOnce({ ...unit, bookmarkedUpdateState: 'loading' });
renderComponent();
});
test('snapshot', () => {
expect(component.snapshot).toMatchSnapshot();
});
test('props', () => {
expect(component.props.isBookmarked).toEqual(false);
expect(component.props.isProcessing).toEqual(true);
expect(component.props.unitId).toEqual(unit.id);
});
});
describe('bookmarked, bookmark update pending', () => {
beforeEach(() => {
useModel.mockReturnValueOnce({ ...unit, bookmarked: true });
renderComponent();
});
test('snapshot', () => {
expect(component.snapshot).toMatchSnapshot();
});
test('props', () => {
expect(component.props.isBookmarked).toEqual(true);
expect(component.props.isProcessing).toEqual(false);
expect(component.props.unitId).toEqual(unit.id);
});
});
});
test('UnitSuspense props', () => {
el = shallow(<Unit {...props} />);
[component] = el.instance.findByType(UnitSuspense);
expect(component.props.courseId).toEqual(props.courseId);
expect(component.props.id).toEqual(props.id);
});
describe('ContentIFrame props', () => {
const testComponentProps = () => {
expect(component.props.elementId).toEqual('unit-iframe');
expect(component.props.id).toEqual(props.id);
expect(component.props.loadingMessage).toEqual(formatMessage(messages.loadingSequence));
expect(component.props.onLoaded).toEqual(props.onLoaded);
expect(component.props.title).toEqual(unit.title);
};
const loadComponent = () => {
el = shallow(<Unit {...props} />);
[component] = el.instance.findByType(ContentIFrame);
};
describe('shouldShowContent', () => {
test('do not show content if displaying honor code', () => {
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(true);
loadComponent();
testComponentProps();
expect(component.props.shouldShowContent).toEqual(false);
});
test('do not show content if examAccess is blocked', () => {
hooks.useExamAccess.mockReturnValueOnce({ ...examAccess, blockAccess: true });
loadComponent();
testComponentProps();
expect(component.props.shouldShowContent).toEqual(false);
});
test('show content if not displaying honor code or blocked by exam access', () => {
loadComponent();
testComponentProps();
expect(component.props.shouldShowContent).toEqual(true);
});
});
describe('iframeUrl', () => {
test('loads iframe url with student view if authenticated user', () => {
loadComponent();
testComponentProps();
expect(component.props.iframeUrl).toEqual(getIFrameUrl({
id: props.id,
view: views.student,
format: props.format,
examAccess,
}));
});
test('loads iframe url with public view if no authenticated user', () => {
React.useContext.mockReturnValueOnce({});
loadComponent();
testComponentProps();
expect(component.props.iframeUrl).toEqual(getIFrameUrl({
id: props.id,
view: views.public,
format: props.format,
examAccess,
}));
});
});
});
});
});

View File

@@ -0,0 +1,28 @@
import { getConfig } from '@edx/frontend-platform';
import { stringify } from 'query-string';
export const iframeParams = {
show_title: 0,
show_bookmark: 0,
recheck_access: 1,
};
export const getIFrameUrl = ({
id,
view,
format,
examAccess,
}) => {
const xblockUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}`;
const params = stringify({
...iframeParams,
view,
...(format && { format }),
...(!examAccess.blockAccess && { exam_access: examAccess.accessToken }),
});
return `${xblockUrl}?${params}`;
};
export default {
getIFrameUrl,
};

View File

@@ -0,0 +1,42 @@
import { getConfig } from '@edx/frontend-platform';
import { stringify } from 'query-string';
import { getIFrameUrl, iframeParams } from './urls';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('query-string', () => ({
stringify: jest.fn((...args) => ({ stringify: args })),
}));
const config = { LMS_BASE_URL: 'test-lms-url' };
getConfig.mockReturnValue(config);
const props = {
id: 'test-id',
view: 'test-view',
format: 'test-format',
examAccess: { blockAccess: false, accessToken: 'test-access-token' },
};
describe('urls module', () => {
describe('getIFrameUrl', () => {
test('format provided, exam access and token available', () => {
const params = stringify({
...iframeParams,
view: props.view,
format: props.format,
exam_access: props.examAccess.accessToken,
});
expect(getIFrameUrl(props)).toEqual(`${config.LMS_BASE_URL}/xblock/${props.id}?${params}`);
});
test('no format provided, exam access blocked', () => {
const params = stringify({ ...iframeParams, view: props.view });
expect(getIFrameUrl({
id: props.id,
view: props.view,
examAccess: { blockAccess: true },
})).toEqual(`${config.LMS_BASE_URL}/xblock/${props.id}?${params}`);
});
});
});

View File

@@ -5,8 +5,6 @@ import React, {
} from 'react';
import { getLocalStorage, setLocalStorage } from '../../../data/localStorage';
import { getSessionStorage } from '../../../data/sessionStorage';
import { useModel } from '../../../generic/model-store';
import SidebarContext from './SidebarContext';
import { SIDEBARS } from './sidebars';
@@ -15,32 +13,18 @@ const SidebarProvider = ({
unitId,
children,
}) => {
const { verifiedMode } = useModel('courseHomeMeta', courseId);
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth;
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth;
const showNotificationsOnLoad = getSessionStorage(`notificationTrayStatus.${courseId}`) !== 'closed';
const query = new URLSearchParams(window.location.search);
if (query.get('sidebar') === 'true') {
localStorage.setItem('showDiscussionSidebar', true);
}
const showDiscussionSidebar = localStorage.getItem('showDiscussionSidebar') !== 'false';
const showNotificationSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad)
? SIDEBARS.NOTIFICATIONS.ID
: null;
const initialSidebar = showDiscussionSidebar
? SIDEBARS.DISCUSSIONS.ID
: showNotificationSidebar;
const initialSidebar = (shouldDisplaySidebarOpen || query.get('sidebar') === 'true') ? SIDEBARS.DISCUSSIONS.ID : null;
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);
const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`));
const [upgradeNotificationCurrentState, setUpgradeNotificationCurrentState] = useState(getLocalStorage(`upgradeNotificationCurrentState.${courseId}`));
useEffect(() => {
// As a one-off set initial sidebar if the verified mode data has just loaded
if (verifiedMode && currentSidebar === null && initialSidebar) {
setCurrentSidebar(initialSidebar);
}
setCurrentSidebar(SIDEBARS.DISCUSSIONS.ID);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialSidebar, verifiedMode]);
}, [unitId]);
const onNotificationSeen = useCallback(() => {
setNotificationStatus('inactive');
@@ -49,11 +33,6 @@ const SidebarProvider = ({
const toggleSidebar = useCallback((sidebarId) => {
// Switch to new sidebar or hide the current sidebar
if (currentSidebar === SIDEBARS.DISCUSSIONS.ID) {
localStorage.setItem('showDiscussionSidebar', false);
} else if (sidebarId === SIDEBARS.DISCUSSIONS.ID) {
localStorage.setItem('showDiscussionSidebar', true);
}
setCurrentSidebar(sidebarId === currentSidebar ? null : sidebarId);
}, [currentSidebar]);

View File

@@ -57,4 +57,5 @@ Factory.define('courseMetadata')
related_programs: null,
user_needs_integrity_signature: false,
recommendations: null,
learning_assistant_launch_url: null,
});

View File

@@ -122,6 +122,7 @@ function normalizeMetadata(metadata) {
relatedPrograms: camelCaseObject(data.related_programs),
userNeedsIntegritySignature: data.user_needs_integrity_signature,
canAccessProctoredExams: data.can_access_proctored_exams,
learningAssistantLaunchUrl: data.learning_assistant_launch_url,
};
}

View File

@@ -1,6 +1,6 @@
import { Pact, Matchers } from '@pact-foundation/pact';
import path from 'path';
import { mergeConfig, getConfig } from '@edx/frontend-platform';
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import {
getCourseMetadata,
@@ -19,8 +19,8 @@ import {
const {
somethingLike: like, term, boolean, string, eachLike, integer,
} = Matchers;
const provider = new Pact({
} = MatchersV3;
const provider = new PactV3({
consumer: 'frontend-app-learning',
provider: 'lms',
log: path.resolve(process.cwd(), 'src/courseware/data/pact-tests/logs', 'pact.log'),
@@ -33,37 +33,13 @@ const provider = new Pact({
describe('Courseware Service', () => {
beforeAll(async () => {
initializeMockApp();
await provider
.setup()
.then((options) => mergeConfig({
LMS_BASE_URL: `http://localhost:${options.port}`,
}, 'Custom app config for pact tests'));
mergeConfig({
LMS_BASE_URL: 'http://localhost:8081',
}, 'Custom app config for pact tests');
});
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
describe('When a request to get a learning sequence outline is made', () => {
it('returns a normalized outline', async () => {
await provider.addInteraction({
state: `Outline exists for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: {
sections: [],
sequences: {},
},
},
},
});
const normalizedOutline = {
courses: {
'course-v1:edX+DemoX+Demo_Course': {
@@ -76,74 +52,32 @@ describe('Courseware Service', () => {
sections: {},
sequences: {},
};
const response = await getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
setTimeout(() => {
provider.addInteraction({
state: `Outline exists for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: {
sections: [],
sequences: {},
},
},
},
});
const response = getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
}, 100);
});
it('skips unreleased sequences', async () => {
await provider.addInteraction({
state: `Outline exists with unreleased sequences for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: like({
sections: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially released',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
],
effective_start: null,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@nope',
title: 'Wholly unreleased',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
],
effective_start: '9999-07-01T17:00:00Z',
},
],
sequences: {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
accessible: true,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
title: 'Released and inaccessible',
accessible: false,
effective_start: '2019-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
title: 'Unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
title: 'Still unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
},
}),
},
},
});
const normalizedOutline = {
courses: {
'course-v1:edX+DemoX+Demo_Course': {
@@ -179,120 +113,78 @@ describe('Courseware Service', () => {
},
},
};
const response = await getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
setTimeout(() => {
provider.addInteraction({
state: `Outline exists with unreleased sequences for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: like({
sections: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially released',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
],
effective_start: null,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@nope',
title: 'Wholly unreleased',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
],
effective_start: '9999-07-01T17:00:00Z',
},
],
sequences: {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
accessible: true,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
title: 'Released and inaccessible',
accessible: false,
effective_start: '2019-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
title: 'Unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
title: 'Still unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
},
}),
},
},
});
const response = getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
}, 100);
});
});
describe('When a request to get course metadata is made', () => {
it('returns normalized course metadata', async () => {
await provider.addInteraction({
state: `course metadata exists for course_id ${courseId}`,
uponReceiving: 'a request to get course metadata',
withRequest: {
method: 'GET',
path: `/api/courseware/course/${courseId}`,
query: {
browser_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
},
willRespondWith: {
status: 200,
body: {
access_expiration: {
expiration_date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
masquerading_expired_course: boolean(false),
upgrade_deadline: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
upgrade_url: string('link'),
},
can_show_upgrade_sock: boolean(false),
content_type_gating_enabled: boolean(false),
end: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
enrollment: {
mode: term({
generate: 'audit',
matcher: '^(audit|verified)$',
}),
is_active: boolean(true),
},
enrollment_start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
enrollment_end: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
id: term({
generate: 'course-v1:edX+DemoX+Demo_Course',
matcher: opaqueKeysRegex,
}),
license: string('all-rights-reserved'),
name: like('Demonstration Course'),
offer: {
code: string('code'),
expiration_date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
original_price: string('$99'),
discounted_price: string('$99'),
percentage: integer(50),
upgrade_url: string('url'),
},
related_programs: null,
short_description: like(''),
start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
user_timezone: null,
verified_mode: like({
access_expiration_date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
currency: 'USD',
currency_symbol: '$',
price: 149,
sku: '8CF08E5',
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
}),
show_calculator: boolean(false),
original_user_is_staff: boolean(true),
is_staff: boolean(true),
course_access: like({
has_access: true,
error_code: null,
developer_message: null,
user_message: null,
additional_context_user_message: null,
user_fragment: null,
}),
notes: { enabled: boolean(false), visible: boolean(true) },
marketing_url: null,
user_has_passing_grade: boolean(false),
course_exit_page_is_active: boolean(false),
certificate_data: {
cert_status: string('audit_passing'), cert_web_view_url: null, certificate_available_date: null,
},
verify_identity_url: null,
verification_status: string('none'),
linkedin_add_to_profile_url: null,
user_needs_integrity_signature: boolean(false),
},
},
});
it('returns normalized course metadata', () => {
const normalizedCourseMetadata = {
accessExpiration: {
expirationDate: '2013-02-05T05:00:00Z',
@@ -336,57 +228,125 @@ describe('Courseware Service', () => {
linkedinAddToProfileUrl: null,
relatedPrograms: null,
userNeedsIntegritySignature: false,
learningAssistantLaunchUrl: null,
};
const response = await getCourseMetadata(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(normalizedCourseMetadata);
setTimeout(() => {
provider.addInteraction({
state: `course metadata exists for course_id ${courseId}`,
uponReceiving: 'a request to get course metadata',
withRequest: {
method: 'GET',
path: `/api/courseware/course/${courseId}`,
query: {
browser_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
},
willRespondWith: {
status: 200,
body: {
access_expiration: {
expiration_date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
masquerading_expired_course: boolean(false),
upgrade_deadline: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
upgrade_url: string('link'),
},
can_show_upgrade_sock: boolean(false),
content_type_gating_enabled: boolean(false),
end: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
enrollment: {
mode: term({
generate: 'audit',
matcher: '^(audit|verified)$',
}),
is_active: boolean(true),
},
enrollment_start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
enrollment_end: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
id: term({
generate: 'course-v1:edX+DemoX+Demo_Course',
matcher: opaqueKeysRegex,
}),
license: string('all-rights-reserved'),
name: like('Demonstration Course'),
offer: {
code: string('code'),
expiration_date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
original_price: string('$99'),
discounted_price: string('$99'),
percentage: integer(50),
upgrade_url: string('url'),
},
related_programs: null,
short_description: like(''),
start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
user_timezone: null,
verified_mode: like({
access_expiration_date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
currency: 'USD',
currency_symbol: '$',
price: 149,
sku: '8CF08E5',
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
}),
show_calculator: boolean(false),
original_user_is_staff: boolean(true),
is_staff: boolean(true),
course_access: like({
has_access: true,
error_code: null,
developer_message: null,
user_message: null,
additional_context_user_message: null,
user_fragment: null,
}),
notes: { enabled: boolean(false), visible: boolean(true) },
marketing_url: null,
user_has_passing_grade: boolean(false),
course_exit_page_is_active: boolean(false),
certificate_data: {
cert_status: string('audit_passing'), cert_web_view_url: null, certificate_available_date: null,
},
verify_identity_url: null,
verification_status: string('none'),
linkedin_add_to_profile_url: null,
user_needs_integrity_signature: boolean(false),
learning_assistant_launch_url: null,
},
},
});
const response = getCourseMetadata(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(normalizedCourseMetadata);
}, 100);
});
});
describe('When a request to get sequence metadata is made', () => {
it('returns normalized sequence metadata ', async () => {
await provider.addInteraction({
state: `sequence metadata data exists for sequence_id ${sequenceId}`,
uponReceiving: 'a request to get sequence metadata',
withRequest: {
method: 'GET',
path: `/api/courseware/sequence/${sequenceId}`,
},
willRespondWith: {
status: 200,
body: {
items: eachLike({
content: '',
page_title: 'Pointing on a Picture',
type: 'problem',
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
bookmarked: false,
path: 'Example Week 1: Getting Started > Homework - Question Styles > Pointing on a Picture',
graded: true,
contains_content_type_gated_content: false,
href: '',
}),
item_id: string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'),
is_time_limited: boolean(false),
is_proctored: boolean(false),
is_hidden_after_due: boolean(false),
position: null,
tag: boolean('sequential'),
banner_text: null,
save_position: boolean(false),
show_completion: boolean(false),
gated_content: like({
prereq_id: null,
prereq_url: null,
prereq_section_name: null,
gated: false,
gated_section_name: 'Homework - Question Styles',
}),
display_name: boolean('Homework - Question Styles'),
format: boolean('Homework'),
},
},
});
it('returns normalized sequence metadata ', () => {
const normalizedSequenceMetadata = {
sequence: {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
@@ -423,102 +383,154 @@ describe('Courseware Service', () => {
containsContentTypeGatedContent: false,
}],
};
const response = await getSequenceMetadata(sequenceId);
expect(response).toBeTruthy();
expect(response).toEqual(normalizedSequenceMetadata);
setTimeout(() => {
provider.addInteraction({
state: `sequence metadata data exists for sequence_id ${sequenceId}`,
uponReceiving: 'a request to get sequence metadata',
withRequest: {
method: 'GET',
path: `/api/courseware/sequence/${sequenceId}`,
},
willRespondWith: {
status: 200,
body: {
items: eachLike({
content: '',
page_title: 'Pointing on a Picture',
type: 'problem',
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
bookmarked: false,
path: 'Example Week 1: Getting Started > Homework - Question Styles > Pointing on a Picture',
graded: true,
contains_content_type_gated_content: false,
href: '',
}),
item_id: string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'),
is_time_limited: boolean(false),
is_proctored: boolean(false),
is_hidden_after_due: boolean(false),
position: null,
tag: boolean('sequential'),
banner_text: null,
save_position: boolean(false),
show_completion: boolean(false),
gated_content: like({
prereq_id: null,
prereq_url: null,
prereq_section_name: null,
gated: false,
gated_section_name: 'Homework - Question Styles',
}),
display_name: boolean('Homework - Question Styles'),
format: boolean('Homework'),
},
},
});
const response = getSequenceMetadata(sequenceId);
expect(response).toBeTruthy();
expect(response).toEqual(normalizedSequenceMetadata);
}, 100);
});
});
describe('When a request to set sequence position against Unit Index is made', () => {
it('returns if the request was success or failure', async () => {
await provider.addInteraction({
state: `sequence position data exists for course_id ${courseId}, sequence_id ${sequenceId} and activeUnitIndex 0`,
uponReceiving: 'a request to set sequence position against activeUnitIndex',
withRequest: {
method: 'POST',
path: `/courses/${courseId}/xblock/${sequenceId}/handler/goto_position`,
body: { position: 1 }, // Position is 1-indexed on the provider side and 0-indexed in the consumer side.
},
willRespondWith: {
status: 200,
body: {
success: boolean(true),
setTimeout(() => {
provider.addInteraction({
state: `sequence position data exists for course_id ${courseId}, sequence_id ${sequenceId} and activeUnitIndex 0`,
uponReceiving: 'a request to set sequence position against activeUnitIndex',
withRequest: {
method: 'POST',
path: `/courses/${courseId}/xblock/${sequenceId}/handler/goto_position`,
body: { position: 1 }, // Position is 1-indexed on the provider side and 0-indexed in the consumer side.
},
},
});
const response = await postSequencePosition(courseId, sequenceId, 0);
expect(response).toBeTruthy();
expect(response).toEqual({ success: true });
willRespondWith: {
status: 200,
body: {
success: boolean(true),
},
},
});
const response = postSequencePosition(courseId, sequenceId, 0);
expect(response).toBeTruthy();
expect(response).toEqual({ success: true });
}, 100);
});
});
describe('When a request to get completion block is made', () => {
it('returns the completion status', async () => {
await provider.addInteraction({
state: `completion block data exists for course_id ${courseId}, sequence_id ${sequenceId} and usageId ${usageId}`,
uponReceiving: 'a request to get completion block',
withRequest: {
method: 'POST',
path: `/courses/${courseId}/xblock/${sequenceId}/handler/get_completion`,
body: { usage_key: usageId },
},
willRespondWith: {
status: 200,
body: {
complete: boolean(true),
setTimeout(() => {
provider.addInteraction({
state: `completion block data exists for course_id ${courseId}, sequence_id ${sequenceId} and usageId ${usageId}`,
uponReceiving: 'a request to get completion block',
withRequest: {
method: 'POST',
path: `/courses/${courseId}/xblock/${sequenceId}/handler/get_completion`,
body: { usage_key: usageId },
},
},
});
const response = await getBlockCompletion(courseId, sequenceId, usageId);
expect(response).toBeTruthy();
expect(response).toEqual(true);
willRespondWith: {
status: 200,
body: {
complete: boolean(true),
},
},
});
const response = getBlockCompletion(courseId, sequenceId, usageId);
expect(response).toBeTruthy();
expect(response).toEqual(true);
}, 100);
});
});
describe('When a request to get resume block is made', () => {
it('returns block id, section id and unit id of the resume block', async () => {
await provider.addInteraction({
state: `Resume block exists for course_id ${courseId}`,
uponReceiving: 'a request to get Resume block',
withRequest: {
method: 'GET',
path: `/api/courseware/resume/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
block_id: string('642fadf46d074aabb637f20af320fb31'),
section_id: string('642fadf46d074aabb637f20af320fb87'),
unit_id: string('642fadf46d074aabb637f20af320fb99'),
},
},
});
const camelCaseResponse = {
blockId: '642fadf46d074aabb637f20af320fb31',
sectionId: '642fadf46d074aabb637f20af320fb87',
unitId: '642fadf46d074aabb637f20af320fb99',
};
const response = await getResumeBlock(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse);
setTimeout(() => {
provider.addInteraction({
state: `Resume block exists for course_id ${courseId}`,
uponReceiving: 'a request to get Resume block',
withRequest: {
method: 'GET',
path: `/api/courseware/resume/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
block_id: string('642fadf46d074aabb637f20af320fb31'),
section_id: string('642fadf46d074aabb637f20af320fb87'),
unit_id: string('642fadf46d074aabb637f20af320fb99'),
},
},
});
const response = getResumeBlock(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse);
}, 100);
});
});
describe('When a request to send activation email is made', () => {
it('returns status code 200', async () => {
await provider.addInteraction({
state: 'A logged-in user may or may not be active',
uponReceiving: 'a request to send activation email',
withRequest: {
method: 'POST',
path: '/api/send_account_activation_email',
},
willRespondWith: {
status: 200,
},
});
const response = await sendActivationEmail();
expect(response).toEqual('');
it('returns status code 200', () => {
setTimeout(() => {
provider.addInteraction({
state: 'A logged-in user may or may not be active',
uponReceiving: 'a request to send activation email',
withRequest: {
method: 'POST',
path: '/api/send_account_activation_email',
},
willRespondWith: {
status: 200,
},
});
const response = sendActivationEmail();
expect(response).toEqual('');
}, 100);
});
});
});

View File

@@ -1,3 +1,7 @@
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as headerMessages } from '@edx/frontend-component-header';
import { messages as paragonMessages } from '@edx/paragon';
import arMessages from './messages/ar.json';
import frMessages from './messages/fr.json';
import es419Messages from './messages/es_419.json';
@@ -8,10 +12,14 @@ import ukMessages from './messages/uk.json';
import deMessages from './messages/de.json';
import ruMessages from './messages/ru.json';
import hiMessages from './messages/hi.json';
import faIRMessages from './messages/fa_IR.json';
import frCAMessages from './messages/fr_CA.json';
import dedeCAMessages from './messages/de_DE.json';
import ititCAMessages from './messages/it_IT.json';
import ptptCAMessages from './messages/pt_PT.json';
// no need to import en messages-- they are in the defaultMessage field
const messages = {
const appMessages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
@@ -20,9 +28,18 @@ const messages = {
it: itMessages,
de: deMessages,
hi: hiMessages,
'fa-ir': faIRMessages,
'fr-ca': frCAMessages,
ru: ruMessages,
uk: ukMessages,
'de-de': dedeCAMessages,
'it-it': ititCAMessages,
'pt-pt': ptptCAMessages,
};
export default messages;
export default [
paragonMessages,
appMessages,
footerMessages,
headerMessages,
];

View File

@@ -0,0 +1,452 @@
{
"learning.accessExpiration.deadline": "Upgraden Sie bis {date}, um unbegrenzten Zugang zu diesem Kurs zu erhalten, solange dieser auf dieser Seite existiert.",
"learning.accessExpiration.header": "Audit-Zugriff gültig bis {date}",
"learning.accessExpiration.body": "Sie verlieren am {date} jeglichen Zugriff auf diesen Kurs, einschließlich Ihres Fortschritts.",
"instructorToolbar.pageBanner.courseHasExpired": "Dieser Teilnehmer hat keinen Zugriff mehr auf diesen Kurs. Der Zugriff ist am {date} abgelaufen.",
"learning.accessExpiration.upgradeNow": "Jetzt aktualisieren",
"learning.activeEnterprise.alert": "{changeActiveEnterprise}.",
"learning.activeEnterprise.change.alert": "Jetzt Unternehmen wechseln",
"learning.outline.alert.start.short": "Der Kurs beginnt {timeRemaining} um {courseStartTime}.",
"learning.outline.alert.end.long": "Dieser Kurs endet am {courseEndDate} {timeRemaining}.",
"learning.outline.alert.end.calendar": "Vergessen Sie nicht, eine Kalendererinnerung einzurichten!",
"instructorToolbar.pageBanner.courseHasNotStarted": "Dieser Teilnehmer hat noch keinen Zugriff auf diesen Kurs. Der Kurs beginnt am {date}.",
"learning.enrollment.alert": "Sie müssen im Kurs eingeschrieben sein, um den Inhalt sehen zu können.",
"learning.staff.enrollment.alert": "Sie betrachten diesen Kurs als Dozent ohne eingeschrieben zu sein.",
"learning.enrollment.enrollNow.Inline": "Jetzt einschreiben",
"learning.enrollment.enrollNow.Sentence": "Jetzt einschreiben",
"learning.enrollment.success": "Sie haben sich erfolgreich für diesen Kurs angemeldet!",
"account-activation.alert.button": "Weiter zu {siteName}",
"account-activation.alert.message": "Wir haben eine E-Mail an {boldEmail} mit einem Link zur Aktivierung Ihres Kontos gesendet. Ist die E-Mail nicht angekommen? Überprüfen Sie Ihren Spam-Ordner oder {sendEmailTag}.",
"account-activation.resend.link": "E-Mail erneut senden",
"learning.logistration.alert": "Um den Inhalt des Kurses sehen zu können, müssen Sie sich erst {sign_in_link} oder {register_link}.",
"account-activation.alert.title": "Aktivieren Sie Ihr Konto um sich wieder anmelden zu können",
"learn.sequence.entranceExamTextNotPassing": "Um auf Kursmaterialien zugreifen zu können, müssen Sie bei dieser Prüfung mindestens {entranceExamMinimumScorePct} % erreichen. Ihre aktuelle Punktzahl beträgt {entranceExamCurrentScore} %.",
"learn.sequence.entranceExamTextPassed": "Ihre Punktzahl beträgt {entranceExamCurrentScore} %, damit haben Sie die Aufnahmeprüfung bestanden.",
"learning.dates.badge.completed": "Abgeschlossen",
"learning.dates.badge.dueNext": "Nächster Abgabetermin",
"learning.dates.badge.pastDue": "Überfällig",
"learning.dates.title": "Wichtige Daten",
"learning.dates.badge.today": "Heute",
"learning.dates.badge.unreleased": "Noch nicht veröffentlicht",
"learning.dates.badge.verifiedOnly": "Nur verifiziert",
"learning.goals.unsubscribe.contact": "kontaktieren Sie den Support",
"learning.goals.unsubscribe.description": "Sie erhalten keine E-Mail-Erinnerungen mehr für {courseTitle}.",
"learning.goals.unsubscribe.errorHeader": "Es ist ein Fehler aufgetreten",
"learning.goals.unsubscribe.goToDashboard": "Zur Übersicht",
"learning.goals.unsubscribe.header": "Sie haben sich von der E-Mail-Liste für die Kurserinnerungen abgemeldet",
"learning.goals.unsubscribe.loading": "Abmelden…",
"learning.goals.unsubscribe.errorDescription": "Wir konnten Sie nicht von Kurserinnerungs-E-Mails abmelden. Bitte versuchen Sie es später erneut oder {contactSupport} um Hilfe.",
"learning.outline.alert.cert.earnedNotAvailable": "Dieser Kurs endet am {courseEndDateFormatted}. Abschlussnoten und erworbene Zertifikate sind voraussichtlich nach {certificateAvailableDate} verfügbar.",
"cert.alert.earned.unavailable.header.v2": "Ihre Note und Ihr Zertifikat werden in Kürze verfügbar sein.",
"cert.alert.earned.ready.header": "Herzliche Glückwünsche! Ihr Zertifikat ist fertig.",
"cert.alert.notPassing.header": "Sie haben noch keinen Anspruch auf ein Zertifikat",
"cert.alert.notPassing.button": "Noten ansehen",
"learning.outline.alert.end.short": "Dieser Kurs endet in {timeRemaining} um {courseEndTime}.",
"alert.enroll": "um auf den vollständigen Kurs zuzugreifen.",
"learning.privateCourse.signInOrRegister": "{signIn} oder {register} und schreiben Sie sich dann für diesen Kurs ein.",
"learning.outline.alert.scheduled-content.heading": "Weitere Inhalte folgen in Kürze!",
"learning.outline.alert.scheduled-content.body": "Für diesen Kurs werden zu einem späteren Zeitpunkt weitere Inhalte veröffentlicht. Achten Sie auf E-Mail-Updates oder schauen Sie in diesem Kurs für Updates.",
"learning.outline.alert.scheduled-content.button": "Kursplan ansehen",
"learning.outline.dates.all": "Alle Kurstermine anzeigen",
"learning.outline.goalButton.casual.text": "1 Tag pro Woche",
"learning.outline.goalButton.screenReader.text": "Lässig",
"learning.outline.certificateAlt": "Beispielzertifikat",
"learning.outline.collapseAll": "Alles zusammenklappen",
"learning.outline.completedAssignment": "Beendet",
"learning.outline.completedSection": "Abgeschlossener Abschnitt",
"learning.outline.dates": "Wichtige Daten",
"learning.outline.editGoal": "Lernziel bearbeiten",
"learning.outline.expandAll": "Alle erweitern",
"learning.outline.goal": "Lernziel",
"learning.outline.goalReminderDetail": "Wenn wir feststellen, dass Sie Ihr Lernziel nicht erreicht haben, senden wir Ihnen eine E-Mail-Erinnerung.",
"learning.outline.goalUnsure": "Noch nicht sicher",
"learning.outline.handouts": "Kursmaterialien",
"learning.outline.incompleteAssignment": "Unvollständig",
"learning.outline.incompleteSection": "Unvollständiger Abschnitt",
"learning.outline.goalButton.intense.text": "5 Tage die Woche",
"learning.outline.goalButton.intense.title": "Intensiv",
"learning.outline.learnMore": "Mehr erfahren",
"learning.outline.altText.openSection": "Öffnen",
"learning.proctoringPanel.header": "Dieser Kurs enthält beaufsichtigte Prüfungen",
"learning.outline.goalButton.regular.text": "3 Tage die Woche",
"learning.outline.goalButton.regular.title": "Regulär",
"learning.outline.resumeBlurb": "Machen Sie dort weiter, wo Sie aufgehört haben",
"learning.outline.resume": "Kurs fortsetzen",
"learning.outline.setGoal": "Legen Sie zunächst ein Lernziel fest, indem Sie unten die Option auswählen, die Ihren Lernplan am besten beschreibt.",
"learning.outline.setGoalReminder": "Definieren Sie Ihre Lernzielerinnerungen",
"learning.outline.goalButton.casual.title": "Definieren Sie einen Lernzielstil.",
"learning.outline.setWeeklyGoal": "Setzen Sie sich ein wöchentliches Lernziel",
"learning.outline.setWeeklyGoalDetail": "Das Setzen eines Lernziels motiviert Sie, den Kurs zu beenden. Sie können es jederzeit anpassen.",
"learning.outline.start": "Kurs starten",
"learning.outline.startBlurb": "Beginnen Sie noch heute Ihren Kurs",
"learning.outline.tools": "Kurswerkzeuge",
"learning.outline.upgradeButton": "Upgrade ({symbol}{price})",
"learning.outline.upgradeTitle": "Erhalten Sie ein Zertifikat",
"learning.outline.welcomeMessage": "Willkommensnachricht",
"learning.outline.welcomeMessageShowMoreButton": "Mehr anzeigen",
"learning.outline.welcomeMessageShowLessButton": "Weniger anzeigen",
"learning.outline.goalWelcome": "Willkommen zu",
"learning.proctoringPanel.status.notStarted": "Nicht begonnen",
"learning.proctoringPanel.status.started": "Gestartet",
"learning.proctoringPanel.status.submitted": "Abgesendet",
"learning.proctoringPanel.status.verified": "Geprüft",
"learning.proctoringPanel.status.rejected": "Zurückgewiesen",
"learning.proctoringPanel.status.error": "Fehler",
"learning.proctoringPanel.status.otherCourseApproved": "In einem anderen Studiengang genehmigt",
"learning.proctoringPanel.status.expiringSoon": "Läuft bald ab",
"learning.proctoringPanel.status.expired": "Abgelaufen",
"learning.proctoringPanel.status": "Aktueller Onboarding-Status:",
"learning.proctoringPanel.message.notStarted": "Sie haben Ihre Onboarding-Prüfung noch nicht begonnen.",
"learning.proctoringPanel.message.started": "Sie haben Ihre Onboarding-Prüfung begonnen.",
"learning.proctoringPanel.message.submitted": "Sie haben Ihre Onboarding-Prüfung eingereicht.",
"learning.proctoringPanel.message.verified": "Ihre Onboarding-Prüfung wurde in diesem Kurs genehmigt.",
"learning.proctoringPanel.message.rejected": "Ihre Onboarding-Prüfung wurde abgelehnt. Bitte versuchen Sie das Onboarding erneut.",
"learning.proctoringPanel.message.error": "Während Ihrer Onboarding-Prüfung ist ein Fehler aufgetreten. Bitte versuchen Sie das Onboarding erneut.",
"learning.proctoringPanel.message.otherCourseApproved": "Ihre Onboarding-Prüfung wurde in einem anderen Kurs genehmigt.",
"learning.proctoringPanel.detail.otherCourseApproved": "Wenn sich Ihr Gerät geändert hat, empfehlen wir Ihnen, die Onboarding-Prüfung dieses Kurses zu absolvieren, um sicherzustellen, dass Ihre Einrichtung weiterhin die Anforderungen für die Aufsicht erfüllt.",
"learning.proctoringPanel.message.expiringSoon": "Ihr Onboarding-Profil wurde genehmigt. Ihr Onboarding-Status läuft jedoch bald ab. Bitte führen Sie das Onboarding erneut durch, um sicherzustellen, dass Sie weiterhin beaufsichtigte Prüfungen ablegen können.",
"learning.proctoringPanel.message.expired": "Ihr Onboarding-Status ist abgelaufen. Bitte schließen Sie das Onboarding erneut ab, um weiterhin beaufsichtigte Prüfungen ablegen zu können.",
"learning.proctoringPanel.generalInfo": "Sie müssen den Onboarding-Prozess abschließen, bevor Sie eine beaufsichtigte Prüfung ablegen.",
"learning.proctoringPanel.generalInfoSubmitted": "Ihr eingereichtes Profil wird überprüft.",
"learning.proctoringPanel.generalTime": "Die Überprüfung des Onboarding-Profils kann mehr als 2 Werktage dauern.",
"learning.proctoringPanel.onboardingButton": "Onboarding vervollständigt",
"learning.proctoringPanel.onboardingPracticeButton": "Onboarding-Prüfung anzeigen",
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding-Öffnungen: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Lesen Sie die Anweisungen und Systemanforderungen",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding überfällig",
"learning.outline.sequence-due-date-set": "{description} fällig {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "Um ein Zertifikat zu generieren, müssen Sie die ID-Verifizierung abschließen. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Präsentieren Sie Ihre Leistung noch heute auf LinkedIn oder in Ihrem Lebenslauf. Sie können Ihr Zertifikat jetzt herunterladen und jederzeit über Ihr Dashboard und Ihr Profil darauf zugreifen.",
"courseCelebration.certificateBody.notAvailable.endDate": "Abschlussnoten und erworbene Zertifikate sind voraussichtlich nach {endDate} verfügbar.",
"progress.certificateStatus.notPassingHeader": "Zertifikatsstatus",
"progress.certificateStatus.notPassingBody": "Um sich für ein Zertifikat zu qualifizieren, müssen Sie eine bestandene Note haben.",
"progress.certificateStatus.inProgressHeader": "Weitere Inhalte folgen in Kürze!",
"progress.certificateStatus.inProgressBody": "Es sieht so aus, als gäbe es in diesem Kurs weitere Inhalte, die in Zukunft veröffentlicht werden. Achten Sie auf E-Mail-Updates oder sehen Sie in Ihrem Kurs nach, wann diese Inhalte verfügbar sein werden.",
"progress.certificateStatus.requestableHeader": "Zertifikatsstatus",
"progress.certificateStatus.requestableBody": "Herzlichen Glückwunsch, Sie haben sich für ein Zertifikat qualifiziert! Um auf Ihr Zertifikat zuzugreifen, fordern Sie es unten an.",
"progress.certificateStatus.requestableButton": "Zertifikat anfordern",
"progress.certificateStatus.unverifiedHeader": "Zertifikatsstatus",
"progress.certificateStatus.unverifiedButton": "ID verifizieren",
"progress.certificateStatus.courseCelebration.verificationPending": "Ihre ID-Überprüfung steht noch aus und Ihr Zertifikat ist verfügbar, sobald es genehmigt wurde.",
"progress.certificateStatus.downloadableHeader": "Ihr Zertifikat liegt vor!",
"progress.certificateStatus.viewableButton": "Sehen Sie sich mein Zertifikat an",
"progress.certificateStatus.notAvailableHeader": "Zertifikatsstatus",
"progress.certificateBody.notAvailable.endDate": "Abschlussnoten und erworbene Zertifikate sind voraussichtlich nach {endDate} verfügbar.",
"progress.certificateStatus.upgradeHeader": "Erhalten Sie ein Zertifikat",
"progress.certificateStatus.upgradeBody": "Sie befinden sich in einem Audit Track und qualifizieren sich nicht für ein Zertifikat. Um auf ein Zertifikat hinzuarbeiten, upgraden Sie noch heute Ihren Kurs.",
"progress.certificateStatus.upgradeButton": "Jetzt aktualisieren",
"progress.certificateStatus.unverifiedHomeHeader.v2": "Überprüfen Sie Ihre Identität, um sich für ein Zertifikat zu qualifizieren.",
"progress.certificateStatus.unverifiedHomeButton": "Meine ID bestätigen",
"progress.certificateStatus.unverifiedHomeBody": "Um ein Zertifikat für diesen Kurs zu generieren, müssen Sie den ID-Verifizierungsprozess abschließen.",
"progress.completion.donut.label": "abgeschlossen",
"progress.completion.body": "Dies stellt dar, wie viel der Kursinhalte Sie abgeschlossen haben. Beachten Sie, dass einige Inhalte möglicherweise noch nicht veröffentlicht wurden.",
"progress.completion.tooltip.locked": "Inhalte, die Sie abgeschlossen haben.",
"progress.completion.header": "Kursabschluss",
"progress.completion.tooltip": "Inhalte, auf die Sie Zugriff haben und die Sie noch nicht abgeschlossen haben.",
"progress.completion.tooltip.complete": "Inhalt, der gesperrt und nur für diejenigen verfügbar ist, die ein Upgrade durchführen.",
"progress.completion.donut.percentComplete": "Sie haben {percent} % des Inhalts dieses Kurses abgeschlossen.",
"progress.completion.donut.percentIncomplete": "Sie haben {percent} % der Inhalte in diesem Kurs, auf die Sie Zugriff haben, noch nicht abgeschlossen.",
"progress.completion.donut.percentLocked": "{percent} % der Inhalte in diesem Kurs sind gesperrt und nur für diejenigen verfügbar, die ein Upgrade durchführen.",
"progress.creditInformation.creditNotEligible": "Sie können diesen Kurs nicht mehr anrechnen. Erfahren Sie mehr über {creditLink}.",
"progress.creditInformation.creditEligible": "\nSie haben die Voraussetzungen für die Anrechnung in diesem Kurs erfüllt. Gehen Sie zu Ihrem\n {dashboardLink}, um Kursguthaben zu erwerben. Oder erfahren Sie mehr über {creditLink}.",
"progress.creditInformation.creditPartialEligible": "Sie haben die Kreditvoraussetzungen noch nicht erfüllt. Erfahren Sie mehr über {creditLink}.",
"progress.creditInformation.completed": "Beendet",
"progress.creditInformation.courseCredit": "Kurskredit",
"progress.creditInformation.minimumGrade": "Mindestnote für Kreditpunkte ({minGrade}%)",
"progress.creditInformation.requirementsHeader": "Voraussetzungen für den Studienkredit",
"progress.creditInformation.upcoming": "Demnächst",
"progress.creditInformation.verificationFailed": "Überprüfung fehlgeschlagen",
"progress.creditInformation.verificationSubmitted": "Bestätigung eingereicht",
"progress.ungradedAlert": "Informationen zum Fortschritt bei nicht benoteten Aspekten des Kurses finden Sie in Ihrem {outlineLink}.",
"progress.footnotes.droppableAssignments": "Die niedrigsten {numDroppable, plural, one{# {assignmentType} Punktzahl ist} other{# {assignmentType} Punktzahlen werden}} fallen gelassen.",
"progress.assignmentType": "Auftragsart",
"progress.footnotes.backToContent": "Zurück zum Inhalt",
"progress.courseGrade.body": "Dies stellt Ihre gewichtete Note gegenüber der Note dar, die zum Bestehen dieses Kurses erforderlich ist.",
"progress.courseGrade.gradeBar.altText": "Ihre aktuelle Note ist {currentGrade} %. Zum Bestehen dieses Kurses ist eine gewichtete Note von {passingGrade} % erforderlich.",
"progress.courseGrade.footer.generic.passing": "Sie absolvieren derzeit diesen Kurs",
"progress.courseGrade.footer.nonPassing": "Zum Bestehen dieses Kurses ist eine gewichtete Note von {passingGrade} % erforderlich",
"progress.courseGrade.footer.passing": "Sie bestehen diesen Kurs derzeit mit der Note {letterGrade} ({minGrade}-{maxGrade}%)",
"progress.courseGrade.preview.headerLocked": "gesperrte Funktion",
"progress.courseGrade.preview.headerLimited": "eingeschränkte Funktion",
"progress.courseGrade.preview.header.ariaHidden": "Vorschau auf a",
"progress.courseGrade.preview.body.unlockCertificate": "Entsperren, um Noten anzuzeigen und auf ein Zertifikat hinzuarbeiten.",
"progress.courseGrade.partialpreview.body.unlockCertificate": "Entsperren, um auf ein Zertifikat hinzuarbeiten.",
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "Die Frist für das Upgrade in diesem Kurs ist abgelaufen.",
"progress.courseGrade.preview.button.upgrade": "Jetzt aktualisieren",
"progress.courseGrade.gradeRange.tooltip": "Notenbereiche für diesen Kurs:",
"progress.courseOutline": "Kursübersicht",
"progress.courseGrade.label.currentGrade": "Ihre aktuelle Note",
"progress.detailedGrades": "Detaillierte Noten",
"progress.detailedGrades.emptyTable": "Sie haben derzeit keine benoteten Problemergebnisse.",
"progress.footnotes.title": "Notenzusammenfassung Fußnoten",
"progress.gradeSummary.grade": "Note",
"progress.courseGrade.grades": "Noten",
"progress.courseGrade.gradesAndCredit": "Noten &amp; Kredit",
"progress.courseGrade.gradeRange.Tooltip": "Kurzinfo zum Notenbereich",
"progress.gradeSummary": "Zusammenfassung der Noten",
"progress.gradeSummary.limitedAccessExplanation": "Sie haben eingeschränkten Zugriff auf benotete Aufgaben im Rahmen des Audit-Tracks in diesem Kurs.",
"progress.gradeSummary.tooltip.alt": "Kurzinfo zur Notenzusammenfassung",
"progress.gradeSummary.tooltip.body": "Das Gewicht Ihrer Kursaufgabe wird von Ihrem Kursleiter festgelegt. Indem Sie Ihre Note mit der Gewichtung für diesen Aufgabentyp multiplizieren, wird Ihre gewichtete Note berechnet. Ihre gewichtete Note wird verwendet, um festzustellen, ob Sie den Kurs bestehen.",
"progress.noAcessToAssignmentType": "Sie haben keinen Zugriff auf Aufgaben des Typs {assignmentType}",
"progress.noAcessToSubsection": "Sie haben keinen Zugriff auf den Unterabschnitt {displayName}",
"progress.courseGrade.label.passingGrade": "Klasse bestehen",
"progress.detailedGrades.problemScore.label": "Problemwerte:",
"progress.detailedGrades.problemScore.toggleButton": "Einzelne Problembewertungen für {subsectionTitle} umschalten",
"progress.detailedGrades.overridden": "Die Note für den Abschnitt wurde überschrieben.",
"progress.score": "Punkte",
"progress.weight": "Gewichtung",
"progress.weightedGrade": "Gewichtete Note",
"progress.weightedGradeSummary": "Ihre aktuelle gewichtete Notenzusammenfassung",
"progress.header": "Dein Fortschritt",
"progress.header.targetUser": "Kursfortschritt für {username}",
"progress.link.studio": "Grading in Studio anzeigen",
"progress.relatedLinks.datesCard.description": "Eine Zeitplanansicht Ihrer Kurstermine und anstehenden Aufgaben.",
"progress.relatedLinks.datesCard.link": "Daten",
"progress.relatedLinks.outlineCard.description": "Ihre Kursinhalte aus der Vogelperspektive.",
"progress.relatedLinks.outlineCard.link": "Kursübersicht",
"progress.relatedLinks": "Ähnliche Links",
"datesBanner.suggestedSchedule": "Wir haben einen vorgeschlagenen Zeitplan erstellt, um Ihnen zu helfen, auf dem richtigen Weg zu bleiben. Aber keine Sorge es ist flexibel, sodass Sie in Ihrem eigenen Tempo lernen können.",
"datesBanner.upgradeToCompleteGradedBanner.header": "Upgrade, um dies freizuschalten.",
"datesBanner.upgradeToCompleteGradedBanner.body": "Sie sind Gasthörer für diesen Kurs, was bedeutet, dass Sie nicht an benoteten Aufgaben teilnehmen können. Um benotete Aufgaben im Rahmen dieses Kurses abzuschließen, können Sie noch heute upgraden.",
"datesBanner.upgradeToCompleteGradedBanner.button": "Jetzt aktualisieren",
"datesBanner.upgradeToResetBanner.body": "Sie können den Ablaufplan individuell anpassen und die überfälligen Aufgaben auf einen späteren Zeitpunkt verschieben. Keine Sorge—Ihr bisheriger Fortschritt geht dabei nicht verloren.",
"datesBanner.upgradeToResetBanner.button": "Jetzt aktualisieren, um Abgabetermine zu verschieben.",
"datesBanner.resetDatesBanner.header": "Es scheint, als wenn Sie ein paar wichtige Fristen unseres vorgeschlagenen Ablaufplans verpasst.",
"datesBanner.resetDatesBanner.body": "Sie können den Ablaufplan individuell anpassen und die überfälligen Aufgaben auf einen späteren Zeitpunkt verschieben. Keine Sorge—Ihr bisheriger Fortschritt geht dabei nicht verloren.",
"datesBanner.resetDatesBanner.button": "Verschiebe Abgabetermin",
"learn.navigation.course.tabs.label": "Kursmaterial",
"unit.bookmark.button.add.bookmark": "Diese Seite merken",
"unit.bookmark.button.remove.bookmark": "Lesezeichen gesetzt",
"learning.celebration.completed": "Sie haben gerade den ersten Abschnitt Ihres Studiums abgeschlossen.",
"learning.celebration.congrats": "Glückwunsch!",
"learning.celebration.earned": "Du hast es verdient!",
"learning.celebration.emailSubject": "Ich bin auf dem Weg, {title} online mit {platform} abzuschließen!",
"learning.celebration.forward": "Mach weiter",
"learning.celebration.goalMet": "Du hast dein Ziel erreicht!",
"learning.celebration.keepItUp": "Weiter so",
"learning.celebration.share": "Nimm dir einen Moment Zeit, um zu feiern und deine Fortschritte zu teilen.",
"learning.celebration.social": "Ich bin dabei, {title} online mit {platform} abzuschließen. Womit verbringst du deine Zeit mit Lernen?",
"learning.celebration.goalCongrats": "Herzlichen Glückwunsch, Sie haben Ihr Lernziel von {nTimes} pro Woche erreicht.",
"learning.celebration.setGoal": "Das Setzen eines Ziels kann Ihnen in Ihrem Kurs {strongText} helfen.",
"calculator.instructions.button.label": "Rechner Anleitung",
"calculator.instructions": "Ausführliche Informationen finden Sie im {expressions_link}.",
"calculator.instructions.support.title": "Hilfe-Center",
"calculator.instructions.useful.tips": "Nützliche Tipps:",
"calculator.hint1": "Verwenden Sie Klammern (), um Ausdrücke deutlich zu machen. Sie können Klammern innerhalb anderer Klammern verwenden.",
"calculator.hint2": "Bitte nutze keine Leerzeichen in deinem Ausdruck",
"calculator.hint3": "Für Konstanten muss die Multiplikation explizit gekennzeichnet werden (Beispiel: 5*c)",
"calculator.hint4": "Bei Einheitszusätzen muss die Einheit der Zahl direkt und ohne eingefuegte Leerzeichen folgen (Beispiel: 5m).",
"calculator.hint5": "Für Funktionen muss der Name der Funktion gefolgt vom Funktionsargument in Klammern eingegeben werden.",
"calculator.instruction.table.to.use.heading": "Zu nutzen",
"calculator.instruction.table.type.heading": "Typ",
"calculator.instruction.table.examples.heading": "Beispiele",
"calculator.instruction.table.to.use.numbers": "Zahlen",
"calculator.instruction.table.to.use.numbers.type1": "ganze Zahlen",
"calculator.instruction.table.to.use.numbers.type2": "Brüche",
"calculator.instruction.table.to.use.numbers.type3": "Dezimalstellen",
"calculator.instruction.table.to.use.operators": "Operatoren",
"calculator.instruction.table.to.use.operators.type1": "(addieren, subtrahieren, multiplizieren, dividieren)",
"calculator.instruction.table.to.use.operators.type2": "(Potenz erheben)",
"calculator.instruction.table.to.use.operators.type3": "(parallele Widerstände)",
"calculator.instruction.table.to.use.constants": "Konstanten",
"calculator.instruction.table.to.use.affixes": "Einheiten",
"calculator.instruction.table.to.use.affixes.type": "Prozentzeichen (%)",
"calculator.instruction.table.to.use.basic.functions": "Basisfunktionen",
"calculator.instruction.table.to.use.trig.functions": "Trigonometrische Funktionen",
"calculator.instruction.table.to.use.scientific.notation": "Wissenschaftliche Schreibweise",
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} und der Exponent",
"calculator.instruction.table.to.use.scientific.notation.type2": "{notationSyntax}-Notation",
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax} und der Exponent",
"calculator.button.label": "Taschenrechner",
"calculator.input.field.label": "Rechnereingabe",
"calculator.submit.button.label": "Berechne",
"calculator.result.field.label": "Rechenergebnis",
"calculator.result.field.placeholder": "Ergebnis",
"notes.button.show": "Notizen anzeigen",
"notes.button.hide": "Notizen ausblenden",
"courseExit.catalogSearchSuggestion": "Möchten Sie mehr erfahren? {searchOurCatalogLink}, um weitere Kurse und Programme zu finden, die Sie erkunden können.",
"courseCelebration.certificateBody.available": "\nPräsentieren Sie Ihre Leistung noch heute auf LinkedIn oder in Ihrem Lebenslauf. \nSie können Ihr Zertifikat jetzt herunterladen und jederzeit von Ihrem \n{dashboardLink} und {profileLink} darauf zugreifen.",
"courseCelebration.certificateBody.notAvailable.endDate.v2": "Dieser Kurs endet am {endDate}. Abschlussnoten und erworbene Zertifikate sind voraussichtlich nach {certAvailableDate} verfügbar.",
"courseCelebration.certificateBody.unverified": "Um ein Zertifikat zu generieren, müssen Sie die ID-Verifizierung abschließen. {idVerificationSupportLink} jetzt.",
"courseCelebration.certificateBody.upgradable": "Es ist noch nicht zu spät für ein Upgrade. Für {price} entsperren Sie den Zugriff auf alle benoteten Aufgaben in diesem Kurs. Nach Abschluss erhalten Sie ein verifiziertes Zertifikat, das ein wertvoller Nachweis ist, um Ihre Berufsaussichten zu verbessern und Ihre Karriere voranzutreiben oder Ihr Zertifikat in Schulbewerbungen hervorzuheben.",
"courseCelebration.upgradeDiscountCodePrompt": "Mit dem Rabattcode {code} erhalten Sie an der Kasse {percent} % Rabatt!",
"courseCelebration.recommendations.heading": "Bauen Sie Ihre Fähigkeiten mit diesen Kursen weiter aus!",
"courseCelebration.recommendations.label": "Kurs",
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {, } conjunction { {sp}und } other { }}",
"courseCelebration.recommendations.browse_catalog": "Entdecken Sie weitere Kurse",
"courseCelebration.recommendations.loading_recommendations": "Lade Empfehlungen",
"courseCelebration.recommendations.card.schools.label": "Schulen und Partner",
"courseCelebration.dashboardInfo": "Sie können auf diesen Kurs und seine Materialien auf Ihrem {dashboardLink} zugreifen.",
"courseExit.programs.applyForCredit": "Kurs-Guthaben beantragen",
"courseCelebration.certificateHeader.downloadable": "Ihr Zertifikat liegt vor!",
"courseCelebration.certificateHeader.notAvailable": "Ihre Note und Ihr Zertifikatsstatus werden in Kürze verfügbar sein.",
"courseCelebration.certificateBody.notAvailable.accessCertificate": "Wenn Sie die Mindestnote erreicht haben, wird Ihr Zertifikat automatisch ausgestellt.",
"courseCelebration.certificateHeader.unverified": "Sie müssen die Verifizierung abschließen, um Ihr Zertifikat zu erhalten.",
"courseCelebration.certificateHeader.requestable": "Gratulation, Sie haben sich für ein Zertifikat qualifiziert!",
"courseCelebration.certificateHeader.upgradable": "Führen Sie ein Upgrade durch, um ein verifiziertes Zertifikat zu erwerben",
"courseCelebration.certificateImage": "Musterzertifikat",
"courseCelebration.completedCourseHeader": "Sie haben Ihren Kurs abgeschlossen.",
"courseCelebration.congratulationsHeader": "Glückwunsch!",
"courseCelebration.congratulationsImage": "Vier Personen heben feierlich die Hände",
"courseExit.courseInProgressDescription": "Es sieht so aus, als gäbe es in diesem Kurs weitere Inhalte, die in Zukunft veröffentlicht werden. Achten Sie auf E-Mail-Updates oder sehen Sie in Ihrem Kurs nach, wann diese Inhalte verfügbar sein werden.",
"courseExit.courseInProgressHeader": "Weitere Inhalte folgen in Kürze!",
"courseExit.dashboardLink": "Übersicht",
"courseExit.endOfCourseDescription": "Leider haben Sie derzeit keinen Anspruch auf ein Zertifikat. Sie müssen eine Mindestnote erreichen, um sich für ein Zertifikat zu qualifizieren.",
"courseExit.endOfCourseHeader": "Sie haben das Ende des Kurses erreicht!",
"courseExit.endOfCourseTitle": "Ende des Kurses",
"courseExit.idVerificationSupportLink": "Erfahren Sie mehr über die Identitätsprüfung",
"courseCelebration.linkedinAddToProfileButton": "Zum LinkedIn-Profil hinzufügen",
"courseExit.programs.microBachelors.learnMore": "Erfahren Sie mehr darüber, wie Ihr MicroBachelors-Zeugnis für eine Anrechnung beantragt werden kann.",
"courseExit.programs.microMasters.learnMore": "Erfahren Sie mehr über das Verfahren zur Anwendung von MicroMasters-Zertifikaten auf Master-Abschlüsse.",
"courseExit.programs.microMasters.mastersMessage": "Wenn Sie daran interessiert sind, Ihr MicroMasters-Zertifikat für ein Masterprogramm zu verwenden, können Sie noch heute loslegen!",
"learn.sequence.navigation.complete.button": "Beenden Sie den Kurs",
"courseExit.nextButton.endOfCourse": "Weiter (Ende natürlich)",
"courseExit.profileLink": "Profil",
"courseExit.programs.lastCourse": "Sie haben den letzten Kurs in {title} abgeschlossen!",
"courseCelebration.requestCertificateBodyText": "Um auf Ihr Zertifikat zuzugreifen, fordern Sie es unten an.",
"courseCelebration.requestCertificateButton": "Zertifikat anfordern",
"courseExit.searchOurCatalogLink": "Suchen Sie in unserem Katalog",
"courseCelebration.shareMessage": "Teilen Sie Ihren Erfolg in den sozialen Medien oder per E-Mail.",
"courseExit.social.shareCompletionMessage": "Ich habe gerade {title} mit {platform} abgeschlossen!",
"courseExit.upgradeButton": "Upgrade jetzt durchführen",
"courseExit.upgradeLink": "Upgrade jetzt durchführen",
"courseCelebration.verificationPending": "Ihre ID-Überprüfung steht noch aus und Ihr Zertifikat ist verfügbar, sobald es genehmigt wurde.",
"courseExit.verifiedCertificateSupportLink": "Erfahren Sie mehr über verifizierte Zertifikate",
"courseCelebration.verifyIdentityButton": "ID jetzt verifizieren",
"courseCelebration.viewCertificateButton": "Sehen Sie sich mein Zertifikat an",
"courseExit.viewCourseScheduleButton": "Kursplan ansehen",
"courseExit.viewCoursesButton": "Sehen Sie sich meine Kurse an",
"courseExit.viewGradesButton": "Noten ansehen",
"courseExit.programCompletion.dashboardMessage": "Um Ihren Zertifikatsstatus anzuzeigen, sehen Sie im Abschnitt &quot;Programme&quot; Ihres {programLink} nach.",
"courseExit.upgradeFootnote": "Der Zugriff auf diesen Kurs und seine Materialien ist auf Ihrem Dashboard bis {expirationDate} verfügbar. Um diese Zugriffsfrist zu verlängern, {upgradeLink}.",
"learn.course.license.allRightsReserved.text": "Alle Rechte vorbehalten",
"learn.course.license.creativeCommons.terms.preamble": "Von Creative Commons lizenzierte Inhalte mit den folgenden Bedingungen:",
"learn.course.license.creativeCommons.terms.by": "Attribution",
"learn.course.license.creativeCommons.terms.nc": "Nicht-kommerziell",
"learn.course.license.creativeCommons.terms.nd": "No Derivatives",
"learn.course.license.creativeCommons.terms.sa": "Share Alike",
"learn.course.license.creativeCommons.terms.zero": "Keine Bedingungen",
"learn.course.license.creativeCommons.text": "Some Rights Reserved",
"learn.breadcrumb.navigation.course.home": "Kurs",
"notification.tray.container": "Benachrichtigungsfach",
"notification.open.button": "Benachrichtigungsleiste anzeigen",
"notification.close.button": "Schließen Sie die Benachrichtigungsleiste",
"responsive.close.notification": "Zurück zum Kurs",
"notification.tray.title": "Benachrichtigungen",
"notification.tray.no.message": "Sie haben derzeit keine neuen Benachrichtigungen.",
"learn.contentLock.content.locked": "Inhalt nicht zugänglich",
"learn.contentLock.complete.prerequisite": "Sie müssen die Voraussetzung erfüllen: &#39;&#39;{prereqSectionName}&#39;&#39;, um auf diesen Inhalt zugreifen zu können.",
"learn.contentLock.goToSection": "Gehen Sie zum Abschnitt „Voraussetzungen“.",
"learn.hiddenAfterDue.gradeAvailable": "Wenn Sie diese Aufgabe abgeschlossen haben, ist Ihre Note im {progressPage} verfügbar.",
"learn.hiddenAfterDue.header": "Die Einschreibungsfrist zu diesem Kurs ist abgelaufen.",
"learn.hiddenAfterDue.description": "Diese Aufgabe ist nicht mehr verfügbar, da die Frist abgelaufen ist.",
"learn.hiddenAfterDue.progressPage": "Fortschrittsseite",
"learn.honorCode.content": "Ehrlichkeit und akademische Integrität sind wichtig für {siteName} und die Institutionen, die Kurse und Programme auf der {siteName}-Website anbieten. Indem ich unten auf „Ich stimme zu“ klicke, bestätige ich, dass ich die {link} für die {siteName}-Site gelesen und verstanden habe und mich daran halten werde.",
"learn.honorCode.name": "Verhaltenskodex",
"learn.honorCode.cancel": "Löschen",
"learn.honorCode.agree": "Ich stimme zu",
"learn.lockPaywall.title": "Benotete Aufgaben sind gesperrt",
"learn.lockPaywall.content": "Führen Sie ein Upgrade durch, um Zugriff auf gesperrte Funktionen wie diese zu erhalten und Ihren Kurs optimal zu nutzen.",
"learn.lockPaywall.content.pastExpiration": "Die Upgrade-Frist für diesen Kurs ist abgelaufen. Um ein Upgrade durchzuführen, melden Sie sich für die nächste verfügbare Sitzung an.",
"learn.lockPaywall.courseDetails": "Kursdetails anzeigen",
"learn.lockPaywall.example.alt": "Beispielzertifikat",
"learn.lockPaywall.list.intro": "Wenn Sie ein Upgrade durchführen, können Sie:",
"learn.header.h2.placeholder": "Überschriften der Ebene 2 können in Zukunft von Kursanbietern erstellt werden.",
"learn.course.load.failure": "Beim Laden dieses Kurses ist ein Fehler aufgetreten.",
"learn.loading.honor.codk": "Ehrencode-Nachrichten werden geladen...",
"learn.loading.content.lock": "Nachrichten zu gesperrten Inhalten werden geladen...",
"learn.loading.learning.sequence": "Lernsequenz wird geladen...",
"learn.sequence.no.content": "Hier gibt es keinen Inhalt.",
"learn.sequence.navigation.next.button": "Weiter",
"learn.sequence.navigation.next.up.button": "Als nächstes: {title}",
"learn.sequence.navigation.previous.button": "Zurück",
"learn.course.sequence.navigation.mobile.menu": "{current} von {total}",
"learn.sequence.share.button": "Teilen Sie diesen Inhalt",
"learn.sequence.share.modal.title": "Titel",
"learn.sequence.share.modal.body": "Kopieren Sie den Link unten, um diesen Inhalt zu teilen.",
"learn.sequence.share.quote": "Hier ist ein lustiger Clip aus einem Kurs, den ich bei @edXonline belege.\n",
"discussions.sidebar.title": "Diskussionen",
"discussions.sidebar.open.button": "Diskussionsablage anzeigen",
"learn.redirect.interstitial.message": "Umleitung...",
"learn.loading.error": "Fehler: {error}",
"learning.celebration.emailBody": "Womit verbringst du deine Zeit mit Lernen?",
"learning.social.shareEmail": "Teilen Sie Ihren Fortschritt per E-Mail.",
"learning.social.shareService": "Teilen Sie Ihren Fortschritt auf {service}.",
"general.altText.close": "Schließen",
"learning.logistration.register": "registrieren",
"learning.logistration.login": "Anmelden",
"general.signIn.sentenceCase": "Anmelden",
"learn.course.tabs.navigation.overflow.menu": "Mehr...",
"learning.offer.screenReaderPrices": "Originalpreis: {originalPrice}, Rabattpreis: {discountedPrice}",
"learning.upgradeButton.screenReaderInlinePrices": "Originalpreis: {originalPrice}",
"learning.upgradeButton.buttonText": "Upgrade für {pricing}",
"learning.upgradeNowButton.buttonText": "Upgrade jetzt für {pricing}",
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "einschließlich etwaiger Fortschritte",
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "Vorteile des Upgrades",
"learning.generic.upgradeNotification.expirationAccessLoss": "Am {date} verlieren Sie jeglichen Zugriff auf diesen Kurs, {includingAnyProgress}.",
"learning.generic.upgradeNotification.expirationVerifiedCert": "Durch das Upgrade Ihres Kurses können Sie ein verifiziertes Zertifikat erwerben und zahlreiche Funktionen freischalten. Mehr unter {benefitsOfUpgrading}.",
"learning.generic.upgradeNotification.pastExpiration.content": "Die Upgrade-Frist für diesen Kurs ist abgelaufen. Um ein Upgrade durchzuführen, melden Sie sich für die nächste verfügbare Sitzung an.",
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, one {Tag} other {Tage}} übrig",
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural, one {Stunde} other {Stunden}} übrig",
"learning.generic.upgradeNotification.expirationMinutes": "Weniger als 1 Stunde übrig",
"learning.generic.upgradeNotification.expiration": "Der Kurszugriff läuft am {date} ab",
"learning.generic.upgradeNotification.pastExpiration.banner": "Upgrade-Frist am {date} abgelaufen",
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% Rabatt für Erstlerner",
"learning.generic.upgradeNotification.accessExpiration": "Führen Sie noch heute ein Upgrade für Ihren Kurs durch",
"learning.generic.upgradeNotification.accessExpirationUrgent": "Ablauf des Kurszugriffs",
"learning.generic.upgradeNotification.accessExpirationPast": "Ablauf des Kurszugriffs",
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Streben Sie ein verifiziertes Zertifikat an",
"learning.generic.upgradeNotification.code": "Verwenden Sie den Code {code} an der Kasse",
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "verifiziertes Zertifikat",
"learning.generic.upsell.verifiedCertBullet": "Bekommen Sie ein {verifiedCertLink} für den Abschluss, zur Nutzung in Ihrem Lebenslauf",
"learning.generic.upsell.unlockGradedBullet.gradedAssignments": "benotete Aufgaben",
"learning.generic.upsell.unlockGradedBullet": "Entsperren Sie Ihren Zugang zu allen Kursaktivitäten, einschließlich {gradedAssignmentsInBoldText}",
"learning.generic.upsell.fullAccessBullet.fullAccess": "Voller Zugriff",
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} auf Kursinhalte und -materialien, auch nach Kursende",
"learning.generic.upsell.supportMissionBullet.mission": "Mission",
"learning.generic.upsell.supportMissionBullet": "Unterstützen Sie unser {missionInBoldText} unter {siteName}",
"masquerade-widget.userName.error.generic": "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"masquerade-widget.userName.input.placeholder": "Benutzername oder E-Mail-Adresse",
"masquerade-widget.userName.input.label": "Geben Sie vor, dieser Benutzer zu sein",
"tours.abandonTour.launchTourCheckpoint.body": "Wissen Sie nicht weiter? Die jederzeit verfügbare Tour hilft Ihnen mit einfachen Tipps, das Beste aus Ihrem Lernerlebnis zu machen.",
"tours.sequenceNavigationCheckpoint.body": "Die obere Leiste in Ihrem Kurs erlaubt Ihnen, zu verschiedenen Abschnitten zu springen. Sie zeigt Ihnen auch, was auf Sie zukommt.",
"tours.existingUserTour.launchTourCheckpoint.body": "Wir haben kürzlich die Benutzerführung geändert. Brauchen Sie Hilfe, sich wieder zurecht zu finden? Machen Sie eine Tour, um mehr zu erfahren.",
"tours.button.dismiss": "Tour Beenden",
"tours.button.next": "Weiter",
"tours.button.okay": "Okay",
"tours.button.beginTour": "Rundgang beginnen",
"tours.button.launchTour": "Rundgang starten",
"tours.newUserModal.body": "Kommen Sie mit auf einen kurzen Rundgang durch {siteName}, damit Sie Ihren Kurs optimal nutzen können.",
"tours.newUserModal.title.welcome": "Willkommen bei Ihrem",
"tours.button.skipForNow": "Jetzt Nicht",
"tours.datesCheckpoint.body": "Wichtige Termine können Ihnen helfen, am Ball zu bleiben.",
"tours.datesCheckpoint.title": "Behalten Sie wichtige Termine im Auge",
"tours.outlineCheckpoint.body": "Sie können Abschnitte des Kurses anhand der folgenden Gliederung erkunden.",
"tours.outlineCheckpoint.title": "Machen Sie den Kurs!",
"tours.tabNavigationCheckpoint.body": "Sie können über diese Registerkarten andere Kursinformationen erreichen, wie z. B. Ihren Fortschritt, Ihren Lehrplan, usw.",
"tours.tabNavigationCheckpoint.title": "Zusätzliches Kursmaterial",
"tours.upgradeCheckpoint.body": "Arbeiten Sie auf ein Zertifikat hin und erhalten Sie vollen Zugriff auf Kursmaterialien. Jetzt upgraden!",
"tours.upgradeCheckpoint.title": "Schalten Sie Ihren Kurs frei",
"tours.weeklyGoalsCheckpoint.body": "Wenn Sie sich ein Ziel setzen, ist es wahrscheinlicher, dass Sie Ihren Kurs abschließen.",
"tours.weeklyGoalsCheckpoint.title": "Legen Sie ein Kursziel fest",
"tours.newUserModal.title": "{welcome} im {siteName} Kurs!",
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
"learning.effortEstimation.activities": "{activityCount, plural, one {# Aktivität} other {# Aktivitäten}}",
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# Mindest} other {# Minuten}}",
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# Minute} other {# Protokoll}}",
"learning.streakCelebration.congratulations": "Glückwunsch!",
"learning.streakCelebration.body": "Weiter so, das läuft ja super!",
"learning.streakCelebration.button": "Weiter so",
"learning.streakCelebration.buttonSrOnly": "Dialogbox schließen und fortfahren",
"learning.streakCelebration.buttonAA759": "Weiter im Kurs",
"learning.streakCelebration.header": "-Tagesserie",
"learning.streakCelebration.factoidABoldedSection": "haben eine 20-mal höhere Wahrscheinlichkeit, ihren Kurs zu bestehen",
"learning.streakCelebration.factoidBBoldedSection": "absolvieren durchschnittlich 5x so viele Kursinhalte",
"learning.streakCelebration.streakDiscountMessage": "Sie haben einen Rabatt von {percent} % freigeschaltet, wenn Sie diesen Kurs nur für eine begrenzte Zeit upgraden.",
"learning.streakcelebration.factoida": "Benutzer, die {streak_length} Tage hintereinander {bolded_section} lernen, als diejenigen, die dies nicht tun.",
"learning.streakcelebration.factoidb": "Benutzer, die {streak_length} Tage hintereinander {bolded_section} lernen, im Vergleich zu denen, die dies nicht tun.",
"learning.streakCelebration.streakCelebrationCouponEndDateMessage": "Endet {date}.",
"learning.loading.failure": "Beim Laden dieses Kurses ist ein Fehler aufgetreten.",
"learning.loading": "Kursseite wird geladen…"
}

View File

@@ -0,0 +1,452 @@
{
"learning.accessExpiration.deadline": "برای دسترسی نامحدود به دوره آموزشی تا زمانی که در وبگاه است، تا {date} ارتقا دهید.",
"learning.accessExpiration.header": "دسترسی حسابرسی منقضی می‌شود {date}",
"learning.accessExpiration.body": "شما همه دسترسی به این دوره آموزشی خود همچون پیشرفت خود را در {date} از دست می‌دهید.",
"instructorToolbar.pageBanner.courseHasExpired": "این فرد، دیگر به این دوره آموزشی دسترسی ندارد. دسترسی آن‌ها در {date} منقضی شد.",
"learning.accessExpiration.upgradeNow": "اکنون روزآمد کنید.",
"learning.activeEnterprise.alert": "{changeActiveEnterprise}.",
"learning.activeEnterprise.change.alert": "تغییر شرکت هم‌اکنون",
"learning.outline.alert.start.short": "دوره آموزشی در {timeRemaining} در {courseStartTime} آغاز می‌شود.",
"learning.outline.alert.end.long": "این دوره آموزشی در {timeRemaining} در {courseEndDate} به پایان می‌رسد.",
"learning.outline.alert.end.calendar": "فراموش نکنید که یک یادآور تقویم بیفزایید!",
"instructorToolbar.pageBanner.courseHasNotStarted": "این فرد هنوز به این دوره آموزشی دسترسی ندارد. دوره آموزشی در تاریخ {date} شروع می‌شود.",
"learning.enrollment.alert": "برای دیدن محتوای دوره آموزشی باید در آن ثبت‌نام کرده باشید.",
"learning.staff.enrollment.alert": "شما در حال مشاهده دوره آموزشی به‌عنوان عضو سامانه هستید اما ثبت‌نام نکرده‌اید.",
"learning.enrollment.enrollNow.Inline": "الان ثبت‌نام کنید",
"learning.enrollment.enrollNow.Sentence": "الان ثبت‌نام کنید.",
"learning.enrollment.success": "شما در این دوره آموزشی ثبت‌نام کردید.",
"account-activation.alert.button": "ادامه در {siteName}",
"account-activation.alert.message": "ما رایانامه‌ای به {boldEmail} با پیوندی برای فعالسازی حساب کاربری شما ارسال کردیم. نمی‎‌توانید آن را پیدا کنید؟ پوشه هرزنامه یا {sendEmailTag} را بررسی کنید.",
"account-activation.resend.link": "ارسال مجدد رایانامه",
"learning.logistration.alert": "برای دیدن محتوای دوره آموزشی، {signIn} یا {register}.",
"account-activation.alert.title": "حساب کاربری خود را فعال کنید تا بتوانید دوباره وارد شوید",
"learn.sequence.entranceExamTextNotPassing": "برای دسترسی به منابع آموزشی، باید در این آزمون امتیاز {entranceExamMinimumScorePct}% یا بالاتر کسب کنید. امتیاز فعلی شما {entranceExamCurrentScore}٪ است.",
"learn.sequence.entranceExamTextPassed": "امتیاز شما {entranceExamCurrentScore}% است. شما در آزمون ورودی پذیرفته شده‌اید.",
"learning.dates.badge.completed": "کامل‌شده",
"learning.dates.badge.dueNext": "موعد بعدی",
"learning.dates.badge.pastDue": "سررسید",
"learning.dates.title": "تاریخ‌های مهم",
"learning.dates.badge.today": "امروز",
"learning.dates.badge.unreleased": "هنوز منتشر نشده‌است",
"learning.dates.badge.verifiedOnly": "فقط تاییدشده",
"learning.goals.unsubscribe.contact": "تماس با پشتیبانی ",
"learning.goals.unsubscribe.description": "از این پس رایانامه یادآوری درباره هدف خود برای {courseTitle} دریافت نخواهید کرد.",
"learning.goals.unsubscribe.errorHeader": "مشکلی پیش آمد",
"learning.goals.unsubscribe.goToDashboard": "بازگشت به پیشخوان",
"learning.goals.unsubscribe.header": "اشتراکتان را از هشدار یادآوری هدف لغو کرده‌اید",
"learning.goals.unsubscribe.loading": "در حال لغو آبونمان...",
"learning.goals.unsubscribe.errorDescription": "ما نتوانستیم اشتراک شما را از رایانامه‌های هشدار هدف لغو کنیم. لطفاً بعداً دوباره تلاش کنید یا برای راهنمایی {با پشتیبانی تماس بگیرید}.",
"learning.outline.alert.cert.earnedNotAvailable": "این دوره آموزشی در {courseEndDateFormatted} پایان می‌یابد. نمرات نهایی و گواهی‌های کسب‌شده طبق برنامه پس از {certificateAvailableDate} فراهم خواهند بود.",
"cert.alert.earned.unavailable.header.v2": "وضعیت نمره و گواهی شما به‌زودی در دسترس خواهد بود.",
"cert.alert.earned.ready.header": "تبریک می‌گوییم! گواهی شما آماده است.",
"cert.alert.notPassing.header": "شما هنوز واجد شرایط دریافت گواهی نیستید",
"cert.alert.notPassing.button": "مشاهده نمرات",
"learning.outline.alert.end.short": "این دوره آموزشی در {timeRemaining} و {courseEndTime} به پایان می‌رسد.",
"alert.enroll": "برای دسترسی به دوره آموزشی کامل.",
"learning.privateCourse.signInOrRegister": "{signIn} یا {register} و سپس در این دوره آموزشی ثبت‌نام کنید.",
"learning.outline.alert.scheduled-content.heading": "محتوای بیشتری در آینده در اختیار شما قرار خواهد گرفت!",
"learning.outline.alert.scheduled-content.body": "این دوره آموزشی محتوای بیشتری در آینده خواهد داشت. برای روزآمدسازی‌ها و بررسی محتوای دوره آموزشی را از طریق صندوق رایانامه بررسی کنید.",
"learning.outline.alert.scheduled-content.button": "مشاهده برنامه آموزشی دوره",
"learning.outline.dates.all": "مشاهد همه تاریخ‌های دوره آموزشی",
"learning.outline.goalButton.casual.text": "1 روز در هفته",
"learning.outline.goalButton.screenReader.text": "گاه‌به‌گاه",
"learning.outline.certificateAlt": "گواهی نمونه",
"learning.outline.collapseAll": "بستن همه موارد",
"learning.outline.completedAssignment": "کامل شده",
"learning.outline.completedSection": "بخش کامل‌شده",
"learning.outline.dates": "تاریخ‌های مهم",
"learning.outline.editGoal": "ویرایش هدف",
"learning.outline.expandAll": "گسترش همه موارد",
"learning.outline.goal": "هدف",
"learning.outline.goalReminderDetail": "اگر متوجه شویم که کاملاً به هدف خود نرسیده‌اید، رایانامه یادآوری برای شما ارسال خواهیم کرد.",
"learning.outline.goalUnsure": "هنوز مطمئن نیستم",
"learning.outline.handouts": "جزوات دوره",
"learning.outline.incompleteAssignment": "ناتمام",
"learning.outline.incompleteSection": "بخش ناتمام",
"learning.outline.goalButton.intense.text": "5 روز در هفته",
"learning.outline.goalButton.intense.title": "شدید",
"learning.outline.learnMore": "بیشتر بدانید",
"learning.outline.altText.openSection": "بازکردن",
"learning.proctoringPanel.header": "این دوره آموزشی شامل آزمون‌های حفاظت‌شده می‌شود",
"learning.outline.goalButton.regular.text": "3 روز در هفته",
"learning.outline.goalButton.regular.title": "منظم",
"learning.outline.resumeBlurb": "از جایی که ترک کردید ادامه دهید",
"learning.outline.resume": "ادامه دوره آموزشی",
"learning.outline.setGoal": "برای آغاز، با انتخاب گزینه زیر که بهترین برنامه آموزشی شما را توصیف می‌کند، هدف خود را از شرکت در دوره آموزشی تعیین کنید.",
"learning.outline.setGoalReminder": "یادآور هدف تنظیم کنید",
"learning.outline.goalButton.casual.title": "سبک هدف یادگیری تعیین کنید.",
"learning.outline.setWeeklyGoal": "یک هدف یادگیری هفتگی تعیین کنید",
"learning.outline.setWeeklyGoalDetail": "تعیین هدف به شما انگیزه می‌دهد تا دوره آموزشی را به پایان برسانید. همیشه امکان تغییر آن را دارید.",
"learning.outline.start": "آغاز دوره آموزشی ",
"learning.outline.startBlurb": "دوره آموزشی خود را از امروز شروع کنید",
"learning.outline.tools": "ابزارهای دوره آموزشی",
"learning.outline.upgradeButton": "ارتقا ({symbol}{price})",
"learning.outline.upgradeTitle": "به دنبال کسب یک گواهی تأییدشده باشید",
"learning.outline.welcomeMessage": "پیام خوش‌آمدگویی",
"learning.outline.welcomeMessageShowMoreButton": "بیشتر",
"learning.outline.welcomeMessageShowLessButton": "کمتر",
"learning.outline.goalWelcome": "خوش آمدید به :",
"learning.proctoringPanel.status.notStarted": "شروع نشده",
"learning.proctoringPanel.status.started": "شروع شده",
"learning.proctoringPanel.status.submitted": "ارائه شده",
"learning.proctoringPanel.status.verified": "تاییدشده",
"learning.proctoringPanel.status.rejected": "رد شده",
"learning.proctoringPanel.status.error": "خطا",
"learning.proctoringPanel.status.otherCourseApproved": "در دوره آموزشی دیگری تایید شد",
"learning.proctoringPanel.status.expiringSoon": "به‌زودی منقضی می‌شود",
"learning.proctoringPanel.status.expired": "منقضی‌شده",
"learning.proctoringPanel.status": "وضعیت ورودی جاری:",
"learning.proctoringPanel.message.notStarted": "شما آزمون ورودی خود را آغاز نموده‌اید.",
"learning.proctoringPanel.message.started": "شما امتحان آزمایشی خود را آغاز کرده‌اید.",
"learning.proctoringPanel.message.submitted": "شما آزمون ورودی خود را ارسال کرده‌اید.",
"learning.proctoringPanel.message.verified": "آزمون ورودی شما در این دوره آموزشی تایید شده‌است.",
"learning.proctoringPanel.message.rejected": "آزمون ورودی شما رد شده است. لطفاً دوباره سعی کنید.",
"learning.proctoringPanel.message.error": "خطایی در طول آزمون آزمایشی شما رخ داده است. لطفاً این آزمون را دوباره امتحان کنید.",
"learning.proctoringPanel.message.otherCourseApproved": "آزمون آزمایشی شما در دوره آموزشی دیگری تایید شده است.",
"learning.proctoringPanel.detail.otherCourseApproved": "اگر دستگاه شما تغییر کرده است، توصیه می‌کنیم که امتحان داخلی این دوره را تکمیل کنید تا مطمئن شوید که راه‌اندازی شما همچنان با الزامات پیش‌بینی شده تطابق دارد.",
"learning.proctoringPanel.message.expiringSoon": "پرونده شخصی ورود شما تایید شد. اما به‌زودی منقضی می‌شود. لطفاً گام های ورود به سامانه را تکمیل کنید تا مطمئن شوید که می‌توانید آزمون‌های همراه با نظارت را ادامه دهید.",
"learning.proctoringPanel.message.expired": "وضعیت ورود شما منقضی شده‌است. لطفاً برای ادامه شرکت در آزمون‌های حفاظت‌شده، ورود را مجددا تکمیل کنید.",
"learning.proctoringPanel.generalInfo": "شما باید پیش از شرکت در هر آزمون آزمایشی، فرآیند حفاظت را تکمیل کنید.",
"learning.proctoringPanel.generalInfoSubmitted": "پرونده شخصی ارسالی شما در حال بررسی است.",
"learning.proctoringPanel.generalTime": "بررسی پرونده ورودی ممکن است 2+ روز کاری طول بکشد.",
"learning.proctoringPanel.onboardingButton": "ورودی کامل",
"learning.proctoringPanel.onboardingPracticeButton": "مشاهده آزمون جاری",
"learning.proctoringPanel.onboardingButtonNotOpen": "آزمایش باز می‌شود: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "دستورالعمل‌ها و سامانه مورد نیاز را بررسی کنید",
"learning.proctoringPanel.onboardingButtonPastDue": "ورودی تاریخ گذشته",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "برای تهیه گواهی، باید تأیید هویت را تکمیل کنید. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "امروز موفقیت خود را در لینکدین یا رزومه خود به نمایش بگذارید. اکنون می‌توانید گواهی خود را بارگیری کنید و هر زمان که بخواهید از پیشخوان و پرونده شخصر خود به آن دسترسی داشته باشید.",
"courseCelebration.certificateBody.notAvailable.endDate": "نمرات نهایی و همه گواهی‌های کسب‌شده قرار است پس از {endDate} در دسترس باشند.",
"progress.certificateStatus.notPassingHeader": "وضعیت گواهی",
"progress.certificateStatus.notPassingBody": "برای اینکه واجد شرایط دریافت گواهی باشید، باید نمره قبولی کسب کنید.",
"progress.certificateStatus.inProgressHeader": "محتوای بیشتری در آینده در اختیار شما قرار خواهد گرفت!",
"progress.certificateStatus.inProgressBody": "به نظر می‌رسد مطالب بیشتری در این دوره آموزشی وجود دارد که در آینده منتشر خواهد شد. از طریق رایانامه، روزآمدسازی‌ها را به اطلاع شما می‌رسانیم یا دوره آموزشی خود را برای دسترسی به محتوا دوباره بررسی کنید.",
"progress.certificateStatus.requestableHeader": "وضعیت گواهی",
"progress.certificateStatus.requestableBody": "تبریک می گوییم، شما واجد شرایط دریافت گواهی هستید! برای دسترسی به گواهی خود، آن را در زیر درخواست کنید.",
"progress.certificateStatus.requestableButton": "درخواست گواهی",
"progress.certificateStatus.unverifiedHeader": "وضعیت گواهی",
"progress.certificateStatus.unverifiedButton": "تایید شناسه",
"progress.certificateStatus.courseCelebration.verificationPending": "تأیید هویت شما در حالت انتظار است و گواهی پس از تأیید در دسترس خواهد بود.",
"progress.certificateStatus.downloadableHeader": "گواهی شما در دسترس است!",
"progress.certificateStatus.viewableButton": "مشاهده گواهی من",
"progress.certificateStatus.notAvailableHeader": "وضعیت گواهی",
"progress.certificateBody.notAvailable.endDate": "نمرات نهایی و همه گواهی‌های کسب‌شده قرار است پس از {endDate} در دسترس باشند.",
"progress.certificateStatus.upgradeHeader": "گواهی کسب کنید",
"progress.certificateStatus.upgradeBody": "شما در مسیر بررسی هستید و واجد شرایط دریافت گواهی نیستید. به‌منظور تلاش برای دریافت گواهی، امروز دوره آموزشی خود را ارتقا دهید.",
"progress.certificateStatus.upgradeButton": "اکنون ارتقا دهید",
"progress.certificateStatus.unverifiedHomeHeader.v2": "هویت خود را تأیید کنید تا واجد شرایط دریافت گواهی شوید.",
"progress.certificateStatus.unverifiedHomeButton": "تایید هویت من",
"progress.certificateStatus.unverifiedHomeBody": "برای تهیه گواهی برای این دوره آموزشی، باید مراحل تایید هویت را تکمیل کنید.",
"progress.completion.donut.label": "تکمیل شد",
"progress.completion.body": "این نشان می‌دهد که چه مقدار از محتوای دوره آموزشی را تکمیل کرده اید. توجه کنید که برخی از محتواها ممکن است هنوز منتشر نشده باشند.",
"progress.completion.tooltip.locked": "محتوایی که تکمیل کرده‌اید.",
"progress.completion.header": "پایان دوره آموزشی",
"progress.completion.tooltip": "محتوایی که به آن دسترسی دارید و آن را تکمیل نکرده‌اید.",
"progress.completion.tooltip.complete": "محتوایی که قفل شده است و فقط برای کسانی در دسترس است که ارتقا می‌دهند.",
"progress.completion.donut.percentComplete": "شما {percent}% از مطالب این دوره آموزشی را تکمیل کرده‌اید.",
"progress.completion.donut.percentIncomplete": "شما {percent}% از محتوای این دوره آموزشی را که به آن دسترسی دارید تکمیل نکرده‌اید.",
"progress.completion.donut.percentLocked": "{percent}% محتوای این دوره قفل شده است و فقط در دسترس کسانی است که ارتقاء می‌دهند.",
"progress.creditInformation.creditNotEligible": "شما دیگر واجد شرایط دریافت اعتبار در این دوره آموزشی نیستید. درباره {creditLink} اطلاعات بیشتری کسب کنید.",
"progress.creditInformation.creditEligible": "\nشما شرایط لازم برای اعتبار را در این دوره آموزشی دارید. \nبرای خرید اعتبار دوره آموزشی به {dashboardLink} خود بروید. یا درباره {creditLink} اطلاعات بیشتری کسب کنید.",
"progress.creditInformation.creditPartialEligible": "شما هنوز شرایط لازم برای دریافت اعتبار را کسب نکرده‌اید. درباره {creditLink} بیشتر بدانید.",
"progress.creditInformation.completed": "کامل شده",
"progress.creditInformation.courseCredit": " اعتبار دوره آموزشی",
"progress.creditInformation.minimumGrade": "حداقل نمره برای اعتبار ({minGrade}%)",
"progress.creditInformation.requirementsHeader": "الزامات اعتبار دوره آموزشی",
"progress.creditInformation.upcoming": "در آینده منتشر می‌شود",
"progress.creditInformation.verificationFailed": "راستی‌آزمایی موفق نبود",
"progress.creditInformation.verificationSubmitted": "راستی‌آزمایی تایید شد",
"progress.ungradedAlert": "برای پیشرفت در جنبه‌های بارم‌بندی نشده دوره آموزشی، {outlineLink} خود را مشاهده کنید.",
"progress.footnotes.droppableAssignments": "کمترین امتیاز {numDroppable, plural, one{# {assignmentType} is} other{# {assignmentType} امتیاز}} حذف شده است.",
"progress.assignmentType": "نوع تکلیف",
"progress.footnotes.backToContent": "بازگشت به محتوا",
"progress.courseGrade.body": "این نشان‌دهنده نمره وزنی شما در برابر نمره مورد نیاز برای گذراندن این دوره آموزشی است.",
"progress.courseGrade.gradeBar.altText": "نمره فعلی شما {currentGrade}٪ است. کسب نمره وزنی {passingGrade}% برای قبولی در این دوره آموزشی الزامی است.",
"progress.courseGrade.footer.generic.passing": "در حال حاضر شما این دوره آموزشی هستید",
"progress.courseGrade.footer.nonPassing": "نمره وزنی {passingGrade}% برای قبولی در این دوره آموزشی الزامی است",
"progress.courseGrade.footer.passing": "شما در حال گذراندن این دوره آموزشی با نمره {letterGrade} ({minGrade}-{maxGrade}%) هستید",
"progress.courseGrade.preview.headerLocked": "ویژگی قفل‌شده",
"progress.courseGrade.preview.headerLimited": "ویژگی محدود",
"progress.courseGrade.preview.header.ariaHidden": "پیش‌نمایش یک",
"progress.courseGrade.preview.body.unlockCertificate": "برای مشاهده نمرات و تلاش برای دریافت گواهی، قفل را باز کنید.",
"progress.courseGrade.partialpreview.body.unlockCertificate": "برای کار با هدف دریافت گواهی، قفل‌گشایی کنید.",
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "مهلت ارتقا در این دوره آموزشی به پایان رسیده است.",
"progress.courseGrade.preview.button.upgrade": "اکنون ارتقا دهید",
"progress.courseGrade.gradeRange.tooltip": "دامنه نمره در این دوره آموزشی",
"progress.courseOutline": "طرح درس دوره آموزشی",
"progress.courseGrade.label.currentGrade": "نمره کنونی شما",
"progress.detailedGrades": "نمرات تفصیلی",
"progress.detailedGrades.emptyTable": "اکنون شما هیچ امتیازی از سوال بارم‌دار ندارید.",
"progress.footnotes.title": "پانویس خلاصه نمره",
"progress.gradeSummary.grade": "مقطع تحصیلی ",
"progress.courseGrade.grades": "نمره‌ها",
"progress.courseGrade.gradesAndCredit": "نمره‌ها و اعتبارها",
"progress.courseGrade.gradeRange.Tooltip": "نکته دامنه نمره",
"progress.gradeSummary": "خلاصه نمره",
"progress.gradeSummary.limitedAccessExplanation": "شما به تکالیف بارم‌بندی‌شده به‌عنوان بخشی از مسیر بررسی در این دوره آموزشی، دسترسی محدودی دارید.",
"progress.gradeSummary.tooltip.alt": "نکته خلاصه نمره",
"progress.gradeSummary.tooltip.body": "وزن تکلیف دوره آموزشی شما به‌دست مربی شما تعیین می‌شود. با ضرب نمره شما در وزن آن نوع تکلیف، نمره وزنی شما محاسبه می‌شود. نمره وزنی شما چیزی است که برای تعیین قبولی در دوره آموزشی استفاده می‌شود.",
"progress.noAcessToAssignmentType": "شما به نوع تکلیف {assignmentType} دسترسی ندارید",
"progress.noAcessToSubsection": "شما به زیربخش {displayName} دسترسی ندارید",
"progress.courseGrade.label.passingGrade": "نمره قبولی",
"progress.detailedGrades.problemScore.label": "امتیازات سوال:",
"progress.detailedGrades.problemScore.toggleButton": "تغییر امتیاز تک تک مسایل برای {subsectionTitle}",
"progress.detailedGrades.overridden": "نمرۀ بخش لغو شده‌است.",
"progress.score": "امتیاز",
"progress.weight": "وزن",
"progress.weightedGrade": "درجه وزنی",
"progress.weightedGradeSummary": "خلاصه نمره وزنی فعلی شما",
"progress.header": "پیشرفت شما",
"progress.header.targetUser": "میزان پیشرفت دوره آموزشی برای {username}",
"progress.link.studio": "مشاهده نمره‌بندی در هنرکده",
"progress.relatedLinks.datesCard.description": "نمای برنامه از تاریخ سررسید دوره آموزشی شما و تکالیف آینده.",
"progress.relatedLinks.datesCard.link": "تاریخ‌ها",
"progress.relatedLinks.outlineCard.description": "نمای کل‌نگر به محتوای دوره آموزشی شما",
"progress.relatedLinks.outlineCard.link": "طرح درس دوره آموزشی",
"progress.relatedLinks": "پیوندهای مرتبط",
"datesBanner.suggestedSchedule": "ما برنامه زمانی پیشنهادی تهیه کرده‌ایم تا به شما کمک کنیم در مسیر خود بمانید. اما نگران نباشید - این برنامه منعطف است تا بتوانید با سرعت موردنظر خود فرابگیرید.",
"datesBanner.upgradeToCompleteGradedBanner.header": "ارتقا برای قفل‌گشایی",
"datesBanner.upgradeToCompleteGradedBanner.body": "شما در حال بررسی این دوره آموزشی هستید، به این معنی که نمی‌توانید در تکالیف بارم‌بندی شده شرکت کنید. برای تکمیل اینگونه تکالیف به‌عنوان بخشی از این دوره آموزشی، می‌توانید همین امروز ارتقا دهید.",
"datesBanner.upgradeToCompleteGradedBanner.button": "اکنون روزآمد کنید",
"datesBanner.upgradeToResetBanner.body": "برای اینکه خود را در مسیر موردنظر نگه دارید، می‌توانید این برنامه را روزآمد کرده و تکالیف گذشته را به آینده منتقل کنید. نگران نباشید، وقتی تاریخ سررسید خود را تغییر دهید، هیچ یک از پیشرفت‌های خود را از دست نخواهید داد.",
"datesBanner.upgradeToResetBanner.button": "برای تغییر سررسید از ارتقا استفاده کنید",
"datesBanner.resetDatesBanner.header": "به نظر می‌رسد بر اساس برنامۀ پیشنهادی ما برخی از زمان‌های مهم را فراموش کرده‌اید.",
"datesBanner.resetDatesBanner.body": "برای اینکه خود را در مسیر موردنظر نگه دارید، می‌توانید این برنامه را روزآمد کرده و تکالیف گذشته را به آینده منتقل کنید. نگران نباشید، وقتی تاریخ سررسید خود را تغییر دهید، هیچ یک از پیشرفت‌های خود را از دست نخواهید داد.",
"datesBanner.resetDatesBanner.button": "تغییر موعد مقرر",
"learn.navigation.course.tabs.label": "منابع آموزشی",
"unit.bookmark.button.add.bookmark": "نشانه‌گذاری صفحه",
"unit.bookmark.button.remove.bookmark": "نشانه‌گذاری شده",
"learning.celebration.completed": "شما به‌تازگی بخش اول دوره آموزشی خود را تکمیل کرده‌اید.",
"learning.celebration.congrats": "تبریک عرض می‌کنم!",
"learning.celebration.earned": "آن را بدست آورده‌اید!",
"learning.celebration.emailSubject": "من در راه تکمیل {title} به‌صورت برخط با {platform} گام برمی‌دارم!",
"learning.celebration.forward": "ادامه دهید",
"learning.celebration.goalMet": "به هدف خود دست یافتید!",
"learning.celebration.keepItUp": "همین‌طور ادامه بده",
"learning.celebration.share": "لحظه‌ای را برای جشن‌گرفتن و به اشتراک گذاری پیشرفت خود اختصاص دهید.",
"learning.celebration.social": "من در راه تکمیل {title} به‌صورت برخط با {platform} گام برمی‌دارم. وقت خود را صرف یادگیری چه چیزی می‌کنید؟",
"learning.celebration.goalCongrats": "تبریک می‌گوییم، شما به هدف یادگیری خود یعنی {nTimes} در هفته دست یافتید.",
"learning.celebration.setGoal": "تعیین هدف می‌تواند به شما در دوره آموزشی {strongText} کمک کند.",
"calculator.instructions.button.label": "دستورالعمل‌های ماشین‌حساب",
"calculator.instructions": "برای اطلاعات دقیق، به {expressions_link} مراجعه کنید.",
"calculator.instructions.support.title": "مرکز پشتيباني",
"calculator.instructions.useful.tips": "نکات مفید:",
"calculator.hint1": "برای شفاف‌سازی عبارات از پرانتز () استفاده کنید. می‌توانید از پرانتزهای تودرتو استفاده کنید.",
"calculator.hint2": "از فاصله در عبارات استفاده نکنید.",
"calculator.hint3": "برای مقادیر ثابت از علامت ضرب استفاده کنید (مثلا: c*5).",
"calculator.hint4": "برای پیوست‌ها، شماره و پیوست را بدون فاصله تایپ کنید (مثلاً: 5c).",
"calculator.hint5": "برای توابع ، نام عملگر و سپس عبارت را داخل پرانتز تایپ کنید.",
"calculator.instruction.table.to.use.heading": "برای استفاده",
"calculator.instruction.table.type.heading": "نوع",
"calculator.instruction.table.examples.heading": "نمونه‌ها",
"calculator.instruction.table.to.use.numbers": "اعداد",
"calculator.instruction.table.to.use.numbers.type1": "اعداد صحیح ",
"calculator.instruction.table.to.use.numbers.type2": "کسرها",
"calculator.instruction.table.to.use.numbers.type3": "اعداد اعشاری",
"calculator.instruction.table.to.use.operators": "عملگرها",
"calculator.instruction.table.to.use.operators.type1": "(جمع، تفریق، ضرب، تقسیم)",
"calculator.instruction.table.to.use.operators.type2": "(پرقدرت برخیز)",
"calculator.instruction.table.to.use.operators.type3": "(مقاومت‌های موازی)",
"calculator.instruction.table.to.use.constants": "موارد ثابت",
"calculator.instruction.table.to.use.affixes": "وندها",
"calculator.instruction.table.to.use.affixes.type": "علامت درصد (%)",
"calculator.instruction.table.to.use.basic.functions": "توابع ابتدایی",
"calculator.instruction.table.to.use.trig.functions": "توابع مثلثاتی",
"calculator.instruction.table.to.use.scientific.notation": "نماد علمی",
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} و شرح‌دهنده",
"calculator.instruction.table.to.use.scientific.notation.type2": "{notationSyntax} یادداشت",
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax} و توان",
"calculator.button.label": " ماشین حساب",
"calculator.input.field.label": "ورودی ماشین‌حساب",
"calculator.submit.button.label": "محاسبه",
"calculator.result.field.label": "نتیجه ماشین حساب",
"calculator.result.field.placeholder": "نتیجه",
"notes.button.show": "نمایش یادداشت‌ها",
"notes.button.hide": "پنهان‌سازی یادداشت‌ها",
"courseExit.catalogSearchSuggestion": "در جستجوی یادگیری بیشتر هستید؟ برای یافتن دوره‌های آموزشی و برنامه‌های بیشتر {searchOurCatalogLink} را ملاحظه کنید.",
"courseCelebration.certificateBody.available": "\nامروز موفقیت خود را در لینکدین یا در رزومه خود به نمایش بگذارید.\nامکان بارگیری گواهینامه خود را دارید و هر زمان که بخواهید از طریق {dashboardLink} و {profileLink} به آن دسترسی دارید",
"courseCelebration.certificateBody.notAvailable.endDate.v2": "این دوره آموزشی در تاریخ {endDate} پایان می‌یابد. نمرات نهایی و گواهی‌های کسب شده طبق برنامه پس از {certAvailableDate} فراهم خواهند بود.",
"courseCelebration.certificateBody.unverified": "برای ایجاد گواهی، باید تأیید هویت را تکمیل کنید. اکنون {idVerificationSupportLink}.",
"courseCelebration.certificateBody.upgradable": "برای ارتقا اصلا دیر نیست. برای {price} قفل دسترسی به همه تکالیف \nبارمبندیشده در این دوره آموزشی را باز می‌کنید. پس از تکمیل آن، شما\nگواهی تاییدشده را دریافت خواهید کرد که \nاعتبار ارزشمندی برای بهبود چشم انداز شغلی و پیشرفت شغلی شما\n دارد، یا گواهی خود را در برنامه های کاربردی دانشکده برجسته می‌کند.",
"courseCelebration.upgradeDiscountCodePrompt": "از کد {code} هنگام تسویه‌حساب برای تخفیف {percent}% استفاده کنید!",
"courseCelebration.recommendations.heading": "با این دوره‌های آموزشی به تقویت مهارت‌های خود ادامه دهید!",
"courseCelebration.recommendations.label": "دوره آموزشی",
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {, } conjunction { {sp}and } other { }}\n ",
"courseCelebration.recommendations.browse_catalog": "کاوش دوره‌های آموزشی بیشتر",
"courseCelebration.recommendations.loading_recommendations": "بارگیری توصیه‌ها",
"courseCelebration.recommendations.card.schools.label": "مدارس و همکاران",
"courseCelebration.dashboardInfo": "می‌توانید به این دوره آموزشی و مطالب آن در {dashboardLink} خود دسترسی داشته باشید.",
"courseExit.programs.applyForCredit": "درخواست اعتبار",
"courseCelebration.certificateHeader.downloadable": "گواهی شما در دسترس است!",
"courseCelebration.certificateHeader.notAvailable": "وضعیت نمره و گواهی شما به‌زودی اعلام خواهد شد.",
"courseCelebration.certificateBody.notAvailable.accessCertificate": "اگر نمره قبولی کسب کرده باشید، گواهی شما به‌صورت خودکار صادر می‌شود.",
"courseCelebration.certificateHeader.unverified": "برای دریافت گواهی خود باید تأیید را تکمیل کنید.",
"courseCelebration.certificateHeader.requestable": "تبریک می گوییم، شما مجوز دریافت گواهی را دریافت کردید!",
"courseCelebration.certificateHeader.upgradable": "برای پیگیری گواهی تاییدشده، ارتقا دهید",
"courseCelebration.certificateImage": "نمونه گواهی",
"courseCelebration.completedCourseHeader": "دوره آموزشی خود را به پایان رساندید.",
"courseCelebration.congratulationsHeader": "تبریک عرض می‌کنیم!",
"courseCelebration.congratulationsImage": "چهار نفر،دستان خود را به‌نشانه جشن بالا می برند",
"courseExit.courseInProgressDescription": "به نظر می‌رسد مطالب بیشتری در این دوره آموزشی وجود دارد که در آینده منتشر خواهد شد. از طریق رایانامه، روزآمدسازی‌ها را به اطلاع شما می‌رسانیم یا دوره آموزشی خود را برای دسترسی به محتوا دوباره بررسی کنید.",
"courseExit.courseInProgressHeader": "محتوای بیشتری در آینده در اختیار شما قرار خواهد گرفت!",
"courseExit.dashboardLink": "پیشخوان",
"courseExit.endOfCourseDescription": "متأسفانه، شما اکنون واجد شرایط دریافت گواهی نیستید. برای دریافت آن باید نمره قبولی دریافت کنید.",
"courseExit.endOfCourseHeader": "شما به پایان دوره آموزشی رسیده‌اید!",
"courseExit.endOfCourseTitle": "پایان دوره آموزشی",
"courseExit.idVerificationSupportLink": "درباره تأیید هویت بیشتر بدانید",
"courseCelebration.linkedinAddToProfileButton": "افزودن به پرونده لینکداین",
"courseExit.programs.microBachelors.learnMore": "درباره نحوه اعمال اعتبار MicroBachelors خود برای دریافت اعتبار بیشتر بدانید.",
"courseExit.programs.microMasters.learnMore": "درباره روند اعمال گواهی MicroMasters در مقاطع کارشناسی ارشد بیشتر بدانید.",
"courseExit.programs.microMasters.mastersMessage": "اگر علاقه مند به استفاده از گواهی MicroMasters خود برای برنامه کارشناسی ارشد هستید، می‌توانید همین امروز شروع کنید!",
"learn.sequence.navigation.complete.button": "این دوره آموزشی را تکمیل کنید",
"courseExit.nextButton.endOfCourse": "بعدی (پایان دوره آموزشی)",
"courseExit.profileLink": "پرونده کاربری",
"courseExit.programs.lastCourse": "شما آخرین دوره آموزشی در {title} را گذرانده‌اید!",
"courseCelebration.requestCertificateBodyText": "برای دسترسی به گواهی خود، آن را در زیر درخواست کنید.",
"courseCelebration.requestCertificateButton": "درخواست گواهی",
"courseExit.searchOurCatalogLink": "جستجوی فهرست ما",
"courseCelebration.shareMessage": "اشتراک میزان موفقیت در رسانه‌های اجتماعی یا رایانامه",
"courseExit.social.shareCompletionMessage": "من به‌تازگی {title} را با {platform} تکمیل کردم!",
"courseExit.upgradeButton": "اکنون روزآمد کنید.",
"courseExit.upgradeLink": "الان ارتقا دهید",
"courseCelebration.verificationPending": "تأیید هویت شما در حالت انتظار است و گواهی پس از تأیید در دسترس خواهد بود.",
"courseExit.verifiedCertificateSupportLink": "درباره گواهی‌های تأییدشده بیشتر بدانید",
"courseCelebration.verifyIdentityButton": "اکنون شناسه را تأیید کنید",
"courseCelebration.viewCertificateButton": "مشاهده گواهی من",
"courseExit.viewCourseScheduleButton": "مشاهده برنامه دوره آموزشی",
"courseExit.viewCoursesButton": "مشاهده گواهی من",
"courseExit.viewGradesButton": "مشاهده نمرات",
"courseExit.programCompletion.dashboardMessage": "برای مشاهده وضعیت گواهی خود، بخش برنامه‌ها را در {programLink} خود بررسی کنید.",
"courseExit.upgradeFootnote": "دسترسی به این دوره آموزشی و مطالب آن تا {expirationDate} در پیشخوان شما موجود است. برای گسترش دسترسی، {upgradeLink}.",
"learn.course.license.allRightsReserved.text": "همه حقوق محفوظ است",
"learn.course.license.creativeCommons.terms.preamble": "محتوا مجوز Creative Commons را دارد، با شرایط زیر:",
"learn.course.license.creativeCommons.terms.by": "انتساب",
"learn.course.license.creativeCommons.terms.nc": "غیرتجاری",
"learn.course.license.creativeCommons.terms.nd": "بدون مشتق",
"learn.course.license.creativeCommons.terms.sa": "اشتراک‌گذاری مشابه",
"learn.course.license.creativeCommons.terms.zero": "هیچ شرایطی وجود ندارد.",
"learn.course.license.creativeCommons.text": "بعضی از حقوق محفوظ است",
"learn.breadcrumb.navigation.course.home": "دوره آموزشی",
"notification.tray.container": "توالی هشدارها",
"notification.open.button": "نمایش توالی هشدارها",
"notification.close.button": "بستن توالی هشدارها",
"responsive.close.notification": "بازگشت به دوره آموزشی",
"notification.tray.title": "اعلان‌ها",
"notification.tray.no.message": "اکنون هیچ اعلان جدیدی ندارید.",
"learn.contentLock.content.locked": "محتوا قفل‌‌شده",
"learn.contentLock.complete.prerequisite": "برای دسترسی به این محتوا لازم است پیش‌نیاز \"{prereqSectionName}\" را تکمیل کنید.",
"learn.contentLock.goToSection": "به بخش پیش‌نیاز بروید",
"learn.hiddenAfterDue.gradeAvailable": "اگر این تکلیف را انجام داده‌اید، نمره شما در {progressPage} است.",
"learn.hiddenAfterDue.header": "موعد ارائۀ این تکلیف گذشته است.",
"learn.hiddenAfterDue.description": "چون از موعد مقرر، گذشته است، این تکلیف دیگر در دسترس نیست.",
"learn.hiddenAfterDue.progressPage": "صفحه پیشرفت",
"learn.honorCode.content": "صداقت و راستی آکادمیک برای {siteName} و موسساتی که دوره‌های آموزشی و برنامه‌ها را در وبگاه {siteName} ارائه می‌دهند، مهم است. با کلیک روی «موافقم» در زیر، تأیید می‌کنم که {link} وبگاه {siteName} را خوانده، درک کرده و از آن تبعیت می‌کنم.",
"learn.honorCode.name": "اصول اخلاقی",
"learn.honorCode.cancel": "لغو",
"learn.honorCode.agree": "موافقم",
"learn.lockPaywall.title": "تکالیف بارم‌بندی شده قفل شده‌اند",
"learn.lockPaywall.content": "ارتقا دهید تا به ویژگی‌های قفل همچون این مورد، دسترسی پیدا کنید و از دوره آموزشی خود بیشترین بهره را ببرید.",
"learn.lockPaywall.content.pastExpiration": "مهلت ارتقای این دوره آموزشی به پایان رسید. برای ارتقا، در جلسه موجود بعدی اقدام به ثبت‌نام کنید.",
"learn.lockPaywall.courseDetails": "مشاهده جزییات دوره آموزشی",
"learn.lockPaywall.example.alt": "گواهی نمونه",
"learn.lockPaywall.list.intro": "هنگامی که ارتقا می‌دهید، شما:",
"learn.header.h2.placeholder": " ارائه‌دهندگان ممکن است سرصفحه‌های سطح 2 دوره آموزشی را در آینده ایجاد کنند.",
"learn.course.load.failure": "خطایی در بارگیری این دوره آموزشی رخ داد.",
"learn.loading.honor.codk": "در حال بارگیری پیام اصول اخلاقی...",
"learn.loading.content.lock": "در حال بارگیری پیام محتوای قفل‌شده...",
"learn.loading.learning.sequence": "در حال بارگیری دنباله یادگیری...",
"learn.sequence.no.content": "هیچ محتوایی در اینجا نیست.",
"learn.sequence.navigation.next.button": "بعدی",
"learn.sequence.navigation.next.up.button": "بعدی: {title}",
"learn.sequence.navigation.previous.button": "قبلی",
"learn.course.sequence.navigation.mobile.menu": "{current} از {total}",
"learn.sequence.share.button": "Share this content",
"learn.sequence.share.modal.title": "Title",
"learn.sequence.share.modal.body": "Copy the link below to share this content.",
"learn.sequence.share.quote": "Here's a fun clip from a class I'm taking on @edXonline.\n",
"discussions.sidebar.title": "گفتگوها",
"discussions.sidebar.open.button": "نمایش توالی گفتگو",
"learn.redirect.interstitial.message": "انتقال...",
"learn.loading.error": "خطا: {خطا}",
"learning.celebration.emailBody": "وقت خود را صرف یادگیری چه چیزی می‌کنید؟",
"learning.social.shareEmail": "پیشرفت خود را از طریق رایانامه به اشتراک بگذارید.",
"learning.social.shareService": "پیشرفت خود را در {service} به اشتراک بگذارید.",
"general.altText.close": "بستن",
"learning.logistration.register": "ثبت‌نام",
"learning.logistration.login": "ورود به سامانه",
"general.signIn.sentenceCase": "ورود به سامانه",
"learn.course.tabs.navigation.overflow.menu": "بیشتر...",
"learning.offer.screenReaderPrices": "قیمت اصلی: {originalPrice}، قیمت با اعمال تخفیف: {discountedPrice}",
"learning.upgradeButton.screenReaderInlinePrices": "قیمت اصلی: {originalPrice}",
"learning.upgradeButton.buttonText": "ارتقا برای {pricing}",
"learning.upgradeNowButton.buttonText": "برای {pricing} الان ارتقا دهید ",
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "از جمله هر پیشرفتی",
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "مزایای ارتقا",
"learning.generic.upgradeNotification.expirationAccessLoss": "شما همه دسترسی‌ها به این دوره آموزشی، {شامل Any Progress} را در تاریخ {date} از دست خواهید داد.",
"learning.generic.upgradeNotification.expirationVerifiedCert": "ارتقاء دوره آموزشی این امکان را به شما می‌دهد تا گواهی تاییدشده را دنبال کنید و ویژگی‌های متعددی را فراهم می‌کند. درباره {benefitsOfUpgrading} بیشتر بدانید.",
"learning.generic.upgradeNotification.pastExpiration.content": "مهلت ارتقای این دوره آموزشی به پایان رسید. برای ارتقا، در جلسه موجود بعدی اقدام به ثبت‌نام کنید.",
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \none {day}\nother {days}} باقی مانده است",
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural, یک {hour} دیگر {hours}} باقی مانده است",
"learning.generic.upgradeNotification.expirationMinutes": "کمتر از 1 ساعت باقی مانده است",
"learning.generic.upgradeNotification.expiration": "دسترسی به دوره آموزشی در {date} انقضا خواهد یافت",
"learning.generic.upgradeNotification.pastExpiration.banner": "مهلت ارتقا در {date} به پایان رسید",
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% تخفیف اولین_بار",
"learning.generic.upgradeNotification.accessExpiration": "امروز دوره آموزشی خود را ارتقا دهید",
"learning.generic.upgradeNotification.accessExpirationUrgent": "پایان دسترسی به دوره آموزشی",
"learning.generic.upgradeNotification.accessExpirationPast": "پایان دسترسی به دوره آموزشی",
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "دنبال یک گواهی تأییدشده باشید",
"learning.generic.upgradeNotification.code": "هنگام تسویه حساب از کد {code} استفاده کنید",
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "گواهی تاییدشده",
"learning.generic.upsell.verifiedCertBullet": "یک {verifiedCertLink} تکمیلی برای نمایش در رزومه خود کسب کنید",
"learning.generic.upsell.unlockGradedBullet.gradedAssignments": "تکالیف بارم‌دار",
"learning.generic.upsell.unlockGradedBullet": "قفل دسترسی خود را به همه فعالیت‌های دوره آموزشی، همچون {gradedAssignmentsInBoldText} باز کنید",
"learning.generic.upsell.fullAccessBullet.fullAccess": "دسترسی کامل",
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} به محتوا و منابع دوره آموزشی، حتی پس از پایان دوره",
"learning.generic.upsell.supportMissionBullet.mission": "ماموریت",
"learning.generic.upsell.supportMissionBullet": "از {missionInBoldText} ما در {siteName} پشتیبانی کنید",
"masquerade-widget.userName.error.generic": "خطایی رخ داده است؛ لطفا دوباره تلاش کنید.",
"masquerade-widget.userName.input.placeholder": "نام کاربری یا نشانی رایانامه",
"masquerade-widget.userName.input.label": "خود را به‌عنوان همین کاربر نشان دهید",
"tours.abandonTour.launchTourCheckpoint.body": "احساس می‌کنید گم شده‌اید؟ تور را هر زمان که خواستید راه اندازی کنید تا نکاتی سریع برای استفاده حداکثری از این تجربه در اختیار شما بگذارد.",
"tours.sequenceNavigationCheckpoint.body": "نوار بالای دوره آموزشی به شما این امکان را می‌دهد به‌راحتی به بخش‌های مختلف بپرید و به شما نشان می‌دهد که چه چیزی در راه است.",
"tours.existingUserTour.launchTourCheckpoint.body": "اخیراً چند ویژگی جدید به دوره آموزشی افزوده‌ایم. مایلید نگاهی بیندازید؟ برای کسب اطلاعات بیشتر به تور بروید.",
"tours.button.dismiss": "نادیده بگیر",
"tours.button.next": "بعدی",
"tours.button.okay": "بسیار خوب",
"tours.button.beginTour": "آغاز تور",
"tours.button.launchTour": "راه‌اندازی تور",
"tours.newUserModal.body": "بیایید یک تور سریع از {siteName} داشته باشیم تا بتوانید بیشترین بهره را از دوره آموزشی خود ببرید.",
"tours.newUserModal.title.welcome": "خوش آمدید به ",
"tours.button.skipForNow": "فعلا بگذرید",
"tours.datesCheckpoint.body": "تاریخ‌های مهم به شما کمک می‌کنند در مسیر خود بمانید.",
"tours.datesCheckpoint.title": "جلوتر از تاریخ‌های مهم باشید",
"tours.outlineCheckpoint.body": "شما امکان کاوش بخش‌های مختلف این دوره آموزشی را با استفاده از طرح درس زیر دارید.",
"tours.outlineCheckpoint.title": "دوره آموزشی را بگذرانید!",
"tours.tabNavigationCheckpoint.body": "از این زبانه‌ها می‌توان برای دسترسی به سایر منابع آموزشی مانند میزان پیشرفت، برنامه آموزشی و سایر موارد استفاده کرد.",
"tours.tabNavigationCheckpoint.title": "منابع آموزشی بیشتر برای این دوره آموزشی ",
"tours.upgradeCheckpoint.body": "برای دریافت گواهی تلاش کنید و به منابع آموزشی دوره دسترسی کامل داشته باشید. برای این کار، اکنون ارتقا دهید!",
"tours.upgradeCheckpoint.title": "دوره آموزشی خود را بگشایید",
"tours.weeklyGoalsCheckpoint.body": "تعیین یک هدف باعث می‌شود احتمالا تمایل بیشتری برای تکمیل دوره آموزشی خود داشته باشید.",
"tours.weeklyGoalsCheckpoint.title": "یک هدف برای دوره آموزشی تعیین کنید",
"tours.newUserModal.title": "دوره آموزشی {welcome} {siteName}!",
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
"learning.effortEstimation.activities": "{activityCount, plural, one {# قعالیت} other {# فعالیت‌ها}}",
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# دقیقه} other {# دقیقه}}",
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# دقیقه} other {# دقیقه}}",
"learning.streakCelebration.congratulations": "تبریک می‌گوییم!",
"learning.streakCelebration.body": "به همین شکل ادامه دهید، شما در حال پیشرفت هستید!",
"learning.streakCelebration.button": "همین‌طور ادامه بده",
"learning.streakCelebration.buttonSrOnly": "بستن مودال و ادامه",
"learning.streakCelebration.buttonAA759": "ادامه دوره آموزشی",
"learning.streakCelebration.header": "خط روز",
"learning.streakCelebration.factoidABoldedSection": "احتمال قبولی در دوره آموزشی آن‌ها 20 برابر بیشتر است",
"learning.streakCelebration.factoidBBoldedSection": "به‌طور متوسط 5 برابر بیشتر محتوای دوره آموزشی را تکمیل کنید",
"learning.streakCelebration.streakDiscountMessage": "وقتی این دوره آموزشی را فقط برای مدت محدودی ارتقا می‌دهید، {percent}% تخفیف دریافت کرده‌اید.",
"learning.streakcelebration.factoida": "کاربرانی که {streak_length} روز متوالی {bolded_section} را آموزشی می‌بینند نسبت به کاربرانی که چنین آموزشی ندارند.",
"learning.streakcelebration.factoidb": "کاربرانی که {streak_length} روز متوالی {bolded_section} را یاد می‌گیرند در مقابل کاربرانی که نمی‌آموزند.",
"learning.streakCelebration.streakCelebrationCouponEndDateMessage": "در {date} پایان می‌یابد.",
"learning.loading.failure": "خطایی در بارگیری این دوره آموزشی رخ داد.",
"learning.loading": "در حال بارگیری صفحه دوره آموزشی…"
}

View File

@@ -0,0 +1,452 @@
{
"learning.accessExpiration.deadline": "Effettuare l'upgrade entro il {date} per ottenere accesso illimitato al corso fino a quando sarà presente sul sito. ",
"learning.accessExpiration.header": "L'accesso Auditore Scade il {date}",
"learning.accessExpiration.body": "L'accesso a questo corso, inclusi i progressi, verrà perso il {date}. ",
"instructorToolbar.pageBanner.courseHasExpired": "Questo studente non ha più accesso a questo corso. Il loro accesso è scaduto il {date}.",
"learning.accessExpiration.upgradeNow": "Esegui l'upgrade ora ",
"learning.activeEnterprise.alert": " {changeActiveEnterprise}.",
"learning.activeEnterprise.change.alert": "change enterprise now",
"learning.outline.alert.start.short": "Il corso inizia tra {timeRemaining} il {courseStartTime}.",
"learning.outline.alert.end.long": "Questo corso finirà {timeRemaining} il {courseEndDate}.",
"learning.outline.alert.end.calendar": "Non dimenticare di aggiungere un promemoria! ",
"instructorToolbar.pageBanner.courseHasNotStarted": "Questo studente non ha ancora accesso a questo corso. Il corso inizia il {date}.",
"learning.enrollment.alert": "È necessario essere iscritti al corso per visualizzarne il contenuto. ",
"learning.staff.enrollment.alert": "Stai visualizzando questo corso come staff e non sei iscritto. ",
"learning.enrollment.enrollNow.Inline": "Iscriviti ora ",
"learning.enrollment.enrollNow.Sentence": "Iscriviti ora. ",
"learning.enrollment.success": "Ti sei correttamente iscritto a questo corso. ",
"account-activation.alert.button": "Continua con {siteName}",
"account-activation.alert.message": "Abbiamo inviato un&#39;email a {boldEmail} con un link per attivare il tuo account. Non riesci a trovarlo? Controlla la tua cartella spam o {sendEmailTag}.",
"account-activation.resend.link": "inviare nuovamente l&#39;e-mail",
"learning.logistration.alert": "Per visualizzare il contenuto del corso, {signIn} o {register}.",
"account-activation.alert.title": "Attiva il tuo account per poterti autenticare nuovamente",
"learn.sequence.entranceExamTextNotPassing": "Per accedere ai materiali del corso, devi ottenere un punteggio di {entranceExamMinimumScorePct}% o superiore in questo esame. Il tuo punteggio attuale è {entranceExamCurrentScore}%.",
"learn.sequence.entranceExamTextPassed": "Il tuo punteggio è {entranceExamCurrentScore}%. Hai superato l&#39;esame di ammissione.",
"learning.dates.badge.completed": "Completato",
"learning.dates.badge.dueNext": "Prossima scadenza ",
"learning.dates.badge.pastDue": "Scaduto ",
"learning.dates.title": "Date importanti",
"learning.dates.badge.today": "Oggi",
"learning.dates.badge.unreleased": "Non ancora rilasciato ",
"learning.dates.badge.verifiedOnly": "Solo verificato ",
"learning.goals.unsubscribe.contact": "contattare il supporto ",
"learning.goals.unsubscribe.description": "Non riceverai più promemoria via email sul tuo obiettivo per {courseTitle}.",
"learning.goals.unsubscribe.errorHeader": "Qualcosa è andato storto",
"learning.goals.unsubscribe.goToDashboard": "Vai alla dashboard",
"learning.goals.unsubscribe.header": "Hai annullato l&#39;iscrizione ai promemoria degli obiettivi",
"learning.goals.unsubscribe.loading": "Cancellazione, cancellami…",
"learning.goals.unsubscribe.errorDescription": "Non siamo stati in grado di cancellarti dalle email di promemoria degli obiettivi. Riprova più tardi o {contactSupport} per ricevere assistenza.",
"learning.outline.alert.cert.earnedNotAvailable": "Questo corso termina il {courseEndDateFormatted}. I voti finali e gli eventuali certificati ottenuti saranno disponibili dopo il {certificateAvailableDate}.",
"cert.alert.earned.unavailable.header.v2": "Il tuo grado e lo stato del certificato saranno presto disponibili.",
"cert.alert.earned.ready.header": "Il tuo certificato è pronto.",
"cert.alert.notPassing.header": "Non sei ancora idoneo per un certificato",
"cert.alert.notPassing.button": "Visualizza valutazioni ",
"learning.outline.alert.end.short": "Questo corso termina tra {timeRemaining} alle {courseEndTime}. ",
"alert.enroll": "per accedere al corso completo.",
"learning.privateCourse.signInOrRegister": "{signIn} o {register} e quindi iscriviti a questo corso. ",
"learning.outline.alert.scheduled-content.heading": "Ulteriore contenuto presto in arrivo!",
"learning.outline.alert.scheduled-content.body": "Questo corso avrà più contenuti rilasciati in una data futura. Cerca gli aggiornamenti via e-mail o ricontrolla questo corso per gli aggiornamenti.",
"learning.outline.alert.scheduled-content.button": "Visualizza il programma del corso",
"learning.outline.dates.all": "Visualizza tutte le date del corso ",
"learning.outline.goalButton.casual.text": "1 giorno a settimana",
"learning.outline.goalButton.screenReader.text": "Casuale",
"learning.outline.certificateAlt": "Certificato di esempio ",
"learning.outline.collapseAll": "Comprimi tutto ",
"learning.outline.completedAssignment": "Completato",
"learning.outline.completedSection": "Sezione completata ",
"learning.outline.dates": "Date importanti",
"learning.outline.editGoal": "Modifica obiettivo ",
"learning.outline.expandAll": "Espandi tutto ",
"learning.outline.goal": "Obiettivo ",
"learning.outline.goalReminderDetail": "Se notiamo che non sei del tutto al tuo obiettivo, ti invieremo un promemoria via email.",
"learning.outline.goalUnsure": "Non ancora sicuro",
"learning.outline.handouts": "Materiali del Corso",
"learning.outline.incompleteAssignment": "Incompleto",
"learning.outline.incompleteSection": "Sezione incompleta ",
"learning.outline.goalButton.intense.text": "5 giorni a settimana",
"learning.outline.goalButton.intense.title": "Intenso",
"learning.outline.learnMore": "Approfondisci",
"learning.outline.altText.openSection": "Apri",
"learning.proctoringPanel.header": "Questo corso contiene esami supervisionati ",
"learning.outline.goalButton.regular.text": "3 giorni a settimana",
"learning.outline.goalButton.regular.title": "Regolare",
"learning.outline.resumeBlurb": "Riprendi da dove eri rimasto",
"learning.outline.resume": "Riprendi corso ",
"learning.outline.setGoal": "Per iniziare, un obiettivo del corso selezionando l'opzione riportata di seguito che meglio descrive il tuo piano di apprendimento. ",
"learning.outline.setGoalReminder": "Imposta un promemoria per l&#39;obiettivo",
"learning.outline.goalButton.casual.title": "Stabilisci uno stile di obiettivi di apprendimento.",
"learning.outline.setWeeklyGoal": "Stabilisci un obiettivo di apprendimento settimanale",
"learning.outline.setWeeklyGoalDetail": "Stabilire un obiettivo ti motiva a finire il corso. Puoi sempre cambiarlo in seguito.",
"learning.outline.start": "Inizio del corso",
"learning.outline.startBlurb": "Inizia oggi il tuo corso",
"learning.outline.tools": "Strumenti del corso ",
"learning.outline.upgradeButton": "Esegui l'upgrade ({symbol}{price})",
"learning.outline.upgradeTitle": "Consegui un certificato verificato ",
"learning.outline.welcomeMessage": "Messaggio di benvenuto ",
"learning.outline.welcomeMessageShowMoreButton": "Mostra altro ",
"learning.outline.welcomeMessageShowLessButton": "Mostra meno ",
"learning.outline.goalWelcome": "Benvenuto a",
"learning.proctoringPanel.status.notStarted": "Non iniziato",
"learning.proctoringPanel.status.started": "Avviato ",
"learning.proctoringPanel.status.submitted": "Inviato",
"learning.proctoringPanel.status.verified": "Verificato",
"learning.proctoringPanel.status.rejected": "Rifiutato",
"learning.proctoringPanel.status.error": "Errore",
"learning.proctoringPanel.status.otherCourseApproved": "Approvato in un altro corso ",
"learning.proctoringPanel.status.expiringSoon": "Prossimo alla scadenza ",
"learning.proctoringPanel.status.expired": "Expired",
"learning.proctoringPanel.status": "Stato di onboarding corrente: ",
"learning.proctoringPanel.message.notStarted": "L'esame di onboarding non è stato iniziato. ",
"learning.proctoringPanel.message.started": "L'esame di onboarding è stato iniziato. ",
"learning.proctoringPanel.message.submitted": "Il tuo esame di onboarding è stato inoltrato. ",
"learning.proctoringPanel.message.verified": "Il tuo esame di onboarding è stato approvato in questo corso.",
"learning.proctoringPanel.message.rejected": "L'esame di onboarding è stato rifiutato. Riprovare l'onboarding. ",
"learning.proctoringPanel.message.error": "Si è verificato un errore durante l'esame di onboarding. Riprovare l'onboarding. ",
"learning.proctoringPanel.message.otherCourseApproved": "Il tuo esame di onboarding è stato approvato in un altro corso.",
"learning.proctoringPanel.detail.otherCourseApproved": "Se il dispositivo è stato modificato, si consiglia di completare l'esame di onboarding di questo corso per accertarsi che la configurazione continui a soddisfare i requisiti per la supervisione. ",
"learning.proctoringPanel.message.expiringSoon": "Il tuo profilo di onboarding è stato approvato. Tuttavia, il tuo stato di onboarding scadrà a breve. Completa di nuovo l'onboarding per assicurarti di poter continuare a sostenere gli esami programmati.",
"learning.proctoringPanel.message.expired": "Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.",
"learning.proctoringPanel.generalInfo": "È necessario completare il processo di onboarding prima di sostenere esami supervisionati. ",
"learning.proctoringPanel.generalInfoSubmitted": "Il profilo inoltrato è in fase di revisione. ",
"learning.proctoringPanel.generalTime": "La revisione del profilo di onboarding può richiedere 2 o più giorni lavorativi. ",
"learning.proctoringPanel.onboardingButton": "Completa onboarding ",
"learning.proctoringPanel.onboardingPracticeButton": "Visualizza esame di onboarding ",
"learning.proctoringPanel.onboardingButtonNotOpen": "L'onboarding si apre il: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Esamina istruzioni e requisiti di sistema ",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding scaduto",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "Per generare un certificato, è necessario completare la verifica dell'identità. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Mostra i tuoi risultati su LinkedIn o il tuo curriculum oggi. Puoi scaricare ora il tuo certificato e accedervi in qualsiasi momento dalla dashboard e dal profilo.",
"courseCelebration.certificateBody.notAvailable.endDate": "I voti finali e tutti i certificati ottenuti saranno disponibili dopo {endDate}.",
"progress.certificateStatus.notPassingHeader": "Stato certificato",
"progress.certificateStatus.notPassingBody": "Per qualificarti per un certificato, devi avere una valutazione sufficiente.",
"progress.certificateStatus.inProgressHeader": "Ulteriore contenuto presto in arrivo!",
"progress.certificateStatus.inProgressBody": "Sembra che questo corso contenga ulteriore contenuto che verrà rilasciato in futuro. Ricercare gli aggiornamenti email o controllare il corso per sapere quando questo contenuto sarà disponibile. ",
"progress.certificateStatus.requestableHeader": "Stato certificato",
"progress.certificateStatus.requestableBody": "Congratulazioni, ti sei qualificato per ottenere un certificato! Per avere accesso al tuo certificato, richiedilo qui sotto.",
"progress.certificateStatus.requestableButton": "Richiedi certificato ",
"progress.certificateStatus.unverifiedHeader": "Stato certificato",
"progress.certificateStatus.unverifiedButton": "Verifica Identità",
"progress.certificateStatus.courseCelebration.verificationPending": "La verifica dell'identità è in attesa e il certificato sarà disponibile una volta approvato. ",
"progress.certificateStatus.downloadableHeader": "Il certificato è disponibile! ",
"progress.certificateStatus.viewableButton": "Visualizza il mio certificato ",
"progress.certificateStatus.notAvailableHeader": "Stato certificato",
"progress.certificateBody.notAvailable.endDate": "I voti finali e tutti i certificati ottenuti saranno disponibili dopo {endDate}.",
"progress.certificateStatus.upgradeHeader": "Acquisisci un certificato",
"progress.certificateStatus.upgradeBody": "Sei ora nella traccia di auditore e non puoi qualificarti per un certificato. Per avere un certificato, esegui l'upgrade del tuo corso oggi stesso.",
"progress.certificateStatus.upgradeButton": "Esegui l'upgrade ora ",
"progress.certificateStatus.unverifiedHomeHeader.v2": "Verifica la tua identità per qualificarti per un certificato.",
"progress.certificateStatus.unverifiedHomeButton": "Verifica il mio documento d&#39;identità",
"progress.certificateStatus.unverifiedHomeBody": "Per generare un certificato per questo corso, devi completare il processo di verifica dell&#39;identità.",
"progress.completion.donut.label": "completato",
"progress.completion.body": "Questo rappresenta la quantità di contenuto del corso che hai completato. Tieni presente che alcuni contenuti potrebbero non essere ancora stati rilasciati.",
"progress.completion.tooltip.locked": "Contenuto che hai completato.",
"progress.completion.header": "Completamento del corso",
"progress.completion.tooltip": "Contenuto a cui hai accesso e non hai completato.",
"progress.completion.tooltip.complete": "Contenuto bloccato e disponibile solo per coloro che eseguono l'upgrade.",
"progress.completion.donut.percentComplete": "Hai completato il {percent}% dei contenuti di questo corso.",
"progress.completion.donut.percentIncomplete": "Non hai completato il {percent}% dei contenuti di questo corso a cui hai accesso.",
"progress.completion.donut.percentLocked": "Il {percent}% dei contenuti di questo corso è bloccato e disponibile solo per coloro che eseguono l'upgrade.",
"progress.creditInformation.creditNotEligible": "Non sei più idoneo per il credito in questo corso. Ulteriori informazioni su {creditLink}.",
"progress.creditInformation.creditEligible": "Hai soddisfatto i requisiti per il credito in questo corso. Vai al tuo {dashboardLink} per acquistare crediti del corso. Oppure scopri di più su {creditLink}.",
"progress.creditInformation.creditPartialEligible": "Non hai ancora soddisfatto i requisiti per il credito. Ulteriori informazioni su {creditLink}.",
"progress.creditInformation.completed": "Completato",
"progress.creditInformation.courseCredit": "credito del corso",
"progress.creditInformation.minimumGrade": "Voto minimo per il credito ({minGrade}%)",
"progress.creditInformation.requirementsHeader": "Requisiti per i crediti del corso",
"progress.creditInformation.upcoming": "Prossimi eventi",
"progress.creditInformation.verificationFailed": "Verifica fallita",
"progress.creditInformation.verificationSubmitted": "Verifica presentata",
"progress.ungradedAlert": "Per i progressi sugli aspetti non valutati del corso, visualizza il tuo {outlineLink}.",
"progress.footnotes.droppableAssignments": "Il punteggio più basso di {numDroppable, plural, one{# {assignmentType} è} other{# {assignmentType} sono}} eliminati.",
"progress.assignmentType": "Tipo di Compito",
"progress.footnotes.backToContent": "Torna al contenuto",
"progress.courseGrade.body": "Questo rappresenta il tuo voto ponderato rispetto al voto necessario per superare questo corso.",
"progress.courseGrade.gradeBar.altText": "Il tuo voto attuale è {currentGrade}%. Per superare questo corso è richiesto un voto pesato di {goingGrade}%.",
"progress.courseGrade.footer.generic.passing": "Al momento stai superando questo corso",
"progress.courseGrade.footer.nonPassing": "Un voto ponderato di {passingGrade}% è richiesto per superare questo corso",
"progress.courseGrade.footer.passing": "Stai attualmente superando questo corso con una valutazione di {letterGrade} ({minGrade}-{maxGrade}%)",
"progress.courseGrade.preview.headerLocked": "funzionalità bloccata",
"progress.courseGrade.preview.headerLimited": "caratteristica limitata",
"progress.courseGrade.preview.header.ariaHidden": "Anteprima di",
"progress.courseGrade.preview.body.unlockCertificate": "Sblocca per visualizzare i voti e lavorare per ottenere un certificato.",
"progress.courseGrade.partialpreview.body.unlockCertificate": "Sblocca per lavorare verso un certificato.",
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "Il termine per l&#39;aggiornamento a questo corso è scaduto.",
"progress.courseGrade.preview.button.upgrade": "Esegui l'upgrade ora ",
"progress.courseGrade.gradeRange.tooltip": "Gamma dei voti per questo corso:",
"progress.courseOutline": "Struttura del corso",
"progress.courseGrade.label.currentGrade": "Il tuo attuale voto",
"progress.detailedGrades": "Valutazioni dettagliate",
"progress.detailedGrades.emptyTable": "Al momento non hai alcun punteggio di compiti valutati.",
"progress.footnotes.title": "Note a piè di pagina di riepilogo delle valutazioni",
"progress.gradeSummary.grade": "Voto",
"progress.courseGrade.grades": "Voti",
"progress.courseGrade.gradesAndCredit": "Voti e crediti",
"progress.courseGrade.gradeRange.Tooltip": "Tooltip gamma voti",
"progress.gradeSummary": "Riepilogo valutazione",
"progress.gradeSummary.limitedAccessExplanation": "Hai un accesso limitato ai compiti valutati come parte dell&#39;audit track in questo corso.",
"progress.gradeSummary.tooltip.alt": "Tooltip sommario voti",
"progress.gradeSummary.tooltip.body": "Il valore del tuo compito nel corso è determinato dal tuo docente. Moltiplicando il tuo voto per il valore di quel tipo di compito, viene calcolato il voto ponderato. Il tuo voto ponderato è ciò che viene utilizzato per determinare se superi il corso.",
"progress.noAcessToAssignmentType": "Non hai accesso ai compiti di tipo {assignmentType}",
"progress.noAcessToSubsection": "Non hai accesso alla sottosezione {displayName}",
"progress.courseGrade.label.passingGrade": "Voto di superamento",
"progress.detailedGrades.problemScore.label": "Punteggi del problema:",
"progress.detailedGrades.problemScore.toggleButton": "Attiva/disattiva i punteggi dei singoli problemi per {subsectionTitle}",
"progress.detailedGrades.overridden": "La valutazione della sezione è stata sovrascritta. ",
"progress.score": "Voto",
"progress.weight": "Peso",
"progress.weightedGrade": "Valutazione pesata",
"progress.weightedGradeSummary": "Il tuo attuale riepilogo pesato delle valutazioni",
"progress.header": "Il tuo progresso",
"progress.header.targetUser": "Avanzamento del corso per {username}",
"progress.link.studio": "Visualizza valutazione in Studio",
"progress.relatedLinks.datesCard.description": "Calendario delle scadenze dei corsi e prossimi compiti.",
"progress.relatedLinks.datesCard.link": "Date",
"progress.relatedLinks.outlineCard.description": "Una panoramica del contenuto del corso.",
"progress.relatedLinks.outlineCard.link": "Struttura del corso",
"progress.relatedLinks": "Link correlati",
"datesBanner.suggestedSchedule": "Abbiamo creato un programma suggerito per aiutarti a rimanere in pista. Ma non preoccuparti: è flessibile, così puoi imparare al tuo ritmo.",
"datesBanner.upgradeToCompleteGradedBanner.header": "Esegui l'upgrade per sbloccare",
"datesBanner.upgradeToCompleteGradedBanner.body": "Stai verificando questo corso, il che significa che non puoi partecipare ai compiti valutati. Per completare i compiti valutati come parte di questo corso, puoi fare un aggiornamento oggi.",
"datesBanner.upgradeToCompleteGradedBanner.button": "Esegui l'upgrade ora ",
"datesBanner.upgradeToResetBanner.body": "Per rispettare la tempistica, è possibile aggiornare questa pianificazione e spostare i compiti scaduti nel futuro. Non preoccuparti—quando le date di scadenza vengono spostate, non viene perso alcun progresso. ",
"datesBanner.upgradeToResetBanner.button": "Aggiorna per spostare le date scadute ",
"datesBanner.resetDatesBanner.header": "Sembra che alcune importanti scadenze non siano state rispettate in base alla pianificazione suggerita. ",
"datesBanner.resetDatesBanner.body": "Per rispettare la tempistica, è possibile aggiornare questa pianificazione e spostare i compiti scaduti nel futuro. Non preoccuparti—quando le date di scadenza vengono spostate, non viene perso alcun progresso. ",
"datesBanner.resetDatesBanner.button": "Sposta date di scadenza",
"learn.navigation.course.tabs.label": "Materiale del corso",
"unit.bookmark.button.add.bookmark": "Aggiungi ai preferiti",
"unit.bookmark.button.remove.bookmark": "Salvato nei segnalibri",
"learning.celebration.completed": "Hai appena completato la prima sezione del tuo corso.",
"learning.celebration.congrats": "Congratulazioni!",
"learning.celebration.earned": "L'hai meritato!",
"learning.celebration.emailSubject": "Sto per completare {title} online con {platform}!",
"learning.celebration.forward": "Continua ",
"learning.celebration.goalMet": "Hai raggiunto il tuo obiettivo!",
"learning.celebration.keepItUp": "Continuate così",
"learning.celebration.share": "Prenditi un attimo per festeggiare e condividere i tuoi progressi!",
"learning.celebration.social": "Sto per completare {title} online on {platform}. Come impieghi il tuo tempo ad imparare? ",
"learning.celebration.goalCongrats": "Congratulazioni, hai raggiunto il tuo obiettivo di apprendimento di {nTimes} a settimana.",
"learning.celebration.setGoal": "Stabilire un obiettivo può aiutarti {strongText} nel tuo corso.",
"calculator.instructions.button.label": "Istruzioni calcolatore ",
"calculator.instructions": "Per informazioni dettagliate, consultare {expressions_link}.",
"calculator.instructions.support.title": "Centro assistenza",
"calculator.instructions.useful.tips": "Suggerimenti utili: ",
"calculator.hint1": "Utilizza le parentesi () per rendere chiare le espressioni. È possibile usare le parentesi all'interno di altre parentesi.",
"calculator.hint2": "Non usare spazi nelle espressioni.",
"calculator.hint3": "Per le costanti, indicare esplicitamente la moltiplicazione (example: 5*c).",
"calculator.hint4": "Per affissi, digitare il numero e l'affisso senza spazio (example: 5c).",
"calculator.hint5": "Per le funzioni, digitare il nome della funzione, poi l'espressione tra parentesi.",
"calculator.instruction.table.to.use.heading": "Usare",
"calculator.instruction.table.type.heading": "Tipo",
"calculator.instruction.table.examples.heading": "Esempi",
"calculator.instruction.table.to.use.numbers": "Numeri",
"calculator.instruction.table.to.use.numbers.type1": "Numeri interi",
"calculator.instruction.table.to.use.numbers.type2": "Frazioni",
"calculator.instruction.table.to.use.numbers.type3": "Decimali",
"calculator.instruction.table.to.use.operators": "Operatori",
"calculator.instruction.table.to.use.operators.type1": "(aggiungi, sottrai, moltiplica, dividi)",
"calculator.instruction.table.to.use.operators.type2": "(eleva a potenza)",
"calculator.instruction.table.to.use.operators.type3": "(resistenze parallele)",
"calculator.instruction.table.to.use.constants": "Costanti",
"calculator.instruction.table.to.use.affixes": "Affissi",
"calculator.instruction.table.to.use.affixes.type": "Segno di percentuale (%)",
"calculator.instruction.table.to.use.basic.functions": "Funzioni di base",
"calculator.instruction.table.to.use.trig.functions": "Funzioni trigonometriche",
"calculator.instruction.table.to.use.scientific.notation": "Notazione Scientifica",
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} e l'esponente ",
"calculator.instruction.table.to.use.scientific.notation.type2": "Notazione {notationSyntax} ",
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax} e l'esponente ",
"calculator.button.label": "Calcolatore ",
"calculator.input.field.label": "Calcolatore input",
"calculator.submit.button.label": "Calcolare",
"calculator.result.field.label": "Risultato calcolatore ",
"calculator.result.field.placeholder": "Risultato ",
"notes.button.show": "Mostra note ",
"notes.button.hide": "Nascondi note ",
"courseExit.catalogSearchSuggestion": "Stai cercando di imparare di più? {searchOurCatalogLink} per trovare ulteriori corsi e programmi da esplorare. ",
"courseCelebration.certificateBody.available": "\n Mostra i tuoi risultati su LinkedIn o il tuo curriculum oggi stesso.\n Puoi scaricare il tuo certificato adesso e accedere in qualsiasi momento da\n {dashboardLink} e {profileLink}.",
"courseCelebration.certificateBody.notAvailable.endDate.v2": "Questo corso termina il {endDate}. I voti finali e gli eventuali certificati ottenuti saranno disponibili dopo il {certAvailableDate}.",
"courseCelebration.certificateBody.unverified": "Per generare un certificato, è necessario completare la verifica dell'identità.\n {idVerificationSupportLink} ora. ",
"courseCelebration.certificateBody.upgradable": "Non è troppo tardi per eseguire l'upgrade. Per {price} sbloccherai l'accesso a tutti i\n compiti valutati in questo corso. Al completamento, riceverai un certificato verificato che rappresenta\n una credenziale di valore per migliorare le tue prospettive lavorative e migliorare la tua carriera o\n evidenziare il certificato nelle applicazioni scolastiche. ",
"courseCelebration.upgradeDiscountCodePrompt": "Utilizza il codice {code} al checkout per uno sconto del {percent}% ! ",
"courseCelebration.recommendations.heading": "Continua a creare le tue competenze con questi corsi! ",
"courseCelebration.recommendations.label": "Corso",
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {, } conjunction { {sp}and } altro { }}",
"courseCelebration.recommendations.browse_catalog": "Esplora altri corsi ",
"courseCelebration.recommendations.loading_recommendations": "Caricamento delle raccomandazioni ",
"courseCelebration.recommendations.card.schools.label": "Scuole e partner ",
"courseCelebration.dashboardInfo": "È possibile accedere a questo corso ed ai relativi materiali da {dashboardLink}.",
"courseExit.programs.applyForCredit": "Richiedi credito ",
"courseCelebration.certificateHeader.downloadable": "Il certificato è disponibile! ",
"courseCelebration.certificateHeader.notAvailable": "Il tuo grado e lo stato del certificato saranno presto disponibili.",
"courseCelebration.certificateBody.notAvailable.accessCertificate": "Se hai ottenuto un voto positivo, il tuo certificato verrà rilasciato automaticamente.",
"courseCelebration.certificateHeader.unverified": "È necessario completare la verifica per ricevere il certificato. ",
"courseCelebration.certificateHeader.requestable": "Congratulazioni! Ti sei qualificato per ottenere un certificato!",
"courseCelebration.certificateHeader.upgradable": "Effettua l'aggiornamento per conseguire un certificato verificato ",
"courseCelebration.certificateImage": "Certificato di esempio ",
"courseCelebration.completedCourseHeader": "Hai completato il tuo corso.",
"courseCelebration.congratulationsHeader": "Congratulazioni!",
"courseCelebration.congratulationsImage": "Quattro persone che alzano le mani per festeggiare ",
"courseExit.courseInProgressDescription": "Sembra che questo corso contenga ulteriore contenuto che verrà rilasciato in futuro. Ricercare gli aggiornamenti email o controllare il corso per sapere quando questo contenuto sarà disponibile. ",
"courseExit.courseInProgressHeader": "Ulteriore contenuto presto in arrivo!",
"courseExit.dashboardLink": "Bacheca",
"courseExit.endOfCourseDescription": "Sfortunatamente, non sei attualmente idoneo per un certificato. È necessario ricevere una valutazione sufficiente per essere idonei per un certificato. ",
"courseExit.endOfCourseHeader": "Hai raggiunto la fine del corso! ",
"courseExit.endOfCourseTitle": "Fine del corso ",
"courseExit.idVerificationSupportLink": "Scopri di più sulla verifica dell'identità",
"courseCelebration.linkedinAddToProfileButton": "Aggiungi al profilo LinkedIn ",
"courseExit.programs.microBachelors.learnMore": "Scopri di più su come utilizzare le credenziali MicroBachelors per il credito ",
"courseExit.programs.microMasters.learnMore": "Scopri di più sul processo di applicazione dei certificati MicroMasters ai Master. ",
"courseExit.programs.microMasters.mastersMessage": "Se sei interessato ad utilizzare il certificato MicroMasters per un programma Master, è possibile iniziare oggi stesso.",
"learn.sequence.navigation.complete.button": "Completa il corso",
"courseExit.nextButton.endOfCourse": "Avanti (fine del corso)",
"courseExit.profileLink": "Profilo",
"courseExit.programs.lastCourse": "Hai completato l'ultimo corso in {title}!",
"courseCelebration.requestCertificateBodyText": "Per accedere al certificato, effettuare la richiesta di seguito. ",
"courseCelebration.requestCertificateButton": "Richiedi certificato ",
"courseExit.searchOurCatalogLink": "Ricerca nel catalogo ",
"courseCelebration.shareMessage": "Condividi il tuo successo sui social media o tramite email.",
"courseExit.social.shareCompletionMessage": "Ho appena completato {title} con {platform}!",
"courseExit.upgradeButton": "Esegui l'upgrade ora ",
"courseExit.upgradeLink": "esegui l'upgrade ora ",
"courseCelebration.verificationPending": "La verifica dell'identità è in attesa e il certificato sarà disponibile una volta approvato. ",
"courseExit.verifiedCertificateSupportLink": "Scopri di più sui certificati verificati ",
"courseCelebration.verifyIdentityButton": "Verifica identità ora ",
"courseCelebration.viewCertificateButton": "Visualizza il mio certificato ",
"courseExit.viewCourseScheduleButton": "Visualizza pianificazione del corso ",
"courseExit.viewCoursesButton": "Visualizza i miei corsi ",
"courseExit.viewGradesButton": "Visualizza valutazioni ",
"courseExit.programCompletion.dashboardMessage": "Per visualizzare lo stato del certificato, controllare la sezione Programmi di {programLink}.",
"courseExit.upgradeFootnote": "L'accesso a questo corso e ai materiali è disponibile nella dashboard fino al {expirationDate}. Per estendere l'accesso, {upgradeLink}.",
"learn.course.license.allRightsReserved.text": "Tutti i diritti riservati",
"learn.course.license.creativeCommons.terms.preamble": "Contenuto fornito su licenza Creative Commons, con i seguenti termini: ",
"learn.course.license.creativeCommons.terms.by": "Attribuzione",
"learn.course.license.creativeCommons.terms.nc": "Non commerciale",
"learn.course.license.creativeCommons.terms.nd": "Non derivati",
"learn.course.license.creativeCommons.terms.sa": "Condividi allo stesso modo",
"learn.course.license.creativeCommons.terms.zero": "Nessun termine ",
"learn.course.license.creativeCommons.text": "Some Rights Reserved",
"learn.breadcrumb.navigation.course.home": "Corso",
"notification.tray.container": "Vassoio di notifica",
"notification.open.button": "Mostra la barra delle notifiche",
"notification.close.button": "Chiudi la barra delle notifiche",
"responsive.close.notification": "Torna al Corso",
"notification.tray.title": "Notifiche",
"notification.tray.no.message": "Non hai alcuna nuova notifica al momento.",
"learn.contentLock.content.locked": "Contenuto bloccato ",
"learn.contentLock.complete.prerequisite": "Devi completare il prerequisito: {prereqSectionName} per accedere a questo contenuto.",
"learn.contentLock.goToSection": "Vai alla sezione Prerequisiti ",
"learn.hiddenAfterDue.gradeAvailable": "Se hai completato questo compito, il tuo voto è disponibile su {progressPage}.",
"learn.hiddenAfterDue.header": "La data di scadenza per questo compito è trascorsa. ",
"learn.hiddenAfterDue.description": "Poiché la data di scadenza è scaduta, questo compito non è più disponibile.",
"learn.hiddenAfterDue.progressPage": "pagina di avanzamento",
"learn.honorCode.content": "L&#39;onestà e l&#39;integrità accademica sono importanti per {siteName} e per le istituzioni che forniscono corsi e programmi sul sito {siteName}. Facendo clic su &quot;Accetto&quot; di seguito, confermo di aver letto, compreso e rispetterò il {link} per il sito {siteName}.",
"learn.honorCode.name": "Codice d'Onore",
"learn.honorCode.cancel": "Annulla",
"learn.honorCode.agree": "Concordo",
"learn.lockPaywall.title": "I compiti valutati sono bloccati",
"learn.lockPaywall.content": "Esegui l'upgrade per ottenere l'accesso a feature bloccate come questa e ottenere il massimo dal tuo corso.",
"learn.lockPaywall.content.pastExpiration": "La scadenza per l&#39;aggiornamento per questo corso è scaduta. Per eseguire l&#39;upgrade, iscriviti alla prossima sessione disponibile.",
"learn.lockPaywall.courseDetails": "Visualizza i dettagli del corso",
"learn.lockPaywall.example.alt": "Certificato di esempio ",
"learn.lockPaywall.list.intro": "Quando esegui l'upgrade, tu:",
"learn.header.h2.placeholder": "I titoli di livello 2 possono essere creati dai fornitori di corsi in futuro.",
"learn.course.load.failure": "Si è verificato un errore durante il caricamento di questo corso. ",
"learn.loading.honor.codk": "Caricamento della messaggistica del codice d&#39;onore in corso...",
"learn.loading.content.lock": "Caricamento della messaggistica del contenuto bloccato... ",
"learn.loading.learning.sequence": "Caricamento della sequenza di apprendimento...",
"learn.sequence.no.content": "Nessun contenuto presente qui. ",
"learn.sequence.navigation.next.button": "Prossimo",
"learn.sequence.navigation.next.up.button": "Prossimo: {title}",
"learn.sequence.navigation.previous.button": "Precedente",
"learn.course.sequence.navigation.mobile.menu": "{current} di {total}",
"learn.sequence.share.button": "Share this content",
"learn.sequence.share.modal.title": "Title",
"learn.sequence.share.modal.body": "Copy the link below to share this content.",
"learn.sequence.share.quote": "Here's a fun clip from a class I'm taking on @edXonline.\n",
"discussions.sidebar.title": "Discussioni",
"discussions.sidebar.open.button": "Mostra la barra delle discussioni",
"learn.redirect.interstitial.message": "Reindirizzamento... ",
"learn.loading.error": "Errore: {error}",
"learning.celebration.emailBody": "Come impieghi il tuo tempo ad imparare? ",
"learning.social.shareEmail": "Condividi i tuoi progressi via email.",
"learning.social.shareService": "Condividi i tuoi progressi su {service}.",
"general.altText.close": "Chiudi",
"learning.logistration.register": "registrati",
"learning.logistration.login": "accedi",
"general.signIn.sentenceCase": "Accedi",
"learn.course.tabs.navigation.overflow.menu": "Altro... ",
"learning.offer.screenReaderPrices": "Prezzo originale: {originalPrice}, prezzo scontato: {discountedPrice}",
"learning.upgradeButton.screenReaderInlinePrices": "Prezzo originale: {originalPrice}",
"learning.upgradeButton.buttonText": "Esegui l'upgrade per {pricing}",
"learning.upgradeNowButton.buttonText": "Esegui l'upgrade ora per {pricing}",
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "compreso qualsiasi progresso",
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "vantaggi dell&#39;aggiornamento",
"learning.generic.upgradeNotification.expirationAccessLoss": "Perderai l'accesso a questo corso, {includingAnyProgress}, il {date}. ",
"learning.generic.upgradeNotification.expirationVerifiedCert": "L'upgrade del corso ti consente di ottenere un certificato verificato e sblocca numerose funzionalità. Ulteriori informazioni sui {benefitsOfUpgrading}.",
"learning.generic.upgradeNotification.pastExpiration.content": "La scadenza per l&#39;aggiornamento per questo corso è scaduta. Per eseguire l&#39;upgrade, iscriviti alla prossima sessione disponibile.",
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \n one {giorno}\n other {giorni}} rimasto/i",
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural,\n one {ora}\n other {ore}} rimasta/e",
"learning.generic.upgradeNotification.expirationMinutes": "Meno di 1 ora rimasta",
"learning.generic.upgradeNotification.expiration": "L'accesso al corso scadrà il {date}",
"learning.generic.upgradeNotification.pastExpiration.banner": "Scadenza dell&#39;aggiornamento scaduta il {date}",
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% di sconto Studente per il primo acquisto",
"learning.generic.upgradeNotification.accessExpiration": "Esegui l'upgrade del corso oggi stesso",
"learning.generic.upgradeNotification.accessExpirationUrgent": "Scadenza Accesso al Corso",
"learning.generic.upgradeNotification.accessExpirationPast": "Scadenza Accesso al Corso",
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Consegui un certificato verificato ",
"learning.generic.upgradeNotification.code": "Utilizza il codice {code} al checkout",
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "certificato verificato",
"learning.generic.upsell.verifiedCertBullet": "Guadagna un {verifiedCertLink} di completamento da mostrare sul tuo curriculum",
"learning.generic.upsell.unlockGradedBullet.gradedAssignments": "Compiti valutati",
"learning.generic.upsell.unlockGradedBullet": "Sblocca l&#39;accesso a tutte le attività del corso, incluso {gradedAssignmentsInBoldText}",
"learning.generic.upsell.fullAccessBullet.fullAccess": "Accesso completo",
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} al contenuto e ai materiali del corso, anche dopo la fine del corso",
"learning.generic.upsell.supportMissionBullet.mission": "missione",
"learning.generic.upsell.supportMissionBullet": "Supporta il nostro {missionInBoldText} su {siteName}",
"masquerade-widget.userName.error.generic": "Si è verificato un errore; riprovare. ",
"masquerade-widget.userName.input.placeholder": "Nome utente o email ",
"masquerade-widget.userName.input.label": "Mascheramento come questo utente ",
"tours.abandonTour.launchTourCheckpoint.body": "Sentirsi persi? Avvia il tour in qualsiasi momento per alcuni suggerimenti rapidi per ottenere il massimo dall&#39;esperienza.",
"tours.sequenceNavigationCheckpoint.body": "La barra in alto all&#39;interno del tuo corso ti consente di passare facilmente a diverse sezioni e ti mostra cosa sta succedendo.",
"tours.existingUserTour.launchTourCheckpoint.body": "Di recente abbiamo aggiunto alcune nuove funzionalità all&#39;esperienza del corso. Vuoi un aiuto per guardarti intorno? Fai un tour per saperne di più.",
"tours.button.dismiss": "Chiudi",
"tours.button.next": "Successivo",
"tours.button.okay": "Bene",
"tours.button.beginTour": "Inizia il tour",
"tours.button.launchTour": "Lancia il tour",
"tours.newUserModal.body": "Facciamo un rapido tour di {siteName} in modo che tu possa ottenere il massimo dal tuo corso.",
"tours.newUserModal.title.welcome": "Benvenuto nel tuo",
"tours.button.skipForNow": "Salta per ora",
"tours.datesCheckpoint.body": "Le date importanti possono aiutarti a rispettare i tempi.",
"tours.datesCheckpoint.title": "Tieniti aggiornato sulle date chiave",
"tours.outlineCheckpoint.body": "Puoi esplorare le sezioni del corso utilizzando lo schema seguente.",
"tours.outlineCheckpoint.title": "Fai il corso!",
"tours.tabNavigationCheckpoint.body": "Queste schede possono essere utilizzate per accedere ad altri materiali del corso, come progressi, programma, ecc.",
"tours.tabNavigationCheckpoint.title": "Risorse aggiuntive per il corso",
"tours.upgradeCheckpoint.body": "Lavora per ottenere un certificato e ottieni pieno accesso ai materiali del corso. Aggiorna ora!",
"tours.upgradeCheckpoint.title": "Sblocca il tuo corso",
"tours.weeklyGoalsCheckpoint.body": "Stabilire un obiettivo ti rende più propenso a completare il tuo corso.",
"tours.weeklyGoalsCheckpoint.title": "Stabilisci un obiettivo del corso",
"tours.newUserModal.title": "{welcome} {siteName} corso!",
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
"learning.effortEstimation.activities": "{activityCount, plural, one {# attività} many {# attività} other {# attività}}",
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# min} many {# min} other {# min}}",
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minuto} many {# minuti} other {# minuti}}",
"learning.streakCelebration.congratulations": "Congratulazioni!",
"learning.streakCelebration.body": "Continua così!",
"learning.streakCelebration.button": "Continua così ",
"learning.streakCelebration.buttonSrOnly": "Chiudi finestra modale e continua ",
"learning.streakCelebration.buttonAA759": "Continua con il corso",
"learning.streakCelebration.header": "serie di giorni ",
"learning.streakCelebration.factoidABoldedSection": "hanno una probabilità 20 volte maggiore di superare il corso ",
"learning.streakCelebration.factoidBBoldedSection": "completare un contenuto del corso 5 volte superiore in media ",
"learning.streakCelebration.streakDiscountMessage": "Hai sbloccato uno sconto del {percent}% quando esegui l&#39;upgrade di questo corso solo per un periodo di tempo limitato.",
"learning.streakcelebration.factoida": "Utenti che apprendono {streak_length} giorni di seguito {bolded_section} rispetto a quelli che non lo fanno. ",
"learning.streakcelebration.factoidb": "Utenti che apprendono {streak_length} giorni di seguito {bolded_section} rispetto a quelli che non lo fanno. ",
"learning.streakCelebration.streakCelebrationCouponEndDateMessage": "Termina {date}.",
"learning.loading.failure": "Si è verificato un errore durante il caricamento di questo corso. ",
"learning.loading": "Caricamento della pagina del corso…"
}

View File

@@ -0,0 +1,452 @@
{
"learning.accessExpiration.deadline": "Atualizar até {date} para obter acesso ilimitado ao curso, desde que exista no website.",
"learning.accessExpiration.header": "O Acesso à Auditoria Expira {date}",
"learning.accessExpiration.body": "Perde todo o acesso a este curso, incluindo o seu progresso, em {date}.",
"instructorToolbar.pageBanner.courseHasExpired": "Este aluno já não tem acesso a este curso. O acesso dele expirou em {date}.",
"learning.accessExpiration.upgradeNow": "Atualize agora",
"learning.activeEnterprise.alert": "{changeActiveEnterprise}.",
"learning.activeEnterprise.change.alert": "mudar de empresa agora",
"learning.outline.alert.start.short": "O curso começa daqui a {timeRemaining} às {courseStartTime}.",
"learning.outline.alert.end.long": "Este curso termina em {timeRemaining} em {courseEndDate}.",
"learning.outline.alert.end.calendar": "Não se esqueça de adicionar um lembrete no calendário!",
"instructorToolbar.pageBanner.courseHasNotStarted": "Este aluno ainda não tem acesso a este curso. O curso começa em {date}.",
"learning.enrollment.alert": "Tem de estar inscrito no curso para ver o conteúdo do curso.",
"learning.staff.enrollment.alert": "Está a ver este curso como um funcionário, e não está inscrito.",
"learning.enrollment.enrollNow.Inline": "Inscreva-se agora",
"learning.enrollment.enrollNow.Sentence": "Inscreva-se agora.",
"learning.enrollment.success": "Inscreveu-se com sucesso neste curso!",
"account-activation.alert.button": "Continuar para {siteName}",
"account-activation.alert.message": "Enviámos um e-mail para {boldEmail} com um link para activar a sua conta. Não o consegue encontrar? Verifique a sua pasta de spam ou\n {sendEmailTag}.",
"account-activation.resend.link": "reenviar o e-mail",
"learning.logistration.alert": "Para ver o conteúdo do curso, {signIn} ou {register}.",
"account-activation.alert.title": "Ative a sua conta para poder voltar a entrar",
"learn.sequence.entranceExamTextNotPassing": "Para ter acesso aos materiais do curso, deverá obter uma pontuação {entranceExamMinimumScorePct}% ou superior neste exame. A sua pontuação actual é {entranceExamCurrentScore}%.",
"learn.sequence.entranceExamTextPassed": "A sua pontuação é {entranceExamCurrentScore}%. Passou no exame de admissão.",
"learning.dates.badge.completed": "Concluído",
"learning.dates.badge.dueNext": "Próxima data",
"learning.dates.badge.pastDue": "Data limite",
"learning.dates.title": "Datas importantes",
"learning.dates.badge.today": "Hoje",
"learning.dates.badge.unreleased": "Ainda não foi divulgado",
"learning.dates.badge.verifiedOnly": "Apenas verificado",
"learning.goals.unsubscribe.contact": "contate o suporte",
"learning.goals.unsubscribe.description": "Não receberá mais lembretes por email sobre a sua meta para {courseTitle}.",
"learning.goals.unsubscribe.errorHeader": "Algo correu mal",
"learning.goals.unsubscribe.goToDashboard": "Ir para Painel de Controlo",
"learning.goals.unsubscribe.header": "Cancelou a sua inscrição para lembretes de objetivos",
"learning.goals.unsubscribe.loading": "A cancelar a inscrição…",
"learning.goals.unsubscribe.errorDescription": "Não foi possível cancelar a sua inscrição dos emails de lembrete de objetivo. Tente novamente mais tarde ou {contactSupport} para obter ajuda.",
"learning.outline.alert.cert.earnedNotAvailable": "Este curso termina a {courseEndDateFormatted}. As notas finais e quaisquer certificados obtidos estão\n programados para estarem disponíveis após {certificateAvailableDate}.",
"cert.alert.earned.unavailable.header.v2": "A sua classificação e estado de certificado estarão disponíveis em breve.",
"cert.alert.earned.ready.header": "Parabéns! O seu certificado está pronto.",
"cert.alert.notPassing.header": "Ainda não é elegível para um certificado",
"cert.alert.notPassing.button": "Ver classificações",
"learning.outline.alert.end.short": "Este curso termina daqui a {timeRemaining} às {courseEndTime}.",
"alert.enroll": "para aceder ao curso completo.",
"learning.privateCourse.signInOrRegister": "{signIn} ou {register} e depois inscreva-se neste curso.",
"learning.outline.alert.scheduled-content.heading": "Mais conteúdos em breve!",
"learning.outline.alert.scheduled-content.body": "Este curso terá mais conteúdos lançados numa data futura. Procure atualizações por email ou volte a consultar este curso para obter atualizações.",
"learning.outline.alert.scheduled-content.button": "Ver Calendário do Curso",
"learning.outline.dates.all": "Ver todas as datas dos cursos",
"learning.outline.goalButton.casual.text": "1 dia por semana",
"learning.outline.goalButton.screenReader.text": "Casual",
"learning.outline.certificateAlt": "Certificado de Exemplo",
"learning.outline.collapseAll": "Encolher tudo",
"learning.outline.completedAssignment": "Concluído",
"learning.outline.completedSection": "Secção completa",
"learning.outline.dates": "Datas importantes",
"learning.outline.editGoal": "Editar objetivo",
"learning.outline.expandAll": "Expandir tudo",
"learning.outline.goal": "Objetivo",
"learning.outline.goalReminderDetail": "Se repararmos que não está a alcançar o seu objetivo, enviar-lhe-emos um lembrete por email.",
"learning.outline.goalUnsure": "Ainda não tenho a certeza",
"learning.outline.handouts": "Materiais de Apoio do Curso",
"learning.outline.incompleteAssignment": "Incompleto",
"learning.outline.incompleteSection": "Secção incompleta",
"learning.outline.goalButton.intense.text": "5 dias por semana",
"learning.outline.goalButton.intense.title": "Intenso",
"learning.outline.learnMore": "Saber Mais",
"learning.outline.altText.openSection": "Abrir",
"learning.proctoringPanel.header": "Este curso contém exames vigiados",
"learning.outline.goalButton.regular.text": "3 dias por semana",
"learning.outline.goalButton.regular.title": "Regular",
"learning.outline.resumeBlurb": "Retomar de onde parou",
"learning.outline.resume": "Continuar curso",
"learning.outline.setGoal": "Para começar, defina um objectivo no curso seleccionando na opção abaixo a melhor que descreve o seu plano de formação.",
"learning.outline.setGoalReminder": "Definir um lembrete para o objetivo",
"learning.outline.goalButton.casual.title": "Definir um estilo de objetivo de aprendizagem.",
"learning.outline.setWeeklyGoal": "Definir um objetivo de aprendizagem semanal",
"learning.outline.setWeeklyGoalDetail": "A definição de um objetivo motiva-o a terminar o curso. Pode sempre alterá-lo mais tarde.",
"learning.outline.start": "Começar curso",
"learning.outline.startBlurb": "Comece o seu curso hoje",
"learning.outline.tools": "Ferramentas do Curso",
"learning.outline.upgradeButton": "Atualizar ({symbol}{price})",
"learning.outline.upgradeTitle": "Obter um certificado validado",
"learning.outline.welcomeMessage": "Mensagem de Boas vindas",
"learning.outline.welcomeMessageShowMoreButton": "Ver Mais",
"learning.outline.welcomeMessageShowLessButton": "Ver Menos",
"learning.outline.goalWelcome": "Bem-vindo a",
"learning.proctoringPanel.status.notStarted": "Não Iniciado",
"learning.proctoringPanel.status.started": "Iniciado",
"learning.proctoringPanel.status.submitted": "Submetido",
"learning.proctoringPanel.status.verified": "Validado",
"learning.proctoringPanel.status.rejected": "Rejeitado",
"learning.proctoringPanel.status.error": "Erro",
"learning.proctoringPanel.status.otherCourseApproved": "Aprovado Noutro Curso",
"learning.proctoringPanel.status.expiringSoon": "Expira em Breve",
"learning.proctoringPanel.status.expired": "Expirado",
"learning.proctoringPanel.status": "Situação Actual de Admissão:",
"learning.proctoringPanel.message.notStarted": "Ainda não começou o seu exame de admissão.",
"learning.proctoringPanel.message.started": "Começou o seu exame de admissão.",
"learning.proctoringPanel.message.submitted": "Submeteu o seu exame de admissão.",
"learning.proctoringPanel.message.verified": "O seu exame de admissão foi aprovado neste curso.",
"learning.proctoringPanel.message.rejected": "O seu exame de admissão foi rejeitado. Por favor, tente de novo a admissão.",
"learning.proctoringPanel.message.error": "Ocorreu um erro durante o seu exame de admissão. Por favor, tente de novo a admissão.",
"learning.proctoringPanel.message.otherCourseApproved": "O seu exame de admissão foi aprovado noutro curso.",
"learning.proctoringPanel.detail.otherCourseApproved": "Se o seu dispositivo tiver mudado, recomendamos que complete o exame de admissão deste curso a fim de garantir que a sua configuração ainda cumpre os requisitos para o exame vigiado.",
"learning.proctoringPanel.message.expiringSoon": "O seu perfil de bordo foi aprovado. No entanto, o seu estatuto de bordo expira em breve. Por favor, complete novamente o embarque para se assegurar de que poderá continuar a fazer exames supervisionados.",
"learning.proctoringPanel.message.expired": "O seu estatuto de embarque expirou. Por favor, complete novamente o embarque para continuar a fazer exames de supervisionados.",
"learning.proctoringPanel.generalInfo": "Deve completar o processo de admissão antes de fazer qualquer exame vigiado. ",
"learning.proctoringPanel.generalInfoSubmitted": "O perfil submetido está em revisão.",
"learning.proctoringPanel.generalTime": "A revisão do perfil de admissão pode demorar mais de 2 dias úteis.",
"learning.proctoringPanel.onboardingButton": "Admissão Completa",
"learning.proctoringPanel.onboardingPracticeButton": "Ver Exame de Admissão",
"learning.proctoringPanel.onboardingButtonNotOpen": "Admissão Abre: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Rever instruções e requisitos do sistema",
"learning.proctoringPanel.onboardingButtonPastDue": "Integração vencida",
"learning.outline.sequence-due-date-set": "{description} previsto {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "A fim de obter um certificado, deve completar a verificação da sua identificação. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Mostre hoje o seu sucesso no LinkedIn ou no seu currículo. Pode descarregar agora o seu certificado e aceder ao mesmo em qualquer altura a partir do seu Painel de controlo e Perfil.",
"courseCelebration.certificateBody.notAvailable.endDate": "As notas finais e quaisquer certificados obtidos estão programados para estarem disponíveis após {endDate}.",
"progress.certificateStatus.notPassingHeader": "Estado do certificado",
"progress.certificateStatus.notPassingBody": "Para se qualificar para um certificado, deve ter uma classificação de aproveitamento.",
"progress.certificateStatus.inProgressHeader": "Mais conteúdos em breve!",
"progress.certificateStatus.inProgressBody": "Parece que há mais conteúdos neste curso a ser disponibilizados no futuro. Fique atento a actualizações por correio electrónico ou verifique novamente o seu curso para saber quando é que estes conteúdos estarão disponíveis.",
"progress.certificateStatus.requestableHeader": "Estado do certificado",
"progress.certificateStatus.requestableBody": "Parabéns, qualificou-se para um certificado! Para aceder ao seu certificado, solicite-o abaixo.",
"progress.certificateStatus.requestableButton": "Pedir certificado",
"progress.certificateStatus.unverifiedHeader": "Estado do certificado",
"progress.certificateStatus.unverifiedButton": "Verificar ID",
"progress.certificateStatus.courseCelebration.verificationPending": "A sua verificação de ID está pendente e o seu certificado estará disponível quando for aprovado.",
"progress.certificateStatus.downloadableHeader": "O seu certificado está disponível!",
"progress.certificateStatus.viewableButton": "Ver o meu certificado",
"progress.certificateStatus.notAvailableHeader": "Estado do certificado",
"progress.certificateBody.notAvailable.endDate": "As notas finais e quaisquer certificados obtidos estão programados para estarem disponíveis após {endDate}.",
"progress.certificateStatus.upgradeHeader": "Obtenha um certificado",
"progress.certificateStatus.upgradeBody": "Está inscrito no modo observação logo não se qualifica para obter um certificado. Para obter um certificado, deverá actualizar hoje a sua inscrição.",
"progress.certificateStatus.upgradeButton": "Atualize agora",
"progress.certificateStatus.unverifiedHomeHeader.v2": "Verifique a sua identidade para se qualificar para um certificado.",
"progress.certificateStatus.unverifiedHomeButton": "Verificar a minha identificação",
"progress.certificateStatus.unverifiedHomeBody": "Para gerar um certificado para este curso, tem de completar o processo de verificação de identidade.",
"progress.completion.donut.label": "concluído",
"progress.completion.body": "Isto representa a quantidade de conteúdos do curso que completou. De notar que alguns conteúdos podem ainda não ter sido divulgados.",
"progress.completion.tooltip.locked": "Conteúdo que tenha concluído.",
"progress.completion.header": "Conclusão do curso",
"progress.completion.tooltip": "Conteúdo a que tem acesso e que ainda não concluiu.",
"progress.completion.tooltip.complete": "Conteúdo que está bloqueado e disponível apenas para quem actualizar.",
"progress.completion.donut.percentComplete": "Concluiu {percent}% do conteúdo deste curso.",
"progress.completion.donut.percentIncomplete": "Não completou {percent}% do conteúdo deste curso a que tem acesso.",
"progress.completion.donut.percentLocked": "{percent}% do conteúdo deste curso está bloqueado e disponível apenas para quem actualizar.",
"progress.creditInformation.creditNotEligible": "Já não é elegível para crédito neste curso. Saiba mais sobre {creditLink}.",
"progress.creditInformation.creditEligible": "\n Cumpriu os requisitos de crédito neste curso. Vá ao seu \n {dashboardLink} para adquirir crédito do curso. Ou saiba mais sobre {creditLink}.",
"progress.creditInformation.creditPartialEligible": "Ainda não cumpriu os requisitos de crédito. Saiba mais sobre {creditLink}.",
"progress.creditInformation.completed": "Concluído",
"progress.creditInformation.courseCredit": "crédito do curso",
"progress.creditInformation.minimumGrade": "Nota mínima para crédito ({minGrade}%)",
"progress.creditInformation.requirementsHeader": "Requisitos para crédito de curso",
"progress.creditInformation.upcoming": "Próximos",
"progress.creditInformation.verificationFailed": "Falha na verificação",
"progress.creditInformation.verificationSubmitted": "Verificação submetida",
"progress.ungradedAlert": "Para progredir em domínios não avaliados do curso, veja o seu {outlineLink}.",
"progress.footnotes.droppableAssignments": "A pontuação mais baixa {numDroppable, plural, one{# {assignmentType} pontuação é} other{# {assignmentType} pontuação é}} caída/removida.",
"progress.assignmentType": "Tipo de tarefa",
"progress.footnotes.backToContent": "Voltar ao índice",
"progress.courseGrade.body": "Isto representa a sua nota ponderada em relação à nota necessária para passar neste curso.",
"progress.courseGrade.gradeBar.altText": "A sua classificação actual é {currentGrade}%. É necessária uma nota ponderada de {passingGrade}% para passar neste curso.",
"progress.courseGrade.footer.generic.passing": "Está actualmente a passar neste curso.",
"progress.courseGrade.footer.nonPassing": "É necessária uma nota ponderada de {passingGrade}% para passar neste curso",
"progress.courseGrade.footer.passing": "Está actualmente a passar neste curso com uma nota de {letterGrade} ({minGrade}-{maxGrade}%)",
"progress.courseGrade.preview.headerLocked": "funcionalidade bloqueada",
"progress.courseGrade.preview.headerLimited": "recurso limitado",
"progress.courseGrade.preview.header.ariaHidden": "Pré-visualização de um ",
"progress.courseGrade.preview.body.unlockCertificate": "Desbloquear para ver as notas e trabalhar para um certificado.",
"progress.courseGrade.partialpreview.body.unlockCertificate": "Desbloqueie para trabalhar para um certificado.",
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "O prazo para a actualização neste curso já passou.",
"progress.courseGrade.preview.button.upgrade": "Atualize agora",
"progress.courseGrade.gradeRange.tooltip": "Categorias de classificação para este curso:",
"progress.courseOutline": "Descrição do Curso",
"progress.courseGrade.label.currentGrade": "A sua classificação actual",
"progress.detailedGrades": "Classificações detalhadas",
"progress.detailedGrades.emptyTable": "Actualmente, não tem pontuações de problemas classificados.",
"progress.footnotes.title": "Notas de rodapé de resumo da classificação",
"progress.gradeSummary.grade": "Classificação",
"progress.courseGrade.grades": "Classificações",
"progress.courseGrade.gradesAndCredit": "Classificações e Crédito",
"progress.courseGrade.gradeRange.Tooltip": "Dica da escala de classificação",
"progress.gradeSummary": "Resumo da classificação",
"progress.gradeSummary.limitedAccessExplanation": "Tem acesso limitado a trabalhos classificados como parte do percurso de auditoria deste curso.",
"progress.gradeSummary.tooltip.alt": "Dica de ferramenta de resumo da classificação",
"progress.gradeSummary.tooltip.body": "O peso da sua actividade de curso é determinado pelo seu formador. Multiplicando a sua nota pelo peso para esse tipo de trabalho, a sua nota ponderada é calculada. A sua classificação ponderada é o que é utilizado para determinar se passa no curso.",
"progress.noAcessToAssignmentType": "Não tem acesso a trabalhos do tipo {assignmentType}",
"progress.noAcessToSubsection": "Não tem acesso à subsecção {displayName}",
"progress.courseGrade.label.passingGrade": "Classificação de aprovação",
"progress.detailedGrades.problemScore.label": "Classificações do Problema: ",
"progress.detailedGrades.problemScore.toggleButton": "Alternar classificações de problemas individuais para {subsectionTitle}",
"progress.detailedGrades.overridden": "A classificação da secção foi substituída.",
"progress.score": "Pontuação",
"progress.weight": "Peso",
"progress.weightedGrade": "Classificação ponderada",
"progress.weightedGradeSummary": "O seu resumo actual da classificação ponderada",
"progress.header": "O seu progresso",
"progress.header.targetUser": "Progresso do curso para {username}",
"progress.link.studio": "Ver classificação no Studio",
"progress.relatedLinks.datesCard.description": "Uma vista do calendário com as datas de fim do seu curso e dos próximos trabalhos.",
"progress.relatedLinks.datesCard.link": "Datas",
"progress.relatedLinks.outlineCard.description": "Uma visão geral do conteúdo do seu curso.",
"progress.relatedLinks.outlineCard.link": "Descrição do Curso",
"progress.relatedLinks": "Ligações relacionadas",
"datesBanner.suggestedSchedule": "Construímos um calendário sugerido para o ajudar a manter-se no seu percurso. Mas não se preocupe - é flexível para que possa aprender ao seu próprio ritmo.",
"datesBanner.upgradeToCompleteGradedBanner.header": "Atualize a sua inscrição para ter acesso",
"datesBanner.upgradeToCompleteGradedBanner.body": "Está a auditar este curso, o que significa que não pode participar em tarefas classificadas. Para completar as tarefas classificadas como parte deste curso, pode atualizar hoje.",
"datesBanner.upgradeToCompleteGradedBanner.button": "Atualize agora",
"datesBanner.upgradeToResetBanner.body": "Para manter o ritmo, pode actualizar este calendário e deslocar os trabalho atrasados para o futuro. Não se preocupe - não perderá nenhum dos progressos que fez quando mudar as suas datas de entrega.",
"datesBanner.upgradeToResetBanner.button": "Atualize para alterar os prazos limites",
"datesBanner.resetDatesBanner.header": "Parece ter falhado alguns prazos importantes com base no nosso calendário sugerido.",
"datesBanner.resetDatesBanner.body": "Para manter o ritmo, pode actualizar este calendário e deslocar os trabalho atrasados para o futuro. Não se preocupe - não perderá nenhum dos progressos que fez quando mudar as suas datas de entrega.",
"datesBanner.resetDatesBanner.button": "Alterar os prazos limite",
"learn.navigation.course.tabs.label": "Material do Curso",
"unit.bookmark.button.add.bookmark": "Marcar esta página nos favoritos",
"unit.bookmark.button.remove.bookmark": "Nos Marcadores",
"learning.celebration.completed": "Acabou de completar a primeira secção do seu curso.",
"learning.celebration.congrats": "Parabéns!",
"learning.celebration.earned": "Ganhou-o!",
"learning.celebration.emailSubject": "Estou a caminho de completar {title} online com {platform}!",
"learning.celebration.forward": "Continuar",
"learning.celebration.goalMet": "Atingiu o seu objetivo!",
"learning.celebration.keepItUp": "Continue assim",
"learning.celebration.share": "Tire um momento para festejar e partilhar os seus progressos.",
"learning.celebration.social": "Estou a caminho de completar {title} online com {platform}. O que aprendes no tempo que tens?",
"learning.celebration.goalCongrats": "Parabéns, atingiu o seu objetivo de aprendizagem de {nTimes} por semana.",
"learning.celebration.setGoal": "O estabelecimento de um objetivo pode ajudá-lo {strongText} no seu curso.",
"calculator.instructions.button.label": "Instruções da calculadora",
"calculator.instructions": "Para informações detalhadas, ver {expressions_link}.",
"calculator.instructions.support.title": "Centro de Ajuda",
"calculator.instructions.useful.tips": "Dicas úteis:",
"calculator.hint1": "Utilize parênteses () para tornar as expressões claras. Pode utilizar os parênteses dentro de outros parênteses.",
"calculator.hint2": "Não utilize espaços nas expressões.",
"calculator.hint3": "Para constantes, indicar explicitamente a multiplicação (exemplo: 5*c).",
"calculator.hint4": "Para os afixos, escrever o número e afixar sem espaço (exemplo: 5c).",
"calculator.hint5": "Para funções, digite o nome da função, depois a expressão em parênteses.",
"calculator.instruction.table.to.use.heading": "Para Utilizar",
"calculator.instruction.table.type.heading": "Tipo",
"calculator.instruction.table.examples.heading": "Exemplos",
"calculator.instruction.table.to.use.numbers": "Números",
"calculator.instruction.table.to.use.numbers.type1": "Números Inteiros",
"calculator.instruction.table.to.use.numbers.type2": "Frações",
"calculator.instruction.table.to.use.numbers.type3": "Decimais",
"calculator.instruction.table.to.use.operators": "Operadores",
"calculator.instruction.table.to.use.operators.type1": "(adicionar, subtrair, multiplicar, dividir)",
"calculator.instruction.table.to.use.operators.type2": "(elevar a uma potência)",
"calculator.instruction.table.to.use.operators.type3": "(resistências paralelas)",
"calculator.instruction.table.to.use.constants": "Constantes",
"calculator.instruction.table.to.use.affixes": "Afixos",
"calculator.instruction.table.to.use.affixes.type": "Símbolo de percentagem (%)",
"calculator.instruction.table.to.use.basic.functions": "Funções Básicas",
"calculator.instruction.table.to.use.trig.functions": "Funções Trigonométricas",
"calculator.instruction.table.to.use.scientific.notation": "Notação Científica",
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} e o expoente",
"calculator.instruction.table.to.use.scientific.notation.type2": "{notationSyntax} registo",
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax} e o expoente",
"calculator.button.label": "Calculadora",
"calculator.input.field.label": "Entrada da Calculadora",
"calculator.submit.button.label": "Calcular",
"calculator.result.field.label": "Resultado da Calculadora",
"calculator.result.field.placeholder": "Resultado",
"notes.button.show": "Mostrar Notas",
"notes.button.hide": "Ocultar Notas",
"courseExit.catalogSearchSuggestion": "Quer saber mais? {searchOurCatalogLink} para encontrar mais cursos e programas para explorar.",
"courseCelebration.certificateBody.available": "\n Mostre a sua conquista no LinkedIn ou no seu currículo hoje.\n Pode descarregar agora o seu certificado e aceder ao mesmo a qualquer momento a partir do seu\n {dashboardLink} e {profileLink}.",
"courseCelebration.certificateBody.notAvailable.endDate.v2": "Este curso termina a {endDate}. As notas finais e quaisquer certificados obtidos estão\n programados para estarem disponíveis após {certAvailableDate}.",
"courseCelebration.certificateBody.unverified": "A fim de criar um certificado, deve completar a verificação da identificação.\n {idVerificationSupportLink} agora.",
"courseCelebration.certificateBody.upgradable": "Não é demasiado tarde para actualizar. Por {price} irá desbloquear o acesso a todos os programas\n tarefas neste curso. Após a conclusão, receberá um certificado verificado que é uma\n credencial importante para melhorar as suas perspectivas de emprego e fazer avançar a sua carreira, ou destacar o seu\n certificado nas candidaturas escolares.",
"courseCelebration.upgradeDiscountCodePrompt": "Use o código {code} no checkout para {percent}% de desconto!",
"courseCelebration.recommendations.heading": "Continue a desenvolver as suas competências com estes cursos!",
"courseCelebration.recommendations.label": "Curso",
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {, } conjunction { {sp}and } other { }}",
"courseCelebration.recommendations.browse_catalog": "Explorar mais cursos",
"courseCelebration.recommendations.loading_recommendations": "A carregar recomendações",
"courseCelebration.recommendations.card.schools.label": "Escolas e Parceiros",
"courseCelebration.dashboardInfo": "Pode aceder a este curso e aos seus materiais no seu {dashboardLink}.",
"courseExit.programs.applyForCredit": "Pedido de crédito",
"courseCelebration.certificateHeader.downloadable": "O seu certificado está disponível!",
"courseCelebration.certificateHeader.notAvailable": "A sua classificação e estado de certificado estarão disponíveis em breve.",
"courseCelebration.certificateBody.notAvailable.accessCertificate": "Se tiver obtido uma nota mínima, o seu certificado será emitido automaticamente.",
"courseCelebration.certificateHeader.unverified": "Deve completar a verificação para receber o seu certificado.",
"courseCelebration.certificateHeader.requestable": "Parabéns, qualificou-se para um certificado!",
"courseCelebration.certificateHeader.upgradable": "Actualize para obter um certificado verificado",
"courseCelebration.certificateImage": "Modelo de certificado",
"courseCelebration.completedCourseHeader": "Concluiu o seu curso.",
"courseCelebration.congratulationsHeader": "Parabéns!",
"courseCelebration.congratulationsImage": "Quatro pessoas levantam as mãos em comemoração",
"courseExit.courseInProgressDescription": "Parece que há mais conteúdos neste curso a ser disponibilizados no futuro. Fique atento a actualizações por correio electrónico ou verifique novamente o seu curso para saber quando é que estes conteúdos estarão disponíveis.",
"courseExit.courseInProgressHeader": "Mais conteúdos em breve!",
"courseExit.dashboardLink": "Painel de Controlo",
"courseExit.endOfCourseDescription": "Infelizmente, actualmente não é elegível para um certificado. Tem de receber uma nota mínima para ser elegível para um certificado.",
"courseExit.endOfCourseHeader": "Chegou ao fim do curso!",
"courseExit.endOfCourseTitle": "Fim do Curso",
"courseExit.idVerificationSupportLink": "Saiba mais sobre a verificação da ID",
"courseCelebration.linkedinAddToProfileButton": "Adicionar ao Perfil do LinkedIn",
"courseExit.programs.microBachelors.learnMore": "Saiba mais sobre como a sua credencial MicroBachelors pode ser aplicada para crédito.",
"courseExit.programs.microMasters.learnMore": "Saiba mais sobre o processo de aplicação de certificados MicroMasters aos Mestrados.",
"courseExit.programs.microMasters.mastersMessage": "Se estiver interessado em usar o seu certificado MicroMasters para um programa de Mestrado, pode começar hoje mesmo!",
"learn.sequence.navigation.complete.button": "Complete o curso",
"courseExit.nextButton.endOfCourse": "Próximo (end of course)",
"courseExit.profileLink": "Perfil",
"courseExit.programs.lastCourse": "Concluiu o último curso em {title}!",
"courseCelebration.requestCertificateBodyText": "A fim de ter acesso ao seu certificado, solicite-o abaixo.",
"courseCelebration.requestCertificateButton": "Pedir certificado",
"courseExit.searchOurCatalogLink": "Pesquise no nosso catálogo",
"courseCelebration.shareMessage": "Partilhe o seu sucesso nas redes sociais ou e-mail.",
"courseExit.social.shareCompletionMessage": "Acabei de completar {title} com {platform}!",
"courseExit.upgradeButton": "Atualize agora",
"courseExit.upgradeLink": "actualizar agora",
"courseCelebration.verificationPending": "A sua verificação de identificação está pendente e o seu certificado estará disponível quando for aprovado.",
"courseExit.verifiedCertificateSupportLink": "Saiba mais sobre certificados verificados",
"courseCelebration.verifyIdentityButton": "Verificar ID agora",
"courseCelebration.viewCertificateButton": "Ver o meu certificado",
"courseExit.viewCourseScheduleButton": "Ver calendário do curso",
"courseExit.viewCoursesButton": "Ver os meus cursos",
"courseExit.viewGradesButton": "Ver classificações",
"courseExit.programCompletion.dashboardMessage": "Para ver o estado do seu certificado, verifique a secção Programas do seu {programLink}.",
"courseExit.upgradeFootnote": "O acesso a este curso e aos seus materiais está disponível no seu painel de controlo até {expirationDate}. Para prolongar o acesso, {upgradeLink}.",
"learn.course.license.allRightsReserved.text": "Todos os Direitos Reservados",
"learn.course.license.creativeCommons.terms.preamble": "Conteúdos licenciados usando Creative Commons, com os termos a seguir:",
"learn.course.license.creativeCommons.terms.by": "Atribuição",
"learn.course.license.creativeCommons.terms.nc": "NãoComercial",
"learn.course.license.creativeCommons.terms.nd": "Sem Derivados ",
"learn.course.license.creativeCommons.terms.sa": "Partilhar Igual",
"learn.course.license.creativeCommons.terms.zero": "Nenhum termo",
"learn.course.license.creativeCommons.text": "Alguns Direitos Reservados",
"learn.breadcrumb.navigation.course.home": "Curso",
"notification.tray.container": "Tabuleiro de notificações",
"notification.open.button": "Mostrar tabuleiro de notificações",
"notification.close.button": "Fechar tabuleiro de notificações",
"responsive.close.notification": "Voltar ao curso",
"notification.tray.title": "Notificações",
"notification.tray.no.message": "Neste momento, não tem novas notificações.",
"learn.contentLock.content.locked": "Conteúdo Bloqueado",
"learn.contentLock.complete.prerequisite": "É necessário completar o pré-requisito: ''{prereqSectionName}'' para aceder a este conteúdo.",
"learn.contentLock.goToSection": "Ir para a Secção de Pré-requisitos",
"learn.hiddenAfterDue.gradeAvailable": "Se completou esta tarefa, a sua classificação está disponível em {progressPage}.",
"learn.hiddenAfterDue.header": "O prazo limite para esta tarefa acabou.",
"learn.hiddenAfterDue.description": "Uma vez que a data limite já passou, esta tarefa já não está disponível.",
"learn.hiddenAfterDue.progressPage": "página de progresso",
"learn.honorCode.content": "Honestidade e integridade académica são importantes para {siteName} e as instituições que fornecem cursos e programas no site {siteName}. Ao clicar em \"Concordo\" abaixo, confirmo que li, compreendo e respeitarei o {link} do site {siteName}.",
"learn.honorCode.name": "Código de Honra",
"learn.honorCode.cancel": "Cancelar",
"learn.honorCode.agree": "Concordo",
"learn.lockPaywall.title": "As tarefas classificadas estão bloqueadas",
"learn.lockPaywall.content": "Actualize para ter acesso a funcionalidades bloqueadas como esta e tirar o máximo partido do seu curso.",
"learn.lockPaywall.content.pastExpiration": "O prazo de atualização para este curso expirou. Para fazer a atualização, inscreva-se na próxima sessão disponível. ",
"learn.lockPaywall.courseDetails": "Ver Detalhes do Curso",
"learn.lockPaywall.example.alt": "Certificado de Exemplo",
"learn.lockPaywall.list.intro": "Quando actualiza, você:",
"learn.header.h2.placeholder": "As rubricas de nível 2 podem ser criadas por fornecedores de cursos no futuro.",
"learn.course.load.failure": "Houve um erro ao carregar este curso.",
"learn.loading.honor.codk": "Carregando mensagem de código de honra...",
"learn.loading.content.lock": "Carregando mensagens com conteúdo bloqueado...",
"learn.loading.learning.sequence": "Carregando sequência de formação...",
"learn.sequence.no.content": "Não há aqui qualquer conteúdo.",
"learn.sequence.navigation.next.button": "Seguinte",
"learn.sequence.navigation.next.up.button": "Próximo: {title}",
"learn.sequence.navigation.previous.button": "Anterior",
"learn.course.sequence.navigation.mobile.menu": "{current} de {total}",
"learn.sequence.share.button": "Partilhar este conteúdo",
"learn.sequence.share.modal.title": "Título",
"learn.sequence.share.modal.body": "Copie o link abaixo para partilhar este conteúdo.",
"learn.sequence.share.quote": "Aqui está um clipe divertido de uma aula que estou a assumir @edXonline.\n",
"discussions.sidebar.title": "Debates",
"discussions.sidebar.open.button": "Mostrar tabuleiro de discussão",
"learn.redirect.interstitial.message": "A redireccionar...",
"learn.loading.error": "Erro: {error}",
"learning.celebration.emailBody": "O que aprendes no tempo que tens?",
"learning.social.shareEmail": "Partilhe o seu progresso por e-mail",
"learning.social.shareService": "Partilhe o seu progresso em {service}.",
"general.altText.close": "Fechar",
"learning.logistration.register": "registe-se",
"learning.logistration.login": "iniciar sessão",
"general.signIn.sentenceCase": "Iniciar Sessão",
"learn.course.tabs.navigation.overflow.menu": "Mais...",
"learning.offer.screenReaderPrices": "Valor original: {originalPrice}, valor com desconto: {discountedPrice}",
"learning.upgradeButton.screenReaderInlinePrices": "Valor original: {originalPrice}",
"learning.upgradeButton.buttonText": "Actualizar para {pricing}",
"learning.upgradeNowButton.buttonText": "Actualizar agora para {pricing}",
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "incluindo quaisquer progressos",
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "vantagens da actualização",
"learning.generic.upgradeNotification.expirationAccessLoss": "Perderá todo o acesso a este curso, {includingAnyProgress}, em {date}.",
"learning.generic.upgradeNotification.expirationVerifiedCert": "A actualização do seu curso permite-lhe obter um certificado validado e desbloquear inúmeras funcionalidades. Saiba mais sobre o {benefitsOfUpgrading}.",
"learning.generic.upgradeNotification.pastExpiration.content": "O prazo de atualização para este curso expirou. Para fazer a atualização, inscreva-se na próxima sessão disponível. ",
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \n um {day}\n outro {days}} esquerda",
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} restantes",
"learning.generic.upgradeNotification.expirationMinutes": "Falta menos de 1 hora",
"learning.generic.upgradeNotification.expiration": "O acesso ao curso expirará {date}",
"learning.generic.upgradeNotification.pastExpiration.banner": "Prazo de atualização expirado em {date}",
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% Desconto Para Novos Estudantes",
"learning.generic.upgradeNotification.accessExpiration": "Actualize o seu curso hoje",
"learning.generic.upgradeNotification.accessExpirationUrgent": "Validade do Acesso ao Curso",
"learning.generic.upgradeNotification.accessExpirationPast": "Validade do Acesso ao Curso",
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Obter um certificado validado",
"learning.generic.upgradeNotification.code": "Utilizar o código {código} na saída",
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "certificado verificado",
"learning.generic.upsell.verifiedCertBullet": "Ganhe um {verifiedCertLink} de conclusão para exibir no seu currículo",
"learning.generic.upsell.unlockGradedBullet.gradedAssignments": "trabalhos avaliados",
"learning.generic.upsell.unlockGradedBullet": "Desbloqueie o seu acesso a todas as actividades do curso, incluindo {gradedAssignmentsInBoldText}",
"learning.generic.upsell.fullAccessBullet.fullAccess": "Acesso completo",
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} ao conteúdo e materiais do curso, mesmo após o curso terminar",
"learning.generic.upsell.supportMissionBullet.mission": "missão",
"learning.generic.upsell.supportMissionBullet": "Apoio a nossa {missionInBoldText} em {siteName}",
"masquerade-widget.userName.error.generic": "Ocorreu um erro; por favor tente novamente.",
"masquerade-widget.userName.input.placeholder": "Nome de utilizador ou e-mail",
"masquerade-widget.userName.input.label": "Mascarado como este utilizador",
"tours.abandonTour.launchTourCheckpoint.body": "Sente-se perdido? Inicie a visita guiada a qualquer altura para obter algumas dicas rápidas para tirar o máximo partido da experiência.",
"tours.sequenceNavigationCheckpoint.body": "A barra superior dentro do seu curso permite-lhe saltar facilmente para diferentes secções e mostrar-lhe o que está para vir.",
"tours.existingUserTour.launchTourCheckpoint.body": "Recentemente adicionámos algumas novas características à experiência do curso. Quer ajuda para dar uma vista de olhos? Faça uma visita guiada para saber mais.",
"tours.button.dismiss": "Ignorar",
"tours.button.next": "Seguinte",
"tours.button.okay": "OK",
"tours.button.beginTour": "Começar visita guiada",
"tours.button.launchTour": "Iniciar visita guiada",
"tours.newUserModal.body": "Vamos fazer uma visita rápida {siteName} para que possa tirar o máximo partido do seu curso.",
"tours.newUserModal.title.welcome": "Bem-vindo ao seu",
"tours.button.skipForNow": "Saltar por enquanto",
"tours.datesCheckpoint.body": "As datas importantes podem ajudá-lo a manter-se no seu percurso.",
"tours.datesCheckpoint.title": "Mantenha-se a par das principais datas",
"tours.outlineCheckpoint.body": "Pode explorar secções do curso utilizando o esquema abaixo.",
"tours.outlineCheckpoint.title": "Faça o curso!",
"tours.tabNavigationCheckpoint.body": "Estes separadores podem ser utilizados para aceder a outros materiais do curso, tais como o seu progresso, programa de estudos, etc.",
"tours.tabNavigationCheckpoint.title": "Recursos adicionais do curso",
"tours.upgradeCheckpoint.body": "Trabalhe para obter um certificado e tenha acesso total aos materiais do curso. Atualize agora!",
"tours.upgradeCheckpoint.title": "Desbloqueie o seu curso",
"tours.weeklyGoalsCheckpoint.body": "A definição de um objetivo aumenta a probabilidade de completar o seu curso.",
"tours.weeklyGoalsCheckpoint.title": "Defina um objetivo de curso",
"tours.newUserModal.title": "{welcome} {siteName} curso!",
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
"learning.effortEstimation.activities": "{activityCount, plural, one {# atividade} many {# Atividades} other {# atividades}}",
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# min} many {# min} other {# min}}",
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minuto} many {# minutos} other {# minutos}}",
"learning.streakCelebration.congratulations": "Parabéns!",
"learning.streakCelebration.body": "Continua assim, estás em alta!",
"learning.streakCelebration.button": "Continue assim",
"learning.streakCelebration.buttonSrOnly": "Fechar o módulo e continuar",
"learning.streakCelebration.buttonAA759": "Continuar com o curso",
"learning.streakCelebration.header": "etapa diária",
"learning.streakCelebration.factoidABoldedSection": "são 20x mais propensos a passar o seu curso",
"learning.streakCelebration.factoidBBoldedSection": "complete 5x mais o conteúdo do curso em média",
"learning.streakCelebration.streakDiscountMessage": "Desbloqueou um desconto de {percent}% ao atualizar este curso por um tempo limitado.",
"learning.streakcelebration.factoida": "Utilizadores que estudam {streak_length} dias consecutivos {bolded_section} do que aqueles que não o fazem.",
"learning.streakcelebration.factoidb": "Utilizadores que estudam {streak_length} dias consecutivos {bolded_section} vs. aqueles que não o fazem.",
"learning.streakCelebration.streakCelebrationCouponEndDateMessage": "Termina {date}.",
"learning.loading.failure": "Houve um erro ao carregar este curso.",
"learning.loading": "Carregar página do curso..."
}

View File

@@ -1,21 +1,21 @@
{
"learning.accessExpiration.deadline": "Upgrade by {date} to get unlimited access to the course as long as it exists on the site.",
"learning.accessExpiration.header": "Audit Access Expires {date}",
"learning.accessExpiration.body": "You lose all access to this course, including your progress, on {date}.",
"learning.accessExpiration.deadline": "Оновіть курс до {date}, щоб отримати необмежений доступ до нього, поки він існує на сайті.",
"learning.accessExpiration.header": "Термін дії аудит доступу до курсу закінчується {date}",
"learning.accessExpiration.body": "Ви втратите весь доступ до цього курсу, включно з вашим прогресом, з {date}.",
"instructorToolbar.pageBanner.courseHasExpired": "This learner no longer has access to this course. Their access expired on {date}.",
"learning.accessExpiration.upgradeNow": "Upgrade now",
"learning.accessExpiration.upgradeNow": "Оновити зараз",
"learning.activeEnterprise.alert": " {changeActiveEnterprise}.",
"learning.activeEnterprise.change.alert": "change enterprise now",
"learning.outline.alert.start.short": "Course starts {timeRemaining} at {courseStartTime}.",
"learning.outline.alert.end.long": "This course is ending {timeRemaining} on {courseEndDate}.",
"learning.outline.alert.end.calendar": "Dont forget to add a calendar reminder!",
"learning.outline.alert.end.calendar": "Не забудьте додати нагадування в календар!",
"instructorToolbar.pageBanner.courseHasNotStarted": "This learner does not yet have access to this course. The course starts on {date}.",
"learning.enrollment.alert": "You must be enrolled in the course to see course content.",
"learning.staff.enrollment.alert": "You are viewing this course as staff, and are not enrolled.",
"learning.enrollment.enrollNow.Inline": "Enroll now",
"learning.enrollment.enrollNow.Sentence": "Enroll now.",
"learning.enrollment.success": "You've successfully enrolled in this course!",
"account-activation.alert.button": "Continue to {siteName}",
"account-activation.alert.button": "Перейти до {siteName}",
"account-activation.alert.message": "We sent an email to {boldEmail} with a link to activate your account. Cant find it? Check your spam folder or\n {sendEmailTag}.",
"account-activation.resend.link": "resend the email",
"learning.logistration.alert": "To see course content, {signIn} or {register}.",
@@ -29,8 +29,8 @@
"learning.dates.badge.today": "Today",
"learning.dates.badge.unreleased": "Not yet released",
"learning.dates.badge.verifiedOnly": "Verified only",
"learning.goals.unsubscribe.contact": "contact support",
"learning.goals.unsubscribe.description": "You will no longer receive email reminders about your goal for {courseTitle}.",
"learning.goals.unsubscribe.contact": "звернутися до служби підтримки",
"learning.goals.unsubscribe.description": "Ви більше не будете отримувати нагадування про вашу мету для {courseTitle}.",
"learning.goals.unsubscribe.errorHeader": "Something went wrong",
"learning.goals.unsubscribe.goToDashboard": "Go to dashboard",
"learning.goals.unsubscribe.header": "Youve unsubscribed from goal reminders",

View File

@@ -1,14 +1,14 @@
{
"learning.accessExpiration.deadline": "Upgrade by {date} to get unlimited access to the course as long as it exists on the site.",
"learning.accessExpiration.header": "Audit Access Expires {date}",
"learning.accessExpiration.body": "You lose all access to this course, including your progress, on {date}.",
"instructorToolbar.pageBanner.courseHasExpired": "This learner no longer has access to this course. Their access expired on {date}.",
"learning.accessExpiration.deadline": "通过 {date} 升级即可无限制地访问该课程,只要它存在于网站上。",
"learning.accessExpiration.header": "旁听课程访问过期 {date}",
"learning.accessExpiration.body": "您将在 {date} 失去对这门课程的所有访问权限,包括您的进度。",
"instructorToolbar.pageBanner.courseHasExpired": "此学员不再有权访问此课程。他们的访问权限已于 {date} 到期。",
"learning.accessExpiration.upgradeNow": "马上升级",
"learning.activeEnterprise.alert": " {changeActiveEnterprise}.",
"learning.activeEnterprise.change.alert": "change enterprise now",
"learning.outline.alert.start.short": "Course starts {timeRemaining} at {courseStartTime}.",
"learning.outline.alert.end.long": "This course is ending {timeRemaining} on {courseEndDate}.",
"learning.outline.alert.end.calendar": "Dont forget to add a calendar reminder!",
"learning.activeEnterprise.alert": "{changeActiveEnterprise}",
"learning.activeEnterprise.change.alert": "现在换单位",
"learning.outline.alert.start.short": "课程从 {timeRemaining} 开始于 {courseStartTime}",
"learning.outline.alert.end.long": "本课程将于 {courseEndDate} 在 {timeRemaining} 结束。",
"learning.outline.alert.end.calendar": "不要忘记添加日历提醒!",
"instructorToolbar.pageBanner.courseHasNotStarted": "This learner does not yet have access to this course. The course starts on {date}.",
"learning.enrollment.alert": "您必须报读此课程才能查看课程内容。",
"learning.staff.enrollment.alert": "You are viewing this course as staff, and are not enrolled.",

View File

@@ -11,13 +11,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Switch } from 'react-router-dom';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as headerMessages } from '@edx/frontend-component-header';
import { Helmet } from 'react-helmet';
import { fetchDiscussionTab, fetchLiveTab } from './course-home/data/thunks';
import DiscussionTab from './course-home/discussion-tab/DiscussionTab';
import appMessages from './i18n';
import messages from './i18n';
import { UserMessagesProvider } from './generic/user-messages';
import './index.scss';
@@ -142,9 +140,5 @@ initialize({
}, 'LearnerAppConfig');
},
},
messages: [
appMessages,
footerMessages,
headerMessages,
],
messages,
});

View File

@@ -299,13 +299,14 @@
"course_exit_page_is_active": false,
"certificate_data": {
"cert_status": "audit_passing",
"cert_web_view_url": null,
"cert_web_view_url": null,
"certificate_available_date": null
},
"verify_identity_url": null,
"verification_status": "none",
"linkedin_add_to_profile_url": null,
"user_needs_integrity_signature": false
"user_needs_integrity_signature": false,
"learning_assistant_launch_url": null
},
"matchingRules": {
"$.body.access_expiration.expiration_date": {
@@ -440,6 +441,9 @@
},
"$.body.user_needs_integrity_signature": {
"match": "type"
},
"$.body.learning_assistant_launch_url": {
"match": "type"
}
}
}

View File

@@ -233,7 +233,7 @@ describe('Courseware Tour', () => {
// Wait for the page spinner to be removed, such that we can wait for our main
// content to load before making any assertions.
await waitForElementToBeRemoved(screen.getByRole('status'));
return container;
return Promise.resolve(container);
}
describe('when receiving successful course data', () => {

View File

@@ -19,7 +19,7 @@ import { reducer as coursewareReducer } from './courseware/data/slice';
import { reducer as modelsReducer } from './generic/model-store';
import { UserMessagesProvider } from './generic/user-messages';
import appMessages from './i18n';
import messages from './i18n';
import { fetchCourse, fetchSequence } from './courseware/data';
import { appendBrowserTimezoneToUrl, executeThunk } from './utils';
import buildSimpleCourseAndSequenceMetadata from './courseware/data/__factories__/sequenceMetadata.factory';
@@ -78,7 +78,7 @@ export function initializeMockApp() {
configureI18n({
config: getConfig(),
loggingService,
messages: [appMessages],
messages,
});
return { loggingService, authService };