mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Ensure that translation values are correctly escaped (#15252)
GitOrigin-RevId: 5a38b4c01921fd4d95dbdb7b9e756443fdb00b80
This commit is contained in:
parent
749aef1c6f
commit
0c81bccfca
61 changed files with 1018 additions and 361 deletions
1012
package-lock.json
generated
1012
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -7,6 +7,7 @@
|
||||||
"standard",
|
"standard",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
|
"plugins": ["@overleaf"],
|
||||||
"env": {
|
"env": {
|
||||||
"es2020": true
|
"es2020": true
|
||||||
},
|
},
|
||||||
|
@ -113,6 +114,9 @@
|
||||||
//
|
//
|
||||||
"files": ["**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}", "**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}"],
|
"files": ["**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}", "**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@overleaf/no-empty-trans": "off",
|
||||||
|
"@overleaf/should-unescape-trans": "error",
|
||||||
|
|
||||||
// https://astexplorer.net/
|
// https://astexplorer.net/
|
||||||
"no-restricted-syntax": [
|
"no-restricted-syntax": [
|
||||||
"error",
|
"error",
|
||||||
|
|
|
@ -78,7 +78,6 @@
|
||||||
"are_you_getting_an_undefined_control_sequence_error": "",
|
"are_you_getting_an_undefined_control_sequence_error": "",
|
||||||
"are_you_still_at": "",
|
"are_you_still_at": "",
|
||||||
"are_you_sure": "",
|
"are_you_sure": "",
|
||||||
"ascending": "",
|
|
||||||
"ask_proj_owner_to_upgrade_for_full_history": "",
|
"ask_proj_owner_to_upgrade_for_full_history": "",
|
||||||
"ask_proj_owner_to_upgrade_for_longer_compiles": "",
|
"ask_proj_owner_to_upgrade_for_longer_compiles": "",
|
||||||
"ask_proj_owner_to_upgrade_for_references_search": "",
|
"ask_proj_owner_to_upgrade_for_references_search": "",
|
||||||
|
@ -251,7 +250,6 @@
|
||||||
"demonstrating_git_integration": "",
|
"demonstrating_git_integration": "",
|
||||||
"demonstrating_track_changes_feature": "",
|
"demonstrating_track_changes_feature": "",
|
||||||
"department": "",
|
"department": "",
|
||||||
"descending": "",
|
|
||||||
"description": "",
|
"description": "",
|
||||||
"dictionary": "",
|
"dictionary": "",
|
||||||
"did_you_know_institution_providing_professional": "",
|
"did_you_know_institution_providing_professional": "",
|
||||||
|
|
|
@ -34,6 +34,8 @@ export default function ErrorMessage({ error }) {
|
||||||
values={{
|
values={{
|
||||||
nameLimit: fileNameLimit,
|
nameLimit: fileNameLimit,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</DangerMessage>
|
</DangerMessage>
|
||||||
)
|
)
|
||||||
|
|
|
@ -215,6 +215,8 @@ function UploadErrorMessage({ error, maxNumberOfFiles }) {
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="maximum_files_uploaded_together"
|
i18nKey="maximum_files_uploaded_together"
|
||||||
values={{ max: maxNumberOfFiles }}
|
values={{ max: maxNumberOfFiles }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@ export default function RedirectToLogin() {
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="session_expired_redirecting_to_login"
|
i18nKey="session_expired_redirecting_to_login"
|
||||||
values={{ seconds: secondsToRedirect }}
|
values={{ seconds: secondsToRedirect }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,8 @@ function FileTreeModalError() {
|
||||||
i18nKey="file_already_exists_in_this_location"
|
i18nKey="file_already_exists_in_this_location"
|
||||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ fileName: error.entityName }}
|
values={{ fileName: error.entityName }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case InvalidFilenameError:
|
case InvalidFilenameError:
|
||||||
|
|
|
@ -123,6 +123,8 @@ function UrlProvider({ file }: UrlProviderProps) {
|
||||||
formattedDate: formatTime(file.created),
|
formattedDate: formatTime(file.created),
|
||||||
relativeDate: relativeDate(file.created),
|
relativeDate: relativeDate(file.created),
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
@ -156,6 +158,8 @@ function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
|
||||||
formattedDate: formatTime(file.created),
|
formattedDate: formatTime(file.created),
|
||||||
relativeDate: relativeDate(file.created),
|
relativeDate: relativeDate(file.created),
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
/* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
|
/* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
|
||||||
|
@ -189,6 +193,8 @@ function ProjectOutputFileProvider({ file }: ProjectOutputFileProviderProps) {
|
||||||
formattedDate: formatTime(file.created),
|
formattedDate: formatTime(file.created),
|
||||||
relativeDate: relativeDate(file.created),
|
relativeDate: relativeDate(file.created),
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
|
|
@ -68,6 +68,8 @@ export default function GroupMembers() {
|
||||||
i18nKey="you_have_added_x_of_group_size_y"
|
i18nKey="you_have_added_x_of_group_size_y"
|
||||||
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ addedUsersSize: users.length, groupSize }}
|
values={{ addedUsersSize: users.length, groupSize }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -69,6 +69,8 @@ function ResendManagedUserInviteSuccess({
|
||||||
values={{
|
values={{
|
||||||
email: invitedUserEmail,
|
email: invitedUserEmail,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -89,6 +91,8 @@ function FailedToResendManagedInvite({
|
||||||
values={{
|
values={{
|
||||||
email: invitedUserEmail,
|
email: invitedUserEmail,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -109,6 +113,8 @@ function ResendGroupInviteSuccess({
|
||||||
values={{
|
values={{
|
||||||
email: invitedUserEmail,
|
email: invitedUserEmail,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -129,6 +135,8 @@ function FailedToResendGroupInvite({
|
||||||
values={{
|
values={{
|
||||||
email: invitedUserEmail,
|
email: invitedUserEmail,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -149,6 +157,8 @@ function TooManyRequests({
|
||||||
values={{
|
values={{
|
||||||
email: invitedUserEmail,
|
email: invitedUserEmail,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -24,6 +24,8 @@ export default function ToolbarDatetime({ selection }: ToolbarDatetimeProps) {
|
||||||
'Do MMMM · h:mm a'
|
'Do MMMM · h:mm a'
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Trans
|
<Trans
|
||||||
|
@ -36,6 +38,8 @@ export default function ToolbarDatetime({ selection }: ToolbarDatetimeProps) {
|
||||||
'Do MMMM · h:mm a'
|
'Do MMMM · h:mm a'
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import displayNameForUser from '../../../ide/history/util/displayNameForUser'
|
||||||
import moment from 'moment/moment'
|
import moment from 'moment/moment'
|
||||||
import ColorManager from '../../../ide/colors/ColorManager'
|
import ColorManager from '../../../ide/colors/ColorManager'
|
||||||
import { DocDiffChunk, Highlight } from '../services/types/doc'
|
import { DocDiffChunk, Highlight } from '../services/types/doc'
|
||||||
import { TFunction } from 'react-i18next'
|
import { TFunction } from 'i18next'
|
||||||
|
|
||||||
export function highlightsFromDiffResponse(
|
export function highlightsFromDiffResponse(
|
||||||
chunks: DocDiffChunk[],
|
chunks: DocDiffChunk[],
|
||||||
|
|
|
@ -201,6 +201,8 @@ function CompileTimeoutMessages() {
|
||||||
i18nKey="compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile"
|
i18nKey="compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile"
|
||||||
components={compileTimeoutBlogLinks}
|
components={compileTimeoutBlogLinks}
|
||||||
values={{ date: 'October 27 2023' }}
|
values={{ date: 'October 27 2023' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<Trans i18nKey="and_you_can_upgrade_for_plenty_more_compile_time" />
|
<Trans i18nKey="and_you_can_upgrade_for_plenty_more_compile_time" />
|
||||||
</p>
|
</p>
|
||||||
|
@ -222,6 +224,8 @@ function CompileTimeoutMessages() {
|
||||||
i18nKey="compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile"
|
i18nKey="compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile"
|
||||||
components={compileTimeoutBlogLinks}
|
components={compileTimeoutBlogLinks}
|
||||||
values={{ date: 'October 27 2023' }}
|
values={{ date: 'October 27 2023' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p className="row-spaced">
|
<p className="row-spaced">
|
||||||
|
|
|
@ -72,6 +72,8 @@ function PdfPreviewError({ error }) {
|
||||||
getMeta('ol-compilesUserContentDomain')
|
getMeta('ol-compilesUserContentDomain')
|
||||||
).hostname,
|
).hostname,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
<code key="domain" />,
|
<code key="domain" />,
|
||||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content */
|
/* eslint-disable-next-line jsx-a11y/anchor-has-content */
|
||||||
|
|
|
@ -112,6 +112,7 @@ const CompileTimeout = memo(function CompileTimeout({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
entryAriaLabel={t('your_compile_timed_out')}
|
entryAriaLabel={t('your_compile_timed_out')}
|
||||||
level="error"
|
level="error"
|
||||||
/>
|
/>
|
||||||
|
@ -163,6 +164,8 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
values={{ date: 'October 27 2023' }}
|
values={{ date: 'October 27 2023' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -229,6 +232,7 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
entryAriaLabel={t('other_ways_to_prevent_compile_timeouts')}
|
entryAriaLabel={t('other_ways_to_prevent_compile_timeouts')}
|
||||||
level="raw"
|
level="raw"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -25,6 +25,8 @@ function GroupPlan({
|
||||||
i18nKey="trial_remaining_days"
|
i18nKey="trial_remaining_days"
|
||||||
components={{ b: <strong /> }}
|
components={{ b: <strong /> }}
|
||||||
values={{ days: remainingTrialDays }}
|
values={{ days: remainingTrialDays }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -24,6 +24,8 @@ function IndividualPlan({
|
||||||
i18nKey="trial_remaining_days"
|
i18nKey="trial_remaining_days"
|
||||||
components={{ b: <strong /> }}
|
components={{ b: <strong /> }}
|
||||||
values={{ days: remainingTrialDays }}
|
values={{ days: remainingTrialDays }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -123,6 +123,8 @@ export default function INRBanner({ variant, splitTestName }: INRBannerProps) {
|
||||||
i18nKey="inr_discount_offer_green_banner"
|
i18nKey="inr_discount_offer_green_banner"
|
||||||
components={[<b />, <br />]} // eslint-disable-line react/jsx-key
|
components={[<b />, <br />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ flag: '🇮🇳' }}
|
values={{ flag: '🇮🇳' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</Notification.Body>
|
</Notification.Body>
|
||||||
<Notification.Action>
|
<Notification.Action>
|
||||||
|
|
|
@ -89,6 +89,8 @@ export default function LATAMBanner() {
|
||||||
i18nKey="latam_discount_offer"
|
i18nKey="latam_discount_offer"
|
||||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ flag, currencyName, discountPercent }}
|
values={{ flag, currencyName, discountPercent }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</Notification.Body>
|
</Notification.Body>
|
||||||
<Notification.Action>
|
<Notification.Action>
|
||||||
|
|
|
@ -62,6 +62,8 @@ function ReconfirmAffiliation({
|
||||||
i18nKey="please_check_your_inbox_to_confirm"
|
i18nKey="please_check_your_inbox_to_confirm"
|
||||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ institutionName: institution.name }}
|
values={{ institutionName: institution.name }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
@ -113,6 +115,8 @@ function ReconfirmAffiliation({
|
||||||
i18nKey="are_you_still_at"
|
i18nKey="are_you_still_at"
|
||||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ institutionName: institution.name }}
|
values={{ institutionName: institution.name }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Trans
|
<Trans
|
||||||
|
|
|
@ -69,6 +69,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
||||||
i18nKey="notification_project_invite_accepted_message"
|
i18nKey="notification_project_invite_accepted_message"
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
values={{ projectName: notification.messageOpts.projectName }}
|
values={{ projectName: notification.messageOpts.projectName }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Trans
|
<Trans
|
||||||
|
@ -78,6 +80,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
||||||
userName: notification.messageOpts.userName,
|
userName: notification.messageOpts.userName,
|
||||||
projectName: notification.messageOpts.projectName,
|
projectName: notification.messageOpts.projectName,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Notification.Body>
|
</Notification.Body>
|
||||||
|
@ -136,6 +140,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
||||||
values={{
|
values={{
|
||||||
institutionName: notification.messageOpts.university_name,
|
institutionName: notification.messageOpts.university_name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
{notification.messageOpts.ssoEnabled ? (
|
{notification.messageOpts.ssoEnabled ? (
|
||||||
|
@ -163,6 +169,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
||||||
values={{
|
values={{
|
||||||
institutionName: notification.messageOpts.university_name,
|
institutionName: notification.messageOpts.university_name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
{t('add_email_to_claim_features')}
|
{t('add_email_to_claim_features')}
|
||||||
|
@ -219,6 +227,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
||||||
i18nKey="dropbox_duplicate_project_names"
|
i18nKey="dropbox_duplicate_project_names"
|
||||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ projectName: notification.messageOpts.projectName }}
|
values={{ projectName: notification.messageOpts.projectName }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -116,6 +116,8 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
|
||||||
institutionName: userEmail.affiliation?.institution.name,
|
institutionName: userEmail.affiliation?.institution.name,
|
||||||
emailAddress: userEmail.email,
|
emailAddress: userEmail.email,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -31,6 +31,8 @@ export default function GroupInvitationCancelIndividualSubscriptionNotification(
|
||||||
values={{
|
values={{
|
||||||
inviterName,
|
inviterName,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</Notification.Body>
|
</Notification.Body>
|
||||||
<Notification.Action className="group-invitation-cancel-subscription-notification-buttons">
|
<Notification.Action className="group-invitation-cancel-subscription-notification-buttons">
|
||||||
|
|
|
@ -29,6 +29,8 @@ export default function GroupInvitationNotificationJoin({
|
||||||
values={{
|
values={{
|
||||||
inviterName,
|
inviterName,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</Notification.Body>
|
</Notification.Body>
|
||||||
<Notification.Action>
|
<Notification.Action>
|
||||||
|
|
|
@ -48,6 +48,8 @@ function Institution() {
|
||||||
i18nKey="can_link_institution_email_acct_to_institution_acct"
|
i18nKey="can_link_institution_email_acct_to_institution_acct"
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
values={{ appName, email, institutionName }}
|
values={{ appName, email, institutionName }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
|
@ -55,6 +57,8 @@ function Institution() {
|
||||||
i18nKey="doing_this_allow_log_in_through_institution"
|
i18nKey="doing_this_allow_log_in_through_institution"
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
values={{ appName }}
|
values={{ appName }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<a href="/learn/how-to/Institutional_Login">
|
<a href="/learn/how-to/Institutional_Login">
|
||||||
{t('learn_more')}
|
{t('learn_more')}
|
||||||
|
@ -82,6 +86,8 @@ function Institution() {
|
||||||
i18nKey="account_has_been_link_to_institution_account"
|
i18nKey="account_has_been_link_to_institution_account"
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
values={{ appName, email, institutionName }}
|
values={{ appName, email, institutionName }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</Notification.Body>
|
</Notification.Body>
|
||||||
</Notification>
|
</Notification>
|
||||||
|
@ -97,11 +103,15 @@ function Institution() {
|
||||||
i18nKey="tried_to_log_in_with_email"
|
i18nKey="tried_to_log_in_with_email"
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
values={{ appName, email: requestedEmail }}
|
values={{ appName, email: requestedEmail }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="in_order_to_match_institutional_metadata_associated"
|
i18nKey="in_order_to_match_institutional_metadata_associated"
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
values={{ email: institutionEmail }}
|
values={{ email: institutionEmail }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</Notification.Body>
|
</Notification.Body>
|
||||||
</Notification>
|
</Notification>
|
||||||
|
@ -117,6 +127,8 @@ function Institution() {
|
||||||
i18nKey="tried_to_register_with_email"
|
i18nKey="tried_to_register_with_email"
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
values={{ appName, email }}
|
values={{ appName, email }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{t('we_logged_you_in')}
|
{t('we_logged_you_in')}
|
||||||
</Notification.Body>
|
</Notification.Body>
|
||||||
|
|
|
@ -23,6 +23,8 @@ function TranslationMessage() {
|
||||||
i18nKey="click_here_to_view_sl_in_lng"
|
i18nKey="click_here_to_view_sl_in_lng"
|
||||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ lngName: config.lngName }}
|
values={{ lngName: config.lngName }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
className="ms-1"
|
className="ms-1"
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default function LastUpdatedCell({ project }: LastUpdatedCellProps) {
|
||||||
}}
|
}}
|
||||||
// eslint-disable-next-line react/jsx-boolean-value
|
// eslint-disable-next-line react/jsx-boolean-value
|
||||||
shouldUnescape={true}
|
shouldUnescape={true}
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
fromNowDate(project.lastUpdated)
|
fromNowDate(project.lastUpdated)
|
||||||
|
|
|
@ -68,8 +68,8 @@ function ProjectListTable() {
|
||||||
aria-sort={
|
aria-sort={
|
||||||
sort.by === 'title'
|
sort.by === 'title'
|
||||||
? sort.order === 'asc'
|
? sort.order === 'asc'
|
||||||
? t('ascending')
|
? 'ascending'
|
||||||
: t('descending')
|
: 'descending'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -92,8 +92,8 @@ function ProjectListTable() {
|
||||||
aria-sort={
|
aria-sort={
|
||||||
sort.by === 'owner'
|
sort.by === 'owner'
|
||||||
? sort.order === 'asc'
|
? sort.order === 'asc'
|
||||||
? t('ascending')
|
? 'ascending'
|
||||||
: t('descending')
|
: 'descending'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -110,8 +110,8 @@ function ProjectListTable() {
|
||||||
aria-sort={
|
aria-sort={
|
||||||
sort.by === 'lastUpdated'
|
sort.by === 'lastUpdated'
|
||||||
? sort.order === 'asc'
|
? sort.order === 'asc'
|
||||||
? t('ascending')
|
? 'ascending'
|
||||||
: t('descending')
|
: 'descending'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -33,6 +33,8 @@ function ConfirmationModal({
|
||||||
i18nKey="do_you_want_to_change_your_primary_email_address_to"
|
i18nKey="do_you_want_to_change_your_primary_email_address_to"
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
values={{ email }}
|
values={{ email }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-0">{t('log_in_with_primary_email_address')}</p>
|
<p className="mb-0">{t('log_in_with_primary_email_address')}</p>
|
||||||
|
|
|
@ -117,6 +117,8 @@ function AddEmail() {
|
||||||
values={{
|
values={{
|
||||||
emailAddressLimit,
|
emailAddressLimit,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -36,6 +36,7 @@ function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
|
||||||
values={{ institutionName: domainInfo.university.name }}
|
values={{ institutionName: domainInfo.university.name }}
|
||||||
// eslint-disable-next-line react/jsx-boolean-value
|
// eslint-disable-next-line react/jsx-boolean-value
|
||||||
shouldUnescape={true}
|
shouldUnescape={true}
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -45,6 +46,7 @@ function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
|
||||||
values={{ institutionName: domainInfo.university.name }}
|
values={{ institutionName: domainInfo.university.name }}
|
||||||
// eslint-disable-next-line react/jsx-boolean-value
|
// eslint-disable-next-line react/jsx-boolean-value
|
||||||
shouldUnescape={true}
|
shouldUnescape={true}
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<a
|
<a
|
||||||
href="/learn/how-to/Institutional_Login"
|
href="/learn/how-to/Institutional_Login"
|
||||||
|
|
|
@ -71,6 +71,8 @@ function ReconfirmationInfoPrompt({
|
||||||
values={{
|
values={{
|
||||||
institutionName: institution.name,
|
institutionName: institution.name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={
|
components={
|
||||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||||
[<strong />]
|
[<strong />]
|
||||||
|
@ -155,6 +157,8 @@ function ReconfirmationInfoPromptText({
|
||||||
values={{
|
values={{
|
||||||
institutionName,
|
institutionName,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={
|
components={
|
||||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||||
[<strong />]
|
[<strong />]
|
||||||
|
|
|
@ -17,6 +17,8 @@ function ReconfirmationInfoSuccess({
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="your_affiliation_is_confirmed"
|
i18nKey="your_affiliation_is_confirmed"
|
||||||
values={{ institutionName: institution.name }}
|
values={{ institutionName: institution.name }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{t('thank_you_exclamation')}
|
{t('thank_you_exclamation')}
|
||||||
|
|
|
@ -98,6 +98,8 @@ function SSOAffiliationInfo({ userEmailData }: SSOAffiliationInfoProps) {
|
||||||
values={{
|
values={{
|
||||||
institutionName: userEmailData.affiliation?.institution.name,
|
institutionName: userEmailData.affiliation?.institution.name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</EmailCell>
|
</EmailCell>
|
||||||
|
@ -120,6 +122,8 @@ function SSOAffiliationInfo({ userEmailData }: SSOAffiliationInfoProps) {
|
||||||
institutionName:
|
institutionName:
|
||||||
userEmailData.affiliation?.institution.name,
|
userEmailData.affiliation?.institution.name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={
|
components={
|
||||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||||
[<strong />]
|
[<strong />]
|
||||||
|
|
|
@ -64,6 +64,8 @@ export function SSOAlert() {
|
||||||
i18nKey="institution_acct_successfully_linked_2"
|
i18nKey="institution_acct_successfully_linked_2"
|
||||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ institutionName: institutionLinked.universityName }}
|
values={{ institutionName: institutionLinked.universityName }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
{institutionLinked.hasEntitlement && (
|
{institutionLinked.hasEntitlement && (
|
||||||
|
@ -72,6 +74,8 @@ export function SSOAlert() {
|
||||||
i18nKey="this_grants_access_to_features_2"
|
i18nKey="this_grants_access_to_features_2"
|
||||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ featureType: t('professional') }}
|
values={{ featureType: t('professional') }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -92,6 +96,8 @@ export function SSOAlert() {
|
||||||
i18nKey="in_order_to_match_institutional_metadata_2"
|
i18nKey="in_order_to_match_institutional_metadata_2"
|
||||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ email: institutionEmailNonCanonical }}
|
values={{ email: institutionEmailNonCanonical }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
|
@ -107,6 +107,8 @@ function LeaveModalForm({
|
||||||
values={{
|
values={{
|
||||||
userDefaultEmail,
|
userDefaultEmail,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
{error ? <LeaveModalFormError error={error} /> : null}
|
{error ? <LeaveModalFormError error={error} /> : null}
|
||||||
|
|
|
@ -25,6 +25,8 @@ export default function ManagedAccountAlert() {
|
||||||
values={{
|
values={{
|
||||||
admin: currentManagedUserAdminEmail,
|
admin: currentManagedUserAdminEmail,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,6 +21,8 @@ export default function AddCollaboratorsUpgradeContentDefault() {
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="collabs_per_proj"
|
i18nKey="collabs_per_proj"
|
||||||
values={{ collabcount: 'Multiple' }}
|
values={{ collabcount: 'Multiple' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -24,6 +24,8 @@ export default function AddCollaboratorsUpgradeContentVariant() {
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="collabs_per_proj"
|
i18nKey="collabs_per_proj"
|
||||||
values={{ collabcount: 'Multiple' }}
|
values={{ collabcount: 'Multiple' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -44,6 +44,7 @@ export default function TransferOwnershipModal({ member, cancel }) {
|
||||||
components={[<strong key="strong-1" />, <strong key="strong-2" />]}
|
components={[<strong key="strong-1" />, <strong key="strong-2" />]}
|
||||||
// eslint-disable-next-line react/jsx-boolean-value
|
// eslint-disable-next-line react/jsx-boolean-value
|
||||||
shouldUnescape={true}
|
shouldUnescape={true}
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -37,6 +37,8 @@ export default function GroupSubscriptionMembership({
|
||||||
groupName: subscription.teamName || '',
|
groupName: subscription.teamName || '',
|
||||||
adminEmail: subscription.admin_id.email,
|
adminEmail: subscription.admin_id.email,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
{subscription.teamNotice && (
|
{subscription.teamNotice && (
|
||||||
|
|
|
@ -31,6 +31,8 @@ function InstitutionMemberships() {
|
||||||
planName: 'Professional',
|
planName: 'Professional',
|
||||||
institutionName: institution.name || '',
|
institutionName: institution.name || '',
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||||
<a href="/user/subscription/plans" rel="noopener" />,
|
<a href="/user/subscription/plans" rel="noopener" />,
|
||||||
|
|
|
@ -30,6 +30,8 @@ export default function ManagedGroupSubscriptions() {
|
||||||
groupName: subscription.teamName || '',
|
groupName: subscription.teamName || '',
|
||||||
adminEmail: subscription.admin_id.email,
|
adminEmail: subscription.admin_id.email,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Trans
|
<Trans
|
||||||
|
@ -40,6 +42,8 @@ export default function ManagedGroupSubscriptions() {
|
||||||
groupName: subscription.teamName || '',
|
groupName: subscription.teamName || '',
|
||||||
adminEmail: subscription.admin_id.email,
|
adminEmail: subscription.admin_id.email,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -48,6 +48,8 @@ export default function ManagedInstitution({
|
||||||
values={{
|
values={{
|
||||||
institutionName: institution.name || '',
|
institutionName: institution.name || '',
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<RowLink
|
<RowLink
|
||||||
|
|
|
@ -18,6 +18,8 @@ export default function ManagedPublisher({ publisher }: ManagedPublisherProps) {
|
||||||
values={{
|
values={{
|
||||||
publisherName: publisher.name || '',
|
publisherName: publisher.name || '',
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<RowLink
|
<RowLink
|
||||||
|
|
|
@ -35,6 +35,8 @@ function PersonalSubscriptionRecurlySyncEmail() {
|
||||||
i18nKey="recurly_email_update_needed"
|
i18nKey="recurly_email_update_needed"
|
||||||
components={[<em />, <em />]} // eslint-disable-line react/jsx-key
|
components={[<em />, <em />]} // eslint-disable-line react/jsx-key
|
||||||
values={{ recurlyEmail, userEmail }}
|
values={{ recurlyEmail, userEmail }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -39,6 +39,8 @@ export function ActiveSubscription({
|
||||||
values={{
|
values={{
|
||||||
planName: subscription.plan.name,
|
planName: subscription.plan.name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -95,6 +97,8 @@ export function ActiveSubscription({
|
||||||
paymentAmmount: subscription.recurly.displayPrice,
|
paymentAmmount: subscription.recurly.displayPrice,
|
||||||
collectionDate: subscription.recurly.nextPaymentDueAt,
|
collectionDate: subscription.recurly.nextPaymentDueAt,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -78,6 +78,8 @@ function NotCancelOption({
|
||||||
values={{
|
values={{
|
||||||
days: 14,
|
days: 14,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -105,6 +107,8 @@ function NotCancelOption({
|
||||||
values={{
|
values={{
|
||||||
price: planToDowngradeTo.displayPrice,
|
price: planToDowngradeTo.displayPrice,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -27,6 +27,8 @@ function GroupPlanCollaboratorCount({ planCode }: { planCode: string }) {
|
||||||
values={{
|
values={{
|
||||||
collabcount: 10,
|
collabcount: 10,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -44,6 +46,8 @@ function EducationDiscountAppliedOrNot({ groupSize }: { groupSize: string }) {
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="educational_percent_discount_applied"
|
i18nKey="educational_percent_discount_applied"
|
||||||
values={{ percent: educationalPercentDiscount }}
|
values={{ percent: educationalPercentDiscount }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
@ -54,6 +58,8 @@ function EducationDiscountAppliedOrNot({ groupSize }: { groupSize: string }) {
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="educational_discount_for_groups_of_x_or_more"
|
i18nKey="educational_discount_for_groups_of_x_or_more"
|
||||||
values={{ size: groupSizeForEducationalDiscount }}
|
values={{ size: groupSizeForEducationalDiscount }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
@ -92,6 +98,8 @@ function GroupPrice({
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="x_price_per_year"
|
i18nKey="x_price_per_year"
|
||||||
values={{ price: groupPlanToChangeToPrice?.totalForDisplay }}
|
values={{ price: groupPlanToChangeToPrice?.totalForDisplay }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -105,6 +113,8 @@ function GroupPrice({
|
||||||
values={{
|
values={{
|
||||||
price: perUserPrice,
|
price: perUserPrice,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
|
@ -116,6 +126,8 @@ function GroupPrice({
|
||||||
values={{
|
values={{
|
||||||
price: perUserPrice,
|
price: perUserPrice,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -208,6 +220,8 @@ export function ChangeToGroupModal() {
|
||||||
values={{
|
values={{
|
||||||
percent: '30',
|
percent: '30',
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
@ -296,6 +310,8 @@ export function ChangeToGroupModal() {
|
||||||
percent: educationalPercentDiscount,
|
percent: educationalPercentDiscount,
|
||||||
size: groupSizeForEducationalDiscount,
|
size: groupSizeForEducationalDiscount,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
/>
|
/>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
@ -345,6 +361,8 @@ export function ChangeToGroupModal() {
|
||||||
subtotal: groupPlanToChangeToPrice.subtotal,
|
subtotal: groupPlanToChangeToPrice.subtotal,
|
||||||
tax: groupPlanToChangeToPrice.tax,
|
tax: groupPlanToChangeToPrice.tax,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
/* eslint-disable-next-line react/jsx-key */
|
/* eslint-disable-next-line react/jsx-key */
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -375,7 +393,12 @@ export function ChangeToGroupModal() {
|
||||||
</button>
|
</button>
|
||||||
<hr className="thin" />
|
<hr className="thin" />
|
||||||
<button className="btn-inline-link" onClick={handleGetInTouchButton}>
|
<button className="btn-inline-link" onClick={handleGetInTouchButton}>
|
||||||
<Trans i18nKey="need_more_than_x_licenses" values={{ x: 50 }} />{' '}
|
<Trans
|
||||||
|
i18nKey="need_more_than_x_licenses"
|
||||||
|
values={{ x: 50 }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
|
/>{' '}
|
||||||
{t('please_get_in_touch')}
|
{t('please_get_in_touch')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -69,6 +69,8 @@ export function ConfirmChangePlanModal() {
|
||||||
values={{
|
values={{
|
||||||
planName: plan.name,
|
planName: plan.name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -57,6 +57,8 @@ export function KeepCurrentPlanModal() {
|
||||||
values={{
|
values={{
|
||||||
planName: personalSubscription.plan.name,
|
planName: personalSubscription.plan.name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -14,6 +14,8 @@ export function PendingAdditionalLicenses({
|
||||||
additionalLicenses,
|
additionalLicenses,
|
||||||
totalLicenses,
|
totalLicenses,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -16,6 +16,8 @@ export function PendingPlanChange({
|
||||||
values={{
|
values={{
|
||||||
pendingPlanName: subscription.pendingPlan.name,
|
pendingPlanName: subscription.pendingPlan.name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -35,6 +37,8 @@ export function PendingPlanChange({
|
||||||
subscription.recurly.pendingAdditionalLicenses,
|
subscription.recurly.pendingAdditionalLicenses,
|
||||||
pendingTotalLicenses: subscription.recurly.pendingTotalLicenses,
|
pendingTotalLicenses: subscription.recurly.pendingTotalLicenses,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -17,6 +17,8 @@ function SubscriptionRemainder({ subscription }: SubscriptionRemainderProps) {
|
||||||
values={{
|
values={{
|
||||||
terminationDate: subscription.recurly.nextPaymentDueAt,
|
terminationDate: subscription.recurly.nextPaymentDueAt,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -28,6 +30,8 @@ function SubscriptionRemainder({ subscription }: SubscriptionRemainderProps) {
|
||||||
values={{
|
values={{
|
||||||
terminationDate: subscription.recurly.nextPaymentDueAt,
|
terminationDate: subscription.recurly.nextPaymentDueAt,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -10,6 +10,8 @@ export function TrialEnding({
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="youre_on_free_trial_which_ends_on"
|
i18nKey="youre_on_free_trial_which_ends_on"
|
||||||
values={{ date: trialEndsAtFormatted }}
|
values={{ date: trialEndsAtFormatted }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -17,6 +17,8 @@ export function CanceledSubscription({
|
||||||
values={{
|
values={{
|
||||||
planName: subscription.plan.name,
|
planName: subscription.plan.name,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
@ -29,6 +31,8 @@ export function CanceledSubscription({
|
||||||
values={{
|
values={{
|
||||||
terminateDate: subscription.recurly.nextPaymentDueAt,
|
terminateDate: subscription.recurly.nextPaymentDueAt,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<strong />,
|
<strong />,
|
||||||
|
|
|
@ -35,6 +35,8 @@ function SuccessfulSubscription() {
|
||||||
paymentAmmount: subscription.recurly.displayPrice,
|
paymentAmmount: subscription.recurly.displayPrice,
|
||||||
collectionDate: subscription.recurly.nextPaymentDueAt,
|
collectionDate: subscription.recurly.nextPaymentDueAt,
|
||||||
}}
|
}}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -33,6 +33,10 @@ i18n.use(initReactI18next).init({
|
||||||
// injection via another nested value
|
// injection via another nested value
|
||||||
skipOnVariables: true,
|
skipOnVariables: true,
|
||||||
|
|
||||||
|
// Do not escape values, as `t` + React will already escape them
|
||||||
|
// (`escapeValue: true` and `shouldUnescape` must be set on each use of `Trans`)
|
||||||
|
escapeValue: false,
|
||||||
|
|
||||||
defaultVariables: {
|
defaultVariables: {
|
||||||
appName: window.ExposedSettings.appName,
|
appName: window.ExposedSettings.appName,
|
||||||
},
|
},
|
||||||
|
|
|
@ -109,9 +109,9 @@
|
||||||
"fuse.js": "^3.0.0",
|
"fuse.js": "^3.0.0",
|
||||||
"globby": "^5.0.0",
|
"globby": "^5.0.0",
|
||||||
"helmet": "^6.0.1",
|
"helmet": "^6.0.1",
|
||||||
"i18next": "^19.6.3",
|
"i18next": "^23.5.1",
|
||||||
"i18next-fs-backend": "^1.0.7",
|
"i18next-fs-backend": "^2.2.0",
|
||||||
"i18next-http-middleware": "^3.0.2",
|
"i18next-http-middleware": "^3.4.1",
|
||||||
"jose": "^4.3.8",
|
"jose": "^4.3.8",
|
||||||
"json2csv": "^4.3.3",
|
"json2csv": "^4.3.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
@ -316,7 +316,7 @@
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-error-boundary": "^2.3.1",
|
"react-error-boundary": "^2.3.1",
|
||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
"react-i18next": "^11.18.6",
|
"react-i18next": "^13.3.0",
|
||||||
"react-linkify": "^1.0.0-alpha",
|
"react-linkify": "^1.0.0-alpha",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"react-resizable-panels": "^0.0.55",
|
"react-resizable-panels": "^0.0.55",
|
||||||
|
|
|
@ -31,7 +31,7 @@ describe('<ProjectListTable />', function () {
|
||||||
const columns = screen.getAllByRole('columnheader')
|
const columns = screen.getAllByRole('columnheader')
|
||||||
columns.forEach(col => {
|
columns.forEach(col => {
|
||||||
if (col.getAttribute('aria-label') === 'Last Modified') {
|
if (col.getAttribute('aria-label') === 'Last Modified') {
|
||||||
expect(col.getAttribute('aria-sort')).to.equal('Descending')
|
expect(col.getAttribute('aria-sort')).to.equal('descending')
|
||||||
foundSortedColumn = true
|
foundSortedColumn = true
|
||||||
} else {
|
} else {
|
||||||
expect(col.getAttribute('aria-sort')).to.be.null
|
expect(col.getAttribute('aria-sort')).to.be.null
|
||||||
|
@ -46,14 +46,14 @@ describe('<ProjectListTable />', function () {
|
||||||
name: /last modified/i,
|
name: /last modified/i,
|
||||||
})
|
})
|
||||||
const lastModifiedCol = lastModifiedBtn.closest('th')
|
const lastModifiedCol = lastModifiedBtn.closest('th')
|
||||||
expect(lastModifiedCol?.getAttribute('aria-sort')).to.equal('Descending')
|
expect(lastModifiedCol?.getAttribute('aria-sort')).to.equal('descending')
|
||||||
const ownerBtn = screen.getByRole('button', { name: /owner/i })
|
const ownerBtn = screen.getByRole('button', { name: /owner/i })
|
||||||
const ownerCol = ownerBtn.closest('th')
|
const ownerCol = ownerBtn.closest('th')
|
||||||
expect(ownerCol?.getAttribute('aria-sort')).to.be.null
|
expect(ownerCol?.getAttribute('aria-sort')).to.be.null
|
||||||
fireEvent.click(ownerBtn)
|
fireEvent.click(ownerBtn)
|
||||||
expect(ownerCol?.getAttribute('aria-sort')).to.equal('Descending')
|
expect(ownerCol?.getAttribute('aria-sort')).to.equal('descending')
|
||||||
fireEvent.click(ownerBtn)
|
fireEvent.click(ownerBtn)
|
||||||
expect(ownerCol?.getAttribute('aria-sort')).to.equal('Ascending')
|
expect(ownerCol?.getAttribute('aria-sort')).to.equal('ascending')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders buttons for sorting all sortable columns', function () {
|
it('renders buttons for sorting all sortable columns', function () {
|
||||||
|
|
144
services/web/test/frontend/infrastructure/i18n.spec.tsx
Normal file
144
services/web/test/frontend/infrastructure/i18n.spec.tsx
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
describe('i18n', function () {
|
||||||
|
describe('t', function () {
|
||||||
|
it('translates a plain string', function () {
|
||||||
|
const Test = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <div>{t('accept')}</div>
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByText('Accept')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses defaultValues', function () {
|
||||||
|
const Test = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <div>{t('welcome_to_sl')}</div>
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByText('Welcome to Overleaf!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses values', function () {
|
||||||
|
const Test = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <div>{t('sort_by_x', { x: 'name' })}</div>
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByText('Sort by name')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Trans', function () {
|
||||||
|
it('translates a plain string', function () {
|
||||||
|
const Test = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Trans i18nKey="accept" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByText('Accept')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses defaultValues', function () {
|
||||||
|
const Test = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Trans i18nKey="welcome_to_sl" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByText('Welcome to Overleaf!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses values', function () {
|
||||||
|
const Test = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Trans
|
||||||
|
i18nKey="sort_by_x"
|
||||||
|
values={{ x: 'name' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByText('Sort by name')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses an object of components', function () {
|
||||||
|
const Test = () => {
|
||||||
|
return (
|
||||||
|
<div data-testid="container">
|
||||||
|
<Trans
|
||||||
|
i18nKey="in_order_to_match_institutional_metadata_associated"
|
||||||
|
components={{ b: <b /> }}
|
||||||
|
values={{ email: 'test@example.com' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByTestId('container')
|
||||||
|
.should(
|
||||||
|
'have.text',
|
||||||
|
'In order to match your institutional metadata, your account is associated with the email test@example.com.'
|
||||||
|
)
|
||||||
|
.find('b')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.should('have.text', 'test@example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses an array of components', function () {
|
||||||
|
const Test = () => {
|
||||||
|
return (
|
||||||
|
<div data-testid="container">
|
||||||
|
<Trans
|
||||||
|
i18nKey="are_you_still_at"
|
||||||
|
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||||
|
values={{ institutionName: 'Test' }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByTestId('container')
|
||||||
|
.should('have.text', 'Are you still at Test?')
|
||||||
|
.find('b')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.should('have.text', 'Test')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('escapes special characters', function () {
|
||||||
|
const Test = () => {
|
||||||
|
return (
|
||||||
|
<div data-testid="container">
|
||||||
|
<Trans
|
||||||
|
i18nKey="are_you_still_at"
|
||||||
|
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||||
|
values={{ institutionName: "T&e's<code>t</code>ing" }}
|
||||||
|
shouldUnescape
|
||||||
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cy.mount(<Test />)
|
||||||
|
cy.findByTestId('container')
|
||||||
|
.should('have.text', "Are you still at T&e's<code>t</code>ing?")
|
||||||
|
.find('b')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.should('have.text', "T&e's<code>t</code>ing")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue