From 1780225da5ab2d41cc916f69bfcc49128b51a2d9 Mon Sep 17 00:00:00 2001 From: Ajay Kumbhare Date: Sat, 15 Jul 2023 07:57:37 +0530 Subject: [PATCH] feat: add typeScript support to enforce valid translation keys (#1954) * #1952 Fix incorrect localization key for sign-up failure message * feat: add typeScript support to enforce valid translation keys * feat: add typeScript support to enforce valid translation keys * fix lint errors * fix lint error --- web/src/components/AboutSiteDialog.tsx | 4 ++-- web/src/components/AppearanceSelect.tsx | 6 +++--- web/src/components/ArchivedMemo.tsx | 4 ++-- web/src/components/ArchivedMemoDialog.tsx | 4 ++-- web/src/components/BetaBadge.tsx | 4 ++-- web/src/components/ChangeMemberPasswordDialog.tsx | 4 ++-- web/src/components/ChangeMemoCreatedTsDialog.tsx | 4 ++-- web/src/components/ChangePasswordDialog.tsx | 4 ++-- .../components/ChangeResourceFilenameDialog.tsx | 4 ++-- .../components/CreateIdentityProviderDialog.tsx | 8 ++++---- web/src/components/CreateResourceDialog.tsx | 4 ++-- web/src/components/CreateShortcutDialog.tsx | 6 +++--- web/src/components/CreateStorageServiceDialog.tsx | 10 ++++++---- web/src/components/CreateTagDialog.tsx | 4 ++-- web/src/components/Dialog/CommonDialog.tsx | 6 +++--- web/src/components/EmbedMemoDialog.tsx | 4 ++-- web/src/components/Header.tsx | 4 ++-- web/src/components/LearnMore.tsx | 5 ++--- web/src/components/Memo.tsx | 6 ++++-- web/src/components/MemoContent.tsx | 4 ++-- .../ActionButton/MemoVisibilitySelector.tsx | 6 +++--- web/src/components/MemoEditor/index.tsx | 4 +++- web/src/components/MemoFilter.tsx | 7 ++++--- web/src/components/MemoList.tsx | 4 ++-- web/src/components/ResourceItemDropdown.tsx | 4 ++-- web/src/components/ResourceSearchBar.tsx | 4 ++-- web/src/components/SearchBar.tsx | 4 ++-- web/src/components/Settings/MemberSection.tsx | 4 ++-- web/src/components/Settings/MyAccountSection.tsx | 4 ++-- web/src/components/Settings/PreferencesSection.tsx | 6 +++--- web/src/components/Settings/SSOSection.tsx | 4 ++-- web/src/components/Settings/StorageSection.tsx | 4 ++-- web/src/components/Settings/SystemSection.tsx | 4 ++-- web/src/components/ShareMemoDialog.tsx | 6 +++--- web/src/components/ShortcutList.tsx | 6 +++--- web/src/components/TagList.tsx | 4 ++-- web/src/components/UpdateAccountDialog.tsx | 4 ++-- .../components/UpdateCustomizedProfileDialog.tsx | 4 ++-- web/src/components/UpdateLocalStorageDialog.tsx | 4 ++-- web/src/components/UsageHeatMap.tsx | 4 ++-- web/src/components/UserBanner.tsx | 4 ++-- web/src/components/kit/DatePicker.tsx | 4 ++-- web/src/components/kit/Selector.tsx | 6 +++--- web/src/helpers/consts.ts | 2 +- web/src/helpers/filter.ts | 6 +++--- web/src/pages/Archived.tsx | 4 ++-- web/src/pages/Auth.tsx | 4 ++-- web/src/pages/AuthCallback.tsx | 4 ++-- web/src/pages/DailyReview.tsx | 4 ++-- web/src/pages/Explore.tsx | 4 ++-- web/src/pages/Home.tsx | 4 ++-- web/src/pages/MemoDetail.tsx | 4 ++-- web/src/pages/NotFound.tsx | 4 ++-- web/src/pages/ResourcesDashboard.tsx | 4 ++-- web/src/pages/Setting.tsx | 4 ++-- web/src/store/module/resource.ts | 4 ++-- web/src/types/utils/nestedKeyOf.types.ts | 3 +++ web/src/utils/i18n.ts | 14 ++++++++++++++ 58 files changed, 148 insertions(+), 125 deletions(-) create mode 100644 web/src/types/utils/nestedKeyOf.types.ts diff --git a/web/src/components/AboutSiteDialog.tsx b/web/src/components/AboutSiteDialog.tsx index 9ddcddd3..e04609a0 100644 --- a/web/src/components/AboutSiteDialog.tsx +++ b/web/src/components/AboutSiteDialog.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useGlobalStore } from "@/store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -7,7 +7,7 @@ import GitHubBadge from "./GitHubBadge"; type Props = DialogProps; const AboutSiteDialog: React.FC = ({ destroy }: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const globalStore = useGlobalStore(); const profile = globalStore.state.systemStatus.profile; const customizedProfile = globalStore.state.systemStatus.customizedProfile; diff --git a/web/src/components/AppearanceSelect.tsx b/web/src/components/AppearanceSelect.tsx index 60e00aa8..521b4084 100644 --- a/web/src/components/AppearanceSelect.tsx +++ b/web/src/components/AppearanceSelect.tsx @@ -1,6 +1,6 @@ import { Option, Select } from "@mui/joy"; import { FC } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import Icon from "./Icon"; interface Props { @@ -9,11 +9,11 @@ interface Props { className?: string; } -const appearanceList = ["system", "light", "dark"]; +const appearanceList = ["system", "light", "dark"] as const; const AppearanceSelect: FC = (props: Props) => { const { onChange, value, className } = props; - const { t } = useTranslation(); + const t = useTranslate(); const getPrefixIcon = (appearance: Appearance) => { const className = "w-4 h-auto"; diff --git a/web/src/components/ArchivedMemo.tsx b/web/src/components/ArchivedMemo.tsx index a9e9418c..370026e5 100644 --- a/web/src/components/ArchivedMemo.tsx +++ b/web/src/components/ArchivedMemo.tsx @@ -1,7 +1,7 @@ import { Tooltip } from "@mui/joy"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; import { useMemoStore } from "@/store/module"; +import { useTranslate } from "@/utils/i18n"; import { getDateTimeString } from "@/helpers/datetime"; import Icon from "./Icon"; import MemoContent from "./MemoContent"; @@ -15,7 +15,7 @@ interface Props { const ArchivedMemo: React.FC = (props: Props) => { const { memo } = props; - const { t } = useTranslation(); + const t = useTranslate(); const memoStore = useMemoStore(); const handleDeleteMemoClick = async () => { diff --git a/web/src/components/ArchivedMemoDialog.tsx b/web/src/components/ArchivedMemoDialog.tsx index 37aec39f..8d08b412 100644 --- a/web/src/components/ArchivedMemoDialog.tsx +++ b/web/src/components/ArchivedMemoDialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useMemoStore } from "@/store/module"; import useLoading from "@/hooks/useLoading"; import Icon from "./Icon"; @@ -11,7 +11,7 @@ import "@/less/archived-memo-dialog.less"; type Props = DialogProps; const ArchivedMemoDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const { destroy } = props; const memoStore = useMemoStore(); const memos = memoStore.state.memos; diff --git a/web/src/components/BetaBadge.tsx b/web/src/components/BetaBadge.tsx index c813b518..917af1ec 100644 --- a/web/src/components/BetaBadge.tsx +++ b/web/src/components/BetaBadge.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; interface Props { className?: string; @@ -6,7 +6,7 @@ interface Props { const BetaBadge: React.FC = (props: Props) => { const { className } = props; - const { t } = useTranslation(); + const t = useTranslate(); return ( = (props: Props) => { const { user: propsUser, destroy } = props; - const { t } = useTranslation(); + const t = useTranslate(); const userStore = useUserStore(); const [newPassword, setNewPassword] = useState(""); const [newPasswordAgain, setNewPasswordAgain] = useState(""); diff --git a/web/src/components/ChangeMemoCreatedTsDialog.tsx b/web/src/components/ChangeMemoCreatedTsDialog.tsx index 55d96ff5..8f397c54 100644 --- a/web/src/components/ChangeMemoCreatedTsDialog.tsx +++ b/web/src/components/ChangeMemoCreatedTsDialog.tsx @@ -1,7 +1,7 @@ import { getNormalizedTimeString, getUnixTime } from "@/helpers/datetime"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useMemoStore } from "@/store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -11,7 +11,7 @@ interface Props extends DialogProps { } const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const { destroy, memoId } = props; const memoStore = useMemoStore(); const [createdAt, setCreatedAt] = useState(""); diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index b5704e76..71c84eb1 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useGlobalStore, useUserStore } from "@/store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -8,7 +8,7 @@ import { generateDialog } from "./Dialog"; type Props = DialogProps; const ChangePasswordDialog: React.FC = ({ destroy }: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const userStore = useUserStore(); const globalStore = useGlobalStore(); const profile = globalStore.state.systemStatus.profile; diff --git a/web/src/components/ChangeResourceFilenameDialog.tsx b/web/src/components/ChangeResourceFilenameDialog.tsx index 9749ea91..61933414 100644 --- a/web/src/components/ChangeResourceFilenameDialog.tsx +++ b/web/src/components/ChangeResourceFilenameDialog.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useResourceStore } from "@/store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -25,7 +25,7 @@ const validateFilename = (filename: string): boolean => { const ChangeResourceFilenameDialog: React.FC = (props: Props) => { const { destroy, resourceId, resourceFilename } = props; - const { t } = useTranslation(); + const t = useTranslate(); const resourceStore = useResourceStore(); const [filename, setFilename] = useState(resourceFilename); diff --git a/web/src/components/CreateIdentityProviderDialog.tsx b/web/src/components/CreateIdentityProviderDialog.tsx index 4e7392f4..d42a4719 100644 --- a/web/src/components/CreateIdentityProviderDialog.tsx +++ b/web/src/components/CreateIdentityProviderDialog.tsx @@ -1,6 +1,6 @@ import { Button, Divider, Input, Option, Select, Typography } from "@mui/joy"; import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { toast } from "react-hot-toast"; import * as api from "@/helpers/api"; import { UNKNOWN_ID } from "@/helpers/consts"; @@ -101,7 +101,7 @@ interface Props extends DialogProps { } const CreateIdentityProviderDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const identityProviderTypes = [...new Set(templateList.map((t) => t.type))]; const { confirmCallback, destroy, identityProvider } = props; const [basicInfo, setBasicInfo] = useState({ @@ -236,7 +236,7 @@ const CreateIdentityProviderDialog: React.FC = (props: Props) => { return ( <>
-

{t("setting.sso-section." + (isCreating ? "create" : "update") + "-sso")}

+

{t(isCreating ? "setting.sso-section.create-sso" : "setting.sso-section.update-sso")}

@@ -411,7 +411,7 @@ const CreateIdentityProviderDialog: React.FC = (props: Props) => { {t("common.cancel")}
diff --git a/web/src/components/CreateResourceDialog.tsx b/web/src/components/CreateResourceDialog.tsx index 32d21825..4a5a07f8 100644 --- a/web/src/components/CreateResourceDialog.tsx +++ b/web/src/components/CreateResourceDialog.tsx @@ -1,7 +1,7 @@ import { Button, Input, Select, Option, Typography, List, ListItem, Autocomplete } from "@mui/joy"; import React, { useRef, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useResourceStore } from "../store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -21,7 +21,7 @@ interface State { } const CreateResourceDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const { destroy, onCancel, onConfirm } = props; const resourceStore = useResourceStore(); const [state, setState] = useState({ diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx index 79aad9a3..e64ba76a 100644 --- a/web/src/components/CreateShortcutDialog.tsx +++ b/web/src/components/CreateShortcutDialog.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useShortcutStore, useTagStore } from "@/store/module"; import { filterConsts, getDefaultFilter, relationConsts } from "@/helpers/filter"; import { getNormalizedTimeString } from "@/helpers/datetime"; @@ -20,7 +20,7 @@ const CreateShortcutDialog: React.FC = (props: Props) => { const [title, setTitle] = useState(""); const [filters, setFilters] = useState([]); const requestState = useLoading(false); - const { t } = useTranslation(); + const t = useTranslate(); useEffect(() => { if (shortcutId) { @@ -158,7 +158,7 @@ interface MemoFilterInputerProps { const MemoFilterInputer: React.FC = (props: MemoFilterInputerProps) => { const { index, filter, handleFilterChange, handleFilterRemove } = props; - const { t } = useTranslation(); + const t = useTranslate(); const tagStore = useTagStore(); const [value, setValue] = useState(filter.value.value); const tags = Array.from(tagStore.getState().tags); diff --git a/web/src/components/CreateStorageServiceDialog.tsx b/web/src/components/CreateStorageServiceDialog.tsx index 4bb072b7..48143f8f 100644 --- a/web/src/components/CreateStorageServiceDialog.tsx +++ b/web/src/components/CreateStorageServiceDialog.tsx @@ -1,7 +1,7 @@ import { Button, Input, Typography } from "@mui/joy"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import * as api from "@/helpers/api"; import { generateDialog } from "./Dialog"; import Icon from "./Icon"; @@ -14,7 +14,7 @@ interface Props extends DialogProps { } const CreateStorageServiceDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const { destroy, storage, confirmCallback } = props; const [basicInfo, setBasicInfo] = useState({ name: "", @@ -106,7 +106,9 @@ const CreateStorageServiceDialog: React.FC = (props: Props) => { return ( <>
- {t("setting.storage-section." + (isCreating ? "create" : "update") + "-storage")} + + {t(isCreating ? "setting.storage-section.create-storage" : "setting.storage-section.update-storage")} + @@ -219,7 +221,7 @@ const CreateStorageServiceDialog: React.FC = (props: Props) => { {t("common.cancel")}
diff --git a/web/src/components/CreateTagDialog.tsx b/web/src/components/CreateTagDialog.tsx index 39aaa475..cef9c543 100644 --- a/web/src/components/CreateTagDialog.tsx +++ b/web/src/components/CreateTagDialog.tsx @@ -1,7 +1,7 @@ import { Input } from "@mui/joy"; import React, { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useTagStore } from "@/store/module"; import { getTagSuggestionList } from "@/helpers/api"; import { matcher } from "@/labs/marked/matcher"; @@ -22,7 +22,7 @@ const validateTagName = (tagName: string): boolean => { const CreateTagDialog: React.FC = (props: Props) => { const { destroy } = props; const tagStore = useTagStore(); - const { t } = useTranslation(); + const t = useTranslate(); const [tagName, setTagName] = useState(""); const [suggestTagNameList, setSuggestTagNameList] = useState([]); const [showTagSuggestions, setShowTagSuggestions] = useState(false); diff --git a/web/src/components/Dialog/CommonDialog.tsx b/web/src/components/Dialog/CommonDialog.tsx index 23b2d68c..4bd9aba8 100644 --- a/web/src/components/Dialog/CommonDialog.tsx +++ b/web/src/components/Dialog/CommonDialog.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import Icon from "../Icon"; import { generateDialog } from "./BaseDialog"; import "@/less/common-dialog.less"; @@ -23,10 +23,10 @@ const defaultProps = { confirmBtnText: "common.confirm", onClose: () => null, onConfirm: () => null, -}; +} as const; const CommonDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const { title, content, destroy, closeBtnText, confirmBtnText, onClose, onConfirm, style } = { ...defaultProps, closeBtnText: t(defaultProps.closeBtnText), diff --git a/web/src/components/EmbedMemoDialog.tsx b/web/src/components/EmbedMemoDialog.tsx index d33e831f..7965ef16 100644 --- a/web/src/components/EmbedMemoDialog.tsx +++ b/web/src/components/EmbedMemoDialog.tsx @@ -1,7 +1,7 @@ import copy from "copy-to-clipboard"; import React from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -10,7 +10,7 @@ interface Props extends DialogProps { } const EmbedMemoDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const { memoId, destroy } = props; const memoEmbeddedCode = () => { diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 9608db5c..5330053d 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { NavLink, useLocation } from "react-router-dom"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useLayoutStore, useUserStore } from "@/store/module"; import { resolution } from "@/utils/layout"; import Icon from "./Icon"; @@ -9,7 +9,7 @@ import showAboutSiteDialog from "./AboutSiteDialog"; import UpgradeVersionView from "./UpgradeVersionBanner"; const Header = () => { - const { t } = useTranslation(); + const t = useTranslate(); const location = useLocation(); const userStore = useUserStore(); const layoutStore = useLayoutStore(); diff --git a/web/src/components/LearnMore.tsx b/web/src/components/LearnMore.tsx index d5b919f0..00e920dc 100644 --- a/web/src/components/LearnMore.tsx +++ b/web/src/components/LearnMore.tsx @@ -1,7 +1,6 @@ import { Tooltip } from "@mui/joy"; import Icon from "./Icon"; -import { useTranslation } from "react-i18next"; - +import { useTranslate } from "@/utils/i18n"; interface Props { className?: string; url: string; @@ -10,7 +9,7 @@ interface Props { const LearnMore: React.FC = (props: Props) => { const { className, url, title } = props; - const { t } = useTranslation(); + const t = useTranslate(); return ( <> diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 890a3145..c6d0d97b 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -2,6 +2,7 @@ import { Divider, Tooltip } from "@mui/joy"; import { isEqual, uniqWith } from "lodash-es"; import { memo, useEffect, useRef, useState } from "react"; import { toast } from "react-hot-toast"; +import { useTranslate } from "@/utils/i18n"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { useFilterStore, useMemoStore, useUserStore } from "@/store/module"; @@ -28,7 +29,8 @@ interface Props { const Memo: React.FC = (props: Props) => { const { memo, showCreator, showVisibility, showRelatedMemos } = props; - const { t, i18n } = useTranslation(); + const { i18n } = useTranslation(); + const t = useTranslate(); const filterStore = useFilterStore(); const userStore = useUserStore(); const memoStore = useMemoStore(); @@ -225,7 +227,7 @@ const Memo: React.FC = (props: Props) => {
{showVisibility && memo.visibility !== "PRIVATE" && ( - + }`)} placement="top">
handleMemoVisibilityClick(memo.visibility)}> {memo.visibility === "PUBLIC" ? ( diff --git a/web/src/components/MemoContent.tsx b/web/src/components/MemoContent.tsx index ba5c4a81..bce5e690 100644 --- a/web/src/components/MemoContent.tsx +++ b/web/src/components/MemoContent.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { marked } from "@/labs/marked"; import { useUserStore } from "@/store/module"; import Icon from "./Icon"; @@ -23,7 +23,7 @@ interface State { const MemoContent: React.FC = (props: Props) => { const { className, content, showFull, onMemoContentClick, onMemoContentDoubleClick } = props; - const { t } = useTranslation(); + const t = useTranslate(); const userStore = useUserStore(); const [state, setState] = useState({ expandButtonStatus: -1, diff --git a/web/src/components/MemoEditor/ActionButton/MemoVisibilitySelector.tsx b/web/src/components/MemoEditor/ActionButton/MemoVisibilitySelector.tsx index 67e9acde..43e5c224 100644 --- a/web/src/components/MemoEditor/ActionButton/MemoVisibilitySelector.tsx +++ b/web/src/components/MemoEditor/ActionButton/MemoVisibilitySelector.tsx @@ -1,5 +1,5 @@ import { toLower } from "lodash-es"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; import { useGlobalStore } from "@/store/module"; import Selector from "@/components/kit/Selector"; @@ -11,14 +11,14 @@ interface Props { const MemoVisibilitySelector = (props: Props) => { const { value, onChange } = props; - const { t } = useTranslation(); + const t = useTranslate(); const { state: { systemStatus }, } = useGlobalStore(); const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => { return { value: item.value, - text: t(`memo.visibility.${toLower(item.value)}`), + text: t(`memo.visibility.${toLower(item.value) as Lowercase}`), }; }); diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 4264ce4f..74bfbb86 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -1,6 +1,7 @@ import { isNumber, last, uniq } from "lodash-es"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "react-hot-toast"; +import { useTranslate } from "@/utils/i18n"; import { useTranslation } from "react-i18next"; import { getMatchedNodes } from "@/labs/marked"; import { upsertMemoResource } from "@/helpers/api"; @@ -42,7 +43,8 @@ interface State { const MemoEditor = (props: Props) => { const { className, memoId, onConfirm } = props; - const { t, i18n } = useTranslation(); + const { i18n } = useTranslation(); + const t = useTranslate(); const { state: { systemStatus }, } = useGlobalStore(); diff --git a/web/src/components/MemoFilter.tsx b/web/src/components/MemoFilter.tsx index 7d72211f..ed489597 100644 --- a/web/src/components/MemoFilter.tsx +++ b/web/src/components/MemoFilter.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useLocation } from "react-router-dom"; import { useFilterStore, useShortcutStore } from "@/store/module"; import { getDateString } from "@/helpers/datetime"; @@ -8,7 +8,7 @@ import Icon from "./Icon"; import "@/less/memo-filter.less"; const MemoFilter = () => { - const { t } = useTranslation(); + const t = useTranslate(); const location = useLocation(); const filterStore = useFilterStore(); const shortcutStore = useShortcutStore(); @@ -46,7 +46,8 @@ const MemoFilter = () => { filterStore.setMemoTypeFilter(undefined); }} > - {t(getTextWithMemoType(memoType as MemoSpecType))} + {" "} + {t(getTextWithMemoType(memoType as MemoSpecType) as Exclude, "">)}
{ - const { t } = useTranslation(); + const t = useTranslate(); const memoStore = useMemoStore(); const userStore = useUserStore(); const shortcutStore = useShortcutStore(); diff --git a/web/src/components/ResourceItemDropdown.tsx b/web/src/components/ResourceItemDropdown.tsx index 52ac80d0..2b7877ae 100644 --- a/web/src/components/ResourceItemDropdown.tsx +++ b/web/src/components/ResourceItemDropdown.tsx @@ -1,7 +1,7 @@ import copy from "copy-to-clipboard"; import React from "react"; import toast from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useResourceStore } from "@/store/module"; import { getResourceUrl } from "@/utils/resource"; import Dropdown from "./kit/Dropdown"; @@ -15,7 +15,7 @@ interface Props { } const ResourceItemDropdown = ({ resource }: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const resourceStore = useResourceStore(); const resources = resourceStore.state.resources; diff --git a/web/src/components/ResourceSearchBar.tsx b/web/src/components/ResourceSearchBar.tsx index 3b5e652f..c37ffa24 100644 --- a/web/src/components/ResourceSearchBar.tsx +++ b/web/src/components/ResourceSearchBar.tsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import Icon from "./Icon"; import useDebounce from "@/hooks/useDebounce"; @@ -7,7 +7,7 @@ interface ResourceSearchBarProps { setQuery: (queryText: string) => void; } const ResourceSearchBar = ({ setQuery }: ResourceSearchBarProps) => { - const { t } = useTranslation(); + const t = useTranslate(); const [queryText, setQueryText] = useState(""); const inputRef = useRef(null); diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index dfa89060..ecdcc9f8 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -1,11 +1,11 @@ import { useEffect, useState, useRef } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import useDebounce from "@/hooks/useDebounce"; import { useFilterStore } from "@/store/module"; import Icon from "./Icon"; const SearchBar = () => { - const { t } = useTranslation(); + const t = useTranslate(); const filterStore = useFilterStore(); const [queryText, setQueryText] = useState(""); const inputRef = useRef(null); diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx index 8d58095f..d873d46b 100644 --- a/web/src/components/Settings/MemberSection.tsx +++ b/web/src/components/Settings/MemberSection.tsx @@ -1,7 +1,7 @@ import { Table } from "@mui/joy"; import React, { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useUserStore } from "@/store/module"; import * as api from "@/helpers/api"; import Dropdown from "../kit/Dropdown"; @@ -15,7 +15,7 @@ interface State { } const PreferencesSection = () => { - const { t } = useTranslation(); + const t = useTranslate(); const userStore = useUserStore(); const currentUser = userStore.state.user; const [state, setState] = useState({ diff --git a/web/src/components/Settings/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx index 839beb7d..08bd5f4b 100644 --- a/web/src/components/Settings/MyAccountSection.tsx +++ b/web/src/components/Settings/MyAccountSection.tsx @@ -1,5 +1,5 @@ import { Button, Input, Textarea } from "@mui/joy"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useUserStore } from "@/store/module"; import { showCommonDialog } from "../Dialog/CommonDialog"; import showChangePasswordDialog from "../ChangePasswordDialog"; @@ -8,7 +8,7 @@ import showUpdateAccountDialog from "../UpdateAccountDialog"; import UserAvatar from "../UserAvatar"; const MyAccountSection = () => { - const { t } = useTranslation(); + const t = useTranslate(); const userStore = useUserStore(); const user = userStore.state.user as User; const openAPIRoute = `${window.location.origin}/api/v1/memo?openId=${user.openId}`; diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index 9ec0bf95..ef64dc61 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -2,7 +2,7 @@ import { Input, Button, Divider, Switch, Option, Select } from "@mui/joy"; import { useState } from "react"; import { toast } from "react-hot-toast"; import React from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useGlobalStore, useUserStore } from "@/store/module"; import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; import AppearanceSelect from "../AppearanceSelect"; @@ -11,7 +11,7 @@ import LearnMore from "../LearnMore"; import "@/less/settings/preferences-section.less"; const PreferencesSection = () => { - const { t } = useTranslation(); + const t = useTranslate(); const globalStore = useGlobalStore(); const userStore = useUserStore(); const { appearance, locale } = globalStore.state; @@ -20,7 +20,7 @@ const PreferencesSection = () => { const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => { return { value: item.value, - text: t(`memo.visibility.${item.text.toLowerCase()}`), + text: t(`memo.visibility.${item.text.toLowerCase() as Lowercase}`), }; }); diff --git a/web/src/components/Settings/SSOSection.tsx b/web/src/components/Settings/SSOSection.tsx index 7d8c6873..83631644 100644 --- a/web/src/components/Settings/SSOSection.tsx +++ b/web/src/components/Settings/SSOSection.tsx @@ -1,7 +1,7 @@ import { Divider } from "@mui/joy"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import * as api from "@/helpers/api"; import showCreateIdentityProviderDialog from "../CreateIdentityProviderDialog"; import Dropdown from "../kit/Dropdown"; @@ -9,7 +9,7 @@ import { showCommonDialog } from "../Dialog/CommonDialog"; import LearnMore from "../LearnMore"; const SSOSection = () => { - const { t } = useTranslation(); + const t = useTranslate(); const [identityProviderList, setIdentityProviderList] = useState([]); useEffect(() => { diff --git a/web/src/components/Settings/StorageSection.tsx b/web/src/components/Settings/StorageSection.tsx index 0ba07b5b..32bff396 100644 --- a/web/src/components/Settings/StorageSection.tsx +++ b/web/src/components/Settings/StorageSection.tsx @@ -1,7 +1,7 @@ import { Divider, Select, Option } from "@mui/joy"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useGlobalStore } from "@/store/module"; import * as api from "@/helpers/api"; import showCreateStorageServiceDialog from "../CreateStorageServiceDialog"; @@ -11,7 +11,7 @@ import { showCommonDialog } from "../Dialog/CommonDialog"; import LearnMore from "../LearnMore"; const StorageSection = () => { - const { t } = useTranslation(); + const t = useTranslate(); const globalStore = useGlobalStore(); const systemStatus = globalStore.state.systemStatus; const [storageServiceId, setStorageServiceId] = useState(systemStatus.storageServiceId); diff --git a/web/src/components/Settings/SystemSection.tsx b/web/src/components/Settings/SystemSection.tsx index 08fdebfd..d88ba11c 100644 --- a/web/src/components/Settings/SystemSection.tsx +++ b/web/src/components/Settings/SystemSection.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { Button, Divider, Input, Switch, Textarea, Tooltip } from "@mui/joy"; import { formatBytes } from "@/helpers/utils"; import { useGlobalStore } from "@/store/module"; @@ -21,7 +21,7 @@ interface State { } const SystemSection = () => { - const { t } = useTranslation(); + const t = useTranslate(); const globalStore = useGlobalStore(); const systemStatus = globalStore.state.systemStatus; const [state, setState] = useState({ diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx index 4924506d..f6c51375 100644 --- a/web/src/components/ShareMemoDialog.tsx +++ b/web/src/components/ShareMemoDialog.tsx @@ -2,7 +2,7 @@ import { Select, Option } from "@mui/joy"; import { QRCodeSVG } from "qrcode.react"; import React, { useEffect, useRef, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import copy from "copy-to-clipboard"; import { toLower } from "lodash-es"; import toImage from "@/labs/html2image"; @@ -30,7 +30,7 @@ interface State { const ShareMemoDialog: React.FC = (props: Props) => { const { memo: propsMemo, destroy } = props; - const { t } = useTranslation(); + const t = useTranslate(); const userStore = useUserStore(); const memoStore = useMemoStore(); const globalStore = useGlobalStore(); @@ -109,7 +109,7 @@ const ShareMemoDialog: React.FC = (props: Props) => { const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => { return { value: item.value, - text: t(`memo.visibility.${toLower(item.value)}`), + text: t(`memo.visibility.${toLower(item.value) as Lowercase}`), }; }); diff --git a/web/src/components/ShortcutList.tsx b/web/src/components/ShortcutList.tsx index f3d1069e..ed2cf4f7 100644 --- a/web/src/components/ShortcutList.tsx +++ b/web/src/components/ShortcutList.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useFilterStore, useShortcutStore } from "@/store/module"; import { getTimeStampByDate } from "@/helpers/datetime"; import useToggle from "@/hooks/useToggle"; @@ -9,7 +9,7 @@ import Icon from "./Icon"; import showCreateShortcutDialog from "./CreateShortcutDialog"; const ShortcutList = () => { - const { t } = useTranslation(); + const t = useTranslate(); const filterStore = useFilterStore(); const shortcutStore = useShortcutStore(); const filter = filterStore.state; @@ -62,7 +62,7 @@ interface ShortcutContainerProps { const ShortcutContainer: React.FC = (props: ShortcutContainerProps) => { const { shortcut, isActive } = props; - const { t } = useTranslation(); + const t = useTranslate(); const filterStore = useFilterStore(); const shortcutStore = useShortcutStore(); const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false); diff --git a/web/src/components/TagList.tsx b/web/src/components/TagList.tsx index 0676af36..fc31a366 100644 --- a/web/src/components/TagList.tsx +++ b/web/src/components/TagList.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useFilterStore, useTagStore } from "@/store/module"; import useToggle from "@/hooks/useToggle"; import Icon from "./Icon"; @@ -12,7 +12,7 @@ interface Tag { } const TagList = () => { - const { t } = useTranslation(); + const t = useTranslate(); const filterStore = useFilterStore(); const tagStore = useTagStore(); const tagsText = tagStore.state.tags; diff --git a/web/src/components/UpdateAccountDialog.tsx b/web/src/components/UpdateAccountDialog.tsx index 85aea77e..00aca1ae 100644 --- a/web/src/components/UpdateAccountDialog.tsx +++ b/web/src/components/UpdateAccountDialog.tsx @@ -1,7 +1,7 @@ import { isEqual } from "lodash-es"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useUserStore } from "@/store/module"; import { convertFileToBase64 } from "@/helpers/utils"; import Icon from "./Icon"; @@ -18,7 +18,7 @@ interface State { } const UpdateAccountDialog: React.FC = ({ destroy }: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const userStore = useUserStore(); const user = userStore.state.user as User; const [state, setState] = useState({ diff --git a/web/src/components/UpdateCustomizedProfileDialog.tsx b/web/src/components/UpdateCustomizedProfileDialog.tsx index 2cf35183..799e4721 100644 --- a/web/src/components/UpdateCustomizedProfileDialog.tsx +++ b/web/src/components/UpdateCustomizedProfileDialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { toast } from "react-hot-toast"; import { useGlobalStore } from "@/store/module"; import * as api from "@/helpers/api"; @@ -12,7 +12,7 @@ import AppearanceSelect from "./AppearanceSelect"; type Props = DialogProps; const UpdateCustomizedProfileDialog: React.FC = ({ destroy }: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const globalStore = useGlobalStore(); const [state, setState] = useState(globalStore.state.systemStatus.customizedProfile); diff --git a/web/src/components/UpdateLocalStorageDialog.tsx b/web/src/components/UpdateLocalStorageDialog.tsx index 82b28abc..466a2b26 100644 --- a/web/src/components/UpdateLocalStorageDialog.tsx +++ b/web/src/components/UpdateLocalStorageDialog.tsx @@ -1,6 +1,6 @@ import { Button, Input } from "@mui/joy"; import { useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { toast } from "react-hot-toast"; import { useGlobalStore } from "@/store/module"; import * as api from "@/helpers/api"; @@ -14,7 +14,7 @@ interface Props extends DialogProps { } const UpdateLocalStorageDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); + const t = useTranslate(); const { destroy, localStoragePath, confirmCallback } = props; const globalStore = useGlobalStore(); const [path, setPath] = useState(localStoragePath || ""); diff --git a/web/src/components/UsageHeatMap.tsx b/web/src/components/UsageHeatMap.tsx index 6f7416ae..78436137 100644 --- a/web/src/components/UsageHeatMap.tsx +++ b/web/src/components/UsageHeatMap.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useFilterStore, useMemoStore, useUserStore } from "../store/module"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { getMemoStats } from "@/helpers/api"; import { DAILY_TIMESTAMP } from "@/helpers/consts"; import { getDateStampByDate, getDateString, getTimeStampByDate } from "@/helpers/datetime"; @@ -29,7 +29,7 @@ interface DailyUsageStat { } const UsageHeatMap = () => { - const { t } = useTranslation(); + const t = useTranslate(); const filterStore = useFilterStore(); const userStore = useUserStore(); const memoStore = useMemoStore(); diff --git a/web/src/components/UserBanner.tsx b/web/src/components/UserBanner.tsx index 5a60544f..720d7e16 100644 --- a/web/src/components/UserBanner.tsx +++ b/web/src/components/UserBanner.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useNavigate } from "react-router-dom"; import { useGlobalStore, useUserStore } from "@/store/module"; import Dropdown from "./kit/Dropdown"; @@ -8,7 +8,7 @@ import UserAvatar from "./UserAvatar"; import showAboutSiteDialog from "./AboutSiteDialog"; const UserBanner = () => { - const { t } = useTranslation(); + const t = useTranslate(); const navigate = useNavigate(); const globalStore = useGlobalStore(); const userStore = useUserStore(); diff --git a/web/src/components/kit/DatePicker.tsx b/web/src/components/kit/DatePicker.tsx index b3192a35..fa6a6063 100644 --- a/web/src/components/kit/DatePicker.tsx +++ b/web/src/components/kit/DatePicker.tsx @@ -1,6 +1,6 @@ import { Badge, Button } from "@mui/joy"; import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { DAILY_TIMESTAMP } from "@/helpers/consts"; import { getMemoStats } from "@/helpers/api"; import { getDateStampByDate } from "@/helpers/datetime"; @@ -15,7 +15,7 @@ interface DatePickerProps { } const DatePicker: React.FC = (props: DatePickerProps) => { - const { t } = useTranslation(); + const t = useTranslate(); const { className, datestamp, handleDateStampChange } = props; const [currentDateStamp, setCurrentDateStamp] = useState(getMonthFirstDayDateStamp(datestamp)); const [countByDate, setCountByDate] = useState(new Map()); diff --git a/web/src/components/kit/Selector.tsx b/web/src/components/kit/Selector.tsx index 301b13cc..86ee7d5e 100644 --- a/web/src/components/kit/Selector.tsx +++ b/web/src/components/kit/Selector.tsx @@ -1,6 +1,6 @@ import { Tooltip } from "@mui/joy"; import { memo, useEffect, useRef } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import useToggle from "@/hooks/useToggle"; import Icon from "../Icon"; import "@/less/common/selector.less"; @@ -20,13 +20,13 @@ interface Props { } const nullItem = { - text: "common.select", + text: "common.select" as const, value: "", }; const Selector: React.FC = (props: Props) => { const { className, dataSource, handleValueChanged, value, disabled, tooltipTitle } = props; - const { t } = useTranslation(); + const t = useTranslate(); const [showSelector, toggleSelectorStatus] = useToggle(false); const selectorElRef = useRef(null); diff --git a/web/src/helpers/consts.ts b/web/src/helpers/consts.ts index f1531409..fa31ca37 100644 --- a/web/src/helpers/consts.ts +++ b/web/src/helpers/consts.ts @@ -11,7 +11,7 @@ export const VISIBILITY_SELECTOR_ITEMS = [ { text: "PRIVATE", value: "PRIVATE" }, { text: "PROTECTED", value: "PROTECTED" }, { text: "PUBLIC", value: "PUBLIC" }, -]; +] as const; // space width for tab action in editor export const TAB_SPACE_WIDTH = 2; diff --git a/web/src/helpers/filter.ts b/web/src/helpers/filter.ts index e3b80ba0..f8dbbdd2 100644 --- a/web/src/helpers/filter.ts +++ b/web/src/helpers/filter.ts @@ -4,7 +4,7 @@ import { TAG_REG, LINK_REG, PLAIN_LINK_REG } from "@/labs/marked/parser"; export const relationConsts = [ { text: "filter.and", value: "AND" }, { text: "filter.or", value: "OR" }, -]; +] as const; export const filterConsts = { TAG: { @@ -105,11 +105,11 @@ export const filterConsts = { }, ], }, -}; +} as const; export const memoSpecialTypes = filterConsts["TYPE"].values; -export const getTextWithMemoType = (type: string): string => { +export const getTextWithMemoType = (type: string) => { for (const t of memoSpecialTypes) { if (t.value === type) { return t.text; diff --git a/web/src/pages/Archived.tsx b/web/src/pages/Archived.tsx index d8ef6ba0..6fc5d98d 100644 --- a/web/src/pages/Archived.tsx +++ b/web/src/pages/Archived.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import toast from "react-hot-toast"; import { useMemoStore } from "@/store/module"; import useLoading from "@/hooks/useLoading"; @@ -9,7 +9,7 @@ import Empty from "@/components/Empty"; import "@/less/archived.less"; const Archived = () => { - const { t } = useTranslation(); + const t = useTranslate(); const memoStore = useMemoStore(); const loadingState = useLoading(); const [archivedMemos, setArchivedMemos] = useState([]); diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index 05989029..c2a32e8e 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -1,7 +1,7 @@ import { Button, Divider } from "@mui/joy"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useGlobalStore, useUserStore } from "@/store/module"; import * as api from "@/helpers/api"; import { absolutifyLink } from "@/helpers/utils"; @@ -11,7 +11,7 @@ import AppearanceSelect from "@/components/AppearanceSelect"; import LocaleSelect from "@/components/LocaleSelect"; const Auth = () => { - const { t } = useTranslation(); + const t = useTranslate(); const globalStore = useGlobalStore(); const userStore = useUserStore(); const actionBtnLoadingState = useLoading(false); diff --git a/web/src/pages/AuthCallback.tsx b/web/src/pages/AuthCallback.tsx index 8105880e..6e08630f 100644 --- a/web/src/pages/AuthCallback.tsx +++ b/web/src/pages/AuthCallback.tsx @@ -1,7 +1,7 @@ import { last } from "lodash-es"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useSearchParams } from "react-router-dom"; import * as api from "@/helpers/api"; import { absolutifyLink } from "@/helpers/utils"; @@ -14,7 +14,7 @@ interface State { } const AuthCallback = () => { - const { t } = useTranslation(); + const t = useTranslate(); const [searchParams] = useSearchParams(); const userStore = useUserStore(); const [state, setState] = useState({ diff --git a/web/src/pages/DailyReview.tsx b/web/src/pages/DailyReview.tsx index e7695251..99900aa1 100644 --- a/web/src/pages/DailyReview.tsx +++ b/web/src/pages/DailyReview.tsx @@ -1,7 +1,7 @@ import { last } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import toast from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useMemoStore, useUserStore } from "@/store/module"; import { DAILY_TIMESTAMP, DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import MobileHeader from "@/components/MobileHeader"; @@ -17,7 +17,7 @@ import { convertToMillis, getDateStampByDate, getNormalizedDateString, getTimeSt import Empty from "@/components/Empty"; const DailyReview = () => { - const { t } = useTranslation(); + const t = useTranslate(); const memoStore = useMemoStore(); const memos = memoStore.state.memos; diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 01a5db37..337a384f 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useLocation } from "react-router-dom"; import { useFilterStore, useMemoStore } from "@/store/module"; import { TAG_REG } from "@/labs/marked/parser"; @@ -16,7 +16,7 @@ interface State { } const Explore = () => { - const { t } = useTranslation(); + const t = useTranslate(); const location = useLocation(); const filterStore = useFilterStore(); const memoStore = useMemoStore(); diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index a1a0b973..eba2645a 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useGlobalStore, useUserStore } from "@/store/module"; import MemoEditor from "@/components/MemoEditor"; import MemoFilter from "@/components/MemoFilter"; @@ -9,7 +9,7 @@ import MobileHeader from "@/components/MobileHeader"; import HomeSidebar from "@/components/HomeSidebar"; function Home() { - const { t } = useTranslation(); + const t = useTranslate(); const globalStore = useGlobalStore(); const userStore = useUserStore(); const user = userStore.state.user; diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index e73ca0fe..58f5b45f 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { Link, useLocation, useParams } from "react-router-dom"; import { UNKNOWN_ID } from "@/helpers/consts"; import { useGlobalStore, useMemoStore } from "@/store/module"; @@ -13,7 +13,7 @@ interface State { } const MemoDetail = () => { - const { t } = useTranslation(); + const t = useTranslate(); const params = useParams(); const location = useLocation(); const globalStore = useGlobalStore(); diff --git a/web/src/pages/NotFound.tsx b/web/src/pages/NotFound.tsx index 3b2a6e70..b9596e39 100644 --- a/web/src/pages/NotFound.tsx +++ b/web/src/pages/NotFound.tsx @@ -1,9 +1,9 @@ -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { Link } from "react-router-dom"; import "@/less/not-found.less"; const NotFound = () => { - const { t } = useTranslation(); + const t = useTranslate(); return (
diff --git a/web/src/pages/ResourcesDashboard.tsx b/web/src/pages/ResourcesDashboard.tsx index 97c98c8c..8fcbae70 100644 --- a/web/src/pages/ResourcesDashboard.tsx +++ b/web/src/pages/ResourcesDashboard.tsx @@ -1,7 +1,7 @@ import { Button } from "@mui/joy"; import { useEffect, useMemo, useState } from "react"; import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import useLoading from "@/hooks/useLoading"; import useEvent from "@/hooks/useEvent"; @@ -17,7 +17,7 @@ import showCreateResourceDialog from "@/components/CreateResourceDialog"; import Empty from "@/components/Empty"; const ResourcesDashboard = () => { - const { t } = useTranslation(); + const t = useTranslate(); const loadingState = useLoading(); const resourceStore = useResourceStore(); const resources = resourceStore.state.resources; diff --git a/web/src/pages/Setting.tsx b/web/src/pages/Setting.tsx index 3ef417d2..72af5e43 100644 --- a/web/src/pages/Setting.tsx +++ b/web/src/pages/Setting.tsx @@ -1,6 +1,6 @@ import { Option, Select } from "@mui/joy"; import { useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; import { useUserStore } from "@/store/module"; import Icon from "@/components/Icon"; import BetaBadge from "@/components/BetaBadge"; @@ -20,7 +20,7 @@ interface State { } const Setting = () => { - const { t } = useTranslation(); + const t = useTranslate(); const userStore = useUserStore(); const user = userStore.state.user; const [state, setState] = useState({ diff --git a/web/src/store/module/resource.ts b/web/src/store/module/resource.ts index 7423d79e..75d72146 100644 --- a/web/src/store/module/resource.ts +++ b/web/src/store/module/resource.ts @@ -3,7 +3,7 @@ import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import store, { useAppSelector } from "../"; import { patchResource, setResources, deleteResource, upsertResources } from "../reducer/resource"; import { useGlobalStore } from "./global"; -import { useTranslation } from "react-i18next"; +import { useTranslate } from "@/utils/i18n"; const convertResponseModelResource = (resource: Resource): Resource => { return { @@ -15,7 +15,7 @@ const convertResponseModelResource = (resource: Resource): Resource => { export const useResourceStore = () => { const state = useAppSelector((state) => state.resource); - const { t } = useTranslation(); + const t = useTranslate(); const globalStore = useGlobalStore(); const maxUploadSizeMiB = globalStore.state.systemStatus.maxUploadSizeMiB; diff --git a/web/src/types/utils/nestedKeyOf.types.ts b/web/src/types/utils/nestedKeyOf.types.ts new file mode 100644 index 00000000..472f7f4d --- /dev/null +++ b/web/src/types/utils/nestedKeyOf.types.ts @@ -0,0 +1,3 @@ +export type NestedKeyOf = K extends keyof T & (string | number) + ? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf}` : never) + : never; diff --git a/web/src/utils/i18n.ts b/web/src/utils/i18n.ts index cfd5f35b..414fb7d0 100644 --- a/web/src/utils/i18n.ts +++ b/web/src/utils/i18n.ts @@ -1,5 +1,8 @@ import i18n, { TLocale, availableLocales } from "@/i18n"; import { FallbackLngObjList } from "i18next"; +import { useTranslation } from "react-i18next"; +import locales from "@/locales/en.json"; +import type { NestedKeyOf } from "@/types/utils/nestedKeyOf.types"; export const findNearestLanguageMatch = (codename: string): Locale => { // Find existing translations for full codes (e.g. "en-US", "zh-Hant") @@ -33,3 +36,14 @@ export const findNearestLanguageMatch = (codename: string): Locale => { // should be "en", so the selector is not empty if there isn't a translation for current user's language return (i18n.store.options.fallbackLng as FallbackLngObjList).default[0] as Locale; }; + +// Represents the keys of nested translation objects. +type Translations = NestedKeyOf; + +// Represents a typed translation function. +type TypedT = (key: Translations, params?: Record) => string; + +export const useTranslate = (): TypedT => { + const { t } = useTranslation(); + return t; +};