feat: use i18next

This commit is contained in:
Steven 2022-09-19 22:27:50 +08:00
parent 307483e499
commit 366afdd1e4
39 changed files with 133 additions and 232 deletions

View File

@ -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"

View File

@ -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,
});

View File

@ -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<Props> = ({ destroy }: Props) => {
const { t } = useI18n();
const { t } = useTranslation();
const [profile, setProfile] = useState<Profile>();
useEffect(() => {

View File

@ -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: Props) => {
createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
archivedAtStr: utils.getDateTimeString(propsMemo.updatedTs ?? Date.now()),
};
const { t } = useI18n();
const { t } = useTranslation();
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);

View File

@ -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: Props) => {
const { t } = useI18n();
const { t } = useTranslation();
const { destroy } = props;
const memos = useAppSelector((state) => state.memo.memos);
const loadingState = useLoading();

View File

@ -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: Props) => {
const { t } = useI18n();
const { t } = useTranslation();
const { destroy, memoId } = props;
const [createdAt, setCreatedAt] = useState("");
const maxDatetimeValue = dayjs().format("YYYY-MM-DDTHH:mm");

View File

@ -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<Props> = ({ destroy }: Props) => {
const { t } = useI18n();
const { t } = useTranslation();
const [newPassword, setNewPassword] = useState("");
const [newPasswordAgain, setNewPasswordAgain] = useState("");

View File

@ -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: Props) => {
const [title, setTitle] = useState<string>("");
const [filters, setFilters] = useState<Filter[]>([]);
const requestState = useLoading(false);
const { t } = useI18n();
const { t } = useTranslation();
const shownMemoLength = memoService.getState().memos.filter((memo) => {
return checkShouldShowMemoWithFilters(memo, filters);

View File

@ -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: 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);

View File

@ -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<EditorRef
onConfirmBtnClick: handleConfirmBtnClickCallback,
onContentChange: handleContentChangeCallback,
} = props;
const { t } = useI18n();
const { t } = useTranslation();
const editorRef = useRef<HTMLTextAreaElement>(null);
const refresh = useRefresh();

View File

@ -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: Props) => {
const memo = props.memo;
const { t, locale } = useI18n();
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs, locale));
const { t, i18n } = useTranslation();
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs, i18n.language));
const memoContainerRef = useRef<HTMLDivElement>(null);
const memoContentContainerRef = useRef<HTMLDivElement>(null);
const isVisitorMode = userService.isVisitorMode();
@ -42,14 +42,14 @@ const Memo: React.FC<Props> = (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);

View File

@ -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: Props) => {
const { t } = useTranslation();
const memos = useAppSelector((state) => state.memo.memos);
const [memo, setMemo] = useState<Memo>({
...props.memo,
});
const [linkMemos, setLinkMemos] = useState<LinkedMemo[]>([]);
const [linkedMemos, setLinkedMemos] = useState<LinkedMemo[]>([]);
const { t } = useI18n();
useEffect(() => {
const fetchLinkedMemos = async () => {

View File

@ -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 (

View File

@ -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 (
<div className={`filter-query-container ${showFilter ? "" : "!hidden"}`}>

View File

@ -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<HTMLDivElement>(null);

View File

@ -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: Props) => {
const { shownStatus, setShownStatus } = props;
const { t } = useI18n();
const { t } = useTranslation();
const navigate = useNavigate();
const popupElRef = useRef<HTMLDivElement>(null);

View File

@ -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: Props) => {
const { destroy } = props;
const { t } = useI18n();
const { t } = useTranslation();
const loadingState = useLoading();
const [state, setState] = useState<State>({
resources: [],

View File

@ -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 ?? {};

View File

@ -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: Props) => {
const { destroy } = props;
const { t } = useI18n();
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user);
const [state, setState] = useState<State>({
selectedSection: "my-account",

View File

@ -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<State>({
createUserEmail: "",

View File

@ -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<string>(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);

View File

@ -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) => {

View File

@ -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: Props) => {
const { memo: propsMemo, destroy } = props;
const { t } = useI18n();
const { t } = useTranslation();
const { user: userinfo } = userService.getState();
const memo = {
...propsMemo,

View File

@ -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<ShortcutContainerProps> = (props: ShortcutContainerProps) => {
const { shortcut, isActive } = props;
const { t } = useI18n();
const { t } = useTranslation();
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
const handleShortcutClick = () => {

View File

@ -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();

View File

@ -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<Tag[]>([]);

View File

@ -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);

View File

@ -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: Props) => {
const { className, dataSource, handleValueChanged, value } = props;
const { t } = useI18n();
const { t } = useTranslation();
const [showSelector, toggleSelectorStatus] = useToggle(false);
const seletorElRef = useRef<HTMLDivElement>(null);

View File

@ -1,3 +0,0 @@
import useI18n from "../labs/i18n/useI18n";
export default useI18n;

23
web/src/i18n.ts Normal file
View File

@ -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;

View File

@ -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: Props) => {
const { children } = props;
const [i18nState, setI18nState] = useState(i18nStore.getState());
useEffect(() => {
const unsubscribe = i18nStore.subscribe((ns) => {
setI18nState(ns);
});
return () => {
unsubscribe();
};
}, []);
return <i18nContext.Provider value={i18nState}>{children}</i18nContext.Provider>;
};
export default I18nProvider;

View File

@ -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<I18nState>) => {
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;

View File

@ -1,9 +0,0 @@
import createI18nStore from "./createI18nStore";
const defaultI18nState = {
locale: "en",
};
const i18nStore = createI18nStore(defaultI18nState);
export default i18nStore;

View File

@ -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<string, any> = {
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;

View File

@ -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(
<I18nProvider>
<Provider store={store}>
<App />
</Provider>
</I18nProvider>
<Provider store={store}>
<App />
</Provider>
);

View File

@ -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<User>();
@ -157,15 +157,15 @@ const Auth = () => {
</div>
<div className="footer-container">
<div className="language-container">
<span className={`locale-item ${locale === "en" ? "active" : ""}`} onClick={() => handleLocaleItemClick("en")}>
<span className={`locale-item ${i18n.language === "en" ? "active" : ""}`} onClick={() => handleLocaleItemClick("en")}>
English
</span>
<span className="split-line">/</span>
<span className={`locale-item ${locale === "zh" ? "active" : ""}`} onClick={() => handleLocaleItemClick("zh")}>
<span className={`locale-item ${i18n.language === "zh" ? "active" : ""}`} onClick={() => handleLocaleItemClick("zh")}>
</span>
<span className="split-line">/</span>
<span className={`locale-item ${locale === "vi" ? "active" : ""}`} onClick={() => handleLocaleItemClick("vi")}>
<span className={`locale-item ${i18n.language === "vi" ? "active" : ""}`} onClick={() => handleLocaleItemClick("vi")}>
Tiếng Việt
</span>
</div>

View File

@ -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 = () => {
<main className="memos-wrapper">
{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 (
<div className="memo-container" key={memo.id}>
<div className="memo-header">

View File

@ -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);

View File

@ -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"