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",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["@overleaf"],
|
||||
"env": {
|
||||
"es2020": true
|
||||
},
|
||||
|
@ -113,6 +114,9 @@
|
|||
//
|
||||
"files": ["**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}", "**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}"],
|
||||
"rules": {
|
||||
"@overleaf/no-empty-trans": "off",
|
||||
"@overleaf/should-unescape-trans": "error",
|
||||
|
||||
// https://astexplorer.net/
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
|
|
|
@ -78,7 +78,6 @@
|
|||
"are_you_getting_an_undefined_control_sequence_error": "",
|
||||
"are_you_still_at": "",
|
||||
"are_you_sure": "",
|
||||
"ascending": "",
|
||||
"ask_proj_owner_to_upgrade_for_full_history": "",
|
||||
"ask_proj_owner_to_upgrade_for_longer_compiles": "",
|
||||
"ask_proj_owner_to_upgrade_for_references_search": "",
|
||||
|
@ -251,7 +250,6 @@
|
|||
"demonstrating_git_integration": "",
|
||||
"demonstrating_track_changes_feature": "",
|
||||
"department": "",
|
||||
"descending": "",
|
||||
"description": "",
|
||||
"dictionary": "",
|
||||
"did_you_know_institution_providing_professional": "",
|
||||
|
|
|
@ -34,6 +34,8 @@ export default function ErrorMessage({ error }) {
|
|||
values={{
|
||||
nameLimit: fileNameLimit,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</DangerMessage>
|
||||
)
|
||||
|
|
|
@ -215,6 +215,8 @@ function UploadErrorMessage({ error, maxNumberOfFiles }) {
|
|||
<Trans
|
||||
i18nKey="maximum_files_uploaded_together"
|
||||
values={{ max: maxNumberOfFiles }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ export default function RedirectToLogin() {
|
|||
<Trans
|
||||
i18nKey="session_expired_redirecting_to_login"
|
||||
values={{ seconds: secondsToRedirect }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ function FileTreeModalError() {
|
|||
i18nKey="file_already_exists_in_this_location"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ fileName: error.entityName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)
|
||||
case InvalidFilenameError:
|
||||
|
|
|
@ -123,6 +123,8 @@ function UrlProvider({ file }: UrlProviderProps) {
|
|||
formattedDate: formatTime(file.created),
|
||||
relativeDate: relativeDate(file.created),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
|
@ -156,6 +158,8 @@ function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
|
|||
formattedDate: formatTime(file.created),
|
||||
relativeDate: relativeDate(file.created),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
/* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
|
@ -189,6 +193,8 @@ function ProjectOutputFileProvider({ file }: ProjectOutputFileProviderProps) {
|
|||
formattedDate: formatTime(file.created),
|
||||
relativeDate: relativeDate(file.created),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
|
|
|
@ -68,6 +68,8 @@ export default function GroupMembers() {
|
|||
i18nKey="you_have_added_x_of_group_size_y"
|
||||
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ addedUsersSize: users.length, groupSize }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</small>
|
||||
)}
|
||||
|
|
|
@ -69,6 +69,8 @@ function ResendManagedUserInviteSuccess({
|
|||
values={{
|
||||
email: invitedUserEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -89,6 +91,8 @@ function FailedToResendManagedInvite({
|
|||
values={{
|
||||
email: invitedUserEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -109,6 +113,8 @@ function ResendGroupInviteSuccess({
|
|||
values={{
|
||||
email: invitedUserEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -129,6 +135,8 @@ function FailedToResendGroupInvite({
|
|||
values={{
|
||||
email: invitedUserEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -149,6 +157,8 @@ function TooManyRequests({
|
|||
values={{
|
||||
email: invitedUserEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -24,6 +24,8 @@ export default function ToolbarDatetime({ selection }: ToolbarDatetimeProps) {
|
|||
'Do MMMM · h:mm a'
|
||||
),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
|
@ -36,6 +38,8 @@ export default function ToolbarDatetime({ selection }: ToolbarDatetimeProps) {
|
|||
'Do MMMM · h:mm a'
|
||||
),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@ import displayNameForUser from '../../../ide/history/util/displayNameForUser'
|
|||
import moment from 'moment/moment'
|
||||
import ColorManager from '../../../ide/colors/ColorManager'
|
||||
import { DocDiffChunk, Highlight } from '../services/types/doc'
|
||||
import { TFunction } from 'react-i18next'
|
||||
import { TFunction } from 'i18next'
|
||||
|
||||
export function highlightsFromDiffResponse(
|
||||
chunks: DocDiffChunk[],
|
||||
|
|
|
@ -201,6 +201,8 @@ function CompileTimeoutMessages() {
|
|||
i18nKey="compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile"
|
||||
components={compileTimeoutBlogLinks}
|
||||
values={{ date: 'October 27 2023' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>{' '}
|
||||
<Trans i18nKey="and_you_can_upgrade_for_plenty_more_compile_time" />
|
||||
</p>
|
||||
|
@ -222,6 +224,8 @@ function CompileTimeoutMessages() {
|
|||
i18nKey="compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile"
|
||||
components={compileTimeoutBlogLinks}
|
||||
values={{ date: 'October 27 2023' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<p className="row-spaced">
|
||||
|
|
|
@ -72,6 +72,8 @@ function PdfPreviewError({ error }) {
|
|||
getMeta('ol-compilesUserContentDomain')
|
||||
).hostname,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
<code key="domain" />,
|
||||
/* 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')}
|
||||
level="error"
|
||||
/>
|
||||
|
@ -163,6 +164,8 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
|
|||
/>,
|
||||
]}
|
||||
values={{ date: 'October 27 2023' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -229,6 +232,7 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
|
|||
</p>
|
||||
</>
|
||||
}
|
||||
// @ts-ignore
|
||||
entryAriaLabel={t('other_ways_to_prevent_compile_timeouts')}
|
||||
level="raw"
|
||||
/>
|
||||
|
|
|
@ -25,6 +25,8 @@ function GroupPlan({
|
|||
i18nKey="trial_remaining_days"
|
||||
components={{ b: <strong /> }}
|
||||
values={{ days: remainingTrialDays }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
|
|
|
@ -24,6 +24,8 @@ function IndividualPlan({
|
|||
i18nKey="trial_remaining_days"
|
||||
components={{ b: <strong /> }}
|
||||
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"
|
||||
components={[<b />, <br />]} // eslint-disable-line react/jsx-key
|
||||
values={{ flag: '🇮🇳' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</Notification.Body>
|
||||
<Notification.Action>
|
||||
|
|
|
@ -89,6 +89,8 @@ export default function LATAMBanner() {
|
|||
i18nKey="latam_discount_offer"
|
||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||
values={{ flag, currencyName, discountPercent }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</Notification.Body>
|
||||
<Notification.Action>
|
||||
|
|
|
@ -62,6 +62,8 @@ function ReconfirmAffiliation({
|
|||
i18nKey="please_check_your_inbox_to_confirm"
|
||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: institution.name }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
|
@ -113,6 +115,8 @@ function ReconfirmAffiliation({
|
|||
i18nKey="are_you_still_at"
|
||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: institution.name }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
|
||||
<Trans
|
||||
|
|
|
@ -69,6 +69,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
|||
i18nKey="notification_project_invite_accepted_message"
|
||||
components={{ b: <b /> }}
|
||||
values={{ projectName: notification.messageOpts.projectName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
|
@ -78,6 +80,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
|||
userName: notification.messageOpts.userName,
|
||||
projectName: notification.messageOpts.projectName,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)}
|
||||
</Notification.Body>
|
||||
|
@ -136,6 +140,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
|||
values={{
|
||||
institutionName: notification.messageOpts.university_name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
<br />
|
||||
{notification.messageOpts.ssoEnabled ? (
|
||||
|
@ -163,6 +169,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
|||
values={{
|
||||
institutionName: notification.messageOpts.university_name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
<br />
|
||||
{t('add_email_to_claim_features')}
|
||||
|
@ -219,6 +227,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
|||
i18nKey="dropbox_duplicate_project_names"
|
||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||
values={{ projectName: notification.messageOpts.projectName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
|
|
|
@ -116,6 +116,8 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
|
|||
institutionName: userEmail.affiliation?.institution.name,
|
||||
emailAddress: userEmail.email,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -31,6 +31,8 @@ export default function GroupInvitationCancelIndividualSubscriptionNotification(
|
|||
values={{
|
||||
inviterName,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</Notification.Body>
|
||||
<Notification.Action className="group-invitation-cancel-subscription-notification-buttons">
|
||||
|
|
|
@ -29,6 +29,8 @@ export default function GroupInvitationNotificationJoin({
|
|||
values={{
|
||||
inviterName,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</Notification.Body>
|
||||
<Notification.Action>
|
||||
|
|
|
@ -48,6 +48,8 @@ function Institution() {
|
|||
i18nKey="can_link_institution_email_acct_to_institution_acct"
|
||||
components={{ b: <b /> }}
|
||||
values={{ appName, email, institutionName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<div>
|
||||
|
@ -55,6 +57,8 @@ function Institution() {
|
|||
i18nKey="doing_this_allow_log_in_through_institution"
|
||||
components={{ b: <b /> }}
|
||||
values={{ appName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>{' '}
|
||||
<a href="/learn/how-to/Institutional_Login">
|
||||
{t('learn_more')}
|
||||
|
@ -82,6 +86,8 @@ function Institution() {
|
|||
i18nKey="account_has_been_link_to_institution_account"
|
||||
components={{ b: <b /> }}
|
||||
values={{ appName, email, institutionName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</Notification.Body>
|
||||
</Notification>
|
||||
|
@ -97,11 +103,15 @@ function Institution() {
|
|||
i18nKey="tried_to_log_in_with_email"
|
||||
components={{ b: <b /> }}
|
||||
values={{ appName, email: requestedEmail }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>{' '}
|
||||
<Trans
|
||||
i18nKey="in_order_to_match_institutional_metadata_associated"
|
||||
components={{ b: <b /> }}
|
||||
values={{ email: institutionEmail }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</Notification.Body>
|
||||
</Notification>
|
||||
|
@ -117,6 +127,8 @@ function Institution() {
|
|||
i18nKey="tried_to_register_with_email"
|
||||
components={{ b: <b /> }}
|
||||
values={{ appName, email }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>{' '}
|
||||
{t('we_logged_you_in')}
|
||||
</Notification.Body>
|
||||
|
|
|
@ -23,6 +23,8 @@ function TranslationMessage() {
|
|||
i18nKey="click_here_to_view_sl_in_lng"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ lngName: config.lngName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
<img
|
||||
className="ms-1"
|
||||
|
|
|
@ -18,6 +18,7 @@ export default function LastUpdatedCell({ project }: LastUpdatedCellProps) {
|
|||
}}
|
||||
// eslint-disable-next-line react/jsx-boolean-value
|
||||
shouldUnescape={true}
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
) : (
|
||||
fromNowDate(project.lastUpdated)
|
||||
|
|
|
@ -68,8 +68,8 @@ function ProjectListTable() {
|
|||
aria-sort={
|
||||
sort.by === 'title'
|
||||
? sort.order === 'asc'
|
||||
? t('ascending')
|
||||
: t('descending')
|
||||
? 'ascending'
|
||||
: 'descending'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
@ -92,8 +92,8 @@ function ProjectListTable() {
|
|||
aria-sort={
|
||||
sort.by === 'owner'
|
||||
? sort.order === 'asc'
|
||||
? t('ascending')
|
||||
: t('descending')
|
||||
? 'ascending'
|
||||
: 'descending'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
@ -110,8 +110,8 @@ function ProjectListTable() {
|
|||
aria-sort={
|
||||
sort.by === 'lastUpdated'
|
||||
? sort.order === 'asc'
|
||||
? t('ascending')
|
||||
: t('descending')
|
||||
? 'ascending'
|
||||
: 'descending'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
|
|
@ -33,6 +33,8 @@ function ConfirmationModal({
|
|||
i18nKey="do_you_want_to_change_your_primary_email_address_to"
|
||||
components={{ b: <b /> }}
|
||||
values={{ email }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<p className="mb-0">{t('log_in_with_primary_email_address')}</p>
|
||||
|
|
|
@ -117,6 +117,8 @@ function AddEmail() {
|
|||
values={{
|
||||
emailAddressLimit,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -36,6 +36,7 @@ function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
|
|||
values={{ institutionName: domainInfo.university.name }}
|
||||
// eslint-disable-next-line react/jsx-boolean-value
|
||||
shouldUnescape={true}
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
|
@ -45,6 +46,7 @@ function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
|
|||
values={{ institutionName: domainInfo.university.name }}
|
||||
// eslint-disable-next-line react/jsx-boolean-value
|
||||
shouldUnescape={true}
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>{' '}
|
||||
<a
|
||||
href="/learn/how-to/Institutional_Login"
|
||||
|
|
|
@ -71,6 +71,8 @@ function ReconfirmationInfoPrompt({
|
|||
values={{
|
||||
institutionName: institution.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<strong />]
|
||||
|
@ -155,6 +157,8 @@ function ReconfirmationInfoPromptText({
|
|||
values={{
|
||||
institutionName,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<strong />]
|
||||
|
|
|
@ -17,6 +17,8 @@ function ReconfirmationInfoSuccess({
|
|||
<Trans
|
||||
i18nKey="your_affiliation_is_confirmed"
|
||||
values={{ institutionName: institution.name }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>{' '}
|
||||
{t('thank_you_exclamation')}
|
||||
|
|
|
@ -98,6 +98,8 @@ function SSOAffiliationInfo({ userEmailData }: SSOAffiliationInfoProps) {
|
|||
values={{
|
||||
institutionName: userEmailData.affiliation?.institution.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
</EmailCell>
|
||||
|
@ -120,6 +122,8 @@ function SSOAffiliationInfo({ userEmailData }: SSOAffiliationInfoProps) {
|
|||
institutionName:
|
||||
userEmailData.affiliation?.institution.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<strong />]
|
||||
|
|
|
@ -64,6 +64,8 @@ export function SSOAlert() {
|
|||
i18nKey="institution_acct_successfully_linked_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: institutionLinked.universityName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
{institutionLinked.hasEntitlement && (
|
||||
|
@ -72,6 +74,8 @@ export function SSOAlert() {
|
|||
i18nKey="this_grants_access_to_features_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ featureType: t('professional') }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
@ -92,6 +96,8 @@ export function SSOAlert() {
|
|||
i18nKey="in_order_to_match_institutional_metadata_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ email: institutionEmailNonCanonical }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
|
|
|
@ -107,6 +107,8 @@ function LeaveModalForm({
|
|||
values={{
|
||||
userDefaultEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</Checkbox>
|
||||
{error ? <LeaveModalFormError error={error} /> : null}
|
||||
|
|
|
@ -25,6 +25,8 @@ export default function ManagedAccountAlert() {
|
|||
values={{
|
||||
admin: currentManagedUserAdminEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
|
|
|
@ -21,6 +21,8 @@ export default function AddCollaboratorsUpgradeContentDefault() {
|
|||
<Trans
|
||||
i18nKey="collabs_per_proj"
|
||||
values={{ collabcount: 'Multiple' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
@ -24,6 +24,8 @@ export default function AddCollaboratorsUpgradeContentVariant() {
|
|||
<Trans
|
||||
i18nKey="collabs_per_proj"
|
||||
values={{ collabcount: 'Multiple' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
@ -44,6 +44,7 @@ export default function TransferOwnershipModal({ member, cancel }) {
|
|||
components={[<strong key="strong-1" />, <strong key="strong-2" />]}
|
||||
// eslint-disable-next-line react/jsx-boolean-value
|
||||
shouldUnescape={true}
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
|
|
|
@ -37,6 +37,8 @@ export default function GroupSubscriptionMembership({
|
|||
groupName: subscription.teamName || '',
|
||||
adminEmail: subscription.admin_id.email,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
{subscription.teamNotice && (
|
||||
|
|
|
@ -31,6 +31,8 @@ function InstitutionMemberships() {
|
|||
planName: 'Professional',
|
||||
institutionName: institution.name || '',
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||
<a href="/user/subscription/plans" rel="noopener" />,
|
||||
|
|
|
@ -30,6 +30,8 @@ export default function ManagedGroupSubscriptions() {
|
|||
groupName: subscription.teamName || '',
|
||||
adminEmail: subscription.admin_id.email,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
|
@ -40,6 +42,8 @@ export default function ManagedGroupSubscriptions() {
|
|||
groupName: subscription.teamName || '',
|
||||
adminEmail: subscription.admin_id.email,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
|
|
|
@ -48,6 +48,8 @@ export default function ManagedInstitution({
|
|||
values={{
|
||||
institutionName: institution.name || '',
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<RowLink
|
||||
|
|
|
@ -18,6 +18,8 @@ export default function ManagedPublisher({ publisher }: ManagedPublisherProps) {
|
|||
values={{
|
||||
publisherName: publisher.name || '',
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<RowLink
|
||||
|
|
|
@ -35,6 +35,8 @@ function PersonalSubscriptionRecurlySyncEmail() {
|
|||
i18nKey="recurly_email_update_needed"
|
||||
components={[<em />, <em />]} // eslint-disable-line react/jsx-key
|
||||
values={{ recurlyEmail, userEmail }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<div>
|
||||
|
|
|
@ -39,6 +39,8 @@ export function ActiveSubscription({
|
|||
values={{
|
||||
planName: subscription.plan.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -95,6 +97,8 @@ export function ActiveSubscription({
|
|||
paymentAmmount: subscription.recurly.displayPrice,
|
||||
collectionDate: subscription.recurly.nextPaymentDueAt,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -78,6 +78,8 @@ function NotCancelOption({
|
|||
values={{
|
||||
days: 14,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -105,6 +107,8 @@ function NotCancelOption({
|
|||
values={{
|
||||
price: planToDowngradeTo.displayPrice,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -27,6 +27,8 @@ function GroupPlanCollaboratorCount({ planCode }: { planCode: string }) {
|
|||
values={{
|
||||
collabcount: 10,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
@ -44,6 +46,8 @@ function EducationDiscountAppliedOrNot({ groupSize }: { groupSize: string }) {
|
|||
<Trans
|
||||
i18nKey="educational_percent_discount_applied"
|
||||
values={{ percent: educationalPercentDiscount }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
|
@ -54,6 +58,8 @@ function EducationDiscountAppliedOrNot({ groupSize }: { groupSize: string }) {
|
|||
<Trans
|
||||
i18nKey="educational_discount_for_groups_of_x_or_more"
|
||||
values={{ size: groupSizeForEducationalDiscount }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
|
@ -92,6 +98,8 @@ function GroupPrice({
|
|||
<Trans
|
||||
i18nKey="x_price_per_year"
|
||||
values={{ price: groupPlanToChangeToPrice?.totalForDisplay }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
|
@ -105,6 +113,8 @@ function GroupPrice({
|
|||
values={{
|
||||
price: perUserPrice,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</span>
|
||||
<span className="sr-only">
|
||||
|
@ -116,6 +126,8 @@ function GroupPrice({
|
|||
values={{
|
||||
price: perUserPrice,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
|
@ -208,6 +220,8 @@ export function ChangeToGroupModal() {
|
|||
values={{
|
||||
percent: '30',
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
|
@ -296,6 +310,8 @@ export function ChangeToGroupModal() {
|
|||
percent: educationalPercentDiscount,
|
||||
size: groupSizeForEducationalDiscount,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
|
@ -345,6 +361,8 @@ export function ChangeToGroupModal() {
|
|||
subtotal: groupPlanToChangeToPrice.subtotal,
|
||||
tax: groupPlanToChangeToPrice.tax,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
/* eslint-disable-next-line react/jsx-key */
|
||||
<strong />,
|
||||
|
@ -375,7 +393,12 @@ export function ChangeToGroupModal() {
|
|||
</button>
|
||||
<hr className="thin" />
|
||||
<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')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -69,6 +69,8 @@ export function ConfirmChangePlanModal() {
|
|||
values={{
|
||||
planName: plan.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -57,6 +57,8 @@ export function KeepCurrentPlanModal() {
|
|||
values={{
|
||||
planName: personalSubscription.plan.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -14,6 +14,8 @@ export function PendingAdditionalLicenses({
|
|||
additionalLicenses,
|
||||
totalLicenses,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -16,6 +16,8 @@ export function PendingPlanChange({
|
|||
values={{
|
||||
pendingPlanName: subscription.pendingPlan.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -35,6 +37,8 @@ export function PendingPlanChange({
|
|||
subscription.recurly.pendingAdditionalLicenses,
|
||||
pendingTotalLicenses: subscription.recurly.pendingTotalLicenses,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -17,6 +17,8 @@ function SubscriptionRemainder({ subscription }: SubscriptionRemainderProps) {
|
|||
values={{
|
||||
terminationDate: subscription.recurly.nextPaymentDueAt,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -28,6 +30,8 @@ function SubscriptionRemainder({ subscription }: SubscriptionRemainderProps) {
|
|||
values={{
|
||||
terminationDate: subscription.recurly.nextPaymentDueAt,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -10,6 +10,8 @@ export function TrialEnding({
|
|||
<Trans
|
||||
i18nKey="youre_on_free_trial_which_ends_on"
|
||||
values={{ date: trialEndsAtFormatted }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -17,6 +17,8 @@ export function CanceledSubscription({
|
|||
values={{
|
||||
planName: subscription.plan.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
@ -29,6 +31,8 @@ export function CanceledSubscription({
|
|||
values={{
|
||||
terminateDate: subscription.recurly.nextPaymentDueAt,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
|
|
|
@ -35,6 +35,8 @@ function SuccessfulSubscription() {
|
|||
paymentAmmount: subscription.recurly.displayPrice,
|
||||
collectionDate: subscription.recurly.nextPaymentDueAt,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
</p>
|
||||
|
|
|
@ -33,6 +33,10 @@ i18n.use(initReactI18next).init({
|
|||
// injection via another nested value
|
||||
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: {
|
||||
appName: window.ExposedSettings.appName,
|
||||
},
|
||||
|
|
|
@ -109,9 +109,9 @@
|
|||
"fuse.js": "^3.0.0",
|
||||
"globby": "^5.0.0",
|
||||
"helmet": "^6.0.1",
|
||||
"i18next": "^19.6.3",
|
||||
"i18next-fs-backend": "^1.0.7",
|
||||
"i18next-http-middleware": "^3.0.2",
|
||||
"i18next": "^23.5.1",
|
||||
"i18next-fs-backend": "^2.2.0",
|
||||
"i18next-http-middleware": "^3.4.1",
|
||||
"jose": "^4.3.8",
|
||||
"json2csv": "^4.3.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
|
@ -316,7 +316,7 @@
|
|||
"react-dom": "^17.0.2",
|
||||
"react-error-boundary": "^2.3.1",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-i18next": "^13.3.0",
|
||||
"react-linkify": "^1.0.0-alpha",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-resizable-panels": "^0.0.55",
|
||||
|
|
|
@ -31,7 +31,7 @@ describe('<ProjectListTable />', function () {
|
|||
const columns = screen.getAllByRole('columnheader')
|
||||
columns.forEach(col => {
|
||||
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
|
||||
} else {
|
||||
expect(col.getAttribute('aria-sort')).to.be.null
|
||||
|
@ -46,14 +46,14 @@ describe('<ProjectListTable />', function () {
|
|||
name: /last modified/i,
|
||||
})
|
||||
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 ownerCol = ownerBtn.closest('th')
|
||||
expect(ownerCol?.getAttribute('aria-sort')).to.be.null
|
||||
fireEvent.click(ownerBtn)
|
||||
expect(ownerCol?.getAttribute('aria-sort')).to.equal('Descending')
|
||||
expect(ownerCol?.getAttribute('aria-sort')).to.equal('descending')
|
||||
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 () {
|
||||
|
|
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