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

View file

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

View file

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

View file

@ -215,6 +215,8 @@ function UploadErrorMessage({ error, maxNumberOfFiles }) {
<Trans
i18nKey="maximum_files_uploaded_together"
values={{ max: maxNumberOfFiles }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
)

View file

@ -34,6 +34,8 @@ export default function RedirectToLogin() {
<Trans
i18nKey="session_expired_redirecting_to_login"
values={{ seconds: secondsToRedirect }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,6 +25,8 @@ function GroupPlan({
i18nKey="trial_remaining_days"
components={{ b: <strong /> }}
values={{ days: remainingTrialDays }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
)
) : (

View file

@ -24,6 +24,8 @@ function IndividualPlan({
i18nKey="trial_remaining_days"
components={{ b: <strong /> }}
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"
components={[<b />, <br />]} // eslint-disable-line react/jsx-key
values={{ flag: '🇮🇳' }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</Notification.Body>
<Notification.Action>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,8 @@ export default function GroupInvitationNotificationJoin({
values={{
inviterName,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</Notification.Body>
<Notification.Action>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -117,6 +117,8 @@ function AddEmail() {
values={{
emailAddressLimit,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[<strong />]} // eslint-disable-line react/jsx-key
/>
</span>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -107,6 +107,8 @@ function LeaveModalForm({
values={{
userDefaultEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</Checkbox>
{error ? <LeaveModalFormError error={error} /> : null}

View file

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

View file

@ -21,6 +21,8 @@ export default function AddCollaboratorsUpgradeContentDefault() {
<Trans
i18nKey="collabs_per_proj"
values={{ collabcount: 'Multiple' }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</li>
<li>

View file

@ -24,6 +24,8 @@ export default function AddCollaboratorsUpgradeContentVariant() {
<Trans
i18nKey="collabs_per_proj"
values={{ collabcount: 'Multiple' }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</li>
<li>

View file

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

View file

@ -37,6 +37,8 @@ export default function GroupSubscriptionMembership({
groupName: subscription.teamName || '',
adminEmail: subscription.admin_id.email,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
{subscription.teamNotice && (

View file

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

View file

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

View file

@ -48,6 +48,8 @@ export default function ManagedInstitution({
values={{
institutionName: institution.name || '',
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
<RowLink

View file

@ -18,6 +18,8 @@ export default function ManagedPublisher({ publisher }: ManagedPublisherProps) {
values={{
publisherName: publisher.name || '',
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
<RowLink

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,6 +14,8 @@ export function PendingAdditionalLicenses({
additionalLicenses,
totalLicenses,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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