From 76242330fd58277bf0eca1ab859a872c18531249 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Mon, 26 Jun 2023 22:32:19 +0200 Subject: [PATCH] fix(frontend): improve performance by caching translated texts Signed-off-by: Tilman Vatteroth --- .../loading-screen/loading-animation.tsx | 3 +- .../loading-screen/random-icon.tsx | 26 ++++++------ .../copy-to-clipboard-button.tsx | 7 ++-- .../common/fields/current-password-field.tsx | 7 ++-- .../common/fields/display-name-field.tsx | 12 +++--- .../common/fields/new-password-field.tsx | 11 +++-- .../common/fields/password-again-field.tsx | 24 +++++------ .../common/fields/username-field.tsx | 6 +-- .../common/fields/username-label-field.tsx | 2 +- .../common/icons/lazy-bootstrap-icon.tsx | 2 +- .../common/links/translated-external-link.tsx | 7 ++-- .../common/links/translated-internal-link.tsx | 8 ++-- .../common/motd-modal/motd-modal.tsx | 2 +- .../create-non-existing-note-hint.tsx | 8 ++-- .../renderer-iframe/renderer-iframe.tsx | 2 +- .../common/user-avatar/user-avatar.tsx | 12 ++++-- .../document-infobar.tsx | 7 ++-- .../app-bar/cheatsheet/cheatsheet-button.tsx | 7 ++-- .../cheatsheet-in-new-tab-button.tsx | 8 ++-- .../app-bar/help-button/help-button.tsx | 7 ++-- .../app-bar/help-button/links-tab-content.tsx | 5 +-- .../app-bar/read-only-mode-button.tsx | 6 +-- .../editor-page/app-bar/slide-mode-button.tsx | 6 +-- .../document-sync/y-text-sync-view-plugin.ts | 6 +-- .../remote-cursors/remote-cursor-marker.ts | 2 +- .../editor-page/editor-pane/editor-pane.tsx | 8 ++-- .../hooks/yjs/frontend-websocket-adapter.ts | 2 +- .../yjs/use-code-mirror-yjs-extension.ts | 3 +- .../emoji-picker/emoji-picker-popover.tsx | 3 +- .../table-picker/custom-table-size-modal.tsx | 14 ++++--- .../table-size-picker-popover.tsx | 2 +- .../hooks/use-on-scroll-with-line-offset.ts | 3 +- .../aliases-modal/aliases-add-form.spec.tsx | 2 +- .../aliases-modal/aliases-add-form.tsx | 9 ++-- .../aliases-modal/aliases-list-entry.spec.tsx | 2 +- .../aliases-modal/aliases-list-entry.tsx | 15 ++++--- .../aliases-modal/aliases-list.spec.tsx | 2 +- .../aliases-modal/aliases-modal.spec.tsx | 2 +- .../export-menu-sidebar-menu.tsx | 10 +++-- .../import-menu-sidebar-menu.tsx | 10 +++-- .../permission-add-entry-field.tsx | 15 +++---- .../permission-entry-buttons.tsx | 24 +++++------ .../permission-entry-special-group.tsx | 16 ++++--- .../permission-owner-change.tsx | 14 +++---- .../permission-owner-info.tsx | 10 ++--- .../revisions-modal/revision-list-entry.tsx | 10 +++-- .../users-online-sidebar-menu.tsx | 3 +- .../splitter/split-divider/split-divider.tsx | 8 ++-- .../editor-page/splitter/splitter.tsx | 2 +- .../history-page/entry-menu/entry-menu.tsx | 4 +- .../history-toolbar/clear-history-button.tsx | 12 +++--- .../history-toolbar/export-history-button.tsx | 6 +-- .../history-refresh-button.tsx | 7 ++-- .../history-toolbar/history-toolbar.tsx | 10 ++--- .../history-view-mode-toggle-button.tsx | 20 +++++---- .../history-toolbar/import-history-button.tsx | 7 ++-- .../history-toolbar/keyword-search-input.tsx | 9 ++-- .../history-toolbar/tag-selection-input.tsx | 6 +-- .../history-page/sort-button/sort-button.tsx | 4 +- .../landing-layout/footer/social-links.tsx | 4 +- .../navigation/sign-in-button.tsx | 11 ++--- .../layout/settings-dialog/settings-modal.tsx | 9 ++-- .../utils/settings-toggle-button.tsx | 9 ++-- .../login-page/auth/fields/password-field.tsx | 6 +-- .../utils/get-one-click-provider-metadata.ts | 18 ++++---- .../components/login-page/auth/via-local.tsx | 6 ++- ...access-token-creation-form-label-field.tsx | 12 +++--- .../profile-account-management.tsx | 3 +- ...uote-extra-tag-markdown-extension.spec.tsx | 2 +- .../highlighted-code-fence-app-extension.ts | 3 +- ...ghlighted-code-markdown-extension.spec.tsx | 2 +- .../asciinema/asciinema-frame.spec.tsx | 2 +- .../vega-lite/vega-lite-chart.tsx | 2 +- .../vimeo/vimeo-frame.spec.tsx | 2 +- .../youtube/youtube-frame.spec.tsx | 2 +- frontend/src/hooks/common/use-note-title.ts | 5 +-- .../src/hooks/common/use-translated-text.ts | 20 +++++++++ .../hooks/common/useTranslatedText.spec.ts | 42 +++++++++++++++++++ frontend/src/redux/realtime/methods.ts | 2 +- frontend/src/redux/realtime/reducers.ts | 2 +- .../src/utils/expected-origin-boundary.tsx | 2 +- 81 files changed, 341 insertions(+), 292 deletions(-) create mode 100644 frontend/src/hooks/common/use-translated-text.ts create mode 100644 frontend/src/hooks/common/useTranslatedText.spec.ts diff --git a/frontend/src/components/application-loader/loading-screen/loading-animation.tsx b/frontend/src/components/application-loader/loading-screen/loading-animation.tsx index 31da93202..c883ceb16 100644 --- a/frontend/src/components/application-loader/loading-screen/loading-animation.tsx +++ b/frontend/src/components/application-loader/loading-screen/loading-animation.tsx @@ -9,8 +9,7 @@ import { createNumberRangeArray } from '../../common/number-range/number-range' import styles from './animations.module.scss' import { IconRow } from './icon-row' import React, { useMemo } from 'react' -import { Pencil as IconPencil } from 'react-bootstrap-icons' -import { PencilFill as IconPencilFill } from 'react-bootstrap-icons' +import { Pencil as IconPencil, PencilFill as IconPencilFill } from 'react-bootstrap-icons' export interface HedgeDocLogoProps { error: boolean diff --git a/frontend/src/components/application-loader/loading-screen/random-icon.tsx b/frontend/src/components/application-loader/loading-screen/random-icon.tsx index bd1bd97d0..11a01d655 100644 --- a/frontend/src/components/application-loader/loading-screen/random-icon.tsx +++ b/frontend/src/components/application-loader/loading-screen/random-icon.tsx @@ -6,18 +6,20 @@ import styles from './animations.module.scss' import React, { Fragment, useEffect, useState } from 'react' import type { Icon } from 'react-bootstrap-icons' -import { FileText as IconFileText } from 'react-bootstrap-icons' -import { File as IconFile } from 'react-bootstrap-icons' -import { Fonts as IconFonts } from 'react-bootstrap-icons' -import { Gear as IconGear } from 'react-bootstrap-icons' -import { KeyboardFill as IconKeyboardFill } from 'react-bootstrap-icons' -import { ListCheck as IconListCheck } from 'react-bootstrap-icons' -import { Markdown as IconMarkdown } from 'react-bootstrap-icons' -import { Pencil as IconPencil } from 'react-bootstrap-icons' -import { Person as IconPerson } from 'react-bootstrap-icons' -import { Tag as IconTag } from 'react-bootstrap-icons' -import { TypeBold as IconTypeBold } from 'react-bootstrap-icons' -import { TypeItalic as IconTypeItalic } from 'react-bootstrap-icons' +import { + File as IconFile, + FileText as IconFileText, + Fonts as IconFonts, + Gear as IconGear, + KeyboardFill as IconKeyboardFill, + ListCheck as IconListCheck, + Markdown as IconMarkdown, + Pencil as IconPencil, + Person as IconPerson, + Tag as IconTag, + TypeBold as IconTypeBold, + TypeItalic as IconTypeItalic +} from 'react-bootstrap-icons' const elements: Icon[] = [ IconFileText, diff --git a/frontend/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx b/frontend/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx index c055f3182..42d3877cf 100644 --- a/frontend/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx +++ b/frontend/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../../hooks/common/use-translated-text' import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute' import { UiIcon } from '../../icons/ui-icon' @@ -11,7 +12,6 @@ import React, { Fragment, useRef } from 'react' import { Button } from 'react-bootstrap' import { Files as IconFiles } from 'react-bootstrap-icons' import type { Variant } from 'react-bootstrap/types' -import { useTranslation } from 'react-i18next' export interface CopyToClipboardButtonProps extends PropsWithDataCypressId { content: string @@ -33,10 +33,9 @@ export const CopyToClipboardButton: React.FC = ({ variant = 'dark', ...props }) => { - const { t } = useTranslation() const button = useRef(null) - const [copyToClipboard, overlayElement] = useCopyOverlay(button, content) + const buttonTitle = useTranslatedText('renderer.highlightCode.copyCode') return ( @@ -44,7 +43,7 @@ export const CopyToClipboardButton: React.FC = ({ ref={button} size={size} variant={variant} - title={t('renderer.highlightCode.copyCode') ?? undefined} + title={buttonTitle} onClick={copyToClipboard} {...cypressId(props)}> diff --git a/frontend/src/components/common/fields/current-password-field.tsx b/frontend/src/components/common/fields/current-password-field.tsx index ab1a3e4cb..ba2877f6b 100644 --- a/frontend/src/components/common/fields/current-password-field.tsx +++ b/frontend/src/components/common/fields/current-password-field.tsx @@ -3,10 +3,11 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import type { CommonFieldProps } from './fields' import React from 'react' import { Form } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' /** * Renders an input field for the current password when changing passwords. @@ -14,7 +15,7 @@ import { Trans, useTranslation } from 'react-i18next' * @param onChange Hook that is called when the entered password changes. */ export const CurrentPasswordField: React.FC = ({ onChange }) => { - const { t } = useTranslation() + const placeholderText = useTranslatedText('login.auth.password') return ( @@ -25,7 +26,7 @@ export const CurrentPasswordField: React.FC = ({ onChange }) = type='password' size='sm' onChange={onChange} - placeholder={t('login.auth.password') ?? undefined} + placeholder={placeholderText} autoComplete='current-password' required /> diff --git a/frontend/src/components/common/fields/display-name-field.tsx b/frontend/src/components/common/fields/display-name-field.tsx index 79b3cbebf..8a8e0059c 100644 --- a/frontend/src/components/common/fields/display-name-field.tsx +++ b/frontend/src/components/common/fields/display-name-field.tsx @@ -3,10 +3,11 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import type { CommonFieldProps } from './fields' import React, { useMemo } from 'react' import { Form } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' interface DisplayNameFieldProps extends CommonFieldProps { initialValue?: string @@ -20,11 +21,8 @@ interface DisplayNameFieldProps extends CommonFieldProps { * @param initialValue The initial input field value. */ export const DisplayNameField: React.FC = ({ onChange, value, initialValue }) => { - const { t } = useTranslation() - - const isValid = useMemo(() => { - return value.trim() !== '' && value !== initialValue - }, [value, initialValue]) + const isValid = useMemo(() => value.trim() !== '' && value !== initialValue, [value, initialValue]) + const placeholderText = useTranslatedText('profile.displayName') return ( @@ -37,7 +35,7 @@ export const DisplayNameField: React.FC = ({ onChange, va value={value} isValid={isValid} onChange={onChange} - placeholder={t('profile.displayName') ?? undefined} + placeholder={placeholderText} autoComplete='name' required /> diff --git a/frontend/src/components/common/fields/new-password-field.tsx b/frontend/src/components/common/fields/new-password-field.tsx index f5eb03d16..8bb930df4 100644 --- a/frontend/src/components/common/fields/new-password-field.tsx +++ b/frontend/src/components/common/fields/new-password-field.tsx @@ -3,10 +3,11 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import type { CommonFieldProps } from './fields' import React, { useMemo } from 'react' import { Form } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' /** * Renders an input field for the new password when registering. @@ -15,11 +16,9 @@ import { Trans, useTranslation } from 'react-i18next' * @param value The currently entered password. */ export const NewPasswordField: React.FC = ({ onChange, value, hasError = false }) => { - const { t } = useTranslation() + const isValid = useMemo(() => value.trim() !== '', [value]) - const isValid = useMemo(() => { - return value.trim() !== '' - }, [value]) + const placeholderText = useTranslatedText('login.auth.password') return ( @@ -32,7 +31,7 @@ export const NewPasswordField: React.FC = ({ onChange, value, isValid={isValid} isInvalid={hasError} onChange={onChange} - placeholder={t('login.auth.password') ?? undefined} + placeholder={placeholderText} autoComplete='new-password' required /> diff --git a/frontend/src/components/common/fields/password-again-field.tsx b/frontend/src/components/common/fields/password-again-field.tsx index 092a61e11..b15e2262c 100644 --- a/frontend/src/components/common/fields/password-again-field.tsx +++ b/frontend/src/components/common/fields/password-again-field.tsx @@ -3,10 +3,11 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import type { CommonFieldProps } from './fields' import React, { useMemo } from 'react' import { Form } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' interface PasswordAgainFieldProps extends CommonFieldProps { password: string @@ -15,9 +16,10 @@ interface PasswordAgainFieldProps extends CommonFieldProps { /** * Renders an input field for typing the new password again when registering. * - * @param onChange Hook that is called when the entered retype of the password changes. - * @param value The currently entered retype of the password. - * @param password The password entered into the password input field. + * @param onChange Hook that is called when the entered retype of the password changes + * @param value The currently entered retype of the password + * @param password The password entered into the password input field + * @param hasError Defines if the password should be shown as invalid */ export const PasswordAgainField: React.FC = ({ onChange, @@ -25,15 +27,9 @@ export const PasswordAgainField: React.FC = ({ password, hasError = false }) => { - const { t } = useTranslation() - - const isInvalid = useMemo(() => { - return value !== '' && password !== value && hasError - }, [password, value, hasError]) - - const isValid = useMemo(() => { - return password !== '' && password === value && !hasError - }, [password, value, hasError]) + const isInvalid = useMemo(() => value !== '' && password !== value && hasError, [password, value, hasError]) + const isValid = useMemo(() => password !== '' && password === value && !hasError, [password, value, hasError]) + const placeholderText = useTranslatedText('login.register.passwordAgain') return ( @@ -46,7 +42,7 @@ export const PasswordAgainField: React.FC = ({ isInvalid={isInvalid} isValid={isValid} onChange={onChange} - placeholder={t('login.register.passwordAgain') ?? undefined} + placeholder={placeholderText} autoComplete='new-password' required /> diff --git a/frontend/src/components/common/fields/username-field.tsx b/frontend/src/components/common/fields/username-field.tsx index 175d1433d..cf3a886ef 100644 --- a/frontend/src/components/common/fields/username-field.tsx +++ b/frontend/src/components/common/fields/username-field.tsx @@ -3,10 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import type { CommonFieldProps } from './fields' import React from 'react' import { Form } from 'react-bootstrap' -import { useTranslation } from 'react-i18next' export interface UsernameFieldProps extends CommonFieldProps { isInvalid?: boolean @@ -22,7 +22,7 @@ export interface UsernameFieldProps extends CommonFieldProps { * @param isInvalid Adds error style to label */ export const UsernameField: React.FC = ({ onChange, value, isValid, isInvalid }) => { - const { t } = useTranslation() + const placeholderText = useTranslatedText('login.auth.username') return ( = ({ onChange, value, i isValid={isValid} isInvalid={isInvalid} onChange={onChange} - placeholder={t('login.auth.username') ?? undefined} + placeholder={placeholderText} autoComplete='username' autoFocus={true} required diff --git a/frontend/src/components/common/fields/username-label-field.tsx b/frontend/src/components/common/fields/username-label-field.tsx index 675e89222..06856f485 100644 --- a/frontend/src/components/common/fields/username-label-field.tsx +++ b/frontend/src/components/common/fields/username-label-field.tsx @@ -3,8 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { UsernameField } from './username-field' import type { UsernameFieldProps } from './username-field' +import { UsernameField } from './username-field' import React from 'react' import { Form } from 'react-bootstrap' import { Trans } from 'react-i18next' diff --git a/frontend/src/components/common/icons/lazy-bootstrap-icon.tsx b/frontend/src/components/common/icons/lazy-bootstrap-icon.tsx index 4021e3896..8fd3a8237 100644 --- a/frontend/src/components/common/icons/lazy-bootstrap-icon.tsx +++ b/frontend/src/components/common/icons/lazy-bootstrap-icon.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { testId } from '../../../utils/test-id' -import { BootstrapLazyIcons } from './bootstrap-icons' import type { BootstrapIconName } from './bootstrap-icons' +import { BootstrapLazyIcons } from './bootstrap-icons' import React, { Suspense, useMemo } from 'react' export interface LazyBootstrapIconProps { diff --git a/frontend/src/components/common/links/translated-external-link.tsx b/frontend/src/components/common/links/translated-external-link.tsx index ef060f0a0..f7f8ae762 100644 --- a/frontend/src/components/common/links/translated-external-link.tsx +++ b/frontend/src/components/common/links/translated-external-link.tsx @@ -3,10 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { ExternalLink } from './external-link' import type { TranslatedLinkProps } from './types' -import React, { useMemo } from 'react' -import { useTranslation } from 'react-i18next' +import React from 'react' /** * An {@link ExternalLink external link} with translated text. @@ -16,7 +16,6 @@ import { useTranslation } from 'react-i18next' * @param props Additional props directly given to the {@link ExternalLink} */ export const TranslatedExternalLink: React.FC = ({ i18nKey, i18nOption, ...props }) => { - const { t } = useTranslation() - const text = useMemo(() => (i18nOption ? t(i18nKey, i18nOption) : t(i18nKey)), [i18nKey, i18nOption, t]) + const text = useTranslatedText(i18nKey, i18nOption) return } diff --git a/frontend/src/components/common/links/translated-internal-link.tsx b/frontend/src/components/common/links/translated-internal-link.tsx index 2bb21de4c..d31f215f0 100644 --- a/frontend/src/components/common/links/translated-internal-link.tsx +++ b/frontend/src/components/common/links/translated-internal-link.tsx @@ -3,10 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { InternalLink } from './internal-link' import type { TranslatedLinkProps } from './types' -import React, { useMemo } from 'react' -import { useTranslation } from 'react-i18next' +import React from 'react' /** * An {@link InternalLink internal link} with translated text. @@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next' * @param props Additional props directly given to the {@link InternalLink} */ export const TranslatedInternalLink: React.FC = ({ i18nKey, i18nOption, ...props }) => { - const { t } = useTranslation() - const text = useMemo(() => (i18nOption ? t(i18nKey, i18nOption) : t(i18nKey)), [i18nKey, i18nOption, t]) + const text = useTranslatedText(i18nKey, i18nOption) + return } diff --git a/frontend/src/components/common/motd-modal/motd-modal.tsx b/frontend/src/components/common/motd-modal/motd-modal.tsx index a61fd53f3..2446a04ec 100644 --- a/frontend/src/components/common/motd-modal/motd-modal.tsx +++ b/frontend/src/components/common/motd-modal/motd-modal.tsx @@ -11,7 +11,7 @@ import { RendererType } from '../../render-page/window-post-message-communicator import { CommonModal } from '../modals/common-modal' import { RendererIframe } from '../renderer-iframe/renderer-iframe' import { fetchMotd, MOTD_LOCAL_STORAGE_KEY } from './fetch-motd' -import React, { useCallback, useMemo, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { Button, Modal } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import { useAsync } from 'react-use' diff --git a/frontend/src/components/common/note-loading-boundary/create-non-existing-note-hint.tsx b/frontend/src/components/common/note-loading-boundary/create-non-existing-note-hint.tsx index 522373026..939a45b9a 100644 --- a/frontend/src/components/common/note-loading-boundary/create-non-existing-note-hint.tsx +++ b/frontend/src/components/common/note-loading-boundary/create-non-existing-note-hint.tsx @@ -10,9 +10,11 @@ import { UiIcon } from '../icons/ui-icon' import { ShowIf } from '../show-if/show-if' import React, { useCallback, useEffect } from 'react' import { Alert, Button } from 'react-bootstrap' -import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons' -import { CheckCircle as IconCheckCircle } from 'react-bootstrap-icons' -import { ExclamationTriangle as IconExclamationTriangle } from 'react-bootstrap-icons' +import { + ArrowRepeat as IconArrowRepeat, + CheckCircle as IconCheckCircle, + ExclamationTriangle as IconExclamationTriangle +} from 'react-bootstrap-icons' import { Trans, useTranslation } from 'react-i18next' import { useAsyncFn } from 'react-use' diff --git a/frontend/src/components/common/renderer-iframe/renderer-iframe.tsx b/frontend/src/components/common/renderer-iframe/renderer-iframe.tsx index 591d46979..4ca0364a1 100644 --- a/frontend/src/components/common/renderer-iframe/renderer-iframe.tsx +++ b/frontend/src/components/common/renderer-iframe/renderer-iframe.tsx @@ -15,9 +15,9 @@ import { useEditorReceiveHandler } from '../../render-page/window-post-message-c import type { ExtensionEvent, OnHeightChangeMessage, + RendererType, SetScrollStateMessage } from '../../render-page/window-post-message-communicator/rendering-message' -import type { RendererType } from '../../render-page/window-post-message-communicator/rendering-message' import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message' import { ShowIf } from '../show-if/show-if' import { WaitSpinner } from '../wait-spinner/wait-spinner' diff --git a/frontend/src/components/common/user-avatar/user-avatar.tsx b/frontend/src/components/common/user-avatar/user-avatar.tsx index 2dedc3f96..106e11a2e 100644 --- a/frontend/src/components/common/user-avatar/user-avatar.tsx +++ b/frontend/src/components/common/user-avatar/user-avatar.tsx @@ -3,13 +3,13 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { ShowIf } from '../show-if/show-if' import defaultAvatar from './default-avatar.png' import styles from './user-avatar.module.scss' import React, { useCallback, useMemo } from 'react' import { OverlayTrigger, Tooltip } from 'react-bootstrap' import type { OverlayInjectedProps } from 'react-bootstrap/Overlay' -import { useTranslation } from 'react-i18next' export interface UserAvatarProps { size?: 'sm' | 'lg' @@ -34,8 +34,6 @@ export const UserAvatar: React.FC = ({ additionalClasses = '', showName = true }) => { - const { t } = useTranslation() - const imageSize = useMemo(() => { switch (size) { case 'sm': @@ -51,7 +49,13 @@ export const UserAvatar: React.FC = ({ return photoUrl || defaultAvatar.src }, [photoUrl]) - const imgDescription = useMemo(() => t('common.avatarOf', { name: displayName }), [t, displayName]) + const imageTranslateOptions = useMemo( + () => ({ + name: displayName + }), + [displayName] + ) + const imgDescription = useTranslatedText('common.avatarOf', imageTranslateOptions) const tooltip = useCallback( (overlayInjectedProps: OverlayInjectedProps) => ( diff --git a/frontend/src/components/document-read-only-page/document-infobar.tsx b/frontend/src/components/document-read-only-page/document-infobar.tsx index 4c959c675..4cefe2036 100644 --- a/frontend/src/components/document-read-only-page/document-infobar.tsx +++ b/frontend/src/components/document-read-only-page/document-infobar.tsx @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from '../../hooks/common/use-application-state' +import { useTranslatedText } from '../../hooks/common/use-translated-text' import { InternalLink } from '../common/links/internal-link' import { ShowIf } from '../common/show-if/show-if' import { NoteInfoLineCreatedAt } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at' @@ -11,16 +12,16 @@ import { NoteInfoLineUpdatedBy } from '../editor-page/sidebar/specific-sidebar-e import styles from './document-infobar.module.scss' import React from 'react' import { Pencil as IconPencil } from 'react-bootstrap-icons' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' /** * Renders an info bar with metadata about the current note. */ export const DocumentInfobar: React.FC = () => { - const { t } = useTranslation() const noteDetails = useApplicationState((state) => state.noteDetails) // TODO Check permissions ("writability") of note and show edit link depending on that. + const linkTitle = useTranslatedText('views.readOnly.editNote') return (
 
@@ -38,7 +39,7 @@ export const DocumentInfobar: React.FC = () => { href={`/n/${noteDetails.primaryAddress}`} icon={IconPencil} className={'text-primary text-decoration-none mx-1'} - title={t('views.readOnly.editNote') ?? undefined} + title={linkTitle} /> diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-button.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-button.tsx index 642307461..adc737b6b 100644 --- a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-button.tsx +++ b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-button.tsx @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useBooleanState } from '../../../../hooks/common/use-boolean-state' +import { useTranslatedText } from '../../../../hooks/common/use-translated-text' import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant' import { cypressId } from '../../../../utils/cypress-attribute' import { CommonModal } from '../../../common/modals/common-modal' @@ -11,21 +12,21 @@ import { CheatsheetContent } from './cheatsheet-content' import { CheatsheetInNewTabButton } from './cheatsheet-in-new-tab-button' import React, { Fragment } from 'react' import { Button, Modal } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' /** * Shows a button that opens the cheatsheet dialog. */ export const CheatsheetButton: React.FC = () => { - const { t } = useTranslation() const [modalVisibility, showModal, closeModal] = useBooleanState() const buttonVariant = useOutlineButtonVariant() + const buttonTitle = useTranslatedText('cheatsheet.button') return ( diff --git a/frontend/src/components/editor-page/app-bar/slide-mode-button.tsx b/frontend/src/components/editor-page/app-bar/slide-mode-button.tsx index e7d97268c..2c31173e8 100644 --- a/frontend/src/components/editor-page/app-bar/slide-mode-button.tsx +++ b/frontend/src/components/editor-page/app-bar/slide-mode-button.tsx @@ -4,25 +4,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from '../../../hooks/common/use-application-state' +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant' import { UiIcon } from '../../common/icons/ui-icon' import Link from 'next/link' import React from 'react' import { Button } from 'react-bootstrap' import { Tv as IconTv } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' /** * Button that links to the slide-show presentation of the current note. */ export const SlideModeButton: React.FC = () => { - const { t } = useTranslation() const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress) const buttonVariant = useOutlineButtonVariant() + const buttonTitle = useTranslatedText('editor.documentBar.slideMode') return ( - diff --git a/frontend/src/components/editor-page/editor-pane/codemirror-extensions/document-sync/y-text-sync-view-plugin.ts b/frontend/src/components/editor-page/editor-pane/codemirror-extensions/document-sync/y-text-sync-view-plugin.ts index 0beb1b51c..0a40a081c 100644 --- a/frontend/src/components/editor-page/editor-pane/codemirror-extensions/document-sync/y-text-sync-view-plugin.ts +++ b/frontend/src/components/editor-page/editor-pane/codemirror-extensions/document-sync/y-text-sync-view-plugin.ts @@ -5,10 +5,8 @@ */ import type { ChangeSpec, Transaction } from '@codemirror/state' import { Annotation } from '@codemirror/state' -import type { EditorView, PluginValue } from '@codemirror/view' -import type { ViewUpdate } from '@codemirror/view' -import type { Text as YText } from 'yjs' -import type { Transaction as YTransaction, YTextEvent } from 'yjs' +import type { EditorView, PluginValue, ViewUpdate } from '@codemirror/view' +import type { Text as YText, Transaction as YTransaction, YTextEvent } from 'yjs' const syncAnnotation = Annotation.define() diff --git a/frontend/src/components/editor-page/editor-pane/codemirror-extensions/remote-cursors/remote-cursor-marker.ts b/frontend/src/components/editor-page/editor-pane/codemirror-extensions/remote-cursors/remote-cursor-marker.ts index 1c6f8ab72..f2eec22de 100644 --- a/frontend/src/components/editor-page/editor-pane/codemirror-extensions/remote-cursors/remote-cursor-marker.ts +++ b/frontend/src/components/editor-page/editor-pane/codemirror-extensions/remote-cursors/remote-cursor-marker.ts @@ -7,7 +7,7 @@ import { concatCssClasses } from '../../../../../utils/concat-css-classes' import { createCursorCssClass } from './create-cursor-css-class' import styles from './style.module.scss' import type { SelectionRange } from '@codemirror/state' -import type { LayerMarker, EditorView, Rect } from '@codemirror/view' +import type { EditorView, LayerMarker, Rect } from '@codemirror/view' import { Direction } from '@codemirror/view' /** diff --git a/frontend/src/components/editor-page/editor-pane/editor-pane.tsx b/frontend/src/components/editor-page/editor-pane/editor-pane.tsx index 0c35e7161..17c5d5254 100644 --- a/frontend/src/components/editor-page/editor-pane/editor-pane.tsx +++ b/frontend/src/components/editor-page/editor-pane/editor-pane.tsx @@ -6,6 +6,7 @@ import { useApplicationState } from '../../../hooks/common/use-application-state' import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url' import { useMayEdit } from '../../../hooks/common/use-may-edit' +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { useDarkModeState } from '../../../hooks/dark-mode/use-dark-mode-state' import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute' import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/_base-classes/code-block-markdown-extension/find-language-by-code-block-name' @@ -42,7 +43,6 @@ import { lintGutter } from '@codemirror/lint' import { oneDark } from '@codemirror/theme-one-dark' import ReactCodeMirror from '@uiw/react-codemirror' import React, { useEffect, useMemo } from 'react' -import { useTranslation } from 'react-i18next' export type EditorPaneProps = ScrollProps @@ -129,7 +129,6 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, o [ligaturesEnabled] ) - const { t } = useTranslation() const darkModeActivated = useDarkModeState() const editorOrigin = useBaseUrl(ORIGIN.EDITOR) const isSynced = useApplicationState((state) => state.realtimeStatus.isSynced) @@ -142,6 +141,9 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, o } }, [messageTransporter]) + const translateOptions = useMemo(() => ({ host: editorOrigin }), [editorOrigin]) + const placeholderText = useTranslatedText('editor.placeholder', translateOptions) + return (
= ({ scrollState, onScroll, o = ({ showModal, onDismiss, onSizeSelect }) => { - const { t } = useTranslation() const [tableSize, setTableSize] = useState(() => initialTableSize) useEffect(() => { @@ -63,6 +62,9 @@ export const CustomTableSizeModal: React.FC = ({ show })) }, []) + const columnPlaceholderText = useTranslatedText('editor.editorToolbar.table.cols') + const rowsPlaceholderText = useTranslatedText('editor.editorToolbar.table.rows') + return ( = ({ show @@ -83,7 +85,7 @@ export const CustomTableSizeModal: React.FC = ({ show diff --git a/frontend/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-picker-popover.tsx b/frontend/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-picker-popover.tsx index baee9c55a..48ed6b2ed 100644 --- a/frontend/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-picker-popover.tsx +++ b/frontend/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-picker-popover.tsx @@ -61,7 +61,7 @@ export const TableSizePickerPopover = React.forwardRef onTableSizeSelected(row + 1, col + 1)} /> ) diff --git a/frontend/src/components/editor-page/renderer-pane/hooks/use-on-scroll-with-line-offset.ts b/frontend/src/components/editor-page/renderer-pane/hooks/use-on-scroll-with-line-offset.ts index 201a29133..00a3ba8b7 100644 --- a/frontend/src/components/editor-page/renderer-pane/hooks/use-on-scroll-with-line-offset.ts +++ b/frontend/src/components/editor-page/renderer-pane/hooks/use-on-scroll-with-line-offset.ts @@ -4,8 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { getGlobalState } from '../../../../redux' -import type { ScrollState } from '../../synced-scroll/scroll-props' -import type { ScrollCallback } from '../../synced-scroll/scroll-props' +import type { ScrollCallback, ScrollState } from '../../synced-scroll/scroll-props' import { useMemo } from 'react' /** diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.spec.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.spec.tsx index 9f774f796..f2df404ef 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.spec.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.spec.tsx @@ -10,7 +10,7 @@ import { mockI18n } from '../../../../../../test-utils/mock-i18n' import { mockNoteOwnership } from '../../../../../../test-utils/note-ownership' import * as useUiNotificationsModule from '../../../../../notifications/ui-notification-boundary' import { AliasesAddForm } from './aliases-add-form' -import { render, act, screen } from '@testing-library/react' +import { act, render, screen } from '@testing-library/react' import testEvent from '@testing-library/user-event' import React from 'react' diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.tsx index c636c9705..0f3f52135 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.tsx @@ -7,6 +7,7 @@ import { addAlias } from '../../../../../../api/alias' import { useApplicationState } from '../../../../../../hooks/common/use-application-state' import { useIsOwner } from '../../../../../../hooks/common/use-is-owner' import { useOnInputChange } from '../../../../../../hooks/common/use-on-input-change' +import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text' import { updateMetadata } from '../../../../../../redux/note-details/methods' import { testId } from '../../../../../../utils/test-id' import { UiIcon } from '../../../../../common/icons/ui-icon' @@ -15,7 +16,6 @@ import type { FormEvent } from 'react' import React, { useCallback, useMemo, useState } from 'react' import { Button, Form, InputGroup } from 'react-bootstrap' import { Plus as IconPlus } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' const validAliasRegex = /^[a-z0-9_-]*$/ @@ -23,7 +23,6 @@ const validAliasRegex = /^[a-z0-9_-]*$/ * Form for adding a new alias to a note. */ export const AliasesAddForm: React.FC = () => { - const { t } = useTranslation() const { showErrorNotification } = useUiNotifications() const noteId = useApplicationState((state) => state.noteDetails.id) const isOwner = useIsOwner() @@ -48,12 +47,14 @@ export const AliasesAddForm: React.FC = () => { return validAliasRegex.test(newAlias) }, [newAlias]) + const addAliasText = useTranslatedText('editor.modal.aliases.addAlias') + return (
{ variant='light' className={'text-secondary ms-2'} disabled={!isOwner || !newAliasValid || newAlias === ''} - title={t('editor.modal.aliases.addAlias') ?? undefined} + title={addAliasText} {...testId('addAliasButton')}> diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.spec.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.spec.tsx index 4bd445cdb..7c3377057 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.spec.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.spec.tsx @@ -10,7 +10,7 @@ import { mockI18n } from '../../../../../../test-utils/mock-i18n' import { mockNoteOwnership } from '../../../../../../test-utils/note-ownership' import * as useUiNotificationsModule from '../../../../../notifications/ui-notification-boundary' import { AliasesListEntry } from './aliases-list-entry' -import { render, act, screen } from '@testing-library/react' +import { act, render, screen } from '@testing-library/react' import React from 'react' jest.mock('../../../../../../api/alias') diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.tsx index b84c8e08b..f093d8557 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.tsx @@ -6,6 +6,7 @@ import { deleteAlias, markAliasAsPrimary } from '../../../../../../api/alias' import type { Alias } from '../../../../../../api/alias/types' import { useIsOwner } from '../../../../../../hooks/common/use-is-owner' +import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text' import { updateMetadata } from '../../../../../../redux/note-details/methods' import { testId } from '../../../../../../utils/test-id' import { UiIcon } from '../../../../../common/icons/ui-icon' @@ -13,9 +14,7 @@ import { ShowIf } from '../../../../../common/show-if/show-if' import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary' import React, { useCallback } from 'react' import { Button } from 'react-bootstrap' -import { StarFill as IconStarFill } from 'react-bootstrap-icons' -import { Star as IconStar } from 'react-bootstrap-icons' -import { X as IconX } from 'react-bootstrap-icons' +import { Star as IconStar, StarFill as IconStarFill, X as IconX } from 'react-bootstrap-icons' import { useTranslation } from 'react-i18next' export interface AliasesListEntryProps { @@ -44,6 +43,10 @@ export const AliasesListEntry: React.FC = ({ alias }) => .catch(showErrorNotification(t('editor.modal.aliases.errorMakingPrimary'))) }, [alias, t, showErrorNotification]) + const isPrimaryText = useTranslatedText('editor.modal.aliases.isPrimary') + const makePrimaryText = useTranslatedText('editor.modal.aliases.makePrimary') + const removeAliasText = useTranslatedText('editor.modal.aliases.removeAlias') + return (
  • {alias.name} @@ -53,7 +56,7 @@ export const AliasesListEntry: React.FC = ({ alias }) => className={'me-2 text-warning'} variant='light' disabled={true} - title={t('editor.modal.aliases.isPrimary') ?? undefined} + title={isPrimaryText} {...testId('aliasIsPrimary')}> @@ -63,7 +66,7 @@ export const AliasesListEntry: React.FC = ({ alias }) => className={'me-2'} variant='light' disabled={!isOwner} - title={t('editor.modal.aliases.makePrimary') ?? undefined} + title={makePrimaryText} onClick={onMakePrimaryClick} {...testId('aliasButtonMakePrimary')}> @@ -73,7 +76,7 @@ export const AliasesListEntry: React.FC = ({ alias }) => variant='light' className={'text-danger'} disabled={!isOwner} - title={t('editor.modal.aliases.removeAlias') ?? undefined} + title={removeAliasText} onClick={onRemoveClick} {...testId('aliasButtonRemove')}> diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list.spec.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list.spec.tsx index 6d51c38e2..e3e902372 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list.spec.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list.spec.tsx @@ -7,8 +7,8 @@ import type { Alias } from '../../../../../../api/alias/types' import * as useApplicationStateModule from '../../../../../../hooks/common/use-application-state' import { mockI18n } from '../../../../../../test-utils/mock-i18n' import { AliasesList } from './aliases-list' -import * as AliasesListEntryModule from './aliases-list-entry' import type { AliasesListEntryProps } from './aliases-list-entry' +import * as AliasesListEntryModule from './aliases-list-entry' import { render } from '@testing-library/react' import React from 'react' diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-modal.spec.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-modal.spec.tsx index 40bf4d297..0f256b33b 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-modal.spec.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-modal.spec.tsx @@ -11,8 +11,8 @@ import * as AliasesAddFormModule from './aliases-add-form' import * as AliasesListModule from './aliases-list' import { AliasesModal } from './aliases-modal' import { render } from '@testing-library/react' -import React from 'react' import type { PropsWithChildren } from 'react' +import React from 'react' jest.mock('./aliases-list') jest.mock('./aliases-add-form') diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/export-menu-sidebar-menu.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/export-menu-sidebar-menu.tsx index 6ff4d1567..21e09f5be 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/export-menu-sidebar-menu.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/export-menu-sidebar-menu.tsx @@ -11,10 +11,12 @@ import type { SpecificSidebarMenuProps } from '../types' import { DocumentSidebarMenuSelection } from '../types' import { ExportMarkdownSidebarEntry } from './export-markdown-sidebar-entry' import React, { Fragment, useCallback } from 'react' -import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons' -import { CloudDownload as IconCloudDownload } from 'react-bootstrap-icons' -import { FileCode as IconFileCode } from 'react-bootstrap-icons' -import { Github as IconGithub } from 'react-bootstrap-icons' +import { + ArrowLeft as IconArrowLeft, + CloudDownload as IconCloudDownload, + FileCode as IconFileCode, + Github as IconGithub +} from 'react-bootstrap-icons' import { Trans, useTranslation } from 'react-i18next' /** diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/import-menu-sidebar-menu.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/import-menu-sidebar-menu.tsx index 2fc8c3551..feea1947e 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/import-menu-sidebar-menu.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/import-menu-sidebar-menu.tsx @@ -11,10 +11,12 @@ import type { SpecificSidebarMenuProps } from '../types' import { DocumentSidebarMenuSelection } from '../types' import { ImportMarkdownSidebarEntry } from './import-markdown-sidebar-entry' import React, { Fragment, useCallback } from 'react' -import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons' -import { Clipboard as IconClipboard } from 'react-bootstrap-icons' -import { CloudUpload as IconCloudUpload } from 'react-bootstrap-icons' -import { Github as IconGithub } from 'react-bootstrap-icons' +import { + ArrowLeft as IconArrowLeft, + Clipboard as IconClipboard, + CloudUpload as IconCloudUpload, + Github as IconGithub +} from 'react-bootstrap-icons' import { Trans, useTranslation } from 'react-i18next' /** diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-add-entry-field.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-add-entry-field.tsx index b24483108..2c8582a34 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-add-entry-field.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-add-entry-field.tsx @@ -4,12 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useLowercaseOnInputChange } from '../../../../../../hooks/common/use-lowercase-on-input-change' +import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text' import { UiIcon } from '../../../../../common/icons/ui-icon' import type { PermissionDisabledProps } from './permission-disabled.prop' import React, { useCallback, useState } from 'react' import { Button, FormControl, InputGroup } from 'react-bootstrap' import { Plus as IconPlus } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' export interface PermissionAddEntryFieldProps { onAddEntry: (identifier: string) => void @@ -28,8 +28,6 @@ export const PermissionAddEntryField: React.FC { - const { t } = useTranslation() - const [newEntryIdentifier, setNewEntryIdentifier] = useState('') const onChange = useLowercaseOnInputChange(setNewEntryIdentifier) @@ -37,19 +35,16 @@ export const PermissionAddEntryField: React.FC - + diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-list-entry.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-list-entry.tsx index 77ca6b888..468ebbb01 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-list-entry.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-list-entry.tsx @@ -14,10 +14,12 @@ import { getUserDataForRevision } from './utils' import { DateTime } from 'luxon' import React, { useMemo } from 'react' import { ListGroup } from 'react-bootstrap' -import { Clock as IconClock } from 'react-bootstrap-icons' -import { FileText as IconFileText } from 'react-bootstrap-icons' -import { Person as IconPerson } from 'react-bootstrap-icons' -import { PersonPlus as IconPersonPlus } from 'react-bootstrap-icons' +import { + Clock as IconClock, + FileText as IconFileText, + Person as IconPerson, + PersonPlus as IconPersonPlus +} from 'react-bootstrap-icons' import { Trans, useTranslation } from 'react-i18next' import { useAsync } from 'react-use' diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/users-online-sidebar-menu/users-online-sidebar-menu.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/users-online-sidebar-menu/users-online-sidebar-menu.tsx index b62196784..209e242db 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/users-online-sidebar-menu/users-online-sidebar-menu.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/users-online-sidebar-menu/users-online-sidebar-menu.tsx @@ -13,8 +13,7 @@ import styles from './online-counter.module.scss' import { OwnUserLine } from './own-user-line' import { UserLine } from './user-line/user-line' import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react' -import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons' -import { People as IconPeople } from 'react-bootstrap-icons' +import { ArrowLeft as IconArrowLeft, People as IconPeople } from 'react-bootstrap-icons' import { Trans, useTranslation } from 'react-i18next' /** diff --git a/frontend/src/components/editor-page/splitter/split-divider/split-divider.tsx b/frontend/src/components/editor-page/splitter/split-divider/split-divider.tsx index a89f888ce..4445178be 100644 --- a/frontend/src/components/editor-page/splitter/split-divider/split-divider.tsx +++ b/frontend/src/components/editor-page/splitter/split-divider/split-divider.tsx @@ -9,9 +9,11 @@ import { UiIcon } from '../../../common/icons/ui-icon' import styles from './split-divider.module.scss' import React, { useMemo } from 'react' import { Button } from 'react-bootstrap' -import { ArrowLeftRight as IconArrowLeftRight } from 'react-bootstrap-icons' -import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons' -import { ArrowRight as IconArrowRight } from 'react-bootstrap-icons' +import { + ArrowLeft as IconArrowLeft, + ArrowLeftRight as IconArrowLeftRight, + ArrowRight as IconArrowRight +} from 'react-bootstrap-icons' export enum DividerButtonsShift { SHIFT_TO_LEFT = 'shift-left', diff --git a/frontend/src/components/editor-page/splitter/splitter.tsx b/frontend/src/components/editor-page/splitter/splitter.tsx index 2e7fd6ad4..7469af408 100644 --- a/frontend/src/components/editor-page/splitter/splitter.tsx +++ b/frontend/src/components/editor-page/splitter/splitter.tsx @@ -7,7 +7,7 @@ import { ShowIf } from '../../common/show-if/show-if' import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts' import { DividerButtonsShift, SplitDivider } from './split-divider/split-divider' import styles from './splitter.module.scss' -import type { ReactElement, TouchEvent, MouseEvent } from 'react' +import type { MouseEvent, ReactElement, TouchEvent } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' export interface SplitterProps { diff --git a/frontend/src/components/history-page/entry-menu/entry-menu.tsx b/frontend/src/components/history-page/entry-menu/entry-menu.tsx index c3f2db7e6..8226355c8 100644 --- a/frontend/src/components/history-page/entry-menu/entry-menu.tsx +++ b/frontend/src/components/history-page/entry-menu/entry-menu.tsx @@ -13,9 +13,7 @@ import styles from './entry-menu.module.scss' import { RemoveNoteEntryItem } from './remove-note-entry-item' import React from 'react' import { Dropdown } from 'react-bootstrap' -import { Cloud as IconCloud } from 'react-bootstrap-icons' -import { Laptop as IconLaptop } from 'react-bootstrap-icons' -import { ThreeDots as IconThreeDots } from 'react-bootstrap-icons' +import { Cloud as IconCloud, Laptop as IconLaptop, ThreeDots as IconThreeDots } from 'react-bootstrap-icons' import { Trans, useTranslation } from 'react-i18next' export interface EntryMenuProps { diff --git a/frontend/src/components/history-page/history-toolbar/clear-history-button.tsx b/frontend/src/components/history-page/history-toolbar/clear-history-button.tsx index 22c85d3b4..e0876faf5 100644 --- a/frontend/src/components/history-page/history-toolbar/clear-history-button.tsx +++ b/frontend/src/components/history-page/history-toolbar/clear-history-button.tsx @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useBooleanState } from '../../../hooks/common/use-boolean-state' +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { deleteAllHistoryEntries } from '../../../redux/history/methods' import { cypressId } from '../../../utils/cypress-attribute' import { UiIcon } from '../../common/icons/ui-icon' @@ -13,14 +14,13 @@ import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-his import React, { Fragment, useCallback } from 'react' import { Button } from 'react-bootstrap' import { Trash as IconTrash } from 'react-bootstrap-icons' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' /** * Renders a button to clear the complete history of the user. * A confirmation modal will be presented to the user after clicking the button. */ export const ClearHistoryButton: React.FC = () => { - const { t } = useTranslation() const [modalVisibility, showModal, closeModal] = useBooleanState() const { showErrorNotification } = useUiNotifications() const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback() @@ -33,13 +33,11 @@ export const ClearHistoryButton: React.FC = () => { closeModal() }, [closeModal, safeRefreshHistoryState, showErrorNotification]) + const buttonTitle = useTranslatedText('landing.history.toolbar.clear') + return ( - { - const { t } = useTranslation() + const buttonTitle = useTranslatedText('landing.history.toolbar.export') return ( - ) diff --git a/frontend/src/components/history-page/history-toolbar/history-refresh-button.tsx b/frontend/src/components/history-page/history-toolbar/history-refresh-button.tsx index 1b6d53759..aa70f4bb6 100644 --- a/frontend/src/components/history-page/history-toolbar/history-refresh-button.tsx +++ b/frontend/src/components/history-page/history-toolbar/history-refresh-button.tsx @@ -3,23 +3,22 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { UiIcon } from '../../common/icons/ui-icon' import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state' import React from 'react' import { Button } from 'react-bootstrap' import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' /** * Fetches the current history from the server. */ export const HistoryRefreshButton: React.FC = () => { - const { t } = useTranslation() - const refreshHistory = useSafeRefreshHistoryStateCallback() + const buttonTitle = useTranslatedText('landing.history.toolbar.refresh') return ( - ) diff --git a/frontend/src/components/history-page/history-toolbar/history-toolbar.tsx b/frontend/src/components/history-page/history-toolbar/history-toolbar.tsx index 906e67b90..842b5471b 100644 --- a/frontend/src/components/history-page/history-toolbar/history-toolbar.tsx +++ b/frontend/src/components/history-page/history-toolbar/history-toolbar.tsx @@ -5,6 +5,7 @@ */ import { HistoryEntryOrigin } from '../../../api/history/types' import { useApplicationState } from '../../../hooks/common/use-application-state' +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { importHistoryEntries, setHistoryEntries } from '../../../redux/history/methods' import { UiIcon } from '../../common/icons/ui-icon' import { ShowIf } from '../../common/show-if/show-if' @@ -23,7 +24,6 @@ import { useSyncToolbarStateToUrlEffect } from './toolbar-context/use-sync-toolb import React, { useCallback } from 'react' import { Button, Col } from 'react-bootstrap' import { CloudUpload as IconCloudUpload } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' export enum ViewStateEnum { CARD, @@ -34,7 +34,6 @@ export enum ViewStateEnum { * Renders the toolbar for the history page that contains controls for filtering and sorting. */ export const HistoryToolbar: React.FC = () => { - const { t } = useTranslation() const historyEntries = useApplicationState((state) => state.history) const userExists = useApplicationState((state) => !!state.user) const { showErrorNotification } = useUiNotifications() @@ -61,6 +60,8 @@ export const HistoryToolbar: React.FC = () => { }) }, [userExists, historyEntries, showErrorNotification, safeRefreshHistoryState]) + const uploadAllButtonTitle = useTranslatedText('landing.history.toolbar.uploadAll') + return (
    @@ -89,10 +90,7 @@ export const HistoryToolbar: React.FC = () => {
    -
    diff --git a/frontend/src/components/history-page/history-toolbar/history-view-mode-toggle-button.tsx b/frontend/src/components/history-page/history-toolbar/history-view-mode-toggle-button.tsx index fc1e935b7..717dd6a63 100644 --- a/frontend/src/components/history-page/history-toolbar/history-view-mode-toggle-button.tsx +++ b/frontend/src/components/history-page/history-toolbar/history-view-mode-toggle-button.tsx @@ -3,21 +3,19 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { cypressId } from '../../../utils/cypress-attribute' import { UiIcon } from '../../common/icons/ui-icon' import { ViewStateEnum } from './history-toolbar' import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state' import React, { useCallback } from 'react' import { Button, ToggleButtonGroup } from 'react-bootstrap' -import { StickyFill as IconStickyFill } from 'react-bootstrap-icons' -import { Table as IconTable } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' +import { StickyFill as IconStickyFill, Table as IconTable } from 'react-bootstrap-icons' /** * Toggles the view mode of the history entries between list and card view. */ export const HistoryViewModeToggleButton: React.FC = () => { - const { t } = useTranslation() const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState() const onViewStateChange = useCallback( @@ -30,19 +28,25 @@ export const HistoryViewModeToggleButton: React.FC = () => { [setHistoryToolbarState] ) + const cardsButtonTitle = useTranslatedText('landing.history.toolbar.cards') + const tableButtonTitle = useTranslatedText('landing.history.toolbar.table') + + const onCardsButtonClick = useCallback(() => onViewStateChange(ViewStateEnum.CARD), [onViewStateChange]) + const onTableButtonClick = useCallback(() => onViewStateChange(ViewStateEnum.TABLE), [onViewStateChange]) + return ( diff --git a/frontend/src/components/history-page/history-toolbar/import-history-button.tsx b/frontend/src/components/history-page/history-toolbar/import-history-button.tsx index fb4a8abf3..2918db665 100644 --- a/frontend/src/components/history-page/history-toolbar/import-history-button.tsx +++ b/frontend/src/components/history-page/history-toolbar/import-history-button.tsx @@ -6,6 +6,7 @@ import type { HistoryEntryWithOrigin } from '../../../api/history/types' import { HistoryEntryOrigin } from '../../../api/history/types' import { useApplicationState } from '../../../hooks/common/use-application-state' +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import { convertV1History, importHistoryEntries, mergeHistoryEntries } from '../../../redux/history/methods' import type { HistoryExportJson, V1HistoryEntry } from '../../../redux/history/types' import { cypressId } from '../../../utils/cypress-attribute' @@ -15,13 +16,11 @@ import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-his import React, { useCallback, useRef, useState } from 'react' import { Button } from 'react-bootstrap' import { Upload as IconUpload } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' /** * Button that lets the user select a history JSON file and uploads imports that into the history. */ export const ImportHistoryButton: React.FC = () => { - const { t } = useTranslation() const userExists = useApplicationState((state) => !!state.user) const historyState = useApplicationState((state) => state.history) const uploadInput = useRef(null) @@ -117,6 +116,8 @@ export const ImportHistoryButton: React.FC = () => { } } + const buttonTitle = useTranslatedText('landing.history.toolbar.import') + return (
    { /> diff --git a/frontend/src/components/layout/settings-dialog/settings-modal.tsx b/frontend/src/components/layout/settings-dialog/settings-modal.tsx index b8e4023d8..1c4d5d0da 100644 --- a/frontend/src/components/layout/settings-dialog/settings-modal.tsx +++ b/frontend/src/components/layout/settings-dialog/settings-modal.tsx @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../hooks/common/use-translated-text' import type { CommonModalProps } from '../../common/modals/common-modal' import { CommonModal } from '../../common/modals/common-modal' import { EditorSettingsTabContent } from './editor/editor-settings-tab-content' @@ -10,7 +11,6 @@ import { GlobalSettingsTabContent } from './global/global-settings-tab-content' import React from 'react' import { Modal, Tab, Tabs } from 'react-bootstrap' import { Gear as IconGear } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' /** * Shows global and scope specific settings @@ -19,7 +19,8 @@ import { useTranslation } from 'react-i18next' * @param onHide callback that is executed if the modal should be closed */ export const SettingsModal: React.FC = ({ show, onHide }) => { - const { t } = useTranslation() + const globalLabelTitle = useTranslatedText('settings.global.label') + const editorLabelTitle = useTranslatedText('settings.editor.label') return ( = ({ show, onHide }) => { showCloseButton={true}> - + - + diff --git a/frontend/src/components/layout/settings-dialog/utils/settings-toggle-button.tsx b/frontend/src/components/layout/settings-dialog/utils/settings-toggle-button.tsx index 074a9bc31..26b669873 100644 --- a/frontend/src/components/layout/settings-dialog/utils/settings-toggle-button.tsx +++ b/frontend/src/components/layout/settings-dialog/utils/settings-toggle-button.tsx @@ -3,11 +3,12 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../../hooks/common/use-translated-text' import type { PropsWithDataTestId } from '../../../../utils/test-id' -import React, { useCallback, useMemo } from 'react' +import React, { useCallback } from 'react' import type { ButtonProps } from 'react-bootstrap' import { Button } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' type DarkModeToggleButtonProps = Omit & PropsWithDataTestId & { @@ -36,9 +37,7 @@ export const SettingsToggleButton = ({ value, ...props }: DarkModeToggleButtonProps) => { - const { t } = useTranslation() - - const title = useMemo(() => t(i18nKeyTooltip), [i18nKeyTooltip, t]) + const title = useTranslatedText(i18nKeyTooltip) const onChange = useCallback(() => { if (!selected) { diff --git a/frontend/src/components/login-page/auth/fields/password-field.tsx b/frontend/src/components/login-page/auth/fields/password-field.tsx index 1fb7e7490..a2063b32d 100644 --- a/frontend/src/components/login-page/auth/fields/password-field.tsx +++ b/frontend/src/components/login-page/auth/fields/password-field.tsx @@ -3,10 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../../hooks/common/use-translated-text' import type { AuthFieldProps } from './fields' import React from 'react' import { Form } from 'react-bootstrap' -import { useTranslation } from 'react-i18next' /** * Renders an input field for the password of a user. @@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next' * @param invalid True when the entered password is invalid, false otherwise. */ export const PasswordField: React.FC = ({ onChange, invalid }) => { - const { t } = useTranslation() + const placeholderText = useTranslatedText('login.auth.password') return ( @@ -23,7 +23,7 @@ export const PasswordField: React.FC = ({ onChange, invalid }) = isInvalid={invalid} type='password' size='sm' - placeholder={t('login.auth.password') ?? undefined} + placeholder={placeholderText} onChange={onChange} autoComplete='current-password' /> diff --git a/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts b/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts index af2412bc8..14bc8d3a3 100644 --- a/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts +++ b/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts @@ -9,14 +9,16 @@ import { Logger } from '../../../../utils/logger' import { IconGitlab } from '../../../common/icons/additional/icon-gitlab' import styles from '../via-one-click.module.scss' import type { Icon } from 'react-bootstrap-icons' -import { Dropbox as IconDropbox } from 'react-bootstrap-icons' -import { Exclamation as IconExclamation } from 'react-bootstrap-icons' -import { Facebook as IconFacebook } from 'react-bootstrap-icons' -import { Github as IconGithub } from 'react-bootstrap-icons' -import { Google as IconGoogle } from 'react-bootstrap-icons' -import { People as IconPeople } from 'react-bootstrap-icons' -import { PersonRolodex as IconPersonRolodex } from 'react-bootstrap-icons' -import { Twitter as IconTwitter } from 'react-bootstrap-icons' +import { + Dropbox as IconDropbox, + Exclamation as IconExclamation, + Facebook as IconFacebook, + Github as IconGithub, + Google as IconGoogle, + People as IconPeople, + PersonRolodex as IconPersonRolodex, + Twitter as IconTwitter +} from 'react-bootstrap-icons' export interface OneClickMetadata { name: string diff --git a/frontend/src/components/login-page/auth/via-local.tsx b/frontend/src/components/login-page/auth/via-local.tsx index e506aa129..33630afa5 100644 --- a/frontend/src/components/login-page/auth/via-local.tsx +++ b/frontend/src/components/login-page/auth/via-local.tsx @@ -14,7 +14,7 @@ import { PasswordField } from './fields/password-field' import { fetchAndSetUser } from './utils' import Link from 'next/link' import type { FormEvent } from 'react' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { Alert, Button, Card, Form } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' @@ -48,11 +48,13 @@ export const ViaLocal: React.FC = () => { const onUsernameChange = useLowercaseOnInputChange(setUsername) const onPasswordChange = useOnInputChange(setPassword) + const translationOptions = useMemo(() => ({ service: t('login.auth.username') }), [t]) + return ( - + diff --git a/frontend/src/components/profile-page/access-tokens/access-token-creation-form/access-token-creation-form-label-field.tsx b/frontend/src/components/profile-page/access-tokens/access-token-creation-form/access-token-creation-form-label-field.tsx index 4f618ca31..53e7c92b1 100644 --- a/frontend/src/components/profile-page/access-tokens/access-token-creation-form/access-token-creation-form-label-field.tsx +++ b/frontend/src/components/profile-page/access-tokens/access-token-creation-form/access-token-creation-form-label-field.tsx @@ -3,11 +3,12 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { useTranslatedText } from '../../../../hooks/common/use-translated-text' import { cypressId } from '../../../../utils/cypress-attribute' import type { ChangeEvent } from 'react' import React, { useMemo } from 'react' import { Form } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' interface AccessTokenCreationFormLabelFieldProps extends AccessTokenCreationFormFieldProps { onChangeLabel: (event: ChangeEvent) => void @@ -23,11 +24,8 @@ export const AccessTokenCreationFormLabelField: React.FC { - const { t } = useTranslation() - - const labelValid = useMemo(() => { - return formValues.label.trim() !== '' - }, [formValues]) + const labelValid = useMemo(() => formValues.label.trim() !== '', [formValues]) + const placeholderText = useTranslatedText('profile.accessTokens.label') return ( @@ -37,7 +35,7 @@ export const AccessTokenCreationFormLabelField: React.FC { diff --git a/frontend/src/extensions/essential-app-extensions/highlighted-code-fence/highlighted-code-fence-app-extension.ts b/frontend/src/extensions/essential-app-extensions/highlighted-code-fence/highlighted-code-fence-app-extension.ts index 1e94410ce..def598af4 100644 --- a/frontend/src/extensions/essential-app-extensions/highlighted-code-fence/highlighted-code-fence-app-extension.ts +++ b/frontend/src/extensions/essential-app-extensions/highlighted-code-fence/highlighted-code-fence-app-extension.ts @@ -8,8 +8,7 @@ import { codeFenceRegex } from '../../../components/editor-page/editor-pane/auto import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/_base-classes/markdown-renderer-extension' import { AppExtension } from '../../_base-classes/app-extension' import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension' -import type { CompletionSource } from '@codemirror/autocomplete' -import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete' +import type { CompletionContext, CompletionResult, CompletionSource } from '@codemirror/autocomplete' import { languages } from '@codemirror/language-data' /** diff --git a/frontend/src/extensions/essential-app-extensions/highlighted-code-fence/highlighted-code-markdown-extension.spec.tsx b/frontend/src/extensions/essential-app-extensions/highlighted-code-fence/highlighted-code-markdown-extension.spec.tsx index f419a2f79..85c22bdb9 100644 --- a/frontend/src/extensions/essential-app-extensions/highlighted-code-fence/highlighted-code-markdown-extension.spec.tsx +++ b/frontend/src/extensions/essential-app-extensions/highlighted-code-fence/highlighted-code-markdown-extension.spec.tsx @@ -3,8 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import * as HighlightedCodeModule from '../../../components/common/highlighted-code/highlighted-code' import type { HighlightedCodeProps } from '../../../components/common/highlighted-code/highlighted-code' +import * as HighlightedCodeModule from '../../../components/common/highlighted-code/highlighted-code' import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer' import { mockI18n } from '../../../test-utils/mock-i18n' import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension' diff --git a/frontend/src/extensions/external-lib-app-extensions/asciinema/asciinema-frame.spec.tsx b/frontend/src/extensions/external-lib-app-extensions/asciinema/asciinema-frame.spec.tsx index a683b01d5..6f5625e0b 100644 --- a/frontend/src/extensions/external-lib-app-extensions/asciinema/asciinema-frame.spec.tsx +++ b/frontend/src/extensions/external-lib-app-extensions/asciinema/asciinema-frame.spec.tsx @@ -3,8 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' +import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' import { AsciinemaFrame } from './asciinema-frame' import { render } from '@testing-library/react' import React from 'react' diff --git a/frontend/src/extensions/external-lib-app-extensions/vega-lite/vega-lite-chart.tsx b/frontend/src/extensions/external-lib-app-extensions/vega-lite/vega-lite-chart.tsx index 10b2ebbb5..f006f27d8 100644 --- a/frontend/src/extensions/external-lib-app-extensions/vega-lite/vega-lite-chart.tsx +++ b/frontend/src/extensions/external-lib-app-extensions/vega-lite/vega-lite-chart.tsx @@ -49,7 +49,7 @@ export const VegaLiteChart: React.FC = ({ code }) => { SVG_ACTION: t('renderer.vega-lite.svg') ?? undefined } }) - }, [code, vegaEmbed]) + }, [code, vegaEmbed, t]) useEffect(() => { if (renderingError) { diff --git a/frontend/src/extensions/external-lib-app-extensions/vimeo/vimeo-frame.spec.tsx b/frontend/src/extensions/external-lib-app-extensions/vimeo/vimeo-frame.spec.tsx index 3da0ae669..3282a2461 100644 --- a/frontend/src/extensions/external-lib-app-extensions/vimeo/vimeo-frame.spec.tsx +++ b/frontend/src/extensions/external-lib-app-extensions/vimeo/vimeo-frame.spec.tsx @@ -3,8 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' +import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' import { VimeoFrame } from './vimeo-frame' import { render } from '@testing-library/react' import React from 'react' diff --git a/frontend/src/extensions/external-lib-app-extensions/youtube/youtube-frame.spec.tsx b/frontend/src/extensions/external-lib-app-extensions/youtube/youtube-frame.spec.tsx index 0d7a341d5..8b2de4936 100644 --- a/frontend/src/extensions/external-lib-app-extensions/youtube/youtube-frame.spec.tsx +++ b/frontend/src/extensions/external-lib-app-extensions/youtube/youtube-frame.spec.tsx @@ -3,8 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' +import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield' import { YouTubeFrame } from './youtube-frame' import { render } from '@testing-library/react' import React from 'react' diff --git a/frontend/src/hooks/common/use-note-title.ts b/frontend/src/hooks/common/use-note-title.ts index 7f184825a..6b317aa8b 100644 --- a/frontend/src/hooks/common/use-note-title.ts +++ b/frontend/src/hooks/common/use-note-title.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from './use-application-state' +import { useTranslatedText } from './use-translated-text' import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' /** * Retrieves the title of the note or a placeholder text, if no title is set. @@ -13,8 +13,7 @@ import { useTranslation } from 'react-i18next' * @return The title of the note */ export const useNoteTitle = (): string => { - const { t } = useTranslation() - const untitledNote = useMemo(() => t('editor.untitledNote'), [t]) + const untitledNote = useTranslatedText('editor.untitledNote') const noteTitle = useApplicationState((state) => state.noteDetails.title) return useMemo(() => (noteTitle === '' ? untitledNote : noteTitle), [noteTitle, untitledNote]) diff --git a/frontend/src/hooks/common/use-translated-text.ts b/frontend/src/hooks/common/use-translated-text.ts new file mode 100644 index 000000000..560601b45 --- /dev/null +++ b/frontend/src/hooks/common/use-translated-text.ts @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { TOptions } from 'i18next' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +/** + * Translates text and caches it. + * + * @param key The translation key + * @param tOptions Optional translation options + * @return the translated text + */ +export const useTranslatedText = (key: string, tOptions?: TOptions): string => { + const { t } = useTranslation() + return useMemo(() => (tOptions ? t(key, tOptions) : t(key)), [key, tOptions, t]) +} diff --git a/frontend/src/hooks/common/useTranslatedText.spec.ts b/frontend/src/hooks/common/useTranslatedText.spec.ts new file mode 100644 index 000000000..32c9ce0dd --- /dev/null +++ b/frontend/src/hooks/common/useTranslatedText.spec.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { useTranslatedText } from './use-translated-text' +import { renderHook } from '@testing-library/react' +import type { Namespace } from 'i18next' +import * as ReactI18NextModule from 'react-i18next' +import type { UseTranslationResponse } from 'react-i18next' +import { Mock } from 'ts-mockery' + +jest.mock('react-i18next') + +describe('useTranslatedText', () => { + const mockTranslation = 'mockTranslation' + const mockKey = 'mockKey' + let translateFunction: jest.Mock + + beforeEach(() => { + translateFunction = jest.fn(() => mockTranslation) + const useTranslateMock = Mock.of>({ + t: translateFunction + }) + jest.spyOn(ReactI18NextModule, 'useTranslation').mockReturnValue(useTranslateMock) + }) + + it('translates text', () => { + const { result } = renderHook(() => useTranslatedText(mockKey)) + + expect(result.current).toBe(mockTranslation) + expect(translateFunction).toBeCalledWith(mockKey) + }) + + it('translates text with options', () => { + const mockOptions = {} + const { result } = renderHook(() => useTranslatedText(mockKey, mockOptions)) + + expect(result.current).toBe(mockTranslation) + expect(translateFunction).toBeCalledWith(mockKey, mockOptions) + }) +}) diff --git a/frontend/src/redux/realtime/methods.ts b/frontend/src/redux/realtime/methods.ts index 8e8d0997d..c35110153 100644 --- a/frontend/src/redux/realtime/methods.ts +++ b/frontend/src/redux/realtime/methods.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { store } from '..' -import type { SetRealtimeSyncStatusAction, SetRealtimeUsersAction, SetRealtimeConnectionStatusAction } from './types' +import type { SetRealtimeConnectionStatusAction, SetRealtimeSyncStatusAction, SetRealtimeUsersAction } from './types' import { RealtimeStatusActionType } from './types' import type { RealtimeUser } from '@hedgedoc/commons' diff --git a/frontend/src/redux/realtime/reducers.ts b/frontend/src/redux/realtime/reducers.ts index 8f3cfcc5f..c8becf160 100644 --- a/frontend/src/redux/realtime/reducers.ts +++ b/frontend/src/redux/realtime/reducers.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { RealtimeStatusActions, RealtimeStatus } from './types' +import type { RealtimeStatus, RealtimeStatusActions } from './types' import { RealtimeStatusActionType } from './types' import type { Reducer } from 'redux' diff --git a/frontend/src/utils/expected-origin-boundary.tsx b/frontend/src/utils/expected-origin-boundary.tsx index 37dc27899..f3893ef01 100644 --- a/frontend/src/utils/expected-origin-boundary.tsx +++ b/frontend/src/utils/expected-origin-boundary.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useBaseUrl } from '../hooks/common/use-base-url' -import React, { Fragment, useMemo } from 'react' import type { PropsWithChildren } from 'react' +import React, { Fragment, useMemo } from 'react' export interface ExpectedOriginBoundaryProps { currentOrigin?: string