chore: add i18n based with useContext

This commit is contained in:
boojack
2022-08-07 22:48:22 +08:00
parent 735938395b
commit 646a41e931
14 changed files with 168 additions and 45 deletions

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import * as api from "../helpers/api"; import * as api from "../helpers/api";
import useI18n from "../hooks/useI18n";
import Only from "./common/OnlyWhen"; import Only from "./common/OnlyWhen";
import Icon from "./Icon"; import Icon from "./Icon";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
@@ -10,6 +11,7 @@ interface Props extends DialogProps {}
const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => { const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
const [profile, setProfile] = useState<Profile>(); const [profile, setProfile] = useState<Profile>();
const { t, setLocale } = useI18n();
useEffect(() => { useEffect(() => {
try { try {
@@ -25,6 +27,10 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
version: "0.0.0", version: "0.0.0",
}); });
} }
setTimeout(() => {
setLocale("zh");
}, 2333);
}, []); }, []);
const handleCloseBtnClick = () => { const handleCloseBtnClick = () => {
@@ -35,7 +41,8 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
<> <>
<div className="dialog-header-container"> <div className="dialog-header-container">
<p className="title-text"> <p className="title-text">
<span className="icon-text">🤠</span>About <b>Memos</b> <span className="icon-text">🤠</span>
{t("about")} <b>Memos</b>
</p> </p>
<button className="btn close-btn" onClick={handleCloseBtnClick}> <button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X /> <Icon.X />

View File

@@ -1,27 +0,0 @@
import { useCallback, useRef } from "react";
/**
* useDebounce: useRef + useCallback
* @param func function
* @param delay delay duration
* @param deps depends
* @returns debounced function
*/
export default function useDebounce<T extends (...args: any[]) => any>(func: T, delay: number, deps: any[] = []): T {
const timer = useRef<number>();
const cancel = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current);
}
}, []);
const run = useCallback((...args: any) => {
cancel();
timer.current = window.setTimeout(() => {
func(...args);
}, delay);
}, deps);
return run as T;
}

3
web/src/hooks/useI18n.ts Normal file
View File

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

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
export default function useLoading(initialState = true) { const useLoading = (initialState = true) => {
const [state, setState] = useState({ isLoading: initialState, isFailed: false, isSucceed: false }); const [state, setState] = useState({ isLoading: initialState, isFailed: false, isSucceed: false });
return { return {
@@ -30,4 +30,6 @@ export default function useLoading(initialState = true) {
}); });
}, },
}; };
} };
export default useLoading;

View File

@@ -1,6 +1,6 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
export default function useRefresh() { const useRefresh = () => {
const [, setBoolean] = useState<boolean>(false); const [, setBoolean] = useState<boolean>(false);
const refresh = useCallback(() => { const refresh = useCallback(() => {
@@ -10,4 +10,6 @@ export default function useRefresh() {
}, []); }, []);
return refresh; return refresh;
} };
export default useRefresh;

View File

@@ -1,7 +1,7 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
// Parameter is the boolean, with default "false" value // Parameter is the boolean, with default "false" value
export default function useToggle(initialState = false): [boolean, (nextState?: boolean) => void] { const useToggle = (initialState = false): [boolean, (nextState?: boolean) => void] => {
// Initialize the state // Initialize the state
const [state, setState] = useState(initialState); const [state, setState] = useState(initialState);
@@ -16,4 +16,6 @@ export default function useToggle(initialState = false): [boolean, (nextState?:
}, []); }, []);
return [state, toggle]; return [state, toggle];
} };
export default useToggle;

View File

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,58 @@
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,
};
};
const defaultI18nState = {
locale: "en",
};
const i18nStore = createI18nStore(defaultI18nState);
export default i18nStore;

View File

@@ -0,0 +1,47 @@
import { useEffect, useState } from "react";
import i18nStore from "./i18nStore";
import enLocale from "../../locales/en.json";
import zhLocale from "../../locales/zh.json";
type Locale = "en" | "zh";
const resources: Record<string, any> = {
en: enLocale,
zh: zhLocale,
};
const useI18n = () => {
const [{ locale }, setState] = useState(i18nStore.getState());
useEffect(() => {
const unsubscribe = i18nStore.subscribe((ns) => {
setState(ns);
});
return () => {
unsubscribe();
};
}, []);
const translate = (key: string) => {
try {
return resources[locale][key] as string;
} catch (error) {
return key;
}
};
const setLocale = (locale: Locale) => {
i18nStore.setState({
locale,
});
};
return {
t: translate,
locale,
setLocale,
};
};
export default useI18n;

3
web/src/locales/en.json Normal file
View File

@@ -0,0 +1,3 @@
{
"about": "About"
}

3
web/src/locales/zh.json Normal file
View File

@@ -0,0 +1,3 @@
{
"about": "关于"
}

View File

@@ -1,5 +1,6 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import I18nProvider from "./labs/i18n/I18nProvider";
import store from "./store"; import store from "./store";
import App from "./App"; import App from "./App";
import "./helpers/polyfill"; import "./helpers/polyfill";
@@ -9,7 +10,9 @@ import "./css/index.css";
const container = document.getElementById("root"); const container = document.getElementById("root");
const root = createRoot(container as HTMLElement); const root = createRoot(container as HTMLElement);
root.render( root.render(
<I18nProvider>
<Provider store={store}> <Provider store={store}>
<App /> <App />
</Provider> </Provider>
</I18nProvider>
); );

View File

@@ -14,11 +14,6 @@ const resourceService = {
const resourceList = data.map((m) => convertResponseModelResource(m)); const resourceList = data.map((m) => convertResponseModelResource(m));
return resourceList; return resourceList;
}, },
/**
* Upload resource file to server,
* @param file file
* @returns resource: id, filename
*/
async upload(file: File): Promise<Resource> { async upload(file: File): Promise<Resource> {
const { name: filename, size } = file; const { name: filename, size } = file;

View File

@@ -1,5 +1,5 @@
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import { TypedUseSelectorHook, useSelector } from "react-redux";
import userReducer from "./modules/user"; import userReducer from "./modules/user";
import memoReducer from "./modules/memo"; import memoReducer from "./modules/memo";
import editorReducer from "./modules/editor"; import editorReducer from "./modules/editor";
@@ -17,9 +17,7 @@ const store = configureStore({
}); });
type AppState = ReturnType<typeof store.getState>; type AppState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector; export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export default store; export default store;