mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: add i18n
based with useContext
This commit is contained in:
@@ -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 />
|
||||||
|
@@ -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
3
web/src/hooks/useI18n.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import useI18n from "../labs/i18n/useI18n";
|
||||||
|
|
||||||
|
export default useI18n;
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
27
web/src/labs/i18n/I18nProvider.tsx
Normal file
27
web/src/labs/i18n/I18nProvider.tsx
Normal 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;
|
58
web/src/labs/i18n/i18nStore.ts
Normal file
58
web/src/labs/i18n/i18nStore.ts
Normal 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;
|
47
web/src/labs/i18n/useI18n.ts
Normal file
47
web/src/labs/i18n/useI18n.ts
Normal 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
3
web/src/locales/en.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"about": "About"
|
||||||
|
}
|
3
web/src/locales/zh.json
Normal file
3
web/src/locales/zh.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"about": "关于"
|
||||||
|
}
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user