diff --git a/web/package.json b/web/package.json index 710feea4..94a9cc2c 100644 --- a/web/package.json +++ b/web/package.json @@ -13,11 +13,13 @@ "copy-to-clipboard": "^3.3.2", "dayjs": "^1.11.3", "emoji-picker-react": "^3.6.2", + "i18next": "^21.9.2", "lodash-es": "^4.17.21", "qs": "^6.11.0", "react": "^18.1.0", "react-dom": "^18.1.0", "react-feather": "^2.0.10", + "react-i18next": "^11.18.6", "react-redux": "^8.0.1", "react-router-dom": "^6.4.0", "vite-plugin-pwa": "^0.12.8" diff --git a/web/src/App.tsx b/web/src/App.tsx index 1a80c877..de047a08 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,13 +1,13 @@ import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { RouterProvider } from "react-router-dom"; -import useI18n from "./hooks/useI18n"; import { globalService, locationService } from "./services"; import { useAppSelector } from "./store"; import router from "./router"; import * as storage from "./helpers/storage"; function App() { - const { setLocale } = useI18n(); + const { i18n } = useTranslation(); const global = useAppSelector((state) => state.global); useEffect(() => { @@ -20,7 +20,7 @@ function App() { }, []); useEffect(() => { - setLocale(global.locale); + i18n.changeLanguage(global.locale); storage.set({ locale: global.locale, }); diff --git a/web/src/components/AboutSiteDialog.tsx b/web/src/components/AboutSiteDialog.tsx index 6217c3b8..dacca902 100644 --- a/web/src/components/AboutSiteDialog.tsx +++ b/web/src/components/AboutSiteDialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import * as api from "../helpers/api"; -import useI18n from "../hooks/useI18n"; import Only from "./common/OnlyWhen"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -10,7 +10,7 @@ import "../less/about-site-dialog.less"; type Props = DialogProps; const AboutSiteDialog: React.FC = ({ destroy }: Props) => { - const { t } = useI18n(); + const { t } = useTranslation(); const [profile, setProfile] = useState(); useEffect(() => { diff --git a/web/src/components/ArchivedMemo.tsx b/web/src/components/ArchivedMemo.tsx index c378fdda..11ccb6c5 100644 --- a/web/src/components/ArchivedMemo.tsx +++ b/web/src/components/ArchivedMemo.tsx @@ -1,5 +1,5 @@ +import { useTranslation } from "react-i18next"; import * as utils from "../helpers/utils"; -import useI18n from "../hooks/useI18n"; import useToggle from "../hooks/useToggle"; import { memoService } from "../services"; import toastHelper from "./Toast"; @@ -18,7 +18,7 @@ const ArchivedMemo: React.FC = (props: Props) => { createdAtStr: utils.getDateTimeString(propsMemo.createdTs), archivedAtStr: utils.getDateTimeString(propsMemo.updatedTs ?? Date.now()), }; - const { t } = useI18n(); + const { t } = useTranslation(); const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false); diff --git a/web/src/components/ArchivedMemoDialog.tsx b/web/src/components/ArchivedMemoDialog.tsx index 88c43e34..1b6085fd 100644 --- a/web/src/components/ArchivedMemoDialog.tsx +++ b/web/src/components/ArchivedMemoDialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import useLoading from "../hooks/useLoading"; -import useI18n from "../hooks/useI18n"; import { memoService } from "../services"; import { useAppSelector } from "../store"; import Icon from "./Icon"; @@ -12,7 +12,7 @@ import "../less/archived-memo-dialog.less"; type Props = DialogProps; const ArchivedMemoDialog: React.FC = (props: Props) => { - const { t } = useI18n(); + const { t } = useTranslation(); const { destroy } = props; const memos = useAppSelector((state) => state.memo.memos); const loadingState = useLoading(); diff --git a/web/src/components/ChangeMemoCreatedTsDialog.tsx b/web/src/components/ChangeMemoCreatedTsDialog.tsx index cc2ddf86..3cb912aa 100644 --- a/web/src/components/ChangeMemoCreatedTsDialog.tsx +++ b/web/src/components/ChangeMemoCreatedTsDialog.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from "react"; import dayjs from "dayjs"; -import useI18n from "../hooks/useI18n"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { memoService } from "../services"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -12,7 +12,7 @@ interface Props extends DialogProps { } const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { - const { t } = useI18n(); + const { t } = useTranslation(); const { destroy, memoId } = props; const [createdAt, setCreatedAt] = useState(""); const maxDatetimeValue = dayjs().format("YYYY-MM-DDTHH:mm"); diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index 83c24032..4e62a399 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { validate, ValidatorConfig } from "../helpers/validator"; -import useI18n from "../hooks/useI18n"; import { userService } from "../services"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -17,7 +17,7 @@ const validateConfig: ValidatorConfig = { type Props = DialogProps; const ChangePasswordDialog: React.FC = ({ destroy }: Props) => { - const { t } = useI18n(); + const { t } = useTranslation(); const [newPassword, setNewPassword] = useState(""); const [newPasswordAgain, setNewPasswordAgain] = useState(""); diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx index ab40200b..fc73999e 100644 --- a/web/src/components/CreateShortcutDialog.tsx +++ b/web/src/components/CreateShortcutDialog.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { memoService, shortcutService } from "../services"; import { checkShouldShowMemoWithFilters, filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter"; import useLoading from "../hooks/useLoading"; @@ -6,7 +7,6 @@ import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import toastHelper from "./Toast"; import Selector from "./common/Selector"; -import useI18n from "../hooks/useI18n"; import "../less/create-shortcut-dialog.less"; interface Props extends DialogProps { @@ -19,7 +19,7 @@ const CreateShortcutDialog: React.FC = (props: Props) => { const [title, setTitle] = useState(""); const [filters, setFilters] = useState([]); const requestState = useLoading(false); - const { t } = useI18n(); + const { t } = useTranslation(); const shownMemoLength = memoService.getState().memos.filter((memo) => { return checkShouldShowMemoWithFilters(memo, filters); diff --git a/web/src/components/DailyReviewDialog.tsx b/web/src/components/DailyReviewDialog.tsx index dd8bbf44..f387e292 100644 --- a/web/src/components/DailyReviewDialog.tsx +++ b/web/src/components/DailyReviewDialog.tsx @@ -1,7 +1,7 @@ import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useAppSelector } from "../store"; import toImage from "../labs/html2image"; -import useI18n from "../hooks/useI18n"; import useToggle from "../hooks/useToggle"; import { DAILY_TIMESTAMP } from "../helpers/consts"; import * as utils from "../helpers/utils"; @@ -20,7 +20,7 @@ const monthChineseStrArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", " const weekdayChineseStrArray = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const DailyReviewDialog: React.FC = (props: Props) => { - const { t } = useI18n(); + const { t } = useTranslation(); const memos = useAppSelector((state) => state.memo.memos); const [currentDateStamp, setCurrentDateStamp] = useState(utils.getDateStampByDate(utils.getDateString(props.currentDateStamp))); const [showDatePicker, toggleShowDatePicker] = useToggle(false); diff --git a/web/src/components/Editor/Editor.tsx b/web/src/components/Editor/Editor.tsx index fd953264..ed2d9445 100644 --- a/web/src/components/Editor/Editor.tsx +++ b/web/src/components/Editor/Editor.tsx @@ -1,5 +1,5 @@ import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef } from "react"; -import useI18n from "../../hooks/useI18n"; +import { useTranslation } from "react-i18next"; import useRefresh from "../../hooks/useRefresh"; import Only from "../common/OnlyWhen"; import "../../less/editor.less"; @@ -35,7 +35,7 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef(null); const refresh = useRefresh(); diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index fa6858eb..a6a13da0 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -2,8 +2,8 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { indexOf } from "lodash-es"; import { memo, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import "dayjs/locale/zh"; -import useI18n from "../hooks/useI18n"; import { UNKNOWN_ID } from "../helpers/consts"; import { DONE_BLOCK_REG, TODO_BLOCK_REG } from "../helpers/marked"; import { editorStateService, locationService, memoService, userService } from "../services"; @@ -32,8 +32,8 @@ export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): s const Memo: React.FC = (props: Props) => { const memo = props.memo; - const { t, locale } = useI18n(); - const [createdAtStr, setCreatedAtStr] = useState(getFormatedMemoCreatedAtStr(memo.createdTs, locale)); + const { t, i18n } = useTranslation(); + const [createdAtStr, setCreatedAtStr] = useState(getFormatedMemoCreatedAtStr(memo.createdTs, i18n.language)); const memoContainerRef = useRef(null); const memoContentContainerRef = useRef(null); const isVisitorMode = userService.isVisitorMode(); @@ -42,14 +42,14 @@ const Memo: React.FC = (props: Props) => { let intervalFlag = -1; if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) { intervalFlag = setInterval(() => { - setCreatedAtStr(getFormatedMemoCreatedAtStr(memo.createdTs, locale)); + setCreatedAtStr(getFormatedMemoCreatedAtStr(memo.createdTs, i18n.language)); }, 1000 * 1); } return () => { clearInterval(intervalFlag); }; - }, [locale]); + }, [i18n.language]); const handleShowMemoStoryDialog = () => { showMemoCardDialog(memo); diff --git a/web/src/components/MemoCardDialog.tsx b/web/src/components/MemoCardDialog.tsx index 24c5ef97..76daafa5 100644 --- a/web/src/components/MemoCardDialog.tsx +++ b/web/src/components/MemoCardDialog.tsx @@ -1,5 +1,6 @@ import copy from "copy-to-clipboard"; import { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { editorStateService, memoService, userService } from "../services"; import { useAppSelector } from "../store"; import { UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts"; @@ -13,7 +14,6 @@ import Selector from "./common/Selector"; import MemoContent from "./MemoContent"; import MemoResources from "./MemoResources"; import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; -import useI18n from "../hooks/useI18n"; import "../less/memo-card-dialog.less"; interface LinkedMemo extends Memo { @@ -26,13 +26,13 @@ interface Props extends DialogProps { } const MemoCardDialog: React.FC = (props: Props) => { + const { t } = useTranslation(); const memos = useAppSelector((state) => state.memo.memos); const [memo, setMemo] = useState({ ...props.memo, }); const [linkMemos, setLinkMemos] = useState([]); const [linkedMemos, setLinkedMemos] = useState([]); - const { t } = useI18n(); useEffect(() => { const fetchLinkedMemos = async () => { diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index 7737ae0f..a953a041 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -1,8 +1,8 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { IEmojiData } from "emoji-picker-react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { UNKNOWN_ID } from "../helpers/consts"; import { editorStateService, locationService, memoService, resourceService } from "../services"; -import useI18n from "../hooks/useI18n"; import { useAppSelector } from "../store"; import * as storage from "../helpers/storage"; import Icon from "./Icon"; @@ -18,7 +18,7 @@ interface State { } const MemoEditor = () => { - const { t, locale } = useI18n(); + const { t, i18n } = useTranslation(); const user = useAppSelector((state) => state.user.user); const editorState = useAppSelector((state) => state.editor); const tags = useAppSelector((state) => state.memo.tags); @@ -276,7 +276,7 @@ const MemoEditor = () => { onConfirmBtnClick: handleSaveBtnClick, onContentChange: handleContentChange, }), - [isEditing, state.fullscreen, locale, editorFontStyle] + [isEditing, state.fullscreen, i18n.language, editorFontStyle] ); return ( diff --git a/web/src/components/MemoFilter.tsx b/web/src/components/MemoFilter.tsx index 73cd6570..f0bce33e 100644 --- a/web/src/components/MemoFilter.tsx +++ b/web/src/components/MemoFilter.tsx @@ -1,17 +1,16 @@ +import { useTranslation } from "react-i18next"; import { useAppSelector } from "../store"; import { locationService, shortcutService } from "../services"; import * as utils from "../helpers/utils"; import { getTextWithMemoType } from "../helpers/filter"; -import useI18n from "../hooks/useI18n"; import "../less/memo-filter.less"; const MemoFilter = () => { + const { t } = useTranslation(); const query = useAppSelector((state) => state.location.query); - useAppSelector((state) => state.shortcut.shortcuts); const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query; const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null; const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut); - const { t } = useI18n(); return (
diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index 6ec896aa..c0d5e1a3 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; import { memoService, shortcutService } from "../services"; -import useI18n from "../hooks/useI18n"; import { useAppSelector } from "../store"; import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/marked"; import * as utils from "../helpers/utils"; @@ -11,7 +11,7 @@ import Memo from "./Memo"; import "../less/memo-list.less"; const MemoList = () => { - const { t } = useI18n(); + const { t } = useTranslation(); const query = useAppSelector((state) => state.location.query); const { memos, isFetching } = useAppSelector((state) => state.memo); const wrapperElement = useRef(null); diff --git a/web/src/components/MenuBtnsPopup.tsx b/web/src/components/MenuBtnsPopup.tsx index 9505ff01..69b56486 100644 --- a/web/src/components/MenuBtnsPopup.tsx +++ b/web/src/components/MenuBtnsPopup.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { userService } from "../services"; -import useI18n from "../hooks/useI18n"; import Only from "./common/OnlyWhen"; import showAboutSiteDialog from "./AboutSiteDialog"; import showArchivedMemoDialog from "./ArchivedMemoDialog"; @@ -15,7 +15,7 @@ interface Props { const MenuBtnsPopup: React.FC = (props: Props) => { const { shownStatus, setShownStatus } = props; - const { t } = useI18n(); + const { t } = useTranslation(); const navigate = useNavigate(); const popupElRef = useRef(null); diff --git a/web/src/components/ResourcesDialog.tsx b/web/src/components/ResourcesDialog.tsx index eb77a2c5..e4f2878c 100644 --- a/web/src/components/ResourcesDialog.tsx +++ b/web/src/components/ResourcesDialog.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from "react"; import copy from "copy-to-clipboard"; -import useI18n from "../hooks/useI18n"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import useLoading from "../hooks/useLoading"; import { resourceService } from "../services"; import Dropdown from "./common/Dropdown"; @@ -20,7 +20,7 @@ interface State { const ResourcesDialog: React.FC = (props: Props) => { const { destroy } = props; - const { t } = useI18n(); + const { t } = useTranslation(); const loadingState = useLoading(); const [state, setState] = useState({ resources: [], diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index a50d92b9..86444053 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -1,13 +1,13 @@ +import { useTranslation } from "react-i18next"; import { locationService } from "../services"; import { useAppSelector } from "../store"; import { memoSpecialTypes } from "../helpers/filter"; import Icon from "./Icon"; -import useI18n from "../hooks/useI18n"; import "../less/search-bar.less"; const SearchBar = () => { + const { t } = useTranslation(); const memoType = useAppSelector((state) => state.location.query?.type); - const { t } = useI18n(); const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => { const { type: prevType } = locationService.getState().query ?? {}; diff --git a/web/src/components/SettingDialog.tsx b/web/src/components/SettingDialog.tsx index 7df9b92b..2cce3576 100644 --- a/web/src/components/SettingDialog.tsx +++ b/web/src/components/SettingDialog.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { useAppSelector } from "../store"; -import useI18n from "../hooks/useI18n"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import MyAccountSection from "./Settings/MyAccountSection"; @@ -18,7 +18,7 @@ interface State { const SettingDialog: React.FC = (props: Props) => { const { destroy } = props; - const { t } = useI18n(); + const { t } = useTranslation(); const user = useAppSelector((state) => state.user.user); const [state, setState] = useState({ selectedSection: "my-account", diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx index be1bee01..592a536f 100644 --- a/web/src/components/Settings/MemberSection.tsx +++ b/web/src/components/Settings/MemberSection.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from "react"; import { isEmpty } from "lodash-es"; -import useI18n from "../../hooks/useI18n"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { userService } from "../../services"; import { useAppSelector } from "../../store"; import * as api from "../../helpers/api"; @@ -15,7 +15,7 @@ interface State { } const PreferencesSection = () => { - const { t } = useI18n(); + const { t } = useTranslation(); const currentUser = useAppSelector((state) => state.user.user); const [state, setState] = useState({ createUserEmail: "", diff --git a/web/src/components/Settings/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx index b67c4aaf..74f1469f 100644 --- a/web/src/components/Settings/MyAccountSection.tsx +++ b/web/src/components/Settings/MyAccountSection.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import useI18n from "../../hooks/useI18n"; +import { useTranslation } from "react-i18next"; import { useAppSelector } from "../../store"; import { userService } from "../../services"; import { validate, ValidatorConfig } from "../../helpers/validator"; @@ -16,7 +16,7 @@ const validateConfig: ValidatorConfig = { }; const MyAccountSection = () => { - const { t, locale } = useI18n(); + const { t, i18n } = useTranslation(); const user = useAppSelector((state) => state.user.user as User); const [username, setUsername] = useState(user.name); const openAPIRoute = `${window.location.origin}/api/memo?openId=${user.openId}`; @@ -33,7 +33,7 @@ const MyAccountSection = () => { const usernameValidResult = validate(username, validateConfig); if (!usernameValidResult.result) { - toastHelper.error(t("common.username") + locale === "zh" ? "" : " " + usernameValidResult.reason); + toastHelper.error(t("common.username") + i18n.language === "zh" ? "" : " " + usernameValidResult.reason); return; } @@ -42,7 +42,7 @@ const MyAccountSection = () => { id: user.id, name: username, }); - toastHelper.info(t("common.username") + locale === "zh" ? "" : " " + t("common.changed")); + toastHelper.info(t("common.username") + i18n.language === "zh" ? "" : " " + t("common.changed")); } catch (error: any) { console.error(error); toastHelper.error(error.response.data.message); diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index 230a31bb..c3fb7195 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -1,7 +1,7 @@ +import { useTranslation } from "react-i18next"; import { globalService, userService } from "../../services"; import { useAppSelector } from "../../store"; import { VISIBILITY_SELECTOR_ITEMS } from "../../helpers/consts"; -import useI18n from "../../hooks/useI18n"; import Selector from "../common/Selector"; import "../../less/settings/preferences-section.less"; @@ -32,7 +32,7 @@ const editorFontStyleSelectorItems = [ ]; const PreferencesSection = () => { - const { t } = useI18n(); + const { t } = useTranslation(); const { setting } = useAppSelector((state) => state.user.user as User); const handleLocaleChanged = async (value: string) => { diff --git a/web/src/components/ShareMemoImageDialog.tsx b/web/src/components/ShareMemoImageDialog.tsx index e7648953..8e063d1c 100644 --- a/web/src/components/ShareMemoImageDialog.tsx +++ b/web/src/components/ShareMemoImageDialog.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { userService } from "../services"; import toImage from "../labs/html2image"; import { ANIMATION_DURATION } from "../helpers/consts"; -import useI18n from "../hooks/useI18n"; import * as utils from "../helpers/utils"; import { IMAGE_URL_REG } from "../helpers/marked"; import Only from "./common/OnlyWhen"; @@ -18,7 +18,7 @@ interface Props extends DialogProps { const ShareMemoImageDialog: React.FC = (props: Props) => { const { memo: propsMemo, destroy } = props; - const { t } = useI18n(); + const { t } = useTranslation(); const { user: userinfo } = userService.getState(); const memo = { ...propsMemo, diff --git a/web/src/components/ShortcutList.tsx b/web/src/components/ShortcutList.tsx index 1324e0a8..18716180 100644 --- a/web/src/components/ShortcutList.tsx +++ b/web/src/components/ShortcutList.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { locationService, shortcutService } from "../services"; import { useAppSelector } from "../store"; -import useI18n from "../hooks/useI18n"; import * as utils from "../helpers/utils"; import useToggle from "../hooks/useToggle"; import useLoading from "../hooks/useLoading"; @@ -14,7 +14,7 @@ const ShortcutList = () => { const query = useAppSelector((state) => state.location.query); const shortcuts = useAppSelector((state) => state.shortcut.shortcuts); const loadingState = useLoading(); - const { t } = useI18n(); + const { t } = useTranslation(); const pinnedShortcuts = shortcuts .filter((s) => s.rowStatus === "ARCHIVED") @@ -59,7 +59,7 @@ interface ShortcutContainerProps { const ShortcutContainer: React.FC = (props: ShortcutContainerProps) => { const { shortcut, isActive } = props; - const { t } = useI18n(); + const { t } = useTranslation(); const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false); const handleShortcutClick = () => { diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index ae1637c7..96f1cbdf 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { userService } from "../services"; -import useI18n from "../hooks/useI18n"; import Icon from "./Icon"; import Only from "./common/OnlyWhen"; import showDailyReviewDialog from "./DailyReviewDialog"; @@ -12,7 +12,7 @@ import TagList from "./TagList"; import "../less/siderbar.less"; const Sidebar = () => { - const { t } = useI18n(); + const { t } = useTranslation(); const handleSettingBtnClick = () => { showSettingDialog(); diff --git a/web/src/components/TagList.tsx b/web/src/components/TagList.tsx index 5294d123..689a16fe 100644 --- a/web/src/components/TagList.tsx +++ b/web/src/components/TagList.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useAppSelector } from "../store"; import { locationService, memoService, userService } from "../services"; -import useI18n from "../hooks/useI18n"; import useToggle from "../hooks/useToggle"; import Icon from "./Icon"; import Only from "./common/OnlyWhen"; @@ -14,7 +14,7 @@ interface Tag { } const TagList = () => { - const { t } = useI18n(); + const { t } = useTranslation(); const { memos, tags: tagsText } = useAppSelector((state) => state.memo); const query = useAppSelector((state) => state.location.query); const [tags, setTags] = useState([]); diff --git a/web/src/components/UserBanner.tsx b/web/src/components/UserBanner.tsx index 313681e6..a97547d1 100644 --- a/web/src/components/UserBanner.tsx +++ b/web/src/components/UserBanner.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import * as utils from "../helpers/utils"; import userService from "../services/userService"; -import useI18n from "../hooks/useI18n"; import { locationService } from "../services"; import { useAppSelector } from "../store"; import Icon from "./Icon"; @@ -9,7 +9,7 @@ import MenuBtnsPopup from "./MenuBtnsPopup"; import "../less/user-banner.less"; const UserBanner = () => { - const { t } = useI18n(); + const { t } = useTranslation(); const { user, owner } = useAppSelector((state) => state.user); const { memos, tags } = useAppSelector((state) => state.memo); const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false); diff --git a/web/src/components/common/Selector.tsx b/web/src/components/common/Selector.tsx index b400ffa3..30818168 100644 --- a/web/src/components/common/Selector.tsx +++ b/web/src/components/common/Selector.tsx @@ -1,5 +1,5 @@ import { memo, useEffect, useRef } from "react"; -import useI18n from "../../hooks/useI18n"; +import { useTranslation } from "react-i18next"; import useToggle from "../../hooks/useToggle"; import Icon from "../Icon"; import "../../less/common/selector.less"; @@ -23,7 +23,7 @@ const nullItem = { const Selector: React.FC = (props: Props) => { const { className, dataSource, handleValueChanged, value } = props; - const { t } = useI18n(); + const { t } = useTranslation(); const [showSelector, toggleSelectorStatus] = useToggle(false); const seletorElRef = useRef(null); diff --git a/web/src/hooks/useI18n.ts b/web/src/hooks/useI18n.ts deleted file mode 100644 index 7a136858..00000000 --- a/web/src/hooks/useI18n.ts +++ /dev/null @@ -1,3 +0,0 @@ -import useI18n from "../labs/i18n/useI18n"; - -export default useI18n; diff --git a/web/src/i18n.ts b/web/src/i18n.ts new file mode 100644 index 00000000..11d8451f --- /dev/null +++ b/web/src/i18n.ts @@ -0,0 +1,23 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import enLocale from "./locales/en.json"; +import zhLocale from "./locales/zh.json"; +import viLocale from "./locales/vi.json"; + +i18n.use(initReactI18next).init({ + resources: { + en: { + translation: enLocale, + }, + zh: { + translation: zhLocale, + }, + vi: { + translation: viLocale, + }, + }, + lng: "en", + fallbackLng: "en", +}); + +export default i18n; diff --git a/web/src/labs/i18n/I18nProvider.tsx b/web/src/labs/i18n/I18nProvider.tsx deleted file mode 100644 index 1a717adc..00000000 --- a/web/src/labs/i18n/I18nProvider.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { createContext, useEffect, useState } from "react"; -import i18nStore from "./i18nStore"; - -interface Props { - children: React.ReactElement; -} - -const i18nContext = createContext(i18nStore.getState()); - -const I18nProvider: React.FC = (props: Props) => { - const { children } = props; - const [i18nState, setI18nState] = useState(i18nStore.getState()); - - useEffect(() => { - const unsubscribe = i18nStore.subscribe((ns) => { - setI18nState(ns); - }); - - return () => { - unsubscribe(); - }; - }, []); - - return {children}; -}; - -export default I18nProvider; diff --git a/web/src/labs/i18n/createI18nStore.tsx b/web/src/labs/i18n/createI18nStore.tsx deleted file mode 100644 index 83c8ace8..00000000 --- a/web/src/labs/i18n/createI18nStore.tsx +++ /dev/null @@ -1,52 +0,0 @@ -type I18nState = Readonly<{ - locale: string; -}>; - -type Listener = (ns: I18nState, ps?: I18nState) => void; - -const createI18nStore = (preloadedState: I18nState) => { - const listeners: Listener[] = []; - let currentState = preloadedState; - - const getState = () => { - return currentState; - }; - - const setState = (state: Partial) => { - const nextState = { - ...currentState, - ...state, - }; - const prevState = currentState; - currentState = nextState; - - for (const cb of listeners) { - cb(currentState, prevState); - } - }; - - const subscribe = (listener: Listener) => { - let isSubscribed = true; - listeners.push(listener); - - const unsubscribe = () => { - if (!isSubscribed) { - return; - } - - const index = listeners.indexOf(listener); - listeners.splice(index, 1); - isSubscribed = false; - }; - - return unsubscribe; - }; - - return { - getState, - setState, - subscribe, - }; -}; - -export default createI18nStore; diff --git a/web/src/labs/i18n/i18nStore.ts b/web/src/labs/i18n/i18nStore.ts deleted file mode 100644 index a3a86ae4..00000000 --- a/web/src/labs/i18n/i18nStore.ts +++ /dev/null @@ -1,9 +0,0 @@ -import createI18nStore from "./createI18nStore"; - -const defaultI18nState = { - locale: "en", -}; - -const i18nStore = createI18nStore(defaultI18nState); - -export default i18nStore; diff --git a/web/src/labs/i18n/useI18n.ts b/web/src/labs/i18n/useI18n.ts deleted file mode 100644 index 3f74b3e5..00000000 --- a/web/src/labs/i18n/useI18n.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect, useState } from "react"; -import i18nStore from "./i18nStore"; -import enLocale from "../../locales/en.json"; -import zhLocale from "../../locales/zh.json"; -import viLocale from "../../locales/vi.json"; - -const resources: Record = { - en: enLocale, - zh: zhLocale, - vi: viLocale, -}; - -const useI18n = () => { - const [{ locale }, setState] = useState(i18nStore.getState()); - - useEffect(() => { - const unsubscribe = i18nStore.subscribe((ns) => { - setState(ns); - }); - - return () => { - unsubscribe(); - }; - }, []); - - const translate = (key: string): string => { - const keys = key.split("."); - let value = resources[locale]; - for (const k of keys) { - if (value) { - value = value[k]; - } else { - return key; - } - } - - if (value) { - return value; - } else { - return key; - } - }; - - const setLocale = (locale: Locale) => { - i18nStore.setState({ - locale, - }); - }; - - return { - t: translate, - locale, - setLocale, - }; -}; - -export default useI18n; diff --git a/web/src/main.tsx b/web/src/main.tsx index bcdf9af8..8a1b1e5f 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,8 +1,8 @@ import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; -import I18nProvider from "./labs/i18n/I18nProvider"; import store from "./store"; import App from "./App"; +import "./i18n"; import "./helpers/polyfill"; import "./less/global.less"; import "./css/index.css"; @@ -10,9 +10,7 @@ import "./css/index.css"; const container = document.getElementById("root"); const root = createRoot(container as HTMLElement); root.render( - - - - - + + + ); diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index b9bfca67..20ad1091 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import * as api from "../helpers/api"; import { validate, ValidatorConfig } from "../helpers/validator"; -import useI18n from "../hooks/useI18n"; import useLoading from "../hooks/useLoading"; import { globalService, userService } from "../services"; import Icon from "../components/Icon"; @@ -18,7 +18,7 @@ const validateConfig: ValidatorConfig = { }; const Auth = () => { - const { t, locale } = useI18n(); + const { t, i18n } = useTranslation(); const navigate = useNavigate(); const pageLoadingState = useLoading(true); const [siteHost, setSiteHost] = useState(); @@ -157,15 +157,15 @@ const Auth = () => {
- handleLocaleItemClick("en")}> + handleLocaleItemClick("en")}> English / - handleLocaleItemClick("zh")}> + handleLocaleItemClick("zh")}> 中文 / - handleLocaleItemClick("vi")}> + handleLocaleItemClick("vi")}> Tiếng Việt
diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 45ac8137..5c322f7a 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -1,10 +1,10 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import { memoService, userService } from "../services"; import { isNullorUndefined } from "../helpers/utils"; import { useAppSelector } from "../store"; -import useI18n from "../hooks/useI18n"; import useQuery from "../hooks/useQuery"; import useLoading from "../hooks/useLoading"; import Only from "../components/common/OnlyWhen"; @@ -17,7 +17,7 @@ interface State { } const Explore = () => { - const { t, locale } = useI18n(); + const { t, i18n } = useTranslation(); const navigate = useNavigate(); const query = useQuery(); const user = useAppSelector((state) => state.user.user); @@ -78,7 +78,7 @@ const Explore = () => {
{state.memos.length > 0 ? ( state.memos.map((memo) => { - const createdAtStr = dayjs(memo.createdTs).locale(locale).format("YYYY/MM/DD HH:mm:ss"); + const createdAtStr = dayjs(memo.createdTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss"); return (
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 01415ac0..f43fdaba 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,8 +1,8 @@ import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; import { globalService, userService } from "../services"; import { useAppSelector } from "../store"; -import useI18n from "../hooks/useI18n"; import { isNullorUndefined } from "../helpers/utils"; import Only from "../components/common/OnlyWhen"; import toastHelper from "../components/Toast"; @@ -14,7 +14,7 @@ import MemoList from "../components/MemoList"; import "../less/home.less"; function Home() { - const { t } = useI18n(); + const { t } = useTranslation(); const location = useLocation(); const navigate = useNavigate(); const user = useAppSelector((state) => state.user.user); diff --git a/web/yarn.lock b/web/yarn.lock index daf0c554..2442ec68 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1001,7 +1001,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/runtime@^7.11.2", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.8.4": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== @@ -2543,6 +2543,20 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + +i18next@^21.9.2: + version "21.9.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.9.2.tgz#3f7c5594393eb27117c1db4c38f5ec766e68de0e" + integrity sha512-00fVrLQOwy45nm3OtC9l1WiLK3nJlIYSljgCt0qzTaAy65aciMdRy9GsuW+a2AtKtdg9/njUGfRH30LRupV7ZQ== + dependencies: + "@babel/runtime" "^7.17.2" + iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -3307,6 +3321,14 @@ react-feather@^2.0.10: dependencies: prop-types "^15.7.2" +react-i18next@^11.18.6: + version "11.18.6" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887" + integrity sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA== + dependencies: + "@babel/runtime" "^7.14.5" + html-parse-stringify "^3.0.1" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -3936,6 +3958,11 @@ vite@^3.0.0: optionalDependencies: fsevents "~2.3.2" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"