Disable transSupportBasicHtmlNodes in react-i18next config (#15430)

* Set transSupportBasicHtmlNodes to false
* Update ESLint rule
* Convert Trans to t
* Convert shouldUnescape={true}
* Convert some arrays to objects
* Update translations

GitOrigin-RevId: 64a50318388abcada408f72d949de148129a9f63
This commit is contained in:
Alf Eaton 2023-10-30 10:29:56 +00:00 committed by Copybot
parent 1314f9082c
commit 221d16f4e4
21 changed files with 97 additions and 176 deletions

View file

@ -114,7 +114,7 @@
// //
"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": "error", "@overleaf/no-unnecessary-trans": "error",
"@overleaf/should-unescape-trans": "error", "@overleaf/should-unescape-trans": "error",
// https://astexplorer.net/ // https://astexplorer.net/

View file

@ -1,5 +1,5 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FetchError } from '../../../../infrastructure/fetch-json' import { FetchError } from '../../../../infrastructure/fetch-json'
import RedirectToLogin from './redirect-to-login' import RedirectToLogin from './redirect-to-login'
import { import {
@ -29,14 +29,9 @@ export default function ErrorMessage({ error }) {
case 'invalid_filename': case 'invalid_filename':
return ( return (
<DangerMessage> <DangerMessage>
<Trans {t('invalid_filename', {
i18nKey="invalid_filename" nameLimit: fileNameLimit,
values={{ })}
nameLimit: fileNameLimit,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</DangerMessage> </DangerMessage>
) )

View file

@ -1,4 +1,4 @@
import { Trans, useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
@ -209,16 +209,12 @@ const projectContextPropTypes = {
} }
function UploadErrorMessage({ error, maxNumberOfFiles }) { function UploadErrorMessage({ error, maxNumberOfFiles }) {
const { t } = useTranslation()
switch (error) { switch (error) {
case 'too-many-files': case 'too-many-files':
return ( return t('maximum_files_uploaded_together', {
<Trans max: maxNumberOfFiles,
i18nKey="maximum_files_uploaded_together" })
values={{ max: maxNumberOfFiles }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
)
default: default:
return <ErrorMessage error={error} /> return <ErrorMessage error={error} />

View file

@ -1,11 +1,12 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Trans } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useProjectContext } from '../../../../shared/context/project-context' import { useProjectContext } from '../../../../shared/context/project-context'
import { useLocation } from '../../../../shared/hooks/use-location' import { useLocation } from '../../../../shared/hooks/use-location'
// handle "not-logged-in" errors by redirecting to the login page // handle "not-logged-in" errors by redirecting to the login page
export default function RedirectToLogin() { export default function RedirectToLogin() {
const { t } = useTranslation()
const { _id: projectId } = useProjectContext(projectContextPropTypes) const { _id: projectId } = useProjectContext(projectContextPropTypes)
const [secondsToRedirect, setSecondsToRedirect] = useState(10) const [secondsToRedirect, setSecondsToRedirect] = useState(10)
const location = useLocation() const location = useLocation()
@ -30,14 +31,9 @@ export default function RedirectToLogin() {
} }
}, [projectId, location]) }, [projectId, location])
return ( return t('session_expired_redirecting_to_login', {
<Trans seconds: secondsToRedirect,
i18nKey="session_expired_redirecting_to_login" })
values={{ seconds: secondsToRedirect }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
)
} }
const projectContextPropTypes = { const projectContextPropTypes = {

View file

@ -90,8 +90,9 @@ function CompileTimeWarning() {
<div className="warning-text"> <div className="warning-text">
<Trans <Trans
i18nKey="approaching_compile_timeout_limit_upgrade_for_more_compile_time" i18nKey="approaching_compile_timeout_limit_upgrade_for_more_compile_time"
// eslint-disable-next-line react/jsx-key components={{
components={[<strong style={{ display: 'inline-block' }} />]} strong: <strong style={{ display: 'inline-block' }} />,
}}
/> />
</div> </div>
<div className="upgrade-prompt"> <div className="upgrade-prompt">

View file

@ -18,7 +18,7 @@ export const CompileTimeoutChangingSoon: FC<{
const { t } = useTranslation() const { t } = useTranslation()
const compileTimeoutBlogLinks = [ const compileTimeoutBlogLinks = [
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */ /* eslint-disable-next-line jsx-a11y/anchor-has-content */
<a <a
aria-label={t('read_more_about_free_compile_timeouts_servers')} aria-label={t('read_more_about_free_compile_timeouts_servers')}
href="/blog/changes-to-free-compile-timeouts-and-servers" href="/blog/changes-to-free-compile-timeouts-and-servers"
@ -27,7 +27,7 @@ export const CompileTimeoutChangingSoon: FC<{
target="_blank" target="_blank"
onClick={sendInfoClickEvent} onClick={sendInfoClickEvent}
/>, />,
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */ /* eslint-disable-next-line jsx-a11y/anchor-has-content */
<a <a
aria-label={t('read_more_about_fix_prevent_timeout')} aria-label={t('read_more_about_fix_prevent_timeout')}
href="/learn/how-to/Fixing_and_preventing_compile_timeouts" href="/learn/how-to/Fixing_and_preventing_compile_timeouts"

View file

@ -1,5 +1,5 @@
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { Dispatch, SetStateAction } from 'react' import type { Dispatch, SetStateAction } from 'react'
import Notification from '../../notification' import Notification from '../../notification'
import { GroupInvitationStatus } from './hooks/use-group-invitation-notification' import { GroupInvitationStatus } from './hooks/use-group-invitation-notification'
@ -26,14 +26,7 @@ export default function GroupInvitationCancelIndividualSubscriptionNotification(
return ( return (
<Notification bsStyle="info" onDismiss={dismissGroupInviteNotification}> <Notification bsStyle="info" onDismiss={dismissGroupInviteNotification}>
<Notification.Body> <Notification.Body>
<Trans {t('invited_to_group_have_individual_subcription', { inviterName })}
i18nKey="invited_to_group_have_individual_subcription"
values={{
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">
<Button <Button

View file

@ -1,5 +1,5 @@
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Notification from '../../notification' import Notification from '../../notification'
import type { NotificationGroupInvitation } from '../../../../../../../../types/project/dashboard/notification' import type { NotificationGroupInvitation } from '../../../../../../../../types/project/dashboard/notification'
@ -24,14 +24,7 @@ export default function GroupInvitationNotificationJoin({
return ( return (
<Notification bsStyle="info" onDismiss={dismissGroupInviteNotification}> <Notification bsStyle="info" onDismiss={dismissGroupInviteNotification}>
<Notification.Body> <Notification.Body>
<Trans {t('invited_to_group', { inviterName })}
i18nKey="invited_to_group"
values={{
inviterName,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</Notification.Body> </Notification.Body>
<Notification.Action> <Notification.Action>
<Button <Button

View file

@ -1,4 +1,4 @@
import { Trans } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { formatDate, fromNowDate } from '../../../../../utils/dates' import { formatDate, fromNowDate } from '../../../../../utils/dates'
import { Project } from '../../../../../../../types/project/dashboard/api' import { Project } from '../../../../../../../types/project/dashboard/api'
import Tooltip from '../../../../../shared/components/tooltip' import Tooltip from '../../../../../shared/components/tooltip'
@ -9,20 +9,14 @@ type LastUpdatedCellProps = {
} }
export default function LastUpdatedCell({ project }: LastUpdatedCellProps) { export default function LastUpdatedCell({ project }: LastUpdatedCellProps) {
const displayText = project.lastUpdatedBy ? ( const { t } = useTranslation()
<Trans
i18nKey="last_updated_date_by_x" const displayText = project.lastUpdatedBy
values={{ ? t('last_updated_date_by_x', {
lastUpdatedDate: fromNowDate(project.lastUpdated), lastUpdatedDate: fromNowDate(project.lastUpdated),
person: getUserName(project.lastUpdatedBy), person: getUserName(project.lastUpdatedBy),
}} })
// eslint-disable-next-line react/jsx-boolean-value : fromNowDate(project.lastUpdated)
shouldUnescape={true}
tOptions={{ interpolation: { escapeValue: true } }}
/>
) : (
fromNowDate(project.lastUpdated)
)
const tooltipText = formatDate(project.lastUpdated) const tooltipText = formatDate(project.lastUpdated)
return ( return (

View file

@ -34,8 +34,7 @@ function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
i18nKey="to_add_email_accounts_need_to_be_linked_2" i18nKey="to_add_email_accounts_need_to_be_linked_2"
components={[<strong />]} // eslint-disable-line react/jsx-key components={[<strong />]} // eslint-disable-line react/jsx-key
values={{ institutionName: domainInfo.university.name }} values={{ institutionName: domainInfo.university.name }}
// eslint-disable-next-line react/jsx-boolean-value shouldUnescape
shouldUnescape={true}
tOptions={{ interpolation: { escapeValue: true } }} tOptions={{ interpolation: { escapeValue: true } }}
/> />
</p> </p>
@ -44,8 +43,7 @@ function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
i18nKey="doing_this_will_verify_affiliation_and_allow_log_in_2" i18nKey="doing_this_will_verify_affiliation_and_allow_log_in_2"
components={[<strong />]} // eslint-disable-line react/jsx-key components={[<strong />]} // eslint-disable-line react/jsx-key
values={{ institutionName: domainInfo.university.name }} values={{ institutionName: domainInfo.university.name }}
// eslint-disable-next-line react/jsx-boolean-value shouldUnescape
shouldUnescape={true}
tOptions={{ interpolation: { escapeValue: true } }} tOptions={{ interpolation: { escapeValue: true } }}
/>{' '} />{' '}
<a <a

View file

@ -57,7 +57,7 @@ function LeaveModalContent({
<p> <p>
<Trans <Trans
i18nKey="delete_account_warning_message_3" i18nKey="delete_account_warning_message_3"
components={[<strong />]} // eslint-disable-line react/jsx-key components={{ strong: <strong /> }}
/> />
</p> </p>
<LeaveModalContentBlock <LeaveModalContentBlock

View file

@ -1,7 +1,8 @@
import { Trans } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
export default function ManagedAccountAlert() { export default function ManagedAccountAlert() {
const { t } = useTranslation()
const isManaged = getMeta('ol-isManagedAccount', false) const isManaged = getMeta('ol-isManagedAccount', false)
const currentManagedUserAdminEmail: string = getMeta( const currentManagedUserAdminEmail: string = getMeta(
'ol-currentManagedUserAdminEmail', 'ol-currentManagedUserAdminEmail',
@ -20,14 +21,9 @@ export default function ManagedAccountAlert() {
<div> <div>
<div> <div>
<strong> <strong>
<Trans {t('account_managed_by_group_administrator', {
i18nKey="account_managed_by_group_administrator" admin: currentManagedUserAdminEmail,
values={{ })}
admin: currentManagedUserAdminEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</strong> </strong>
</div> </div>
<div> <div>

View file

@ -1,5 +1,5 @@
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { Trans, useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
export default function AddCollaboratorsUpgradeContentDefault() { export default function AddCollaboratorsUpgradeContentDefault() {
const { t } = useTranslation() const { t } = useTranslation()
@ -18,12 +18,9 @@ export default function AddCollaboratorsUpgradeContentDefault() {
<li> <li>
<Icon type="check" /> <Icon type="check" />
&nbsp; &nbsp;
<Trans {t('collabs_per_proj', {
i18nKey="collabs_per_proj" collabcount: 'Multiple',
values={{ collabcount: 'Multiple' }} })}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</li> </li>
<li> <li>
<Icon type="check" /> <Icon type="check" />

View file

@ -1,5 +1,5 @@
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { Trans, useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
export default function AddCollaboratorsUpgradeContentVariant() { export default function AddCollaboratorsUpgradeContentVariant() {
const { t } = useTranslation() const { t } = useTranslation()
@ -23,12 +23,9 @@ export default function AddCollaboratorsUpgradeContentVariant() {
<li> <li>
<Icon type="check" /> <Icon type="check" />
&nbsp; &nbsp;
<Trans {t('collabs_per_proj', {
i18nKey="collabs_per_proj" collabcount: 'Multiple',
values={{ collabcount: 'Multiple' }} })}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</li> </li>
<li> <li>
<Icon type="check" /> <Icon type="check" />

View file

@ -42,8 +42,7 @@ export default function TransferOwnershipModal({ member, cancel }) {
i18nKey="project_ownership_transfer_confirmation_1" i18nKey="project_ownership_transfer_confirmation_1"
values={{ user: member.email, project: projectName }} values={{ user: member.email, project: projectName }}
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 shouldUnescape
shouldUnescape={true}
tOptions={{ interpolation: { escapeValue: true } }} tOptions={{ interpolation: { escapeValue: true } }}
/> />
</p> </p>

View file

@ -16,8 +16,10 @@ function ToggleWidget() {
})} })}
onClick={toggleReviewPanel} onClick={toggleReviewPanel}
> >
{/* eslint-disable-next-line react/jsx-key */} <Trans
<Trans i18nKey="track_changes_is_on" components={[<strong />]} /> i18nKey="track_changes_is_on"
components={{ strong: <strong /> }}
/>
</button> </button>
) )
} }

View file

@ -68,11 +68,15 @@ function ToggleMenu() {
onClick={handleToggleFullTCStateCollapse} onClick={handleToggleFullTCStateCollapse}
> >
{wantTrackChanges ? ( {wantTrackChanges ? (
// eslint-disable-next-line react/jsx-key <Trans
<Trans i18nKey="track_changes_is_on" components={[<strong />]} /> i18nKey="track_changes_is_on"
components={{ strong: <strong /> }}
/>
) : ( ) : (
// eslint-disable-next-line react/jsx-key <Trans
<Trans i18nKey="track_changes_is_off" components={[<strong />]} /> i18nKey="track_changes_is_off"
components={{ strong: <strong /> }}
/>
)} )}
<span <span
className={classnames('rp-tc-state-collapse', { className={classnames('rp-tc-state-collapse', {

View file

@ -80,10 +80,7 @@ function NotCancelOption({
}} }}
shouldUnescape shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }} tOptions={{ interpolation: { escapeValue: true } }}
components={[ components={{ strong: <strong /> }}
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/> />
</p> </p>
<p> <p>

View file

@ -22,14 +22,9 @@ function GroupPlanCollaboratorCount({ planCode }: { planCode: string }) {
if (planCode === 'collaborator') { if (planCode === 'collaborator') {
return ( return (
<> <>
<Trans {t('collabs_per_proj', {
i18nKey="collabs_per_proj" collabcount: 10,
values={{ })}
collabcount: 10,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</> </>
) )
} else if (planCode === 'professional') { } else if (planCode === 'professional') {
@ -39,28 +34,23 @@ function GroupPlanCollaboratorCount({ planCode }: { planCode: string }) {
} }
function EducationDiscountAppliedOrNot({ groupSize }: { groupSize: string }) { function EducationDiscountAppliedOrNot({ groupSize }: { groupSize: string }) {
const { t } = useTranslation()
const size = parseInt(groupSize) const size = parseInt(groupSize)
if (size >= groupSizeForEducationalDiscount) { if (size >= groupSizeForEducationalDiscount) {
return ( return (
<p className="applied"> <p className="applied">
<Trans {t('educational_percent_discount_applied', {
i18nKey="educational_percent_discount_applied" percent: educationalPercentDiscount,
values={{ percent: educationalPercentDiscount }} })}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p> </p>
) )
} }
return ( return (
<p className="ineligible"> <p className="ineligible">
<Trans {t('educational_discount_for_groups_of_x_or_more', {
i18nKey="educational_discount_for_groups_of_x_or_more" size: groupSizeForEducationalDiscount,
values={{ size: groupSizeForEducationalDiscount }} })}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p> </p>
) )
} }
@ -92,44 +82,27 @@ function GroupPrice({
{totalPrice} <span className="small">/ {t('year')}</span> {totalPrice} <span className="small">/ {t('year')}</span>
</span> </span>
<span className="sr-only"> <span className="sr-only">
{queryingGroupPlanToChangeToPrice ? ( {queryingGroupPlanToChangeToPrice
t('loading_prices') ? t('loading_prices')
) : ( : t('x_price_per_year', {
<Trans price: groupPlanToChangeToPrice?.totalForDisplay,
i18nKey="x_price_per_year" })}
values={{ price: groupPlanToChangeToPrice?.totalForDisplay }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
)}
</span> </span>
<br /> <br />
<span className="circle-subtext"> <span className="circle-subtext">
<span aria-hidden> <span aria-hidden>
<Trans {t('x_price_per_user', {
i18nKey="x_price_per_user" price: perUserPrice,
values={{ })}
price: perUserPrice,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</span> </span>
<span className="sr-only"> <span className="sr-only">
{queryingGroupPlanToChangeToPrice ? ( {queryingGroupPlanToChangeToPrice
t('loading_prices') ? t('loading_prices')
) : ( : t('x_price_per_user', {
<Trans
i18nKey="x_price_per_user"
values={{
price: perUserPrice, price: perUserPrice,
}} })}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
)}
</span> </span>
</span> </span>
</> </>
@ -215,14 +188,9 @@ export function ChangeToGroupModal() {
<div className="modal-title"> <div className="modal-title">
<h2>{t('customize_your_group_subscription')}</h2> <h2>{t('customize_your_group_subscription')}</h2>
<h3> <h3>
<Trans {t('save_x_percent_or_more', {
i18nKey="save_x_percent_or_more" percent: '30',
values={{ })}
percent: '30',
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</h3> </h3>
</div> </div>
</Modal.Header> </Modal.Header>
@ -304,15 +272,10 @@ export function ChangeToGroupModal() {
<div className="form-group"> <div className="form-group">
<strong> <strong>
<Trans {t('percent_discount_for_groups', {
i18nKey="percent_discount_for_groups" percent: educationalPercentDiscount,
values={{ size: groupSizeForEducationalDiscount,
percent: educationalPercentDiscount, })}
size: groupSizeForEducationalDiscount,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</strong> </strong>
</div> </div>
@ -393,12 +356,9 @@ 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 {t('need_more_than_x_licenses', {
i18nKey="need_more_than_x_licenses" x: 50,
values={{ x: 50 }} })}{' '}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>{' '}
{t('please_get_in_touch')} {t('please_get_in_touch')}
</button> </button>
</div> </div>

View file

@ -23,6 +23,9 @@ i18n.use(initReactI18next).init({
// translation strings asynchronously, we need to trigger a re-render once // translation strings asynchronously, we need to trigger a re-render once
// they've loaded // they've loaded
bindI18nStore: 'added', bindI18nStore: 'added',
// Disable automatic conversion of basic markup to React components
transSupportBasicHtmlNodes: false,
}, },
interpolation: { interpolation: {

View file

@ -371,7 +371,7 @@
"delete": "Delete", "delete": "Delete",
"delete_account": "Delete Account", "delete_account": "Delete Account",
"delete_account_confirmation_label": "I understand this will delete all projects in my __appName__ account with email address <0>__userDefaultEmail__</0>", "delete_account_confirmation_label": "I understand this will delete all projects in my __appName__ account with email address <0>__userDefaultEmail__</0>",
"delete_account_warning_message_3": "You are about to permanently <0>delete all of your account data</0>, including your projects and settings. Please type your account email address and password in the boxes below to proceed.", "delete_account_warning_message_3": "You are about to permanently <strong>delete all of your account data</strong>, including your projects and settings. Please type your account email address and password in the boxes below to proceed.",
"delete_acct_no_existing_pw": "Please use the password reset form to set a password before deleting your account", "delete_acct_no_existing_pw": "Please use the password reset form to set a password before deleting your account",
"delete_and_leave": "Delete / Leave", "delete_and_leave": "Delete / Leave",
"delete_and_leave_projects": "Delete and Leave Projects", "delete_and_leave_projects": "Delete and Leave Projects",