From 2f843862b467c1758846648ac0864b42bd363b89 Mon Sep 17 00:00:00 2001 From: cnwhy Date: Thu, 13 Jun 2024 18:27:46 +0800 Subject: [PATCH] Add i18n --- .gitignore | 3 +- package-lock.json | 77 ++++++++++++++++++++++++++++ package.json | 10 +++- script/language.js | 78 +++++++++++++++++++++++++++++ src/App.tsx | 14 +++++- src/Pages/Home.tsx | 32 ++++++------ src/Pages/Manage.tsx | 47 +++++++++++------- src/components/Forms/AddButton.tsx | 8 +-- src/components/Forms/Json.tsx | 59 ++++++++++++---------- src/components/Language.tsx | 17 +++++++ src/components/List/Public.tsx | 33 +++++++----- src/components/ListCard/index.tsx | 19 +++++-- src/components/LocalServers.tsx | 5 +- src/components/Theme.tsx | 2 +- src/components/viewer/chain.tsx | 6 ++- src/components/viewer/node.tsx | 6 ++- src/components/viewer/services.tsx | 9 ++-- src/i18n/languages.csv | 80 ++++++++++++++++++++++++++++++ src/templates/admissions.ts | 9 ++-- src/templates/authers.ts | 26 ++-------- src/templates/bypasses.ts | 10 ++-- src/templates/chains.ts | 9 ++-- src/templates/climiters.ts | 10 ++-- src/templates/default.ts | 39 +++++++++++++-- src/templates/hops.ts | 10 ++-- src/templates/hosts.ts | 40 ++++++++------- src/templates/ingresses.ts | 9 ++-- src/templates/limiters.ts | 12 +++-- src/templates/observers.ts | 10 ++-- src/templates/otherOrigin.ts | 4 +- src/templates/resolvers.ts | 58 +++------------------- src/templates/rlimiters.ts | 12 +++-- src/templates/routers.ts | 10 ++-- src/templates/sds.ts | 7 +-- src/templates/services.ts | 44 ++++++++-------- src/templates/type.ts | 10 ++-- src/uitls/i18n.ts | 34 +++++++++++++ src/uitls/require.ts | 5 +- src/uitls/server.ts | 3 +- src/uitls/useMonacoEdit.tsx | 10 +++- 40 files changed, 612 insertions(+), 274 deletions(-) create mode 100644 script/language.js create mode 100644 src/components/Language.tsx create mode 100644 src/i18n/languages.csv create mode 100644 src/uitls/i18n.ts diff --git a/.gitignore b/.gitignore index 5b1329a..a5dfca5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ dist-ssr *.sln *.sw? -public/monaco-editor \ No newline at end of file +public/monaco-editor +src/i18n/*.json \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c299fb6..f8b3a28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "axios": "^1.6.0", "classnames": "^2.5.1", "events": "^3.3.0", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", "idb": "^7.1.1", "idb-open-plus": "^1.0.0", "jsonc-parser": "^3.2.0", @@ -21,6 +23,7 @@ "qs": "^6.11.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^14.1.2", "uuid": "^9.0.1" }, "devDependencies": { @@ -33,6 +36,7 @@ "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", "cpx2": "^7.0.1", + "csv-parse": "^5.5.6", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", @@ -1947,6 +1951,12 @@ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/csv-parse": { + "version": "5.5.6", + "resolved": "https://registry.npmmirror.com/csv-parse/-/csv-parse-5.5.6.tgz", + "integrity": "sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==", + "dev": true + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz", @@ -2812,6 +2822,44 @@ "node": ">= 0.4" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "23.11.5", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-23.11.5.tgz", + "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", + "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.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmmirror.com/idb/-/idb-7.1.1.tgz", @@ -6664,6 +6712,27 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "14.1.2", + "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-14.1.2.tgz", + "integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "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.npmmirror.com/react-is/-/react-is-18.2.0.tgz", @@ -7353,6 +7422,14 @@ "source-map": "^0.7.4" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 44f5569..125b0fa 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,12 @@ "analyzer": "tsc && vite build --mode=analyzer", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", + "init": "npm run init:monaco && npm run init:i18n", + "init:i18n": "node ./script/language.js", "init:monaco": "cpx ./node_modules/monaco-editor/min/** ./public/monaco-editor/min", - "predev": "npm run init:monaco" + "predev": "npm run init", + "prebuild": "npm run init:i18n", + "preanalyzer": "npm run init:i18n" }, "dependencies": { "@ant-design/icons": "^5.2.6", @@ -18,6 +22,8 @@ "axios": "^1.6.0", "classnames": "^2.5.1", "events": "^3.3.0", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", "idb": "^7.1.1", "idb-open-plus": "^1.0.0", "jsonc-parser": "^3.2.0", @@ -26,6 +32,7 @@ "qs": "^6.11.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^14.1.2", "uuid": "^9.0.1" }, "devDependencies": { @@ -38,6 +45,7 @@ "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", "cpx2": "^7.0.1", + "csv-parse": "^5.5.6", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", diff --git a/script/language.js b/script/language.js new file mode 100644 index 0000000..a4d6e19 --- /dev/null +++ b/script/language.js @@ -0,0 +1,78 @@ +import fs from "fs"; +import { writeFile } from "fs/promises"; +import path from "path"; +import url from "url"; +import { parse } from "csv-parse"; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const importFile = path.join(__dirname, "../src/i18n/languages.csv"); +const exportDir = path.join(__dirname, "../src/i18n"); + +let languages; +const langMap = new Map(); +const init = (arr) => { + const [_key, ...langs] = arr; + languages = langs.map((langName) => { + const lang = {}; + langMap.set(langName, lang); + return lang; + }); +}; +const output = () => { + // 保存为json文件 + const langList = []; + const arr = []; + langMap.forEach((json, lang) => { + langList.push({ lang, label: json.language }); + arr.push( + writeFile( + path.join(exportDir, `${lang}.json`), + JSON.stringify(json, undefined, 4) + ) + ); + }); + arr.push( + writeFile( + path.join(exportDir, `languages.json`), + JSON.stringify(langList, undefined, 4) + ) + ); + Promise.all(arr).then(() => console.log("done")); +}; + +const setValue = (obj, key, value) => { + if (!obj || !key) return; + const keys = key.split("."); + let _obj = obj; + while (keys.length > 1) { + const _key = keys.shift(); + if (_obj[_key] === undefined) { + _obj = _obj[_key] = {}; + } else if (typeof _obj[_key] === "object") { + _obj = _obj[_key]; + } else { + console.error(`Key [${key}] error!`); + } + } + const _key = keys[0]; + if (_obj[_key] !== undefined) { + console.error(`Key [${key}] repeat!`); + } + _obj[_key] = value || ""; +}; + +function main(){ + const csvp = parse(); + csvp.on("end", output); + csvp.on("data", function (chunk) { + // 解析language.csv + if (!languages) return init(chunk); // 第一行为表头 + const [key, ...values] = chunk; + values.forEach((value, index) => { + setValue(languages[index], key, value); + }); + }); + fs.createReadStream(importFile).pipe(csvp); +} + +main(); \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 01417a1..2bf72da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,9 +20,12 @@ import "./App.css"; import { configEvent } from "./uitls/events"; import zhCN from "antd/locale/zh_CN"; +import enGB from "antd/locale/en_GB"; import { ServerComm } from "./api/local"; import Ctx from "./uitls/ctx"; import { useIsDark } from "./uitls/useTheme"; +import './uitls/i18n.ts'; +import { useTranslation } from "react-i18next"; const Manage = React.lazy(() => import("./Pages/Manage")); @@ -45,6 +48,7 @@ const AntdGlobal: React.FC<{ children: React.ReactNode }> = (props) => { function App() { const info = useInfo(); + const { t, i18n } = useTranslation(); // const [gostConfig, setGostConfig] = useState(null); // const [localConfig, setLocalConfig] = useState(null); const gostConfig = useServerConfig(); @@ -53,10 +57,12 @@ function App() { // const [userTheme, setUserTheme] = useState(null); // 用户主题 const [serverLoading, setServerLoading] = useState(false); const [localLoading, setLocalLoading] = useState(false); + const [locale, setLocale] = useState(zhCN); const isLoading = useMemo( () => serverLoading || localLoading, [serverLoading, localLoading] ); + const slef = useRef({ update: async () => { try { @@ -137,6 +143,12 @@ function App() { (window as any)?.monaco?.editor.setTheme("vs"); } }, [isDark]); + + useEffect(() => { + const lang = i18n.resolvedLanguage; + setLocale(lang === 'zh' ? zhCN : enGB); + },[i18n.resolvedLanguage]) + return ( diff --git a/src/Pages/Home.tsx b/src/Pages/Home.tsx index 351a452..6b1ff32 100644 --- a/src/Pages/Home.tsx +++ b/src/Pages/Home.tsx @@ -1,12 +1,14 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import React from "react"; import { GlobalOutlined, LockOutlined, UserOutlined } from "@ant-design/icons"; -import { Button, Checkbox, Form, Input } from "antd"; +import { Button, Checkbox, Form, Input, Space } from "antd"; import { login } from "../uitls/server"; import LocalServers from "../components/LocalServers"; import { ThemeButton } from "../components/Theme"; - +import { useTranslation, Trans } from 'react-i18next'; +import { LanguageButton } from "../components/Language"; const Home: React.FC = () => { + const { t, i18n } = useTranslation(); return ( <>
{ ); }} > -

GOST API Manage

-

首先连接API服务

+

{t('base.title')}

+

{t('base.description')}

} > @@ -76,18 +75,21 @@ const Home: React.FC = () => { > - 保存到本地 + {t('base.form.local')}
- +
+ + + + +
); }; diff --git a/src/Pages/Manage.tsx b/src/Pages/Manage.tsx index 964268f..c8b7c24 100644 --- a/src/Pages/Manage.tsx +++ b/src/Pages/Manage.tsx @@ -36,6 +36,8 @@ import { SettingOutlined, } from "@ant-design/icons"; import { ThemeButton } from "../components/Theme"; +import { LanguageButton } from "../components/Language"; +import { useTranslation } from "react-i18next"; const colSpan = { xs: 24, @@ -52,6 +54,7 @@ const colSpan1 = { const Manage = () => { const info = useInfo()!; + const { t } = useTranslation(); const gostConfig = useServerConfig(); const [show, setShow] = useState(false); const [loading, setLoading] = useState(false); @@ -128,7 +131,7 @@ const Manage = () => { if (locals.length) { items.push({ key: "2", - label: " 切换 ", + label: t("manage.cmd.switch"), children: locals, }); items.push({ @@ -137,13 +140,13 @@ const Manage = () => { } items.push({ key: "new", - label: "打开新链接", + label: t("manage.cmd.new"), onClick: () => { window.open(location.href, undefined, "noopener"); }, }); return items; - }, [locals]); + }, [locals, t]); return ( @@ -162,7 +165,7 @@ const Manage = () => { useServerConfig.set((await API.getConfig()) as any); }} > - 刷新配置 + {t("manage.cmd.reload")} {info.addr} @@ -174,7 +177,7 @@ const Manage = () => { loading={loading} onClick={() => ref.current?.onSave?.()} > - 保存到服务器 + {t("manage.cmd.save")} - 退出 - {/* */} + {t("manage.cmd.logout")} - + + @@ -218,17 +219,29 @@ const Manage = () => { } }} > - + - + json yaml - - + + @@ -276,7 +289,7 @@ const Manage = () => { - + @@ -293,7 +306,7 @@ const Manage = () => { = (props) => { + const { t } = useTranslation(); const { keyName, title } = props; const { comm } = useContext(CardCtx); const templates = UseTemplates({ name: keyName! }); - + return ( } size="small" />} onFinish={async (values: any) => { const { value } = values; const json = jsonParse(value); await comm!.addValue(json); - return true + return true; }} > ); diff --git a/src/components/Forms/Json.tsx b/src/components/Forms/Json.tsx index c6a949e..90cab22 100644 --- a/src/components/Forms/Json.tsx +++ b/src/components/Forms/Json.tsx @@ -1,6 +1,13 @@ import React, { useRef } from "react"; import ReactDOM from "react-dom/client"; -import { Button, Dropdown, Form, FormInstance, Space, ConfigProvider } from "antd"; +import { + Button, + Dropdown, + Form, + FormInstance, + Space, + ConfigProvider, +} from "antd"; import { DownOutlined } from "@ant-design/icons"; import { jsonFormat, jsonStringFormat, jsonParse, jsonEdit } from "../../uitls"; import { Template } from "../../templates"; @@ -10,6 +17,8 @@ import ModalForm, { ModalFormProps } from "../ModalForm"; import { CodeEditor } from "../../uitls/useMonacoEdit"; import { globalConfig } from "antd/es/config-provider"; import { HookAPI } from "antd/es/modal/useModal"; +import { useTranslation } from "react-i18next"; +import { getLabel } from "../../uitls/i18n"; const template2menu: any = (template: Template) => { const { children, ...other } = template; @@ -49,12 +58,15 @@ type JsonFromProps = ModalFormProps & { const JsonForm: React.FC = (props) => { const { templates, ...other } = props; + const { t } = useTranslation(); const formRef = useRef>(); const templateHandle = (json: string) => { let value = json; if (props.initialValues?.value) { const _old = jsonParse(props.initialValues.value); - value = _old.name ? jsonEdit(json, [{path:'name', value: _old.name}]) : json; + value = _old.name + ? jsonEdit(json, [{ path: "name", value: _old.name }]) + : json; } value = jsonStringFormat(value); formRef.current?.setFieldValue("value", value); @@ -70,8 +82,10 @@ const JsonForm: React.FC = (props) => { children: children.map(template2menu), }; } else if (json) { + const { label } = other; return { ...other, + label: getLabel(label), title: cli, onClick: () => templateHandle(json), }; @@ -91,8 +105,11 @@ const JsonForm: React.FC = (props) => { }} > {hasTemplate ? ( - - 模板: + + {t("terms.template")}: {templates.map((template, index) => { if (template.children?.length) { const menu = { @@ -115,7 +132,7 @@ const JsonForm: React.FC = (props) => { onClick={() => templateHandle(template.json)} menu={menu} > - {template.label} + {getLabel(template.label)} ); } else { @@ -123,7 +140,7 @@ const JsonForm: React.FC = (props) => { @@ -138,7 +155,7 @@ const JsonForm: React.FC = (props) => { title={template.cli} onClick={() => templateHandle(template.json)} > - {template.label} + {getLabel(template.label)} ); } else { @@ -147,25 +164,10 @@ const JsonForm: React.FC = (props) => { })} ) : null} - {/* { - return new Promise((resolve, reject) => { - if (value) jsonParse(value); - resolve(null); - }); - }, - }, - ]} - > */} { return new Promise((resolve, reject) => { @@ -175,7 +177,7 @@ const JsonForm: React.FC = (props) => { resolve(null); }).catch((err) => { console.error(err); - throw new Error("json 格式错误"); + throw new Error(t("msg.formatError", { name: "JSON" })); }); }, }, @@ -187,7 +189,8 @@ const JsonForm: React.FC = (props) => { language="json" options={{ minimap: { enabled: false }, - }} /> + }} + /> {/* { const config = globalConfig(); timeoutId = setTimeout(() => { document.body.append(container); - root.render(); + root.render( + + + + ); }, 100); } diff --git a/src/components/Language.tsx b/src/components/Language.tsx new file mode 100644 index 0000000..0102f60 --- /dev/null +++ b/src/components/Language.tsx @@ -0,0 +1,17 @@ +import { Button, ButtonProps } from "antd"; +import { useTranslation } from "react-i18next"; + +export const LanguageButton = (props: ButtonProps) => { + const { i18n } = useTranslation(); + return ( + + ); +}; diff --git a/src/components/List/Public.tsx b/src/components/List/Public.tsx index 19e5f8a..1690a07 100644 --- a/src/components/List/Public.tsx +++ b/src/components/List/Public.tsx @@ -21,6 +21,7 @@ import { GostCommit } from "../../api/local"; import Ctx, { CardCtx } from "../../uitls/ctx"; import { useListData1, UseTemplates } from "../ListCard/hooks"; import { configEvent } from "../../uitls/events"; +import { useTranslation } from "react-i18next"; export type PublicListProps = { name: string; @@ -56,6 +57,7 @@ const PublicList: React.FC = (props) => { renderConfig = defaultRenderConfig, filter = defFilter, } = props; + const { t } = useTranslation(); const { localList, comm } = useContext(CardCtx); const { gostConfig, localConfig } = useContext(Ctx); const { dataList, dataSource } = useListData1({ @@ -116,7 +118,7 @@ const PublicList: React.FC = (props) => { }); }; showJsonForm({ - title: "修改", + title: t("base.cmd.edit"), initialValues: { value: jsonFormatValue(v) }, onFinish: async (values: any) => { onup(jsonParse(values.value)); @@ -142,9 +144,14 @@ const PublicList: React.FC = (props) => { size="small" dataSource={showList} columns={[ - { title: rowKey, dataIndex: rowKey, ellipsis: true, width: 100 }, { - title: "详情", + title: t("base.form.name"), + dataIndex: rowKey, + ellipsis: true, + width: 100, + }, + { + title: t("base.form.details"), ellipsis: true, render: (value, record, index) => { const isEnable = dataList.includes(record); @@ -165,7 +172,7 @@ const PublicList: React.FC = (props) => { }, }, { - title: "操作", + title: t("base.cmd.controls"), width: localApi ? 120 : 90, align: "right", dataIndex: rowKey, @@ -179,7 +186,7 @@ const PublicList: React.FC = (props) => { {localApi ? ( isEnable ? ( ) : (