Ensure that translation values are correctly escaped (#15252)

GitOrigin-RevId: 5a38b4c01921fd4d95dbdb7b9e756443fdb00b80
This commit is contained in:
Alf Eaton 2023-10-19 09:27:45 +01:00 committed by Copybot
parent 749aef1c6f
commit 0c81bccfca
61 changed files with 1018 additions and 361 deletions

1012
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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": "",

View file

@ -34,6 +34,8 @@ export default function ErrorMessage({ error }) {
values={{ values={{
nameLimit: fileNameLimit, nameLimit: fileNameLimit,
}} }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/> />
</DangerMessage> </DangerMessage>
) )

View file

@ -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 } }}
/> />
) )

View file

@ -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 } }}
/> />
) )
} }

View file

@ -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:

View file

@ -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>
) )

View file

@ -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>
)} )}

View file

@ -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 />,

View file

@ -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>

View file

@ -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[],

View file

@ -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">

View file

@ -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 */

View file

@ -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"
/> />

View file

@ -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 } }}
/> />
) )
) : ( ) : (

View file

@ -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 } }}
/> />
) )
) : ( ) : (

View file

@ -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>

View file

@ -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>

View file

@ -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 } }}
/> />
&nbsp; &nbsp;
{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 } }}
/> />
&nbsp; &nbsp;
<Trans <Trans

View file

@ -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>

View file

@ -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
/> />
</> </>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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)

View file

@ -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
} }
> >

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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 />]

View file

@ -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')}

View file

@ -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 />]

View file

@ -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>

View file

@ -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}

View file

@ -25,6 +25,8 @@ export default function ManagedAccountAlert() {
values={{ values={{
admin: currentManagedUserAdminEmail, admin: currentManagedUserAdminEmail,
}} }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/> />
</strong> </strong>
</div> </div>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 && (

View file

@ -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" />,

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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 />,

View file

@ -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 />,

View file

@ -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>

View file

@ -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 />,

View file

@ -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 />,

View file

@ -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 />,

View file

@ -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 />,

View file

@ -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 />,

View file

@ -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 />,

View file

@ -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 />,

View file

@ -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>

View file

@ -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,
}, },

View file

@ -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",

View file

@ -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 () {

View 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")
})
})
})