From 4991f7ff39626156077674f2ef932ea9467d6e89 Mon Sep 17 00:00:00 2001 From: GyDi Date: Sat, 12 Mar 2022 23:07:45 +0800 Subject: [PATCH] feat: i18n supports --- package.json | 2 + src-tauri/src/core/verge.rs | 8 +++- src-tauri/src/utils/tmpl.rs | 1 + src/components/setting/setting-clash.tsx | 30 ++++++++------- src/components/setting/setting-system.tsx | 32 ++++++++-------- src/components/setting/setting-verge.tsx | 46 +++++++++++++++++------ src/locales/en.json | 42 +++++++++++++++++++++ src/locales/zh.json | 42 +++++++++++++++++++++ src/main.tsx | 1 + src/pages/_layout.tsx | 11 +++++- src/pages/_routers.tsx | 10 ++--- src/pages/connections.tsx | 5 ++- src/pages/logs.tsx | 6 ++- src/pages/profiles.tsx | 10 +++-- src/pages/proxies.tsx | 6 ++- src/pages/settings.tsx | 9 +++-- src/services/i18n.ts | 17 +++++++++ src/services/types.ts | 1 + yarn.lock | 35 ++++++++++++++++- 19 files changed, 254 insertions(+), 60 deletions(-) create mode 100644 src/locales/en.json create mode 100644 src/locales/zh.json create mode 100644 src/services/i18n.ts diff --git a/package.json b/package.json index 5c08f03..2439d87 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "ahooks": "^3.1.13", "axios": "^0.26.0", "dayjs": "^1.10.8", + "i18next": "^21.6.14", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-i18next": "^11.15.6", "react-router-dom": "^6.2.2", "react-virtuoso": "^2.7.0", "recoil": "^0.6.1", diff --git a/src-tauri/src/core/verge.rs b/src-tauri/src/core/verge.rs index 01fad60..615f917 100644 --- a/src-tauri/src/core/verge.rs +++ b/src-tauri/src/core/verge.rs @@ -1,6 +1,6 @@ +use crate::log_if_err; use crate::{ core::Clash, - log_if_err, utils::{config, dirs, sysopt::SysProxyConfig}, }; use anyhow::{bail, Result}; @@ -12,6 +12,9 @@ use tauri::{async_runtime::Mutex, utils::platform::current_exe}; /// ### `verge.yaml` schema #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct VergeConfig { + // i18n + pub language: Option, + /// `light` or `dark` pub theme_mode: Option, @@ -188,6 +191,9 @@ impl Verge { /// so call the save_file at the end is savely pub fn patch_config(&mut self, patch: VergeConfig) -> Result<()> { // only change it + if patch.language.is_some() { + self.config.language = patch.language; + } if patch.theme_mode.is_some() { self.config.theme_mode = patch.theme_mode; } diff --git a/src-tauri/src/utils/tmpl.rs b/src-tauri/src/utils/tmpl.rs index 979ff86..bf7acee 100644 --- a/src-tauri/src/utils/tmpl.rs +++ b/src-tauri/src/utils/tmpl.rs @@ -21,6 +21,7 @@ items: ~ /// template for `verge.yaml` pub const VERGE_CONFIG: &[u8] = b"# Defaulf Config For Clash Verge +language: en theme_mode: light theme_blur: false traffic_graph: true diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 7ae9dd1..5106e6f 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -1,5 +1,6 @@ import useSWR, { useSWRConfig } from "swr"; import { useSetRecoilState } from "recoil"; +import { useTranslation } from "react-i18next"; import { ListItemText, TextField, @@ -21,15 +22,16 @@ interface Props { } const SettingClash = ({ onError }: Props) => { + const { t } = useTranslation(); const { mutate } = useSWRConfig(); const { data: clashConfig } = useSWR("getClashConfig", getClashConfig); const { data: versionData } = useSWR("getVersion", getVersion); const { - ipv6 = false, - "allow-lan": allowLan = false, - "log-level": logLevel = "silent", - "mixed-port": mixedPort = 0, + ipv6, + "allow-lan": allowLan, + "log-level": logLevel, + "mixed-port": mixedPort, } = clashConfig ?? {}; const setGlobalClashPort = useSetRecoilState(atomClashPort); @@ -64,11 +66,11 @@ const SettingClash = ({ onError }: Props) => { : versionData?.version || "-"; return ( - + - + { - + { - + e.target.value} onChange={(e) => onChangeData({ "log-level": e })} @@ -113,9 +115,9 @@ const SettingClash = ({ onError }: Props) => { - + +e.target.value?.replace(/\D+/, "")} onChange={(e) => onChangeData({ "mixed-port": e })} @@ -127,7 +129,7 @@ const SettingClash = ({ onError }: Props) => { - + {clashVer} diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index dfc482e..a09b3b4 100644 --- a/src/components/setting/setting-system.tsx +++ b/src/components/setting/setting-system.tsx @@ -1,4 +1,5 @@ import useSWR, { useSWRConfig } from "swr"; +import { useTranslation } from "react-i18next"; import { Box, ListItemText, Switch, TextField } from "@mui/material"; import { getVergeConfig, patchVergeConfig } from "../../services/cmds"; import { SettingList, SettingItem } from "./setting"; @@ -11,15 +12,16 @@ interface Props { } const SettingSystem = ({ onError }: Props) => { + const { t } = useTranslation(); const { mutate } = useSWRConfig(); const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig); const { - enable_tun_mode = false, - enable_auto_launch = false, - enable_system_proxy = false, - system_proxy_bypass = "", - enable_proxy_guard = false, + enable_tun_mode, + enable_auto_launch, + enable_system_proxy, + system_proxy_bypass, + enable_proxy_guard, } = vergeConfig ?? {}; const onSwitchFormat = (_e: any, value: boolean) => value; @@ -28,11 +30,11 @@ const SettingSystem = ({ onError }: Props) => { }; return ( - + - + { - + { - System Proxy + {t("System Proxy")} } /> { {enable_system_proxy && ( - + { {enable_system_proxy && ( - + { + const { t } = useTranslation(); const { mutate } = useSWRConfig(); const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig); - const { theme_mode = "light", theme_blur = false, traffic_graph } = - vergeConfig ?? {}; + const { theme_mode, theme_blur, traffic_graph, language } = vergeConfig ?? {}; const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { @@ -30,9 +38,9 @@ const SettingVerge = ({ onError }: Props) => { }; return ( - + - + { - + { - + { - + + e.target.value} + onChange={(e) => onChangeData({ language: e })} + onGuard={(e) => patchVergeConfig({ language: e })} + > + + + + + + - + - + v{version} diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..aba0ea1 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,42 @@ +{ + "Label-Proxies": "Proxies", + "Label-Profiles": "Profiles", + "Label-Connections": "Connections", + "Label-Logs": "Logs", + "Label-Settings": "Settings", + + "Connections": "Connections", + "Logs": "Logs", + "Clear": "Clear", + "Proxies": "Proxies", + "Proxy Groups": "Proxy Groups", + "rule": "rule", + "global": "global", + "direct": "direct", + "Profiles": "Profiles", + "Profile URL": "Profile URL", + "Import": "Import", + "New": "New", + + "Settings": "Settings", + "Clash Setting": "Clash Setting", + "System Setting": "System Setting", + "Verge Setting": "Verge Setting", + "Allow Lan": "Allow Lan", + "IPv6": "IPv6", + "Log Level": "Log Level", + "Mixed Port": "Mixed Port", + "Clash core": "Clash core", + "Tun Mode": "Tun Mode", + "Auto Launch": "Auto Launch", + "System Proxy": "System Proxy", + "Proxy Guard": "Proxy Guard", + "Proxy Bypass": "Proxy Bypass", + "Theme Mode": "Theme Mode", + "Theme Blur": "Theme Blur", + "Traffic Graph": "Traffic Graph", + "Language": "Language", + "Open App Dir": "Open App Dir", + "Open Logs Dir": "Open Logs Dir", + "Version": "Version" +} diff --git a/src/locales/zh.json b/src/locales/zh.json new file mode 100644 index 0000000..a51b132 --- /dev/null +++ b/src/locales/zh.json @@ -0,0 +1,42 @@ +{ + "Label-Proxies": "代 理", + "Label-Profiles": "配 置", + "Label-Connections": "连 接", + "Label-Logs": "日 志", + "Label-Settings": "设 置", + + "Connections": "连接", + "Logs": "日志", + "Clear": "清除", + "Proxies": "代理", + "Proxy Groups": "代理组", + "rule": "规则", + "global": "全局", + "direct": "直连", + "Profiles": "配置", + "Profile URL": "配置文件链接", + "Import": "导入", + "New": "新建", + + "Settings": "设置", + "Clash Setting": "Clash 设置", + "System Setting": "系统设置", + "Verge Setting": "Verge 设置", + "Allow Lan": "局域网连接", + "IPv6": "IPv6", + "Log Level": "日志等级", + "Mixed Port": "端口设置", + "Clash core": "Clash 内核", + "Tun Mode": "Tun 模式", + "Auto Launch": "开机自启", + "System Proxy": "系统代理", + "Proxy Guard": "系统代理守卫", + "Proxy Bypass": "Proxy Bypass", + "Theme Mode": "暗夜模式", + "Theme Blur": "背景模糊", + "Traffic Graph": "流量图显", + "Language": "语言设置", + "Open App Dir": "应用目录", + "Open Logs Dir": "日志目录", + "Version": "版本" +} diff --git a/src/main.tsx b/src/main.tsx index 7845c70..849f898 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,6 +7,7 @@ import { RecoilRoot } from "recoil"; import { BrowserRouter } from "react-router-dom"; import Layout from "./pages/_layout"; import enhance from "./services/enhance"; +import "./services/i18n"; enhance.setup(); diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index e54b4f4..46d3a9d 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -1,5 +1,7 @@ +import i18next from "i18next"; import useSWR, { SWRConfig, useSWRConfig } from "swr"; import { useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Route, Routes } from "react-router-dom"; import { alpha, createTheme, List, Paper, ThemeProvider } from "@mui/material"; import { listen } from "@tauri-apps/api/event"; @@ -16,6 +18,7 @@ import UpdateButton from "../components/layout/update-button"; const isMacos = navigator.userAgent.includes("Mac OS X"); const Layout = () => { + const { t } = useTranslation(); const { mutate } = useSWRConfig(); const { data } = useSWR("getVergeConfig", getVergeConfig); @@ -37,6 +40,12 @@ const Layout = () => { }); }, []); + useEffect(() => { + if (data?.language) { + i18next.changeLanguage(data.language); + } + }, [data?.language]); + const theme = useMemo(() => { // const background = mode === "light" ? "#f5f5f5" : "#000"; const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5"; @@ -87,7 +96,7 @@ const Layout = () => { {routers.map((router) => ( - {router.label} + {t(router.label)} ))} diff --git a/src/pages/_routers.tsx b/src/pages/_routers.tsx index f7d2f75..9ab4620 100644 --- a/src/pages/_routers.tsx +++ b/src/pages/_routers.tsx @@ -6,27 +6,27 @@ import ConnectionsPage from "./connections"; export const routers = [ { - label: "Proxies", + label: "Label-Proxies", link: "/", ele: ProxiesPage, }, { - label: "Profiles", + label: "Label-Profiles", link: "/profile", ele: ProfilesPage, }, { - label: "Connections", + label: "Label-Connections", link: "/connections", ele: ConnectionsPage, }, { - label: "Logs", + label: "Label-Logs", link: "/logs", ele: LogsPage, }, { - label: "Settings", + label: "Label-Settings", link: "/settings", ele: SettingsPage, }, diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 603f150..16ec936 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { Paper } from "@mui/material"; import { Virtuoso } from "react-virtuoso"; +import { useTranslation } from "react-i18next"; import { ApiType } from "../services/types"; import { getInfomation } from "../services/api"; import BasePage from "../components/base/base-page"; @@ -8,6 +9,8 @@ import ConnectionItem from "../components/connection/connection-item"; const ConnectionsPage = () => { const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; + + const { t } = useTranslation(); const [conn, setConn] = useState(initConn); useEffect(() => { @@ -27,7 +30,7 @@ const ConnectionsPage = () => { }, []); return ( - + { + const { t } = useTranslation(); const [logData, setLogData] = useRecoilState(atomLogData); return ( { variant="contained" onClick={() => setLogData([])} > - Clear + {t("Clear")} } > diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index e138e52..5cd331d 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -2,6 +2,7 @@ import useSWR, { useSWRConfig } from "swr"; import { useLockFn } from "ahooks"; import { useEffect, useMemo, useState } from "react"; import { Box, Button, Grid, TextField } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { getProfiles, patchProfile, @@ -19,6 +20,7 @@ import ProfileItem from "../components/profile/profile-item"; import ProfileMore from "../components/profile/profile-more"; const ProfilePage = () => { + const { t } = useTranslation(); const { mutate } = useSWRConfig(); const [url, setUrl] = useState(""); @@ -175,12 +177,12 @@ const ProfilePage = () => { }); return ( - + { onClick={onImport} sx={{ mr: 1 }} > - Import + {t("Import")} diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx index 20f6935..0df7cd1 100644 --- a/src/pages/proxies.tsx +++ b/src/pages/proxies.tsx @@ -1,6 +1,7 @@ import useSWR, { useSWRConfig } from "swr"; import { useEffect } from "react"; import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; import { Button, ButtonGroup, List, Paper } from "@mui/material"; import { getClashConfig, updateConfigs } from "../services/api"; import { patchClashConfig } from "../services/cmds"; @@ -10,6 +11,7 @@ import ProxyGroup from "../components/proxy/proxy-group"; import ProxyGlobal from "../components/proxy/proxy-global"; const ProxyPage = () => { + const { t } = useTranslation(); const { mutate } = useSWRConfig(); const { data: proxiesData } = useSWR("getProxies", getProxies); const { data: clashConfig } = useSWR("getClashConfig", getClashConfig); @@ -45,7 +47,7 @@ const ProxyPage = () => { return ( {modeList.map((mode) => ( @@ -55,7 +57,7 @@ const ProxyPage = () => { onClick={() => onChangeMode(mode)} sx={{ textTransform: "capitalize" }} > - {mode} + {t(mode)} ))} diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index a7aea42..e8c2446 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -1,4 +1,5 @@ import { Paper } from "@mui/material"; +import { useTranslation } from "react-i18next"; import Notice from "../components/base/base-notice"; import BasePage from "../components/base/base-page"; import SettingVerge from "../components/setting/setting-verge"; @@ -6,12 +7,14 @@ import SettingClash from "../components/setting/setting-clash"; import SettingSystem from "../components/setting/setting-system"; const SettingPage = () => { - const onError = (error: any) => { - error && Notice.error(error.toString()); + const { t } = useTranslation(); + + const onError = (err: any) => { + Notice.error(err?.message || err.toString()); }; return ( - + diff --git a/src/services/i18n.ts b/src/services/i18n.ts new file mode 100644 index 0000000..e3d8d31 --- /dev/null +++ b/src/services/i18n.ts @@ -0,0 +1,17 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import en from "../locales/en.json"; +import zh from "../locales/zh.json"; + +const resources = { + en: { translation: en }, + zh: { translation: zh }, +}; + +i18n.use(initReactI18next).init({ + resources, + lng: "en", + interpolation: { + escapeValue: false, + }, +}); diff --git a/src/services/types.ts b/src/services/types.ts index 5642cb4..2a059bd 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -120,6 +120,7 @@ export namespace CmdType { } export interface VergeConfig { + language?: string; theme_mode?: "light" | "dark"; theme_blur?: boolean; traffic_graph?: boolean; diff --git a/yarn.lock b/yarn.lock index 6d242cc..399e9f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -227,7 +227,7 @@ "@babel/plugin-syntax-jsx" "^7.16.7" "@babel/types" "^7.17.0" -"@babel/runtime@^7.13.10", "@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.13.10", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -1291,6 +1291,18 @@ hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" +html-escaper@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +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" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -1301,6 +1313,13 @@ husky@^7.0.0: resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== +i18next@^21.6.14: + version "21.6.14" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.14.tgz#2bc199fba7f4da44b5952d7df0a3814a6e5c3943" + integrity sha512-XL6WyD+xlwQwbieXRlXhKWoLb/rkch50/rA+vl6untHnJ+aYnkQ0YDZciTWE78PPhOpbi2gR0LTJCJpiAhA+uQ== + dependencies: + "@babel/runtime" "^7.17.2" + ignore@^5.1.4: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -1653,6 +1672,15 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-i18next@^11.15.6: + version "11.15.6" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.15.6.tgz#693430fbee5ac7d0774bd88683575d62adb24afb" + integrity sha512-OUWcFdNgIA9swVx3JGIreuquglAinpRwB/HYrCprTN+s9BQDt9LSiY7x5DGc2JzVpwqtpoTV7oRUTOxEPNyUPw== + dependencies: + "@babel/runtime" "^7.14.5" + html-escaper "^2.0.2" + 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" @@ -1901,6 +1929,11 @@ vite@^2.8.6: 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 sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= + web-streams-polyfill@^3.0.3: version "3.2.0" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"