From 7e16f16c7670e1a23d2b73602a89e8b071b2e290 Mon Sep 17 00:00:00 2001 From: slhmy <1484836413@qq.com> Date: Tue, 26 Sep 2023 20:11:13 +0800 Subject: [PATCH] Support i18n --- package-lock.json | 61 +++++++++++++++++++++++++++++ package.json | 2 + src/i18n.ts | 27 +++++++++++++ src/i18n/README.md | 1 - src/i18n/en_US.ts | 11 ++++++ src/i18n/zh_CN.ts | 11 ++++++ src/index.tsx | 6 ++- src/layouts/adminLayout/Sidebar.tsx | 6 ++- 8 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 src/i18n.ts delete mode 100644 src/i18n/README.md create mode 100644 src/i18n/en_US.ts create mode 100644 src/i18n/zh_CN.ts diff --git a/package-lock.json b/package-lock.json index 6978b5ca8..50cc0c486 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,10 +26,12 @@ "axios": "^1.3.4", "framer-motion": "^10.16.1", "graphql": "16.6.0", + "i18next": "^23.5.1", "monaco-editor": "^0.39.0", "msw": "^1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^13.2.2", "react-markdown": "^8.0.7", "react-redux": "^8.0.4", "react-router-dom": "^6.4.2", @@ -8827,6 +8829,14 @@ "node": ">=12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -8852,6 +8862,28 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "23.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.5.1.tgz", + "integrity": "sha512-JelYzcaCoFDaa+Ysbfz2JsGAKkrHiMG6S61+HLBUEIPaF40WMwW9hCPymlQGrP+wWawKxKPuSuD71WZscCsWHg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.22.5" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -11370,6 +11402,27 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.2.2.tgz", + "integrity": "sha512-+nFUkbRByFwnrfDcYqvzBuaeZb+nACHx+fAWN/pZMddWOCJH5hoc21+Sa/N/Lqi6ne6/9wC/qRGOoQhJa6IkEQ==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -13202,6 +13255,14 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 7c566bc2f..cf8475bdc 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,12 @@ "axios": "^1.3.4", "framer-motion": "^10.16.1", "graphql": "16.6.0", + "i18next": "^23.5.1", "monaco-editor": "^0.39.0", "msw": "^1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^13.2.2", "react-markdown": "^8.0.7", "react-redux": "^8.0.4", "react-router-dom": "^6.4.2", diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 000000000..bfa791c84 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,27 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import EN_US_TRANSLATIONS from "./i18n/en_US"; +import ZH_CN_TRANSLATIONS from "./i18n/zh_CN"; + +// the translations +// (tip move them in a JSON file and import them, +// or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files) +const resources = { + en_US: EN_US_TRANSLATIONS, + zh_CN: ZH_CN_TRANSLATIONS, +}; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + lng: "en_US", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + + interpolation: { + escapeValue: false, // react already safes from xss + }, + }); + +export default i18n; diff --git a/src/i18n/README.md b/src/i18n/README.md deleted file mode 100644 index aae636455..000000000 --- a/src/i18n/README.md +++ /dev/null @@ -1 +0,0 @@ -# i18n \ No newline at end of file diff --git a/src/i18n/en_US.ts b/src/i18n/en_US.ts new file mode 100644 index 000000000..772cd5266 --- /dev/null +++ b/src/i18n/en_US.ts @@ -0,0 +1,11 @@ +import { Resource } from "i18next"; + +const EN_US_TRANSLATIONS: Resource = { + translation: { + Problem: "Problem", + User: "User", + Contest: "Contest", + }, +}; + +export default EN_US_TRANSLATIONS; diff --git a/src/i18n/zh_CN.ts b/src/i18n/zh_CN.ts new file mode 100644 index 000000000..cbbdf64d0 --- /dev/null +++ b/src/i18n/zh_CN.ts @@ -0,0 +1,11 @@ +import { Resource } from "i18next"; + +const ZH_CN_TRANSLATIONS: Resource = { + translation: { + Problem: "问题", + User: "用户", + Contest: "竞赛", + }, +}; + +export default ZH_CN_TRANSLATIONS; diff --git a/src/index.tsx b/src/index.tsx index ae3a6ebe0..93962b25b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,10 +1,12 @@ +import App from "./App"; import React from "react"; import ReactDOM from "react-dom/client"; -import "./index.css"; -import App from "./App"; import reportWebVitals from "./reportWebVitals"; import { worker } from "./mocks/server"; +import "./i18n"; +import "./index.css"; + console.log("Running in:", import.meta.env.MODE); if (import.meta.env.MODE === "mock") { worker.start(); diff --git a/src/layouts/adminLayout/Sidebar.tsx b/src/layouts/adminLayout/Sidebar.tsx index 6cf5d9163..de82c60c1 100644 --- a/src/layouts/adminLayout/Sidebar.tsx +++ b/src/layouts/adminLayout/Sidebar.tsx @@ -8,6 +8,7 @@ import { } from "@heroicons/react/24/outline"; import { ChevronDownIcon } from "@heroicons/react/20/solid"; import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; const navigation = [ { name: "Problem", href: "/admin/problem", icon: HomeIcon, current: true }, @@ -34,6 +35,7 @@ interface SidebarProps { export default function Sidebar(props: SidebarProps) { const [sidebarOpen, setSidebarOpen] = useState(false); const navigate = useNavigate(); + const { t } = useTranslation(); return ( <> @@ -126,7 +128,7 @@ export default function Sidebar(props: SidebarProps) { )} aria-hidden="true" /> - {item.name} + {t(item.name)} ))} @@ -177,7 +179,7 @@ export default function Sidebar(props: SidebarProps) { )} aria-hidden="true" /> - {item.name} + {t(item.name)} ))}