Просмотр исходного кода

Enable ESLint rule that enforces `t` when preferable to `Trans` (#15253)

GitOrigin-RevId: d11b3f587b462d400a8d68128dc8be342415bf7d
Alf Eaton 2 лет назад
Родитель
Сommit
7755203ff7
29 измененных файлов с 170 добавлено и 172 удалено
  1. 1 1
      services/web/.eslintrc
  2. 5 5
      services/web/frontend/extracted-translations.json
  3. 1 1
      services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.jsx
  4. 7 5
      services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input.jsx
  5. 7 11
      services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx
  6. 11 7
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.jsx
  7. 4 4
      services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx
  8. 1 3
      services/web/frontend/js/features/project-list/components/notifications/ads/inr-banner.tsx
  9. 31 7
      services/web/frontend/js/features/project-list/components/notifications/groups-and-enterprise-banner.tsx
  10. 3 4
      services/web/frontend/js/features/settings/components/beta-program-section.tsx
  11. 3 4
      services/web/frontend/js/features/settings/components/labs-program-section.tsx
  12. 6 6
      services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade-content-default.jsx
  13. 9 7
      services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade-content-variant.jsx
  14. 4 5
      services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.jsx
  15. 2 2
      services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx
  16. 7 5
      services/web/frontend/js/features/share-project-modal/components/edit-member.jsx
  17. 6 4
      services/web/frontend/js/features/share-project-modal/components/invite.jsx
  18. 21 27
      services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx
  19. 5 3
      services/web/frontend/js/features/share-project-modal/components/member-privileges.jsx
  20. 3 2
      services/web/frontend/js/features/share-project-modal/components/owner-info.jsx
  21. 3 2
      services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx
  22. 4 3
      services/web/frontend/js/features/share-project-modal/components/send-invites-notice.jsx
  23. 13 11
      services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.jsx
  24. 8 10
      services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.jsx
  25. 2 6
      services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-help.tsx
  26. 1 1
      services/web/locales/da.json
  27. 1 1
      services/web/locales/de.json
  28. 1 1
      services/web/locales/en.json
  29. 0 24
      services/web/test/frontend/infrastructure/i18n.spec.tsx

+ 1 - 1
services/web/.eslintrc

@@ -114,7 +114,7 @@
       //
       "files": ["**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}", "**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}"],
       "rules": {
-        "@overleaf/no-empty-trans": "off",
+        "@overleaf/no-empty-trans": "error",
         "@overleaf/should-unescape-trans": "error",
 
         // https://astexplorer.net/

+ 5 - 5
services/web/frontend/extracted-translations.json

@@ -92,7 +92,7 @@
   "back_to_subscription": "",
   "back_to_your_projects": "",
   "beta_program_already_participating": "",
-  "beta_program_benefits": "<0></0>",
+  "beta_program_benefits": "",
   "beta_program_not_participating": "",
   "binary_history_error": "",
   "blank_project": "",
@@ -288,7 +288,7 @@
   "dropbox_sync_now_rate_limited": "",
   "dropbox_sync_now_running": "",
   "dropbox_sync_out": "",
-  "dropbox_sync_troubleshoot": "<0></0>",
+  "dropbox_sync_troubleshoot": "",
   "dropbox_synced": "",
   "dropbox_unlinked_premium_feature": "",
   "duplicate_file": "",
@@ -305,7 +305,7 @@
   "edit_tag": "",
   "editing": "",
   "editing_captions": "",
-  "editor_and_pdf": "&",
+  "editor_and_pdf": "",
   "editor_disconected_click_to_reconnect": "",
   "editor_only_hide_pdf": "",
   "editor_theme": "",
@@ -567,7 +567,7 @@
   "labels_help_you_to_easily_reference_your_figures": "",
   "labels_help_you_to_reference_your_tables": "",
   "labs_program_already_participating": "",
-  "labs_program_benefits": "<0></0>",
+  "labs_program_benefits": "",
   "labs_program_not_participating": "",
   "large_or_high-resolution_images_taking_too_long": "",
   "last_active": "",
@@ -1050,7 +1050,7 @@
   "something_went_wrong_loading_pdf_viewer": "",
   "something_went_wrong_processing_the_request": "",
   "something_went_wrong_rendering_pdf": "",
-  "something_went_wrong_rendering_pdf_expected": "<0></0>",
+  "something_went_wrong_rendering_pdf_expected": "",
   "something_went_wrong_server": "",
   "somthing_went_wrong_compiling": "",
   "sorry_your_table_cant_be_displayed_at_the_moment": "",

+ 1 - 1
services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.jsx

@@ -171,7 +171,7 @@ function LayoutDropdownButton() {
               />
             }
             icon={<IconSplit />}
-            text={<Trans i18nKey="editor_and_pdf">&</Trans>}
+            text={t('editor_and_pdf')}
           />
 
           <LayoutMenuItem

+ 7 - 5
services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input.jsx

@@ -2,7 +2,7 @@ import ControlLabel from 'react-bootstrap/lib/ControlLabel'
 import { Alert, FormControl } from 'react-bootstrap'
 import FormGroup from 'react-bootstrap/lib/FormGroup'
 import { useCallback } from 'react'
-import { Trans, useTranslation } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import { useFileTreeCreateName } from '../../contexts/file-tree-create-name'
 import PropTypes from 'prop-types'
 import {
@@ -57,7 +57,7 @@ export default function FileTreeCreateNameInput({
 
       {touchedName && !validName && (
         <Alert bsStyle="danger" className="row-spaced-small">
-          <Trans i18nKey="files_cannot_include_invalid_characters" />
+          {t('files_cannot_include_invalid_characters')}
         </Alert>
       )}
 
@@ -77,6 +77,8 @@ FileTreeCreateNameInput.propTypes = {
 }
 
 function ErrorMessage({ error }) {
+  const { t } = useTranslation()
+
   // if (typeof error === 'string') {
   //   return error
   // }
@@ -85,21 +87,21 @@ function ErrorMessage({ error }) {
     case DuplicateFilenameError:
       return (
         <Alert bsStyle="danger" className="row-spaced-small">
-          <Trans i18nKey="file_already_exists" />
+          {t('file_already_exists')}
         </Alert>
       )
 
     case InvalidFilenameError:
       return (
         <Alert bsStyle="danger" className="row-spaced-small">
-          <Trans i18nKey="files_cannot_include_invalid_characters" />
+          {t('files_cannot_include_invalid_characters')}
         </Alert>
       )
 
     case BlockedFilenameError:
       return (
         <Alert bsStyle="danger" className="row-spaced-small">
-          <Trans i18nKey="blocked_filename" />
+          {t('blocked_filename')}
         </Alert>
       )
 

+ 7 - 11
services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx

@@ -154,21 +154,15 @@ function CompileTimeoutMessages() {
           content={
             <div>
               <div>
-                <span>
-                  <Trans i18nKey="your_project_near_compile_timeout_limit" />
-                </span>
+                <span>{t('your_project_near_compile_timeout_limit')}</span>
               </div>
               {showNewCompileTimeoutUI === 'active' ? (
                 <>
-                  <strong>
-                    <Trans i18nKey="upgrade_for_12x_more_compile_time" />
-                  </strong>
+                  <strong>{t('upgrade_for_12x_more_compile_time')}</strong>
                   {'. '}
                 </>
               ) : (
-                <strong>
-                  <Trans i18nKey="upgrade_for_plenty_more_compile_time" />
-                </strong>
+                <strong>{t('upgrade_for_plenty_more_compile_time')}</strong>
               )}
             </div>
           }
@@ -204,7 +198,7 @@ function CompileTimeoutMessages() {
                     shouldUnescape
                     tOptions={{ interpolation: { escapeValue: true } }}
                   />{' '}
-                  <Trans i18nKey="and_you_can_upgrade_for_plenty_more_compile_time" />
+                  {t('and_you_can_upgrade_for_plenty_more_compile_time')}
                 </p>
               </div>
             }
@@ -229,7 +223,9 @@ function CompileTimeoutMessages() {
                   />
                 </p>
                 <p className="row-spaced">
-                  <Trans i18nKey="tell_the_project_owner_to_upgrade_plan_for_more_compile_time" />
+                  {t(
+                    'tell_the_project_owner_to_upgrade_plan_for_more_compile_time'
+                  )}
                 </p>
               </div>
             }

+ 11 - 7
services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.jsx

@@ -19,13 +19,17 @@ function PdfPreviewError({ error }) {
           headerTitle={t('pdf_rendering_error')}
           formattedContent={
             <>
-              <Trans i18nKey="something_went_wrong_rendering_pdf_expected">
-                <Button
-                  bsSize="xs"
-                  bsStyle="info"
-                  onClick={() => startCompile()}
-                />
-              </Trans>
+              <Trans
+                i18nKey="something_went_wrong_rendering_pdf_expected"
+                components={[
+                  // eslint-disable-next-line react/jsx-key
+                  <Button
+                    bsSize="xs"
+                    bsStyle="info"
+                    onClick={() => startCompile()}
+                  />,
+                ]}
+              />
               <br />
               <br />
               <Trans

+ 4 - 4
services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx

@@ -78,10 +78,10 @@ const CompileTimeout = memo(function CompileTimeout({
                 </>
               ) : (
                 <>
-                  <strong>
-                    <Trans i18nKey="upgrade_for_12x_more_compile_time" />
-                  </strong>{' '}
-                  <Trans i18nKey="plus_additional_collaborators_document_history_track_changes_and_more" />
+                  <strong>{t('upgrade_for_12x_more_compile_time')}</strong>{' '}
+                  {t(
+                    'plus_additional_collaborators_document_history_track_changes_and_more'
+                  )}
                 </>
               )}
             </p>

+ 1 - 3
services/web/frontend/js/features/project-list/components/notifications/ads/inr-banner.tsx

@@ -154,9 +154,7 @@ export default function INRBanner({ variant, splitTestName }: INRBannerProps) {
               }}
             />
           </p>
-          <p>
-            <Trans i18nKey="inr_discount_modal_info" />
-          </p>
+          <p>{t('inr_discount_modal_info')}</p>
         </Modal.Body>
         <Modal.Footer>
           <Button bsStyle="default" onClick={handleMaybeLater}>

+ 31 - 7
services/web/frontend/js/features/project-list/components/notifications/groups-and-enterprise-banner.tsx

@@ -4,7 +4,7 @@ import * as eventTracking from '../../../../infrastructure/event-tracking'
 import getMeta from '../../../../utils/meta'
 import customLocalStorage from '../../../../infrastructure/local-storage'
 import { useProjectListContext } from '../../context/project-list-context'
-import { Trans, useTranslation } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 
 const variants = ['did-you-know', 'on-premise', 'people', 'FOMO'] as const
 type GroupsAndEnterpriseBannerVariant = typeof variants[number]
@@ -64,7 +64,7 @@ export default function GroupsAndEnterpriseBanner() {
   return (
     <Notification bsStyle="info" onDismiss={handleClose}>
       <Notification.Body>
-        <span>{getText(groupsAndEnterpriseBannerVariant)}</span>
+        <BannerContent variant={groupsAndEnterpriseBannerVariant} />
       </Notification.Body>
       <Notification.Action>
         <a
@@ -85,16 +85,40 @@ function isVariantValid(variant: GroupsAndEnterpriseBannerVariant) {
   return variants.includes(variant)
 }
 
-function getText(variant: GroupsAndEnterpriseBannerVariant) {
+function BannerContent({
+  variant,
+}: {
+  variant: GroupsAndEnterpriseBannerVariant
+}) {
+  const { t } = useTranslation()
+
   switch (variant) {
     case 'did-you-know':
-      return <Trans i18nKey="did_you_know_that_overleaf_offers" />
+      return <span>{t('did_you_know_that_overleaf_offers')}</span>
     case 'on-premise':
-      return 'Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.'
+      return (
+        <span>
+          Overleaf On-Premises: Does your company want to keep its data within
+          its firewall? Overleaf offers Server Pro, an on-premises solution for
+          companies. Get in touch to learn more.
+        </span>
+      )
     case 'people':
-      return 'Other people at your company may already be using Overleaf. Save money with Overleaf group and company-wide subscriptions. Request more information.'
+      return (
+        <span>
+          Other people at your company may already be using Overleaf. Save money
+          with Overleaf group and company-wide subscriptions. Request more
+          information.
+        </span>
+      )
     case 'FOMO':
-      return 'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'
+      return (
+        <span>
+          Why do Fortune 500 companies and top research institutions trust
+          Overleaf to streamline their collaboration? Get in touch to learn
+          more.
+        </span>
+      )
   }
 }
 

+ 3 - 4
services/web/frontend/js/features/settings/components/beta-program-section.tsx

@@ -1,4 +1,4 @@
-import { useTranslation, Trans } from 'react-i18next'
+import { Trans, useTranslation } from 'react-i18next'
 import { useUserContext } from '../../../shared/context/user-context'
 
 function BetaProgramSection() {
@@ -10,9 +10,8 @@ function BetaProgramSection() {
       <h3>{t('sharelatex_beta_program')}</h3>
       {betaProgram ? null : (
         <p className="small">
-          <Trans i18nKey="beta_program_benefits">
-            <span />
-          </Trans>
+          {/* eslint-disable-next-line react/jsx-key */}
+          <Trans i18nKey="beta_program_benefits" components={[<span />]} />
         </p>
       )}
       <p className="small">

+ 3 - 4
services/web/frontend/js/features/settings/components/labs-program-section.tsx

@@ -1,4 +1,4 @@
-import { useTranslation, Trans } from 'react-i18next'
+import { Trans, useTranslation } from 'react-i18next'
 import { useUserContext } from '../../../shared/context/user-context'
 
 function LabsProgramSection() {
@@ -10,9 +10,8 @@ function LabsProgramSection() {
       <h3>{t('overleaf_labs')}</h3>
       {labsProgram ? null : (
         <p className="small">
-          <Trans i18nKey="labs_program_benefits">
-            <span />
-          </Trans>
+          {/* eslint-disable-next-line react/jsx-key */}
+          <Trans i18nKey="labs_program_benefits" components={[<span />]} />
         </p>
       )}
       <p className="small">

+ 6 - 6
services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade-content-default.jsx

@@ -7,13 +7,13 @@ export default function AddCollaboratorsUpgradeContentDefault() {
   return (
     <>
       <p className="text-center">
-        <Trans i18nKey="need_to_upgrade_for_more_collabs" />. {t('also')}:
+        {t('need_to_upgrade_for_more_collabs')}. {t('also')}:
       </p>
       <ul className="list-unstyled">
         <li>
           <Icon type="check" />
           &nbsp;
-          <Trans i18nKey="unlimited_projects" />
+          {t('unlimited_projects')}
         </li>
         <li>
           <Icon type="check" />
@@ -28,22 +28,22 @@ export default function AddCollaboratorsUpgradeContentDefault() {
         <li>
           <Icon type="check" />
           &nbsp;
-          <Trans i18nKey="full_doc_history" />
+          {t('full_doc_history')}
         </li>
         <li>
           <Icon type="check" />
           &nbsp;
-          <Trans i18nKey="sync_to_dropbox" />
+          {t('sync_to_dropbox')}
         </li>
         <li>
           <Icon type="check" />
           &nbsp;
-          <Trans i18nKey="sync_to_github" />
+          {t('sync_to_github')}
         </li>
         <li>
           <Icon type="check" />
           &nbsp;
-          <Trans i18nKey="compile_larger_projects" />
+          {t('compile_larger_projects')}
         </li>
       </ul>
     </>

+ 9 - 7
services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade-content-variant.jsx

@@ -1,13 +1,15 @@
 import Icon from '../../../shared/components/icon'
-import { Trans } from 'react-i18next'
+import { Trans, useTranslation } from 'react-i18next'
 
 export default function AddCollaboratorsUpgradeContentVariant() {
+  const { t } = useTranslation()
+
   return (
     <>
       <div className="row">
         <div className="col-xs-10 col-xs-offset-1">
           <p className="text-center">
-            <Trans i18nKey="need_to_upgrade_for_more_collabs_variant" />
+            {t('need_to_upgrade_for_more_collabs_variant')}
           </p>
         </div>
       </div>
@@ -16,7 +18,7 @@ export default function AddCollaboratorsUpgradeContentVariant() {
           <li>
             <Icon type="check" />
             &nbsp;
-            <Trans i18nKey="unlimited_projects" />
+            {t('unlimited_projects')}
           </li>
           <li>
             <Icon type="check" />
@@ -31,24 +33,24 @@ export default function AddCollaboratorsUpgradeContentVariant() {
           <li>
             <Icon type="check" />
             &nbsp;
-            <Trans i18nKey="full_doc_history" />
+            {t('full_doc_history')}
           </li>
         </ul>
         <ul className="list-unstyled col-xs-5">
           <li>
             <Icon type="check" />
             &nbsp;
-            <Trans i18nKey="sync_to_dropbox" />
+            {t('sync_to_dropbox')}
           </li>
           <li>
             <Icon type="check" />
             &nbsp;
-            <Trans i18nKey="sync_to_github" />
+            {t('sync_to_github')}
           </li>
           <li>
             <Icon type="check" />
             &nbsp;
-            <Trans i18nKey="compile_larger_projects" />
+            {t('compile_larger_projects')}
           </li>
         </ul>
       </div>

+ 4 - 5
services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.jsx

@@ -1,5 +1,5 @@
 import { useState } from 'react'
-import { Trans } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import { Button } from 'react-bootstrap'
 import PropTypes from 'prop-types'
 
@@ -12,6 +12,7 @@ import AddCollaboratorsUpgradeContentVariant from './add-collaborators-upgrade-c
 import { useSplitTestContext } from '../../../shared/context/split-test-context'
 
 export default function AddCollaboratorsUpgrade() {
+  const { t } = useTranslation()
   const user = useUserContext({
     allowedFreeTrial: PropTypes.bool,
   })
@@ -45,14 +46,12 @@ export default function AddCollaboratorsUpgrade() {
               setStartedFreeTrial(true)
             }}
           >
-            <Trans i18nKey="upgrade" />
+            {t('upgrade')}
           </Button>
         )}
       </p>
       {startedFreeTrial && (
-        <p className="small">
-          <Trans i18nKey="refresh_page_after_starting_free_trial" />
-        </p>
+        <p className="small">{t('refresh_page_after_starting_free_trial')}</p>
       )}
     </div>
   )

+ 2 - 2
services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx

@@ -1,5 +1,5 @@
 import { useState, useMemo } from 'react'
-import { useTranslation, Trans } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import { Form, FormGroup, FormControl, Button } from 'react-bootstrap'
 import { useMultipleSelection } from 'downshift'
 import { useShareProjectContext } from './share-project-modal'
@@ -152,7 +152,7 @@ export default function AddCollaborators() {
           </FormControl>
           <span>&nbsp;&nbsp;</span>
           <Button type="submit" bsStyle="primary">
-            <Trans i18nKey="share" />
+            {t('share')}
           </Button>
         </div>
       </FormGroup>

+ 7 - 5
services/web/frontend/js/features/share-project-modal/components/edit-member.jsx

@@ -1,6 +1,6 @@
 import { useState, useEffect } from 'react'
 import PropTypes from 'prop-types'
-import { Trans, useTranslation } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import { useShareProjectContext } from './share-project-modal'
 import TransferOwnershipModal from './transfer-ownership-modal'
 import { removeMemberFromProject, updateMember } from '../utils/api'
@@ -137,7 +137,7 @@ function RemoveMemberAction({ member }) {
     <FormControl.Static className="text-center">
       <Tooltip
         id="remove-collaborator"
-        description={<Trans i18nKey="remove_collaborator" />}
+        description={t('remove_collaborator')}
         overlayProps={{ placement: 'bottom' }}
       >
         <Button
@@ -163,16 +163,18 @@ RemoveMemberAction.propTypes = {
 }
 
 function ChangePrivilegesActions({ handleReset }) {
+  const { t } = useTranslation()
+
   return (
     <div className="text-center">
       <Button type="submit" bsSize="sm" bsStyle="primary">
-        <Trans i18nKey="change_or_cancel-change" />
+        {t('change_or_cancel-change')}
       </Button>
       <div className="text-sm">
-        <Trans i18nKey="change_or_cancel-or" />
+        {t('change_or_cancel-or')}
         &nbsp;
         <Button type="button" className="btn-inline-link" onClick={handleReset}>
-          <Trans i18nKey="change_or_cancel-cancel" />
+          {t('change_or_cancel-cancel')}
         </Button>
       </div>
     </div>

+ 6 - 4
services/web/frontend/js/features/share-project-modal/components/invite.jsx

@@ -4,20 +4,21 @@ import { useShareProjectContext } from './share-project-modal'
 import Icon from '../../../shared/components/icon'
 import { Button, Col, Row } from 'react-bootstrap'
 import Tooltip from '../../../shared/components/tooltip'
-import { Trans, useTranslation } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import MemberPrivileges from './member-privileges'
 import { resendInvite, revokeInvite } from '../utils/api'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { sendMB } from '../../../infrastructure/event-tracking'
 
 export default function Invite({ invite, isProjectOwner }) {
+  const { t } = useTranslation()
   return (
     <Row className="project-invite">
       <Col xs={7}>
         <div>{invite.email}</div>
 
         <div className="small">
-          <Trans i18nKey="invite_not_accepted" />
+          {t('invite_not_accepted')}
           .&nbsp;
           {isProjectOwner && <ResendInvite invite={invite} />}
         </div>
@@ -42,6 +43,7 @@ Invite.propTypes = {
 }
 
 function ResendInvite({ invite }) {
+  const { t } = useTranslation()
   const { monitorRequest } = useShareProjectContext()
   const { _id: projectId } = useProjectContext()
 
@@ -66,7 +68,7 @@ function ResendInvite({ invite }) {
       onClick={handleClick}
       // ref={buttonRef}
     >
-      <Trans i18nKey="resend" />
+      {t('resend')}
     </Button>
   )
 }
@@ -99,7 +101,7 @@ function RevokeInvite({ invite }) {
   return (
     <Tooltip
       id="revoke-invite"
-      description={<Trans i18nKey="revoke_invite" />}
+      description={t('revoke_invite')}
       overlayProps={{ placement: 'bottom' }}
     >
       <Button

+ 21 - 27
services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx

@@ -1,7 +1,7 @@
 import { useCallback, useState, useEffect } from 'react'
 import PropTypes from 'prop-types'
 import { Button, Col, Row } from 'react-bootstrap'
-import { Trans } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { useShareProjectContext } from './share-project-modal'
@@ -86,10 +86,11 @@ LinkSharing.propTypes = {
 }
 
 function PrivateSharing({ setAccessLevel, inflight, projectId }) {
+  const { t } = useTranslation()
   return (
     <Row className="public-access-level">
       <Col xs={12} className="text-center">
-        <Trans i18nKey="link_sharing_is_off" />
+        {t('link_sharing_is_off')}
         <span>&nbsp;&nbsp;</span>
         <Button
           type="button"
@@ -101,7 +102,7 @@ function PrivateSharing({ setAccessLevel, inflight, projectId }) {
           }}
           disabled={inflight}
         >
-          <Trans i18nKey="turn_on_link_sharing" />
+          {t('turn_on_link_sharing')}
         </Button>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
@@ -117,6 +118,7 @@ PrivateSharing.propTypes = {
 }
 
 function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
+  const { t } = useTranslation()
   const { _id: projectId } = useProjectContext()
 
   const [tokens, setTokens] = useState(null)
@@ -132,9 +134,7 @@ function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
   return (
     <Row className="public-access-level">
       <Col xs={12} className="text-center">
-        <strong>
-          <Trans i18nKey="link_sharing_is_on" />
-        </strong>
+        <strong>{t('link_sharing_is_on')}</strong>
         <span>&nbsp;&nbsp;</span>
         <Button
           bsStyle="link"
@@ -142,16 +142,14 @@ function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
           onClick={() => setAccessLevel('private')}
           disabled={inflight}
         >
-          <Trans i18nKey="turn_off_link_sharing" />
+          {t('turn_off_link_sharing')}
         </Button>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
       </Col>
       <Col xs={12} className="access-token-display-area">
         <div className="access-token-wrapper">
-          <strong>
-            <Trans i18nKey="anyone_with_link_can_edit" />
-          </strong>
+          <strong>{t('anyone_with_link_can_edit')}</strong>
           <AccessToken
             token={tokens?.readAndWrite}
             path="/"
@@ -159,9 +157,7 @@ function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
           />
         </div>
         <div className="access-token-wrapper">
-          <strong>
-            <Trans i18nKey="anyone_with_link_can_view" />
-          </strong>
+          <strong>{t('anyone_with_link_can_view')}</strong>
           <AccessToken
             token={tokens?.readOnly}
             path="/read/"
@@ -180,16 +176,14 @@ TokenBasedSharing.propTypes = {
 }
 
 function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
+  const { t } = useTranslation()
+
   return (
     <Row className="public-access-level">
       <Col xs={12} className="text-center">
         <strong>
-          {accessLevel === 'readAndWrite' && (
-            <Trans i18nKey="this_project_is_public" />
-          )}
-          {accessLevel === 'readOnly' && (
-            <Trans i18nKey="this_project_is_public_read_only" />
-          )}
+          {accessLevel === 'readAndWrite' && t('this_project_is_public')}
+          {accessLevel === 'readOnly' && t('this_project_is_public_read_only')}
         </strong>
         <span>&nbsp;&nbsp;</span>
         <Button
@@ -199,7 +193,7 @@ function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
           onClick={() => setAccessLevel('private')}
           disabled={inflight}
         >
-          <Trans i18nKey="make_private" />
+          {t('make_private')}
         </Button>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
@@ -215,6 +209,7 @@ LegacySharing.propTypes = {
 }
 
 export function ReadOnlyTokenLink() {
+  const { t } = useTranslation()
   const { _id: projectId } = useProjectContext()
 
   const [tokens, setTokens] = useState(null)
@@ -231,9 +226,7 @@ export function ReadOnlyTokenLink() {
     <Row className="public-access-level">
       <Col xs={12} className="access-token-display-area">
         <div className="access-token-wrapper">
-          <strong>
-            <Trans i18nKey="anyone_with_link_can_view" />
-          </strong>
+          <strong>{t('anyone_with_link_can_view')}</strong>
           <AccessToken
             token={tokens?.readOnly}
             path="/read/"
@@ -246,14 +239,13 @@ export function ReadOnlyTokenLink() {
 }
 
 function AccessToken({ token, path, tooltipId }) {
+  const { t } = useTranslation()
   const { isAdmin } = useUserContext()
 
   if (!token) {
     return (
       <pre className="access-token">
-        <span>
-          <Trans i18nKey="loading" />…
-        </span>
+        <span>{t('loading')}…</span>
       </pre>
     )
   }
@@ -279,10 +271,12 @@ AccessToken.propTypes = {
 }
 
 function LinkSharingInfo() {
+  const { t } = useTranslation()
+
   return (
     <Tooltip
       id="link-sharing-info"
-      description={<Trans i18nKey="learn_more_about_link_sharing" />}
+      description={t('learn_more_about_link_sharing')}
     >
       <a
         href="/learn/how-to/What_is_Link_Sharing%3F"

+ 5 - 3
services/web/frontend/js/features/share-project-modal/components/member-privileges.jsx

@@ -1,13 +1,15 @@
 import PropTypes from 'prop-types'
-import { Trans } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 
 export default function MemberPrivileges({ privileges }) {
+  const { t } = useTranslation()
+
   switch (privileges) {
     case 'readAndWrite':
-      return <Trans i18nKey="can_edit" />
+      return t('can_edit')
 
     case 'readOnly':
-      return <Trans i18nKey="read_only" />
+      return t('read_only')
 
     default:
       return null

+ 3 - 2
services/web/frontend/js/features/share-project-modal/components/owner-info.jsx

@@ -1,15 +1,16 @@
 import { useProjectContext } from '../../../shared/context/project-context'
 import { Col, Row } from 'react-bootstrap'
-import { Trans } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 
 export default function OwnerInfo() {
+  const { t } = useTranslation()
   const { owner } = useProjectContext()
 
   return (
     <Row className="project-member">
       <Col xs={7}>{owner?.email}</Col>
       <Col xs={3} className="text-left">
-        <Trans i18nKey="owner" />
+        {t('owner')}
       </Col>
     </Row>
   )

+ 3 - 2
services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx

@@ -1,6 +1,6 @@
 import { useEffect, useMemo, useState, useRef, useCallback } from 'react'
 import PropTypes from 'prop-types'
-import { Trans, useTranslation } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import { matchSorter } from 'match-sorter'
 import { useCombobox } from 'downshift'
 import classnames from 'classnames'
@@ -19,6 +19,7 @@ export default function SelectCollaborators({
   placeholder,
   multipleSelectionProps,
 }) {
+  const { t } = useTranslation()
   const {
     getSelectedItemProps,
     getDropdownProps,
@@ -141,7 +142,7 @@ export default function SelectCollaborators({
     <div className="tags-input">
       {/* eslint-disable-next-line jsx-a11y/label-has-for */}
       <label className="small" {...getLabelProps()}>
-        <Trans i18nKey="share_with_your_collabs" />
+        {t('share_with_your_collabs')}
         &nbsp;
         {loading && <Icon type="refresh" spin />}
       </label>

+ 4 - 3
services/web/frontend/js/features/share-project-modal/components/send-invites-notice.jsx

@@ -1,6 +1,6 @@
 import { Col, Row } from 'react-bootstrap'
 import PropTypes from 'prop-types'
-import { Trans } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import { useProjectContext } from '../../../shared/context/project-context'
 
 export default function SendInvitesNotice() {
@@ -16,12 +16,13 @@ export default function SendInvitesNotice() {
 }
 
 function AccessLevel({ level }) {
+  const { t } = useTranslation()
   switch (level) {
     case 'private':
-      return <Trans i18nKey="to_add_more_collaborators" />
+      return t('to_add_more_collaborators')
 
     case 'tokenBased':
-      return <Trans i18nKey="to_change_access_permissions" />
+      return t('to_change_access_permissions')
 
     default:
       return null

+ 13 - 11
services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.jsx

@@ -1,5 +1,5 @@
 import { Button, Modal, Grid } from 'react-bootstrap'
-import { Trans } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import Icon from '../../../shared/components/icon'
 import AccessibleModal from '../../../shared/components/accessible-modal'
 import PropTypes from 'prop-types'
@@ -23,6 +23,8 @@ export default function ShareProjectModalContent({
   inFlight,
   error,
 }) {
+  const { t } = useTranslation()
+
   const { isRestrictedTokenMember } = useEditorContext({
     isRestrictedTokenMember: PropTypes.bool,
   })
@@ -30,9 +32,7 @@ export default function ShareProjectModalContent({
   return (
     <AccessibleModal show={show} onHide={cancel} animation={animation}>
       <Modal.Header closeButton>
-        <Modal.Title>
-          <Trans i18nKey="share_project" />
-        </Modal.Title>
+        <Modal.Title>{t('share_project')}</Modal.Title>
       </Modal.Header>
 
       <Modal.Body className="modal-body-share">
@@ -65,7 +65,7 @@ export default function ShareProjectModalContent({
             className="btn-secondary"
             disabled={inFlight}
           >
-            <Trans i18nKey="close" />
+            {t('close')}
           </Button>
         </div>
       </Modal.Footer>
@@ -81,24 +81,26 @@ ShareProjectModalContent.propTypes = {
 }
 
 function ErrorMessage({ error }) {
+  const { t } = useTranslation()
+
   switch (error) {
     case 'cannot_invite_non_user':
-      return <Trans i18nKey="cannot_invite_non_user" />
+      return t('cannot_invite_non_user')
 
     case 'cannot_verify_user_not_robot':
-      return <Trans i18nKey="cannot_verify_user_not_robot" />
+      return t('cannot_verify_user_not_robot')
 
     case 'cannot_invite_self':
-      return <Trans i18nKey="cannot_invite_self" />
+      return t('cannot_invite_self')
 
     case 'invalid_email':
-      return <Trans i18nKey="invalid_email" />
+      return t('invalid_email')
 
     case 'too_many_requests':
-      return <Trans i18nKey="too_many_requests" />
+      return t('too_many_requests')
 
     default:
-      return <Trans i18nKey="generic_something_went_wrong" />
+      return t('generic_something_went_wrong')
   }
 }
 ErrorMessage.propTypes = {

+ 8 - 10
services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.jsx

@@ -1,6 +1,6 @@
 import { useState } from 'react'
 import { Modal, Button } from 'react-bootstrap'
-import { Trans } from 'react-i18next'
+import { Trans, useTranslation } from 'react-i18next'
 import PropTypes from 'prop-types'
 import Icon from '../../../shared/components/icon'
 import { transferProjectOwnership } from '../utils/api'
@@ -9,6 +9,8 @@ import { useProjectContext } from '../../../shared/context/project-context'
 import { useLocation } from '../../../shared/hooks/use-location'
 
 export default function TransferOwnershipModal({ member, cancel }) {
+  const { t } = useTranslation()
+
   const [inflight, setInflight] = useState(false)
   const [error, setError] = useState(false)
   const location = useLocation()
@@ -32,9 +34,7 @@ export default function TransferOwnershipModal({ member, cancel }) {
   return (
     <AccessibleModal show onHide={cancel}>
       <Modal.Header closeButton>
-        <Modal.Title>
-          <Trans i18nKey="change_project_owner" />
-        </Modal.Title>
+        <Modal.Title>{t('change_project_owner')}</Modal.Title>
       </Modal.Header>
       <Modal.Body>
         <p>
@@ -47,16 +47,14 @@ export default function TransferOwnershipModal({ member, cancel }) {
             tOptions={{ interpolation: { escapeValue: true } }}
           />
         </p>
-        <p>
-          <Trans i18nKey="project_ownership_transfer_confirmation_2" />
-        </p>
+        <p>{t('project_ownership_transfer_confirmation_2')}</p>
       </Modal.Body>
       <Modal.Footer>
         <div className="modal-footer-left">
           {inflight && <Icon type="refresh" spin />}
           {error && (
             <span className="text-danger">
-              <Trans i18nKey="generic_something_went_wrong" />
+              {t('generic_something_went_wrong')}
             </span>
           )}
         </div>
@@ -68,7 +66,7 @@ export default function TransferOwnershipModal({ member, cancel }) {
             onClick={cancel}
             disabled={inflight}
           >
-            <Trans i18nKey="cancel" />
+            {t('cancel')}
           </Button>
           <Button
             type="button"
@@ -76,7 +74,7 @@ export default function TransferOwnershipModal({ member, cancel }) {
             onClick={confirm}
             disabled={inflight}
           >
-            <Trans i18nKey="change_owner" />
+            {t('change_owner')}
           </Button>
         </div>
       </Modal.Footer>

+ 2 - 6
services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-help.tsx

@@ -9,13 +9,9 @@ export const FigureModalHelp = () => {
   const { t } = useTranslation()
   return (
     <>
-      <p>
-        <Trans i18nKey="this_tool_helps_you_insert_figures" />
-      </p>
+      <p>{t('this_tool_helps_you_insert_figures')}</p>
       <b>{t('editing_captions')}</b>
-      <p>
-        <Trans i18nKey="when_you_tick_the_include_caption_box" />
-      </p>
+      <p>{t('when_you_tick_the_include_caption_box')}</p>
 
       <b>{t('understanding_labels')}</b>
       <p>

+ 1 - 1
services/web/locales/da.json

@@ -410,7 +410,7 @@
   "edit_tag": "Redigér tag",
   "editing": "Redigering",
   "editing_captions": "Redigering af billedtekster",
-  "editor_and_pdf": "Skrivevindue <0></0> PDF",
+  "editor_and_pdf": "Skrivevindue & PDF",
   "editor_disconected_click_to_reconnect": "Skriveprogrammets forbindelse afbrudt, klik hvor som helst for at forbinde igen.",
   "editor_only_hide_pdf": "Kun skrivevindue <0>(gem PDF)</0>",
   "editor_resources": "Læringsmidler til skriveprogrammet",

+ 1 - 1
services/web/locales/de.json

@@ -412,7 +412,7 @@
   "edit_tag": "Schlagwort bearbeiten",
   "editing": "Bearbeitung",
   "editing_captions": "Beschriftungen bearbeiten",
-  "editor_and_pdf": "Editor <0></0> PDF",
+  "editor_and_pdf": "Editor & PDF",
   "editor_disconected_click_to_reconnect": "Editor wurde getrennt",
   "editor_only_hide_pdf": "Nur Editor <0>(PDF ausblenden)</0>",
   "editor_resources": "Editor-Literatur",

+ 1 - 1
services/web/locales/en.json

@@ -460,7 +460,7 @@
   "edit_tag": "Edit Tag",
   "editing": "Editing",
   "editing_captions": "Editing captions",
-  "editor_and_pdf": "Editor <0></0> PDF",
+  "editor_and_pdf": "Editor & PDF",
   "editor_disconected_click_to_reconnect": "Editor disconnected, click anywhere to reconnect.",
   "editor_only_hide_pdf": "Editor only <0>(hide PDF)</0>",
   "editor_resources": "Editor Resources",

+ 0 - 24
services/web/test/frontend/infrastructure/i18n.spec.tsx

@@ -31,30 +31,6 @@ describe('i18n', function () {
   })
 
   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 (