From 8210b477abe5c2b0cb6e10072c199338e6174220 Mon Sep 17 00:00:00 2001 From: pompurin404 Date: Tue, 4 Feb 2025 07:38:53 +0800 Subject: [PATCH] feat: add i18n support with English translations --- package.json | 2 + pnpm-lock.yaml | 55 ++ src/main/index.ts | 18 +- src/main/resolve/tray.ts | 42 +- src/main/utils/ipc.ts | 8 + src/renderer/src/App.tsx | 375 +++++---- .../components/base/base-error-boundary.tsx | 7 +- .../src/components/base/base-page.tsx | 5 +- .../components/base/base-password-modal.tsx | 8 +- .../connections/connection-detail-modal.tsx | 72 +- .../connections/connection-item.tsx | 2 +- .../src/components/mihomo/interface-modal.tsx | 10 +- .../components/override/edit-file-modal.tsx | 11 +- .../components/override/edit-info-modal.tsx | 15 +- .../components/override/exec-log-modal.tsx | 7 +- .../src/components/override/override-item.tsx | 18 +- .../components/profiles/edit-file-modal.tsx | 16 +- .../components/profiles/edit-info-modal.tsx | 22 +- .../src/components/profiles/profile-item.tsx | 22 +- .../src/components/proxies/proxy-item.tsx | 10 +- .../src/components/resources/geo-data.tsx | 26 +- .../components/resources/proxy-provider.tsx | 2 +- .../components/resources/rule-provider.tsx | 2 +- .../src/components/settings/actions.tsx | 42 +- .../components/settings/css-editor-modal.tsx | 10 +- .../components/settings/general-config.tsx | 74 +- .../src/components/settings/mihomo-config.tsx | 66 +- .../components/settings/shortcut-config.tsx | 31 +- .../src/components/settings/sider-config.tsx | 127 ++- .../components/settings/substore-config.tsx | 46 +- .../src/components/settings/webdav-config.tsx | 22 +- .../settings/webdav-restore-modal.tsx | 14 +- .../src/components/sider/config-viewer.tsx | 7 +- .../src/components/sider/conn-card.tsx | 8 +- .../src/components/sider/dns-card.tsx | 6 +- .../src/components/sider/log-card.tsx | 6 +- .../src/components/sider/mihomo-core-card.tsx | 8 +- .../sider/outbound-mode-switcher.tsx | 8 +- .../src/components/sider/override-card.tsx | 6 +- .../src/components/sider/profile-card.tsx | 20 +- .../src/components/sider/proxy-card.tsx | 6 +- .../src/components/sider/resource-card.tsx | 6 +- .../src/components/sider/rule-card.tsx | 6 +- .../src/components/sider/sniff-card.tsx | 6 +- .../src/components/sider/substore-card.tsx | 6 +- .../components/sider/sysproxy-switcher.tsx | 6 +- .../src/components/sider/tun-switcher.tsx | 6 +- .../components/sysproxy/pac-editor-modal.tsx | 10 +- .../src/components/updater/updater-modal.tsx | 12 +- src/renderer/src/i18n.ts | 18 + src/renderer/src/locales/en-US.json | 723 ++++++++++++++++++ src/renderer/src/locales/zh-CN.json | 723 ++++++++++++++++++ src/renderer/src/main.tsx | 1 + src/renderer/src/pages/connections.tsx | 25 +- src/renderer/src/pages/dns.tsx | 57 +- src/renderer/src/pages/logs.tsx | 9 +- src/renderer/src/pages/mihomo.tsx | 110 +-- src/renderer/src/pages/override.tsx | 26 +- src/renderer/src/pages/profiles.tsx | 20 +- src/renderer/src/pages/proxies.tsx | 22 +- src/renderer/src/pages/resources.tsx | 6 +- src/renderer/src/pages/rules.tsx | 6 +- src/renderer/src/pages/settings.tsx | 11 +- src/renderer/src/pages/sniffer.tsx | 40 +- src/renderer/src/pages/substore.tsx | 6 +- src/renderer/src/pages/syspeoxy.tsx | 32 +- src/renderer/src/pages/tun.tsx | 40 +- src/renderer/src/utils/dayjs.ts | 22 + src/shared/i18n.ts | 31 + src/shared/types.d.ts | 1 + tsconfig.node.json | 2 +- tsconfig.web.json | 5 +- 72 files changed, 2500 insertions(+), 756 deletions(-) create mode 100644 src/renderer/src/i18n.ts create mode 100644 src/renderer/src/locales/en-US.json create mode 100644 src/renderer/src/locales/zh-CN.json create mode 100644 src/renderer/src/utils/dayjs.ts create mode 100644 src/shared/i18n.ts diff --git a/package.json b/package.json index 4e9c32b..07156aa 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,9 @@ "crypto-js": "^4.2.0", "dayjs": "^1.11.13", "express": "^5.0.1", + "i18next": "^24.2.2", "iconv-lite": "^0.6.3", + "react-i18next": "^15.4.0", "webdav": "^5.7.1", "ws": "^8.18.0", "yaml": "^2.6.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3605bee..d472162 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,15 @@ importers: express: specifier: ^5.0.1 version: 5.0.1 + i18next: + specifier: ^24.2.2 + version: 24.2.2(typescript@5.7.3) iconv-lite: specifier: ^0.6.3 version: 0.6.3 + react-i18next: + specifier: ^15.4.0 + version: 15.4.0(i18next@24.2.2(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) webdav: specifier: ^5.7.1 version: 5.7.1 @@ -3681,6 +3687,9 @@ packages: hot-patcher@2.0.1: resolution: {integrity: sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -3706,6 +3715,14 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + i18next@24.2.2: + resolution: {integrity: sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} @@ -4711,6 +4728,19 @@ packages: peerDependencies: react: '>=16.13.1' + react-i18next@15.4.0: + resolution: {integrity: sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + react-icons@5.4.0: resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==} peerDependencies: @@ -5457,6 +5487,10 @@ packages: yaml: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -10179,6 +10213,10 @@ snapshots: hot-patcher@2.0.1: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + html-url-attributes@3.0.1: {} http-cache-semantics@4.1.1: {} @@ -10215,6 +10253,12 @@ snapshots: dependencies: ms: 2.1.3 + i18next@24.2.2(typescript@5.7.3): + dependencies: + '@babel/runtime': 7.26.7 + optionalDependencies: + typescript: 5.7.3 + iconv-corefoundation@1.1.7: dependencies: cli-truncate: 2.1.0 @@ -11303,6 +11347,15 @@ snapshots: '@babel/runtime': 7.26.7 react: 19.0.0 + react-i18next@15.4.0(i18next@24.2.2(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.26.7 + html-parse-stringify: 3.0.1 + i18next: 24.2.2(typescript@5.7.3) + react: 19.0.0 + optionalDependencies: + react-dom: 19.0.0(react@19.0.0) + react-icons@5.4.0(react@19.0.0): dependencies: react: 19.0.0 @@ -12224,6 +12277,8 @@ snapshots: tsx: 4.19.2 yaml: 2.7.0 + void-elements@3.1.0: {} + vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: diff --git a/src/main/index.ts b/src/main/index.ts index 67bc430..4866216 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -19,6 +19,8 @@ import path from 'path' import { startMonitor } from './resolve/trafficMonitor' import { showFloatingWindow } from './resolve/floatingWindow' import iconv from 'iconv-lite' +import { initI18n } from '../shared/i18n' +import i18next from 'i18next' let quitTimeout: NodeJS.Timeout | null = null export let mainWindow: BrowserWindow | null = null @@ -48,8 +50,8 @@ if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin') // ignore } dialog.showErrorBox( - '首次启动请以管理员权限运行', - `首次启动请以管理员权限运行\n${createErrorStr}\n${eStr}` + i18next.t('main.error.adminRequired'), + `${i18next.t('main.error.adminRequired')}\n${createErrorStr}\n${eStr}` ) } finally { app.exit() @@ -122,9 +124,11 @@ app.whenReady().then(async () => { // Set app user model id for windows electronApp.setAppUserModelId('party.mihomo.app') try { + const appConfig = await getAppConfig() + await initI18n({ lng: appConfig.language }) await initPromise } catch (e) { - dialog.showErrorBox('应用初始化失败', `${e}`) + dialog.showErrorBox(i18next.t('main.error.initFailed'), `${e}`) app.quit() } try { @@ -133,7 +137,7 @@ app.whenReady().then(async () => { await initProfileUpdater() }) } catch (e) { - dialog.showErrorBox('内核启动出错', `${e}`) + dialog.showErrorBox(i18next.t('main.error.coreStartFailed'), `${e}`) } try { await startMonitor() @@ -174,7 +178,7 @@ async function handleDeepLink(url: string): Promise { const profileUrl = urlObj.searchParams.get('url') const profileName = urlObj.searchParams.get('name') if (!profileUrl) { - throw new Error('缺少参数 url') + throw new Error(i18next.t('main.error.urlParamMissing')) } await addProfileItem({ type: 'remote', @@ -182,10 +186,10 @@ async function handleDeepLink(url: string): Promise { url: profileUrl }) mainWindow?.webContents.send('profileConfigUpdated') - new Notification({ title: '订阅导入成功' }).show() + new Notification({ title: i18next.t('main.notification.importSuccess') }).show() break } catch (e) { - dialog.showErrorBox('订阅导入失败', `${url}\n${e}`) + dialog.showErrorBox(i18next.t('main.error.importFailed'), `${url}\n${e}`) } } } diff --git a/src/main/resolve/tray.ts b/src/main/resolve/tray.ts index a17544b..fbee02a 100644 --- a/src/main/resolve/tray.ts +++ b/src/main/resolve/tray.ts @@ -21,10 +21,16 @@ import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs' import { triggerSysProxy } from '../sys/sysproxy' import { quitWithoutCore, restartCore } from '../core/manager' import { floatingWindow, triggerFloatingWindow } from './floatingWindow' +import { t } from 'i18next' export let tray: Tray | null = null export const buildContextMenu = async (): Promise => { + // 添加调试日志 + console.log('Current translation for tray.showWindow:', t('tray.showWindow')) + console.log('Current translation for tray.hideFloatingWindow:', t('tray.hideFloatingWindow')) + console.log('Current translation for tray.showFloatingWindow:', t('tray.showFloatingWindow')) + const { mode, tun } = await getControledMihomoConfig() const { sysProxy, @@ -86,7 +92,7 @@ export const buildContextMenu = async (): Promise => { { id: 'show', accelerator: showWindowShortcut, - label: '显示窗口', + label: t('tray.showWindow'), type: 'normal', click: (): void => { showMainWindow() @@ -95,7 +101,7 @@ export const buildContextMenu = async (): Promise => { { id: 'show-floating', accelerator: showFloatingWindowShortcut, - label: floatingWindow?.isVisible() ? '关闭悬浮窗' : '显示悬浮窗', + label: floatingWindow?.isVisible() ? t('tray.hideFloatingWindow') : t('tray.showFloatingWindow'), type: 'normal', click: async (): Promise => { await triggerFloatingWindow() @@ -103,7 +109,7 @@ export const buildContextMenu = async (): Promise => { }, { id: 'rule', - label: '规则模式', + label: t('tray.ruleMode'), accelerator: ruleModeShortcut, type: 'radio', checked: mode === 'rule', @@ -117,7 +123,7 @@ export const buildContextMenu = async (): Promise => { }, { id: 'global', - label: '全局模式', + label: t('tray.globalMode'), accelerator: globalModeShortcut, type: 'radio', checked: mode === 'global', @@ -131,7 +137,7 @@ export const buildContextMenu = async (): Promise => { }, { id: 'direct', - label: '直连模式', + label: t('tray.directMode'), accelerator: directModeShortcut, type: 'radio', checked: mode === 'direct', @@ -146,7 +152,7 @@ export const buildContextMenu = async (): Promise => { { type: 'separator' }, { type: 'checkbox', - label: '系统代理', + label: t('tray.systemProxy'), accelerator: triggerSysProxyShortcut, checked: sysProxy.enable, click: async (item): Promise => { @@ -165,7 +171,7 @@ export const buildContextMenu = async (): Promise => { }, { type: 'checkbox', - label: '虚拟网卡', + label: t('tray.tun'), accelerator: triggerTunShortcut, checked: tun?.enable ?? false, click: async (item): Promise => { @@ -190,7 +196,7 @@ export const buildContextMenu = async (): Promise => { { type: 'separator' }, { type: 'submenu', - label: '订阅配置', + label: t('tray.profiles'), submenu: items.map((item) => { return { type: 'radio', @@ -208,26 +214,26 @@ export const buildContextMenu = async (): Promise => { { type: 'separator' }, { type: 'submenu', - label: '打开目录', + label: t('tray.openDirectories.title'), submenu: [ { type: 'normal', - label: '应用目录', + label: t('tray.openDirectories.appDir'), click: (): Promise => shell.openPath(dataDir()) }, { type: 'normal', - label: '工作目录', + label: t('tray.openDirectories.workDir'), click: (): Promise => shell.openPath(mihomoWorkDir()) }, { type: 'normal', - label: '内核目录', + label: t('tray.openDirectories.coreDir'), click: (): Promise => shell.openPath(mihomoCoreDir()) }, { type: 'normal', - label: '日志目录', + label: t('tray.openDirectories.logDir'), click: (): Promise => shell.openPath(logDir()) } ] @@ -235,7 +241,7 @@ export const buildContextMenu = async (): Promise => { envType.length > 1 ? { type: 'submenu', - label: '复制环境变量', + label: t('tray.copyEnv'), submenu: envType.map((type) => { return { id: type, @@ -249,7 +255,7 @@ export const buildContextMenu = async (): Promise => { } : { id: 'copyenv', - label: '复制环境变量', + label: t('tray.copyEnv'), type: 'normal', click: async (): Promise => { await copyEnv(envType[0]) @@ -258,14 +264,14 @@ export const buildContextMenu = async (): Promise => { { type: 'separator' }, { id: 'quitWithoutCore', - label: '轻量模式', + label: t('actions.lightMode.button'), type: 'normal', accelerator: quitWithoutCoreShortcut, click: quitWithoutCore }, { id: 'restart', - label: '重启应用', + label: t('actions.restartApp'), type: 'normal', accelerator: restartAppShortcut, click: (): void => { @@ -275,7 +281,7 @@ export const buildContextMenu = async (): Promise => { }, { id: 'quit', - label: '退出应用', + label: t('actions.quit.button'), type: 'normal', accelerator: 'CommandOrControl+Q', click: (): void => app.quit() diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 304da09..1d29725 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -87,6 +87,7 @@ import { getGistUrl } from '../resolve/gistApi' import { getImageDataURL } from './image' import { startMonitor } from '../resolve/trafficMonitor' import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow' +import i18next from 'i18next' function ipcErrorWrapper( // eslint-disable-next-line @typescript-eslint/no-explicit-any fn: (...args: any[]) => Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -254,4 +255,11 @@ export function registerIpcMainHandlers(): void { }) ipcMain.handle('quitWithoutCore', ipcErrorWrapper(quitWithoutCore)) ipcMain.handle('quitApp', () => app.quit()) + + // Add language change handler + ipcMain.handle('changeLanguage', async (_e, lng) => { + await i18next.changeLanguage(lng) + // 触发托盘菜单更新 + ipcMain.emit('updateTrayMenu') + }) } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4e4a253..5b0d7fe 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -35,10 +35,17 @@ import SubStoreCard from '@renderer/components/sider/substore-card' import MihomoIcon from './components/base/mihomo-icon' import { driver } from 'driver.js' import 'driver.js/dist/driver.css' +import { useTranslation } from 'react-i18next' let navigate: NavigateFunction +let driverInstance: ReturnType | null = null + +export function getDriver(): ReturnType | null { + return driverInstance +} const App: React.FC = () => { + const { t } = useTranslation() const { appConfig, patchAppConfig } = useAppConfig() const { appTheme = 'system', @@ -96,12 +103,188 @@ const App: React.FC = () => { }, [siderWidthValue, resizing]) useEffect(() => { + driverInstance = driver({ + showProgress: true, + nextBtnText: t('common.next'), + prevBtnText: t('common.prev'), + doneBtnText: t('common.done'), + progressText: '{{current}} / {{total}}', + overlayOpacity: 0.9, + steps: [ + { + element: 'none', + popover: { + title: t('guide.welcome.title'), + description: t('guide.welcome.description'), + side: 'over', + align: 'center' + } + }, + { + element: '.side', + popover: { + title: t('guide.sider.title'), + description: t('guide.sider.description'), + side: 'right', + align: 'center' + } + }, + { + element: '.sysproxy-card', + popover: { + title: t('guide.card.title'), + description: t('guide.card.description'), + side: 'right', + align: 'start' + } + }, + { + element: '.main', + popover: { + title: t('guide.main.title'), + description: t('guide.main.description'), + side: 'left', + align: 'center' + } + }, + { + element: '.profile-card', + popover: { + title: t('guide.profile.title'), + description: t('guide.profile.description'), + side: 'right', + align: 'start', + onNextClick: async (): Promise => { + navigate('/profiles') + setTimeout(() => { + driverInstance?.moveNext() + }, 0) + } + } + }, + { + element: '.profiles-sticky', + popover: { + title: t('guide.import.title'), + description: t('guide.import.description'), + side: 'bottom', + align: 'start' + } + }, + { + element: '.substore-import', + popover: { + title: t('guide.substore.title'), + description: t('guide.substore.description'), + side: 'bottom', + align: 'start' + } + }, + { + element: '.new-profile', + popover: { + title: t('guide.localProfile.title'), + description: t('guide.localProfile.description'), + side: 'bottom', + align: 'start' + } + }, + { + element: '.sysproxy-card', + popover: { + title: t('guide.sysproxy.title'), + description: t('guide.sysproxy.description'), + side: 'right', + align: 'start', + onNextClick: async (): Promise => { + navigate('/sysproxy') + setTimeout(() => { + driverInstance?.moveNext() + }, 0) + } + } + }, + { + element: '.sysproxy-settings', + popover: { + title: t('guide.sysproxySetting.title'), + description: t('guide.sysproxySetting.description'), + side: 'top', + align: 'start' + } + }, + { + element: '.tun-card', + popover: { + title: t('guide.tun.title'), + description: t('guide.tun.description'), + side: 'right', + align: 'start', + onNextClick: async (): Promise => { + navigate('/tun') + setTimeout(() => { + driverInstance?.moveNext() + }, 0) + } + } + }, + { + element: '.tun-settings', + popover: { + title: t('guide.tunSetting.title'), + description: t('guide.tunSetting.description'), + side: 'bottom', + align: 'start' + } + }, + { + element: '.override-card', + popover: { + title: t('guide.override.title'), + description: t('guide.override.description'), + side: 'right', + align: 'center' + } + }, + { + element: '.dns-card', + popover: { + title: t('guide.dns.title'), + description: t('guide.dns.description'), + side: 'right', + align: 'center', + onNextClick: async (): Promise => { + navigate('/profiles') + setTimeout(() => { + driverInstance?.moveNext() + }, 0) + } + } + }, + { + element: 'none', + popover: { + title: t('guide.end.title'), + description: t('guide.end.description'), + side: 'top', + align: 'center', + onNextClick: async (): Promise => { + navigate('/profiles') + setTimeout(() => { + driverInstance?.destroy() + }, 0) + } + } + } + ] + }) + const tourShown = window.localStorage.getItem('tourShown') if (!tourShown) { window.localStorage.setItem('tourShown', 'true') - firstDriver.drive() + driverInstance.drive() } - }, []) + }, [t]) useEffect(() => { setNativeTheme(appTheme) @@ -295,191 +478,3 @@ const App: React.FC = () => { } export default App - -export const firstDriver = driver({ - showProgress: true, - nextBtnText: '下一步', - prevBtnText: '上一步', - doneBtnText: '完成', - progressText: '{{current}} / {{total}}', - overlayOpacity: 0.9, - steps: [ - { - element: 'none', - popover: { - title: '欢迎使用 Mihomo Party', - description: - '这是一份交互式使用教程,如果您已经完全熟悉本软件的操作,可以直接点击右上角关闭按钮,后续您可以随时从设置中打开本教程', - side: 'over', - align: 'center' - } - }, - { - element: '.side', - popover: { - title: '导航栏', - description: - '左侧是应用的导航栏,兼顾仪表盘功能,在这里可以切换不同页面,也可以概览常用的状态信息', - side: 'right', - align: 'center' - } - }, - { - element: '.sysproxy-card', - popover: { - title: '卡片', - description: '点击导航栏卡片可以跳转到对应页面,拖动导航栏卡片可以自由排列卡片顺序', - side: 'right', - align: 'start' - } - }, - { - element: '.main', - popover: { - title: '主要区域', - description: '右侧是应用的主要区域,展示了导航栏所选页面的内容', - side: 'left', - align: 'center' - } - }, - { - element: '.profile-card', - popover: { - title: '订阅管理', - description: - '订阅管理卡片展示当前运行的订阅配置信息,点击进入订阅管理页面可以在这里管理订阅配置', - side: 'right', - align: 'start', - onNextClick: async (): Promise => { - navigate('/profiles') - setTimeout(() => { - firstDriver.moveNext() - }, 0) - } - } - }, - { - element: '.profiles-sticky', - popover: { - title: '订阅导入', - description: - 'Mihomo Party 支持多种订阅导入方式,在此输入订阅链接,点击导入即可导入您的订阅配置,如果您的订阅需要代理才能更新,请勾选“代理”再点击导入,当然这需要已经有一个可以正常使用的订阅才可以', - side: 'bottom', - align: 'start' - } - }, - { - element: '.substore-import', - popover: { - title: 'Sub-Store', - description: - 'Mihomo Party 深度集成了 Sub-Store,您可以点击该按钮进入 Sub-Store 或直接导入您通过 Sub-Store 管理的订阅,Mihomo Party 默认使用内置的 Sub-Store 后端,如果您有自建的 Sub-Store 后端,可以在设置页面中配置,如果您不使用 Sub-Store 也可以在设置页面中关闭', - side: 'bottom', - align: 'start' - } - }, - { - element: '.new-profile', - popover: { - title: '本地订阅', - description: '点击“+”可以选择本地文件进行导入或者直接新建空白配置进行编辑', - side: 'bottom', - align: 'start' - } - }, - { - element: '.sysproxy-card', - popover: { - title: '系统代理', - description: - '导入订阅之后,内核已经开始运行并监听指定端口,此时您已经可以通过指定代理端口来使用代理了,如果您要使大部分应用自动使用该端口的代理,您还需要打开系统代理开关', - side: 'right', - align: 'start', - onNextClick: async (): Promise => { - navigate('/sysproxy') - setTimeout(() => { - firstDriver.moveNext() - }, 0) - } - } - }, - { - element: '.sysproxy-settings', - popover: { - title: '系统代理设置', - description: - '在此您可以进行系统代理相关设置,选择代理模式,如果某些 Windows 应用不遵循系统代理,还可以使用“UWP 工具”解除本地回环限制,对于“手动代理模式”和“PAC 代理模式”的区别,请自行百度', - side: 'top', - align: 'start' - } - }, - { - element: '.tun-card', - popover: { - title: '虚拟网卡', - description: - '虚拟网卡,即同类软件中常见的“Tun 模式”,对于某些不遵循系统代理的应用,您可以打开虚拟网卡以让内核接管所有流量', - side: 'right', - align: 'start', - onNextClick: async (): Promise => { - navigate('/tun') - setTimeout(() => { - firstDriver.moveNext() - }, 0) - } - } - }, - { - element: '.tun-settings', - popover: { - title: '虚拟网卡设置', - description: - '这里可以更改虚拟网卡相关设置,Mihomo Party 理论上已经完全解决权限问题,如果您的虚拟网卡仍然不可用,可以尝试重设防火墙(Windows)或手动授权内核(MacOS/Linux)后重启内核', - side: 'bottom', - align: 'start' - } - }, - { - element: '.override-card', - popover: { - title: '覆写', - description: - 'Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,编辑好覆写文件一定要记得在需要覆写的订阅上启用,覆写文件的语法请参考 官方文档', - side: 'right', - align: 'center' - } - }, - { - element: '.dns-card', - popover: { - title: 'DNS', - description: - '软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭“接管 DNS 设置”,域名嗅探同理', - side: 'right', - align: 'center', - onNextClick: async (): Promise => { - navigate('/profiles') - setTimeout(() => { - firstDriver.moveNext() - }, 0) - } - } - }, - { - element: 'none', - popover: { - title: '教程结束', - description: - '现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 Telegram 群组 获取最新资讯', - side: 'top', - align: 'center', - onNextClick: async (): Promise => { - navigate('/profiles') - setTimeout(() => { - firstDriver.destroy() - }, 0) - } - } - } - ] -}) diff --git a/src/renderer/src/components/base/base-error-boundary.tsx b/src/renderer/src/components/base/base-error-boundary.tsx index 884e867..6a1d062 100644 --- a/src/renderer/src/components/base/base-error-boundary.tsx +++ b/src/renderer/src/components/base/base-error-boundary.tsx @@ -1,12 +1,15 @@ import { Button } from '@nextui-org/react' import { ReactNode } from 'react' import { ErrorBoundary, FallbackProps } from 'react-error-boundary' +import { useTranslation } from 'react-i18next' const ErrorFallback = ({ error }: FallbackProps): JSX.Element => { + const { t } = useTranslation() + return (

- {'应用崩溃了 :( 请将以下信息提交给开发者以排查错误'} + {t('common.error.appCrash')}

{error.message}

diff --git a/src/renderer/src/components/base/base-page.tsx b/src/renderer/src/components/base/base-page.tsx index 77d80f5..99687aa 100644 --- a/src/renderer/src/components/base/base-page.tsx +++ b/src/renderer/src/components/base/base-page.tsx @@ -4,6 +4,8 @@ import { platform } from '@renderer/utils/init' import { isAlwaysOnTop, setAlwaysOnTop } from '@renderer/utils/ipc' import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react' import { RiPushpin2Fill, RiPushpin2Line } from 'react-icons/ri' +import { useTranslation } from 'react-i18next' + interface Props { title?: React.ReactNode header?: React.ReactNode @@ -13,6 +15,7 @@ interface Props { let saveOnTop = false const BasePage = forwardRef((props, ref) => { + const { t } = useTranslation() const { appConfig } = useAppConfig() const { useWindowFrame = false } = appConfig || {} const [overlayWidth, setOverlayWidth] = React.useState(0) @@ -51,7 +54,7 @@ const BasePage = forwardRef((props, ref) => { size="sm" className="app-nodrag" isIconOnly - title="窗口置顶" + title={t('common.pinWindow')} variant="light" color={onTop ? 'primary' : 'default'} onPress={async () => { diff --git a/src/renderer/src/components/base/base-password-modal.tsx b/src/renderer/src/components/base/base-password-modal.tsx index 898b970..7925a3c 100644 --- a/src/renderer/src/components/base/base-password-modal.tsx +++ b/src/renderer/src/components/base/base-password-modal.tsx @@ -8,6 +8,7 @@ import { Input } from '@nextui-org/react' import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' interface Props { onCancel: () => void @@ -15,22 +16,23 @@ interface Props { } const BasePasswordModal: React.FC = (props) => { + const { t } = useTranslation() const { onCancel, onConfirm } = props const [password, setPassword] = useState('') return ( - 请输入root密码 + {t('common.enterRootPassword')} diff --git a/src/renderer/src/components/connections/connection-detail-modal.tsx b/src/renderer/src/components/connections/connection-detail-modal.tsx index 2fbb7f4..f12af39 100644 --- a/src/renderer/src/components/connections/connection-detail-modal.tsx +++ b/src/renderer/src/components/connections/connection-detail-modal.tsx @@ -13,8 +13,9 @@ import { import React from 'react' import SettingItem from '../base/base-setting-item' import { calcTraffic } from '@renderer/utils/calc' -import dayjs from 'dayjs' +import dayjs from '@renderer/utils/dayjs' import { BiCopy } from 'react-icons/bi' +import { useTranslation } from 'react-i18next' interface Props { connection: IMihomoConnectionDetail @@ -27,6 +28,7 @@ const CopyableSettingItem: React.FC<{ displayName?: string prefix?: string[] }> = ({ title, value, displayName, prefix = [] }) => { + const { t } = useTranslation() const getSubDomains = (domain: string): string[] => domain.split('.').length <= 2 ? [domain] @@ -93,7 +95,7 @@ const CopyableSettingItem: React.FC<{ actions={ - @@ -120,6 +122,8 @@ const CopyableSettingItem: React.FC<{ const ConnectionDetailModal: React.FC = (props) => { const { connection, onClose } = props + const { t } = useTranslation() + return ( = (props) => { scrollBehavior="inside" > - 连接详情 + {t('connections.detail.title')} - {dayjs(connection.start).fromNow()} - + {dayjs(connection.start).fromNow()} + {connection.rule} {connection.rulePayload ? `(${connection.rulePayload})` : ''} - {[...connection.chains].reverse().join('>>')} - {calcTraffic(connection.uploadSpeed || 0)}/s - {calcTraffic(connection.downloadSpeed || 0)}/s - {calcTraffic(connection.upload)} - {calcTraffic(connection.download)} + {[...connection.chains].reverse().join('>>')} + {calcTraffic(connection.uploadSpeed || 0)}/s + {calcTraffic(connection.downloadSpeed || 0)}/s + {calcTraffic(connection.upload)} + {calcTraffic(connection.download)} {connection.metadata.host && ( )} {connection.metadata.sniffHost && ( )} {connection.metadata.process && ( = (props) => { )} {connection.metadata.processPath && ( )} {connection.metadata.sourceIP && ( )} {connection.metadata.sourceGeoIP && connection.metadata.sourceGeoIP.length > 0 && ( )} {connection.metadata.sourceIPASN && ( )} {connection.metadata.destinationIP && ( @@ -212,83 +216,83 @@ const ConnectionDetailModal: React.FC = (props) => { {connection.metadata.destinationGeoIP && connection.metadata.destinationGeoIP.length > 0 && ( )} {connection.metadata.destinationIPASN && ( )} {connection.metadata.sourcePort && ( )} {connection.metadata.destinationPort && ( )} {connection.metadata.inboundIP && ( )} {connection.metadata.inboundPort && ( )} {connection.metadata.inboundName && ( )} {connection.metadata.inboundUser && ( )} {connection.metadata.remoteDestination && ( - {connection.metadata.remoteDestination} + {connection.metadata.remoteDestination} )} {connection.metadata.dnsMode && ( - {connection.metadata.dnsMode} + {connection.metadata.dnsMode} )} {connection.metadata.specialProxy && ( - {connection.metadata.specialProxy} + {connection.metadata.specialProxy} )} {connection.metadata.specialRules && ( - {connection.metadata.specialRules} + {connection.metadata.specialRules} )} diff --git a/src/renderer/src/components/connections/connection-item.tsx b/src/renderer/src/components/connections/connection-item.tsx index 12d374f..725b594 100644 --- a/src/renderer/src/components/connections/connection-item.tsx +++ b/src/renderer/src/components/connections/connection-item.tsx @@ -1,6 +1,6 @@ import { Button, Card, CardFooter, CardHeader, Chip } from '@nextui-org/react' import { calcTraffic } from '@renderer/utils/calc' -import dayjs from 'dayjs' +import dayjs from '@renderer/utils/dayjs' import React, { useEffect } from 'react' import { CgClose, CgTrash } from 'react-icons/cg' diff --git a/src/renderer/src/components/mihomo/interface-modal.tsx b/src/renderer/src/components/mihomo/interface-modal.tsx index 105d6a3..5784e6b 100644 --- a/src/renderer/src/components/mihomo/interface-modal.tsx +++ b/src/renderer/src/components/mihomo/interface-modal.tsx @@ -9,11 +9,15 @@ import { } from '@nextui-org/react' import React, { useEffect, useState } from 'react' import { getInterfaces } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' + interface Props { onClose: () => void } + const InterfaceModal: React.FC = (props) => { const { onClose } = props + const { t } = useTranslation() const [info, setInfo] = useState>({}) const getInfo = async (): Promise => { setInfo(await getInterfaces()) @@ -33,7 +37,7 @@ const InterfaceModal: React.FC = (props) => { scrollBehavior="inside" > - 网络信息 + {t('mihomo.interface.title')} {Object.entries(info).map(([key, value]) => { return ( @@ -57,7 +61,7 @@ const InterfaceModal: React.FC = (props) => { @@ -65,4 +69,4 @@ const InterfaceModal: React.FC = (props) => { ) } -export default InterfaceModal +export default InterfaceModal \ No newline at end of file diff --git a/src/renderer/src/components/override/edit-file-modal.tsx b/src/renderer/src/components/override/edit-file-modal.tsx index 4029611..c885922 100644 --- a/src/renderer/src/components/override/edit-file-modal.tsx +++ b/src/renderer/src/components/override/edit-file-modal.tsx @@ -2,6 +2,8 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from import React, { useEffect, useState } from 'react' import { BaseEditor } from '../base/base-editor' import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' + interface Props { id: string language: 'javascript' | 'yaml' @@ -10,6 +12,7 @@ interface Props { const EditFileModal: React.FC = (props) => { const { id, language, onClose } = props const [currData, setCurrData] = useState('') + const { t } = useTranslation() const getContent = async (): Promise => { setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml')) @@ -31,7 +34,9 @@ const EditFileModal: React.FC = (props) => { > - 编辑覆写{language === 'javascript' ? '脚本' : '配置'} + {t('override.editFile.title', { + type: language === 'javascript' ? t('override.editFile.script') : t('override.editFile.config') + })} = (props) => { diff --git a/src/renderer/src/components/override/edit-info-modal.tsx b/src/renderer/src/components/override/edit-info-modal.tsx index e182abe..2e44e03 100644 --- a/src/renderer/src/components/override/edit-info-modal.tsx +++ b/src/renderer/src/components/override/edit-info-modal.tsx @@ -11,6 +11,8 @@ import { import React, { useState } from 'react' import SettingItem from '../base/base-setting-item' import { restartCore } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' + interface Props { item: IOverrideItem updateOverrideItem: (item: IOverrideItem) => Promise @@ -19,6 +21,7 @@ interface Props { const EditInfoModal: React.FC = (props) => { const { item, updateOverrideItem, onClose } = props const [values, setValues] = useState(item) + const { t } = useTranslation() const onSave = async (): Promise => { await updateOverrideItem(values) @@ -36,9 +39,9 @@ const EditInfoModal: React.FC = (props) => { scrollBehavior="inside" > - 编辑信息 + {t('override.editInfo.title')} - + = (props) => { /> {values.type === 'remote' && ( - + = (props) => { /> )} - + = (props) => { diff --git a/src/renderer/src/components/override/exec-log-modal.tsx b/src/renderer/src/components/override/exec-log-modal.tsx index 9f33e04..81a0dbd 100644 --- a/src/renderer/src/components/override/exec-log-modal.tsx +++ b/src/renderer/src/components/override/exec-log-modal.tsx @@ -9,6 +9,8 @@ import { } from '@nextui-org/react' import React, { useEffect, useState } from 'react' import { getOverride } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' + interface Props { id: string onClose: () => void @@ -16,6 +18,7 @@ interface Props { const ExecLogModal: React.FC = (props) => { const { id, onClose } = props const [logs, setLogs] = useState([]) + const { t } = useTranslation() const getLog = async (): Promise => { setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean)) @@ -35,7 +38,7 @@ const ExecLogModal: React.FC = (props) => { scrollBehavior="inside" > - 执行日志 + {t('override.execLog.title')} {logs.map((log) => { return ( @@ -48,7 +51,7 @@ const ExecLogModal: React.FC = (props) => { diff --git a/src/renderer/src/components/override/override-item.tsx b/src/renderer/src/components/override/override-item.tsx index a3af40e..359bb9f 100644 --- a/src/renderer/src/components/override/override-item.tsx +++ b/src/renderer/src/components/override/override-item.tsx @@ -9,7 +9,7 @@ import { DropdownTrigger } from '@nextui-org/react' import { IoMdMore, IoMdRefresh } from 'react-icons/io' -import dayjs from 'dayjs' +import dayjs from '@renderer/utils/dayjs' import React, { Key, useEffect, useMemo, useState } from 'react' import EditFileModal from './edit-file-modal' import EditInfoModal from './edit-info-modal' @@ -17,6 +17,7 @@ import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import ExecLogModal from './exec-log-modal' import { openFile, restartCore } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' interface Props { info: IOverrideItem @@ -35,6 +36,7 @@ interface MenuItem { } const OverrideItem: React.FC = (props) => { + const { t } = useTranslation() const { info, addOverrideItem, removeOverrideItem, mutateOverrideConfig, updateOverrideItem } = props const [updating, setUpdating] = useState(false) @@ -57,35 +59,35 @@ const OverrideItem: React.FC = (props) => { const list = [ { key: 'edit-info', - label: '编辑信息', + label: t('override.menuItems.editInfo'), showDivider: false, color: 'default', className: '' } as MenuItem, { key: 'edit-file', - label: '编辑文件', + label: t('override.menuItems.editFile'), showDivider: false, color: 'default', className: '' } as MenuItem, { key: 'open-file', - label: '打开文件', + label: t('override.menuItems.openFile'), showDivider: false, color: 'default', className: '' } as MenuItem, { key: 'exec-log', - label: '执行日志', + label: t('override.menuItems.execLog'), showDivider: true, color: 'default', className: '' } as MenuItem, { key: 'delete', - label: '删除', + label: t('override.menuItems.delete'), showDivider: false, color: 'danger', className: 'text-danger' @@ -95,7 +97,7 @@ const OverrideItem: React.FC = (props) => { list.splice(3, 1) } return list - }, [info]) + }, [info, t]) const onMenuAction = (key: Key): void => { switch (key) { case 'edit-info': { @@ -228,7 +230,7 @@ const OverrideItem: React.FC = (props) => {
{info.global && ( - 全局 + {t('override.labels.global')} )} diff --git a/src/renderer/src/components/profiles/edit-file-modal.tsx b/src/renderer/src/components/profiles/edit-file-modal.tsx index 4e7fc5e..f444f3c 100644 --- a/src/renderer/src/components/profiles/edit-file-modal.tsx +++ b/src/renderer/src/components/profiles/edit-file-modal.tsx @@ -3,14 +3,18 @@ import React, { useEffect, useState } from 'react' import { BaseEditor } from '../base/base-editor' import { getProfileStr, setProfileStr } from '@renderer/utils/ipc' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' + interface Props { id: string onClose: () => void } + const EditFileModal: React.FC = (props) => { const { id, onClose } = props const [currData, setCurrData] = useState('') const navigate = useNavigate() + const { t } = useTranslation() const getContent = async (): Promise => { setCurrData(await getProfileStr(id)) @@ -33,9 +37,9 @@ const EditFileModal: React.FC = (props) => {
-
编辑订阅
+
{t('profiles.editFile.title')}
- 注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用 + {t('profiles.editFile.notice')} - 功能 + {t('profiles.editFile.feature')}
@@ -56,7 +60,7 @@ const EditFileModal: React.FC = (props) => {
diff --git a/src/renderer/src/components/profiles/edit-info-modal.tsx b/src/renderer/src/components/profiles/edit-info-modal.tsx index 02b54c4..ff72f6b 100644 --- a/src/renderer/src/components/profiles/edit-info-modal.tsx +++ b/src/renderer/src/components/profiles/edit-info-modal.tsx @@ -19,6 +19,7 @@ import { useOverrideConfig } from '@renderer/hooks/use-override-config' import { restartCore } from '@renderer/utils/ipc' import { MdDeleteForever } from 'react-icons/md' import { FaPlus } from 'react-icons/fa6' +import { useTranslation } from 'react-i18next' interface Props { item: IProfileItem @@ -31,6 +32,7 @@ const EditInfoModal: React.FC = (props) => { const { items: overrideItems = [] } = overrideConfig || {} const [values, setValues] = useState(item) const inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]' + const { t } = useTranslation() const onSave = async (): Promise => { try { @@ -62,9 +64,9 @@ const EditInfoModal: React.FC = (props) => { scrollBehavior="inside" > - 编辑信息 + {t('profiles.editInfo.title')} - + = (props) => { {values.type === 'remote' && ( <> - + = (props) => { }} /> - + = (props) => { }} /> - + = (props) => { )} - +
{overrideItems .filter((i) => i.global) @@ -116,7 +118,7 @@ const EditInfoModal: React.FC = (props) => { return (
) @@ -153,7 +155,7 @@ const EditInfoModal: React.FC = (props) => { { setValues({ ...values, @@ -173,10 +175,10 @@ const EditInfoModal: React.FC = (props) => { diff --git a/src/renderer/src/components/profiles/profile-item.tsx b/src/renderer/src/components/profiles/profile-item.tsx index 733a5b0..20a233d 100644 --- a/src/renderer/src/components/profiles/profile-item.tsx +++ b/src/renderer/src/components/profiles/profile-item.tsx @@ -13,7 +13,7 @@ import { } from '@nextui-org/react' import { calcPercent, calcTraffic } from '@renderer/utils/calc' import { IoMdMore, IoMdRefresh } from 'react-icons/io' -import dayjs from 'dayjs' +import dayjs from '@renderer/utils/dayjs' import React, { Key, useEffect, useMemo, useState } from 'react' import EditFileModal from './edit-file-modal' import EditInfoModal from './edit-info-modal' @@ -21,6 +21,7 @@ import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { openFile } from '@renderer/utils/ipc' import { useAppConfig } from '@renderer/hooks/use-app-config' +import { useTranslation } from 'react-i18next' interface Props { info: IProfileItem @@ -40,6 +41,7 @@ interface MenuItem { className: string } const ProfileItem: React.FC = (props) => { + const { t } = useTranslation() const { info, addProfileItem, @@ -75,28 +77,28 @@ const ProfileItem: React.FC = (props) => { const list = [ { key: 'edit-info', - label: '编辑信息', + label: t('profiles.editInfo.title'), showDivider: false, color: 'default', className: '' } as MenuItem, { key: 'edit-file', - label: '编辑文件', + label: t('profiles.editFile.title'), showDivider: false, color: 'default', className: '' } as MenuItem, { key: 'open-file', - label: '打开文件', + label: t('profiles.openFile'), showDivider: true, color: 'default', className: '' } as MenuItem, { key: 'delete', - label: '删除', + label: t('common.delete'), showDivider: false, color: 'danger', className: 'text-danger' @@ -105,14 +107,14 @@ const ProfileItem: React.FC = (props) => { if (info.home) { list.unshift({ key: 'home', - label: '主页', + label: t('profiles.home'), showDivider: false, color: 'default', className: '' } as MenuItem) } return list - }, [info]) + }, [info, t]) const onMenuAction = async (key: Key): Promise => { switch (key) { @@ -282,7 +284,7 @@ const ProfileItem: React.FC = (props) => { variant="bordered" className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`} > - 远程 + {t('profiles.remote')} {dayjs(info.updated).fromNow()}
@@ -296,14 +298,14 @@ const ProfileItem: React.FC = (props) => { variant="bordered" className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`} > - 本地 + {t('profiles.local')}
)} {extra && ( void @@ -14,6 +15,7 @@ interface Props { } const ProxyItem: React.FC = (props) => { + const { t } = useTranslation() const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay } = props const delay = useMemo(() => { @@ -32,8 +34,8 @@ const ProxyItem: React.FC = (props) => { } function delayText(delay: number): string { - if (delay === -1) return '测试' - if (delay === 0) return '超时' + if (delay === -1) return t('proxies.delay.test') + if (delay === 0) return t('proxies.delay.timeout') return delay.toString() } @@ -74,7 +76,7 @@ const ProxyItem: React.FC = (props) => { {fixed && ( )}
- +
{geositeInput !== geoxUrl.geosite && ( )}
- +
{mmdbInput !== geoxUrl.mmdb && ( )}
- +
{asnInput !== geoxUrl.asn && ( )}
- + { { setUpdating(true) try { await mihomoUpgradeGeo() - new Notification('Geo 数据库更新成功') + new Notification(t('resources.geoData.updateSuccess')) } catch (e) { alert(e) } finally { @@ -141,7 +143,7 @@ const GeoData: React.FC = () => { /> {geoAutoUpdate && ( - + { const [showDetails, setShowDetails] = useState({ diff --git a/src/renderer/src/components/settings/actions.tsx b/src/renderer/src/components/settings/actions.tsx index 1a80925..31b71f4 100644 --- a/src/renderer/src/components/settings/actions.tsx +++ b/src/renderer/src/components/settings/actions.tsx @@ -12,9 +12,11 @@ import { useState } from 'react' import UpdaterModal from '../updater/updater-modal' import { version } from '@renderer/utils/init' import { IoIosHelpCircle } from 'react-icons/io' -import { firstDriver } from '@renderer/App' +import { getDriver } from '@renderer/App' +import { useTranslation } from 'react-i18next' const Actions: React.FC = () => { + const { t } = useTranslation() const [newVersion, setNewVersion] = useState('') const [changelog, setChangelog] = useState('') const [openUpdate, setOpenUpdate] = useState(false) @@ -30,12 +32,12 @@ const Actions: React.FC = () => { /> )} - - - + + @@ -72,13 +76,13 @@ const Actions: React.FC = () => { divider > + @@ -87,13 +91,13 @@ const Actions: React.FC = () => { divider > + @@ -102,15 +106,15 @@ const Actions: React.FC = () => { divider > - + - +
v{version}
diff --git a/src/renderer/src/components/settings/css-editor-modal.tsx b/src/renderer/src/components/settings/css-editor-modal.tsx index 9b18034..4f953c1 100644 --- a/src/renderer/src/components/settings/css-editor-modal.tsx +++ b/src/renderer/src/components/settings/css-editor-modal.tsx @@ -2,12 +2,16 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from import { BaseEditor } from '@renderer/components/base/base-editor' import { readTheme } from '@renderer/utils/ipc' import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + interface Props { theme: string onCancel: () => void onConfirm: (script: string) => void } + const CSSEditorModal: React.FC = (props) => { + const { t } = useTranslation() const { theme, onCancel, onConfirm } = props const [currData, setCurrData] = useState('') @@ -30,7 +34,7 @@ const CSSEditorModal: React.FC = (props) => { scrollBehavior="inside" > - 编辑主题 + {t('theme.editor.title')} = (props) => { diff --git a/src/renderer/src/components/settings/general-config.tsx b/src/renderer/src/components/settings/general-config.tsx index 6069552..bcbb8b5 100644 --- a/src/renderer/src/components/settings/general-config.tsx +++ b/src/renderer/src/components/settings/general-config.tsx @@ -29,8 +29,10 @@ import { useTheme } from 'next-themes' import { IoIosHelpCircle, IoMdCloudDownload } from 'react-icons/io' import { MdEditDocument } from 'react-icons/md' import CSSEditorModal from './css-editor-modal' +import { useTranslation } from 'react-i18next' const GeneralConfig: React.FC = () => { + const { t, i18n } = useTranslation() const { data: enable, mutate: mutateEnable } = useSWR('checkAutoRun', checkAutoRun) const { appConfig, patchAppConfig } = useAppConfig() const [customThemes, setCustomThemes] = useState<{ key: string; label: string }[]>() @@ -52,7 +54,8 @@ const GeneralConfig: React.FC = () => { customTheme = 'default.css', envType = [platform === 'win32' ? 'powershell' : 'bash'], autoCheckUpdate, - appTheme = 'system' + appTheme = 'system', + language = 'zh-CN' } = appConfig || {} useEffect(() => { @@ -75,7 +78,24 @@ const GeneralConfig: React.FC = () => { /> )} - + + + + { }} /> - + { }} /> - + { /> + @@ -132,12 +152,12 @@ const GeneralConfig: React.FC = () => { /> {autoQuitWithoutCore && ( - + { let num = parseInt(v) @@ -149,7 +169,7 @@ const GeneralConfig: React.FC = () => { )} ( @@ -189,7 +191,7 @@ const MihomoConfig: React.FC = () => { }} /> - + { }} /> - + { }} /> - + { }} /> - + {pauseSSIDInput.join('') !== pauseSSID.join('') && ( )} diff --git a/src/renderer/src/components/settings/shortcut-config.tsx b/src/renderer/src/components/settings/shortcut-config.tsx index 25f4f9b..6e1f1d4 100644 --- a/src/renderer/src/components/settings/shortcut-config.tsx +++ b/src/renderer/src/components/settings/shortcut-config.tsx @@ -5,6 +5,7 @@ import { useAppConfig } from '@renderer/hooks/use-app-config' import React, { KeyboardEvent, useState } from 'react' import { platform } from '@renderer/utils/init' import { registerShortcut } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' const keyMap = { Backquote: '`', @@ -40,6 +41,7 @@ const keyMap = { } const ShortcutConfig: React.FC = () => { + const { t } = useTranslation() const { appConfig, patchAppConfig } = useAppConfig() const { showWindowShortcut = '', @@ -54,8 +56,8 @@ const ShortcutConfig: React.FC = () => { } = appConfig || {} return ( - - + +
{ />
- +
{ />
- +
{ />
- +
{ />
- +
{ />
- +
{ />
- +
{ />
- +
{ />
- +
) => Promise }> = (props) => { + const { t } = useTranslation() const { value, action, patchAppConfig } = props const [inputValue, setInputValue] = useState(value) @@ -210,18 +213,18 @@ const ShortcutInput: React.FC<{ await patchAppConfig({ [action]: inputValue }) window.electron.ipcRenderer.send('updateTrayMenu') } else { - alert('快捷键注册失败') + alert(t('common.error.shortcutRegistrationFailed')) } } catch (e) { - alert(`快捷键注册失败: ${e}`) + alert(t('common.error.shortcutRegistrationFailedWithError', { error: e })) } }} > - 确认 + {t('common.confirm')} )} { parseShortcut(e, setInputValue) }} diff --git a/src/renderer/src/components/settings/sider-config.tsx b/src/renderer/src/components/settings/sider-config.tsx index af3610a..c550e6c 100644 --- a/src/renderer/src/components/settings/sider-config.tsx +++ b/src/renderer/src/components/settings/sider-config.tsx @@ -1,76 +1,73 @@ -import React from 'react' -import SettingCard from '../base/base-setting-card' -import SettingItem from '../base/base-setting-item' -import { RadioGroup, Radio } from '@nextui-org/react' +import SettingCard from '@renderer/components/base/base-setting-card' +import SettingItem from '@renderer/components/base/base-setting-item' import { useAppConfig } from '@renderer/hooks/use-app-config' -const titleMap = { - sysproxyCardStatus: '系统代理', - tunCardStatus: '虚拟网卡', - profileCardStatus: '订阅管理', - proxyCardStatus: '代理组', - ruleCardStatus: '规则', - resourceCardStatus: '外部资源', - overrideCardStatus: '覆写', - connectionCardStatus: '连接', - mihomoCoreCardStatus: '内核', - dnsCardStatus: 'DNS', - sniffCardStatus: '域名嗅探', - logCardStatus: '日志', - substoreCardStatus: 'Sub-Store' +import { Radio, RadioGroup } from '@nextui-org/react' +import { useTranslation } from 'react-i18next' +import type { FC } from 'react' + +const titleMap: Record = { + sysproxyCardStatus: 'sider.cards.systemProxy', + tunCardStatus: 'sider.cards.tun', + profileCardStatus: 'sider.cards.profiles', + proxyCardStatus: 'sider.cards.proxies', + ruleCardStatus: 'sider.cards.rules', + resourceCardStatus: 'sider.cards.resources', + overrideCardStatus: 'sider.cards.override', + connectionCardStatus: 'sider.cards.connections', + mihomoCoreCardStatus: 'sider.cards.core', + dnsCardStatus: 'sider.cards.dns', + sniffCardStatus: 'sider.cards.sniff', + logCardStatus: 'sider.cards.logs', + substoreCardStatus: 'sider.cards.substore' } -const SiderConfig: React.FC = () => { + +const sizeMap: Record = { + 'col-span-2': 'sider.size.large', + 'col-span-1': 'sider.size.small', + hidden: 'sider.size.hidden' +} + +const SiderConfig: FC = () => { + const { t } = useTranslation() const { appConfig, patchAppConfig } = useAppConfig() - const { - sysproxyCardStatus = 'col-span-1', - tunCardStatus = 'col-span-1', - profileCardStatus = 'col-span-2', - proxyCardStatus = 'col-span-1', - ruleCardStatus = 'col-span-1', - resourceCardStatus = 'col-span-1', - overrideCardStatus = 'col-span-1', - connectionCardStatus = 'col-span-2', - mihomoCoreCardStatus = 'col-span-2', - dnsCardStatus = 'col-span-1', - sniffCardStatus = 'col-span-1', - logCardStatus = 'col-span-1', - substoreCardStatus = 'col-span-1' - } = appConfig || {} const cardStatus = { - sysproxyCardStatus, - tunCardStatus, - profileCardStatus, - proxyCardStatus, - ruleCardStatus, - resourceCardStatus, - overrideCardStatus, - connectionCardStatus, - mihomoCoreCardStatus, - dnsCardStatus, - sniffCardStatus, - logCardStatus, - substoreCardStatus + sysproxyCardStatus: appConfig?.sysproxyCardStatus || 'col-span-1', + tunCardStatus: appConfig?.tunCardStatus || 'col-span-1', + profileCardStatus: appConfig?.profileCardStatus || 'col-span-2', + proxyCardStatus: appConfig?.proxyCardStatus || 'col-span-1', + ruleCardStatus: appConfig?.ruleCardStatus || 'col-span-1', + resourceCardStatus: appConfig?.resourceCardStatus || 'col-span-1', + overrideCardStatus: appConfig?.overrideCardStatus || 'col-span-1', + connectionCardStatus: appConfig?.connectionCardStatus || 'col-span-2', + mihomoCoreCardStatus: appConfig?.mihomoCoreCardStatus || 'col-span-2', + dnsCardStatus: appConfig?.dnsCardStatus || 'col-span-1', + sniffCardStatus: appConfig?.sniffCardStatus || 'col-span-1', + logCardStatus: appConfig?.logCardStatus || 'col-span-1', + substoreCardStatus: appConfig?.substoreCardStatus || 'col-span-1' } return ( - - {Object.keys(cardStatus).map((key, index, array) => { - return ( - - { - patchAppConfig({ [key]: v as CardStatus }) - }} - > - - - 隐藏 - - - ) - })} + + {Object.entries(cardStatus).map(([key, value]) => ( + + { + if (v === 'col-span-1' || v === 'col-span-2' || v === 'hidden') { + patchAppConfig({ [key]: v }) + } + }} + > + {Object.entries(sizeMap).map(([size, label]) => ( + + {t(label)} + + ))} + + + ))} ) } diff --git a/src/renderer/src/components/settings/substore-config.tsx b/src/renderer/src/components/settings/substore-config.tsx index 7a04d55..75785ea 100644 --- a/src/renderer/src/components/settings/substore-config.tsx +++ b/src/renderer/src/components/settings/substore-config.tsx @@ -11,8 +11,10 @@ import { import { useAppConfig } from '@renderer/hooks/use-app-config' import debounce from '@renderer/utils/debounce' import { isValidCron } from 'cron-validator' +import { useTranslation } from 'react-i18next' const SubStoreConfig: React.FC = () => { + const { t } = useTranslation() const { appConfig, patchAppConfig } = useAppConfig() const { useSubStore = true, @@ -37,8 +39,8 @@ const SubStoreConfig: React.FC = () => { const [subStoreBackendUploadCronValue, setSubStoreBackendUploadCronValue] = useState(subStoreBackendUploadCron) return ( - - + + { {useSubStore && ( <> - + { }} /> - + { /> {useCustomSubStore ? ( - + { setCustomSubStoreUrlValue(v) setCustomSubStoreUrl(v) @@ -112,7 +114,7 @@ const SubStoreConfig: React.FC = () => { ) : ( <> - + { }} /> - +
{subStoreBackendSyncCronValue !== subStoreBackendSyncCron && ( )} { setSubStoreBackendSyncCronValue(v) }} />
- +
{subStoreBackendDownloadCronValue !== subStoreBackendDownloadCron && ( )} { setSubStoreBackendDownloadCronValue(v) }} />
- +
{subStoreBackendUploadCronValue !== subStoreBackendUploadCron && ( )} { setSubStoreBackendUploadCronValue(v) }} diff --git a/src/renderer/src/components/settings/webdav-config.tsx b/src/renderer/src/components/settings/webdav-config.tsx index c419794..e189b43 100644 --- a/src/renderer/src/components/settings/webdav-config.tsx +++ b/src/renderer/src/components/settings/webdav-config.tsx @@ -6,8 +6,10 @@ import { listWebdavBackups, webdavBackup } from '@renderer/utils/ipc' import WebdavRestoreModal from './webdav-restore-modal' import debounce from '@renderer/utils/debounce' import { useAppConfig } from '@renderer/hooks/use-app-config' +import { useTranslation } from 'react-i18next' const WebdavConfig: React.FC = () => { + const { t } = useTranslation() const { appConfig, patchAppConfig } = useAppConfig() const { webdavUrl, webdavUsername, webdavPassword, webdavDir = 'mihomo-party' } = appConfig || {} const [backuping, setBackuping] = useState(false) @@ -23,7 +25,9 @@ const WebdavConfig: React.FC = () => { setBackuping(true) try { await webdavBackup() - new window.Notification('备份成功', { body: '备份文件已上传至 WebDAV' }) + new window.Notification(t('webdav.notification.backupSuccess.title'), { + body: t('webdav.notification.backupSuccess.body') + }) } catch (e) { alert(e) } finally { @@ -38,7 +42,7 @@ const WebdavConfig: React.FC = () => { setFilenames(filenames) setRestoreOpen(true) } catch (e) { - alert(`获取备份列表失败: ${e}`) + alert(t('common.error.getBackupListFailed', { error: e })) } finally { setRestoring(false) } @@ -48,8 +52,8 @@ const WebdavConfig: React.FC = () => { {restoreOpen && ( setRestoreOpen(false)} /> )} - - + + { }} /> - + { }} /> - + { }} /> - + {
diff --git a/src/renderer/src/components/settings/webdav-restore-modal.tsx b/src/renderer/src/components/settings/webdav-restore-modal.tsx index e3c24a4..0b8f3b4 100644 --- a/src/renderer/src/components/settings/webdav-restore-modal.tsx +++ b/src/renderer/src/components/settings/webdav-restore-modal.tsx @@ -2,11 +2,15 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from import { relaunchApp, webdavDelete, webdavRestore } from '@renderer/utils/ipc' import React, { useState } from 'react' import { MdDeleteForever } from 'react-icons/md' +import { useTranslation } from 'react-i18next' + interface Props { filenames: string[] onClose: () => void } + const WebdavRestoreModal: React.FC = (props) => { + const { t } = useTranslation() const { filenames: names, onClose } = props const [filenames, setFilenames] = useState(names) const [restoring, setRestoring] = useState(false) @@ -21,10 +25,10 @@ const WebdavRestoreModal: React.FC = (props) => { scrollBehavior="inside" > - 恢复备份 + {t('webdav.restore.title')} {filenames.length === 0 ? ( -
还没有备份
+
{t('webdav.restore.noBackups')}
) : ( filenames.map((filename) => (
@@ -39,7 +43,7 @@ const WebdavRestoreModal: React.FC = (props) => { await webdavRestore(filename) await relaunchApp() } catch (e) { - alert(`恢复失败: ${e}`) + alert(t('common.error.restoreFailed', { error: e })) } finally { setRestoring(false) } @@ -57,7 +61,7 @@ const WebdavRestoreModal: React.FC = (props) => { await webdavDelete(filename) setFilenames(filenames.filter((name) => name !== filename)) } catch (e) { - alert(`删除失败: ${e}`) + alert(t('common.error.deleteFailed', { error: e })) } }} > @@ -69,7 +73,7 @@ const WebdavRestoreModal: React.FC = (props) => { diff --git a/src/renderer/src/components/sider/config-viewer.tsx b/src/renderer/src/components/sider/config-viewer.tsx index 1d2c1b6..f379844 100644 --- a/src/renderer/src/components/sider/config-viewer.tsx +++ b/src/renderer/src/components/sider/config-viewer.tsx @@ -2,10 +2,13 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from import React, { useEffect, useState } from 'react' import { BaseEditor } from '../base/base-editor' import { getRuntimeConfigStr } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' + interface Props { onClose: () => void } const ConfigViewer: React.FC = (props) => { + const { t } = useTranslation() const { onClose } = props const [currData, setCurrData] = useState('') @@ -28,13 +31,13 @@ const ConfigViewer: React.FC = (props) => { scrollBehavior="inside" > - 当前运行时配置 + {t('sider.cards.config')} diff --git a/src/renderer/src/components/sider/conn-card.tsx b/src/renderer/src/components/sider/conn-card.tsx index 923319b..8ae6b04 100644 --- a/src/renderer/src/components/sider/conn-card.tsx +++ b/src/renderer/src/components/sider/conn-card.tsx @@ -10,6 +10,7 @@ import { useTheme } from 'next-themes' import { useAppConfig } from '@renderer/hooks/use-app-config' import { platform } from '@renderer/utils/init' import { Area, AreaChart, ResponsiveContainer } from 'recharts' +import { useTranslation } from 'react-i18next' let currentUpload: number | undefined = undefined let currentDownload: number | undefined = undefined @@ -27,6 +28,7 @@ const ConnCard: React.FC = (props) => { const location = useLocation() const navigate = useNavigate() const match = location.pathname.includes('/connections') + const { t } = useTranslation() const [upload, setUpload] = useState(0) const [download, setDownload] = useState(0) @@ -95,7 +97,7 @@ const ConnCard: React.FC = (props) => { if (iconOnly) { return (
- + ) : (
@@ -197,14 +199,14 @@ const ProfileCard: React.FC = (props) => { variant="bordered" className={`${match ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`} > - 本地 + {t('sider.cards.local')}
)} {extra && ( @@ -238,7 +240,7 @@ const ProfileCard: React.FC = (props) => {

- 订阅管理 + {t('sider.cards.profiles')}

diff --git a/src/renderer/src/components/sider/proxy-card.tsx b/src/renderer/src/components/sider/proxy-card.tsx index 3a811c5..76383fd 100644 --- a/src/renderer/src/components/sider/proxy-card.tsx +++ b/src/renderer/src/components/sider/proxy-card.tsx @@ -6,12 +6,14 @@ import { useLocation, useNavigate } from 'react-router-dom' import { useGroups } from '@renderer/hooks/use-groups' import { useAppConfig } from '@renderer/hooks/use-app-config' import React from 'react' +import { useTranslation } from 'react-i18next' interface Props { iconOnly?: boolean } const ProxyCard: React.FC = (props) => { + const { t } = useTranslation() const { appConfig } = useAppConfig() const { iconOnly } = props const { proxyCardStatus = 'col-span-1' } = appConfig || {} @@ -34,7 +36,7 @@ const ProxyCard: React.FC = (props) => { if (iconOnly) { return (
- + diff --git a/src/renderer/src/components/updater/updater-modal.tsx b/src/renderer/src/components/updater/updater-modal.tsx index 4f13c41..935316f 100644 --- a/src/renderer/src/components/updater/updater-modal.tsx +++ b/src/renderer/src/components/updater/updater-modal.tsx @@ -10,15 +10,19 @@ import { import ReactMarkdown from 'react-markdown' import React, { useState } from 'react' import { downloadAndInstallUpdate } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' interface Props { version: string changelog: string onClose: () => void } + const UpdaterModal: React.FC = (props) => { const { version, changelog, onClose } = props const [downloading, setDownloading] = useState(false) + const { t } = useTranslation() + const onUpdate = async (): Promise => { try { await downloadAndInstallUpdate(version) @@ -38,7 +42,7 @@ const UpdaterModal: React.FC = (props) => { > -
v{version} 版本就绪
+
{t('common.updater.versionReady', { version })}
@@ -65,7 +69,7 @@ const UpdaterModal: React.FC = (props) => {
diff --git a/src/renderer/src/i18n.ts b/src/renderer/src/i18n.ts new file mode 100644 index 0000000..358a6e7 --- /dev/null +++ b/src/renderer/src/i18n.ts @@ -0,0 +1,18 @@ +import { initReactI18next } from 'react-i18next' +import i18n, { initI18n } from '../../shared/i18n' +import { getAppConfig } from './utils/ipc' + +// 初始化 React i18next +i18n.use(initReactI18next) + +// 从配置中读取语言设置并初始化 +getAppConfig().then((config) => { + initI18n({ lng: config.language }) +}) + +// 通知主进程语言变更 +i18n.on('languageChanged', (lng) => { + window.electron.ipcRenderer.invoke('changeLanguage', lng) +}) + +export default i18n \ No newline at end of file diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json new file mode 100644 index 0000000..3186825 --- /dev/null +++ b/src/renderer/src/locales/en-US.json @@ -0,0 +1,723 @@ +{ + "common": { + "settings": "Settings", + "profiles": "Profiles", + "proxies": "Proxies", + "connections": "Connections", + "dns": "DNS", + "tun": "TUN", + "save": "Save", + "cancel": "Cancel", + "edit": "Edit", + "delete": "Delete", + "seconds": "seconds", + "confirm": "Confirm", + "auto": "Auto", + "default": "Default", + "close": "Close", + "pinWindow": "Pin Window", + "enterRootPassword": "Please enter root password", + "next": "Next", + "prev": "Previous", + "done": "Done", + "notification": { + "restartRequired": "Restart required for changes to take effect" + }, + "error": { + "appCrash": "Application crashed :( Please submit the following information to the developer to troubleshoot", + "copyErrorMessage": "Copy Error Message", + "invalidCron": "Invalid Cron expression", + "getBackupListFailed": "Failed to get backup list: {{error}}", + "restoreFailed": "Failed to restore: {{error}}", + "deleteFailed": "Failed to delete: {{error}}", + "shortcutRegistrationFailed": "Failed to register shortcut", + "shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}" + }, + "updater": { + "versionReady": "v{{version}} Version Ready", + "goToDownload": "Go to Download", + "update": "Update" + } + }, + "settings": { + "general": "General Settings", + "mihomo": "Mihomo Settings", + "language": "Language", + "theme": "Theme", + "darkMode": "Dark Mode", + "lightMode": "Light Mode", + "autoStart": "Auto Start", + "autoCheckUpdate": "Auto Check Update", + "silentStart": "Silent Start", + "autoQuitWithoutCore": "Auto Enable Light Mode", + "autoQuitWithoutCoreTooltip": "Automatically enter light mode after closing window for specified time", + "autoQuitWithoutCoreDelay": "Light Mode Auto Enable Delay", + "envType": "Environment Variable Type", + "showFloatingWindow": "Show Floating Window", + "spinFloatingIcon": "Spin Floating Icon Based on Network Speed", + "disableTray": "Disable Tray Icon", + "proxyInTray": "Show Proxy Info in Tray Menu", + "showTraffic_windows": "Show Network Speed in Taskbar", + "showTraffic_mac": "Show Network Speed in Status Bar", + "showDockIcon": "Show Dock Icon", + "useWindowFrame": "Use System Title Bar", + "backgroundColor": "Background Color", + "backgroundAuto": "Auto", + "backgroundDark": "Dark", + "backgroundLight": "Light", + "fetchTheme": "Fetch Theme", + "importTheme": "Import Theme", + "editTheme": "Edit Theme", + "selectTheme": "Select Theme", + "links": { + "docs": "Documentation", + "github": "GitHub Repository", + "telegram": "Telegram Group" + }, + "title": "Application Settings" + }, + "mihomo": { + "userAgent": "Subscription User Agent", + "userAgentPlaceholder": "Default: clash.meta", + "delayTest": { + "url": "Delay Test URL", + "urlPlaceholder": "Default: https://www.gstatic.com/generate_204", + "concurrency": "Delay Test Concurrency", + "concurrencyPlaceholder": "Default: 50", + "timeout": "Delay Test Timeout", + "timeoutPlaceholder": "Default: 5000" + }, + "gist": { + "title": "Sync Runtime Config to Gist", + "copyUrl": "Copy Gist URL", + "token": "GitHub Token" + }, + "proxyColumns": { + "title": "Proxy Display Columns", + "auto": "Auto", + "one": "One Column", + "two": "Two Columns", + "three": "Three Columns", + "four": "Four Columns" + }, + "cpuPriority": { + "title": "Core Process Priority", + "realtime": "Realtime", + "high": "High", + "aboveNormal": "Above Normal", + "normal": "Normal", + "belowNormal": "Below Normal", + "low": "Low" + }, + "workDir": { + "title": "Separate Work Directory for Different Subscriptions", + "tooltip": "Enable to avoid conflicts when different subscriptions have proxy groups with the same name" + }, + "controlDns": "Control DNS Settings", + "controlSniff": "Control Domain Sniffing", + "autoCloseConnection": "Auto Close Connection", + "pauseSSID": { + "title": "Direct Connection for Specific WiFi SSIDs", + "placeholder": "Enter SSID" + }, + "title": "Core Settings", + "restart": "Restart Core", + "memory": "Memory Usage", + "coreVersion": "Core Version", + "upgradeCore": "Upgrade Core", + "CoreAuthLost": "Core Authorization Lost", + "coreUpgradeSuccess": "Core upgrade successful. If you want to use virtual network card (Tun), please go to the virtual network card page to manually authorize the Core again", + "alreadyLatestVersion": "Already Latest Version", + "selectCoreVersion": "Select Core Version", + "stableVersion": "Stable Version", + "alphaVersion": "Alpha Version", + "mixedPort": "Mixed Port", + "confirm": "Confirm", + "socksPort": "Socks Port", + "httpPort": "Http Port", + "redirPort": "Redir Port", + "externalController": "External Controller Address", + "externalControllerSecret": "External Controller Secret", + "ipv6": "IPv6", + "allowLanConnection": "Allow LAN Connection", + "allowedIpSegments": "Allowed IP Segments", + "disallowedIpSegments": "Disallowed IP Segments", + "userVerification": "User Verification", + "skipAuthPrefixes": "Skip Auth IP Segments", + "useRttDelayTest": "Use RTT Delay Test", + "tcpConcurrent": "TCP Concurrent", + "storeSelectedNode": "Store Selected Node", + "storeFakeIp": "Store Fake IP", + "logRetentionDays": "Log Retention Days", + "logLevel": "Log Level", + "selectLogLevel": "Select Log Level", + "silent": "Silent", + "error": "Error", + "warning": "Warning", + "info": "Info", + "debug": "Debug", + "findProcess": "Find Process", + "selectFindProcessMode": "Select Process Find Mode", + "strict": "Auto", + "off": "Off", + "always": "Always", + "username": { + "placeholder": "Username" + }, + "password": { + "placeholder": "Password" + }, + "ipSegment": { + "placeholder": "IP Segment" + }, + "interface": { + "title": "Network Information" + } + }, + "substore": { + "title": "Sub-Store", + "openInBrowser": "Open in Browser", + "enable": "Enable Sub-Store", + "allowLan": "Allow LAN Access", + "useCustomBackend": "Use Custom Sub-Store Backend", + "customBackendUrl": { + "title": "Custom Sub-Store Backend URL", + "placeholder": "Must include protocol" + }, + "useProxy": "Enable Proxy for All Sub-Store Requests", + "sync": { + "title": "Schedule Subscription/File Sync", + "placeholder": "Cron expression" + }, + "restore": { + "title": "Schedule Config Restore", + "placeholder": "Cron expression" + }, + "backup": { + "title": "Schedule Config Backup", + "placeholder": "Cron expression" + } + }, + "webdav": { + "title": "WebDAV Backup", + "url": "WebDAV URL", + "dir": "WebDAV Backup Directory", + "username": "WebDAV Username", + "password": "WebDAV Password", + "backup": "Backup", + "restore": { + "title": "Restore Backup", + "noBackups": "No backups available" + }, + "notification": { + "backupSuccess": { + "title": "Backup Successful", + "body": "Backup file has been uploaded to WebDAV" + } + } + }, + "shortcuts": { + "title": "Keyboard Shortcuts", + "toggleWindow": "Toggle Window", + "toggleFloatingWindow": "Toggle Floating Window", + "toggleSystemProxy": "Toggle System Proxy", + "toggleTun": "Toggle TUN", + "toggleRuleMode": "Toggle Rule Mode", + "toggleGlobalMode": "Toggle Global Mode", + "toggleDirectMode": "Toggle Direct Mode", + "toggleLightMode": "Toggle Light Mode", + "restartApp": "Restart App", + "input": { + "placeholder": "Click to input shortcut" + } + }, + "sider": { + "title": "Sidebar Settings", + "cards": { + "systemProxy": "Sys Proxy", + "tun": "TUN", + "profiles": "Profiles", + "proxies": "Proxy Groups", + "rules": "Rules", + "resources": "Ext. Res.", + "override": "Override", + "connections": "Connections", + "core": "Core Settings", + "dns": "DNS", + "sniff": "Sniffing", + "logs": "Logs", + "substore": "Sub-Store", + "config": "Runtime Config", + "emptyProfile": "Empty Profile", + "viewRuntimeConfig": "View Runtime Config", + "remote": "Remote", + "local": "Local", + "trafficUsage": "Traffic Usage Progress", + "neverExpire": "Never Expire", + "outbound": { + "title": "Outbound Mode", + "rule": "Rule", + "global": "Global", + "direct": "Direct" + } + }, + "size": { + "large": "Large", + "small": "Small", + "hidden": "Hidden" + } + }, + "actions": { + "guide": { + "title": "Open Guide", + "button": "Open Guide" + }, + "update": { + "title": "Check for Updates", + "button": "Check for Updates", + "upToDate": { + "title": "Up to Date", + "body": "No updates available" + } + }, + "reset": { + "title": "Reset App", + "button": "Reset App", + "tooltip": "Delete all configurations and restore the app to its initial state" + }, + "heapSnapshot": { + "title": "Create Heap Snapshot", + "button": "Create Heap Snapshot", + "tooltip": "Create a heap snapshot of the main process for memory issue debugging" + }, + "lightMode": { + "title": "Light Mode", + "button": "Light Mode", + "tooltip": "Completely exit the app while keeping only the core process" + }, + "restartApp": "Restart App", + "quit": { + "title": "Quit App", + "button": "Quit App" + }, + "version": { + "title": "App Version" + } + }, + "theme": { + "editor": { + "title": "Edit Theme" + } + }, + "proxies": { + "card": { + "title": "ProxyGrp" + }, + "delay": { + "test": "Test", + "timeout": "Timeout" + }, + "unpin": "Unpin", + "order": { + "default": "Default", + "delay": "Delay", + "name": "Name" + }, + "mode": { + "full": "Detailed Info", + "simple": "Simple Info", + "direct": "Direct Mode" + }, + "search": { + "placeholder": "Search Proxies" + }, + "locate": "Locate Current Proxy" + }, + "sniffer": { + "title": "Domain Sniffing Settings", + "parsePureIP": "Sniff Unmapped IP Addresses", + "forceDNSMapping": "Sniff Real IP Mappings", + "overrideDestination": "Override Connection Address", + "sniff": { + "title": "HTTP Port Sniffing", + "tls": "TLS Port Sniffing", + "quic": "QUIC Port Sniffing", + "ports": { + "placeholder": "Port numbers, separated by commas" + } + }, + "skipDomain": { + "title": "Skip Domain Sniffing", + "placeholder": "Example: +.push.apple.com" + }, + "forceDomain": { + "title": "Force Domain Sniffing", + "placeholder": "Example: v2ex.com" + }, + "skipDstAddress": { + "title": "Skip Destination Address Sniffing", + "placeholder": "Example: 1.1.1.1/32" + }, + "skipSrcAddress": { + "title": "Skip Source Address Sniffing", + "placeholder": "Example: 192.168.1.1/24" + } + }, + "sysproxy": { + "title": "System Proxy", + "host": { + "title": "Proxy Host", + "placeholder": "Default 127.0.0.1, do not modify unless necessary" + }, + "mode": { + "title": "Proxy Mode", + "manual": "Manual", + "pac": "PAC" + }, + "uwp": { + "title": "UWP Tool", + "open": "Open UWP Tool" + }, + "pac": { + "edit": "Edit PAC Script" + }, + "bypass": { + "title": "Proxy Bypass", + "addDefault": "Add Default Bypass", + "placeholder": "Example: *.baidu.com" + } + }, + "tun": { + "title": "TUN", + "firewall": { + "title": "Reset Firewall", + "reset": "Reset Firewall" + }, + "core": { + "title": "Manual Authorization", + "auth": "Authorize Core" + }, + "dns": { + "autoSet": "Auto Set System DNS" + }, + "stack": { + "title": "Tun Mode Stack" + }, + "device": { + "title": "Tun Device Name" + }, + "strictRoute": "Strict Route", + "autoRoute": "Auto Set Global Route", + "autoRedirect": "Auto Set TCP Redirect", + "autoDetectInterface": "Auto Detect Interface", + "dnsHijack": "DNS Hijack", + "excludeAddress": { + "title": "Exclude Custom Networks", + "placeholder": "Example: 172.20.0.0/16" + }, + "notifications": { + "coreAuthSuccess": "Core Authorization Successful", + "firewallResetSuccess": "Firewall Reset Successful" + } + }, + "dns": { + "title": "DNS Settings", + "enhancedMode": { + "title": "Domain Mapping Mode", + "fakeIp": "Fake IP", + "redirHost": "Real IP", + "normal": "No Mapping" + }, + "fakeIp": { + "range": "Response Range", + "rangePlaceholder": "Example: 198.18.0.1/16", + "filter": "Real IP Response", + "filterPlaceholder": "Example: +.lan" + }, + "respectRules": "Respect Rules", + "defaultNameserver": "DNS Server Domain Resolution", + "defaultNameserverPlaceholder": "Example: 223.5.5.5, IP only", + "proxyServerNameserver": "Proxy Server Domain Resolution", + "proxyServerNameserverPlaceholder": "Example: tls://dns.alidns.com", + "nameserver": "Default Resolution Server", + "nameserverPlaceholder": "Example: tls://dns.alidns.com", + "directNameserver": "Direct Resolution Server", + "directNameserverPlaceholder": "Example: tls://dns.alidns.com", + "nameserverPolicy": { + "title": "Override DNS Policy", + "list": "DNS Policy List", + "domainPlaceholder": "Domain", + "serverPlaceholder": "DNS Server" + }, + "systemHosts": { + "title": "Use System Hosts" + }, + "customHosts": { + "title": "Custom Hosts", + "list": "Hosts List", + "domainPlaceholder": "Domain", + "valuePlaceholder": "Domain or IP" + } + }, + "profiles": { + "title": "Profile Management", + "updateAll": "Update All Profiles", + "useProxy": "Proxy", + "import": "Import", + "open": "Open", + "new": "New", + "newProfile": "New Profile", + "substore": { + "visit": "Visit Sub-Store" + }, + "error": { + "unsupportedFileType": "Unsupported file type" + }, + "emptyProfile": "Empty Profile", + "viewRuntimeConfig": "View Current Runtime Config", + "neverExpire": "Never Expire", + "remote": "Remote", + "local": "Local", + "trafficUsage": "Traffic Usage Progress", + "editInfo": { + "title": "Edit Information", + "name": "Name", + "url": "Subscription URL", + "useProxy": "Use Proxy to Update", + "interval": "Upd. Interval (min)", + "override": { + "title": "Override", + "global": "Global", + "noAvailable": "No available overrides", + "add": "Add Override" + } + }, + "editFile": { + "title": "Edit Profile", + "notice": "Note: Changes made here will be reset after profile update. For custom configurations, please use", + "override": "Override", + "feature": "feature" + }, + "openFile": "Open File", + "home": "Home", + "traffic": { + "usage": "{{used}}/{{total}}", + "unlimited": "Unlimited", + "expired": "Expired", + "remainingDays": "{{days}} days", + "lastUpdate": "Last updated: {{time}}" + } + }, + "outbound": { + "title": "Outbound Mode", + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + } + }, + "rules": { + "title": "Rules", + "filter": "Filter Rules" + }, + "override": { + "title": "Override", + "import": "Import", + "docs": "Documentation", + "repository": "Override Repository", + "unsupportedFileType": "Unsupported file type", + "actions": { + "open": "Open", + "newYaml": "New YAML", + "newJs": "New JavaScript" + }, + "defaultContent": { + "yaml": "# https://mihomo.party/docs/guide/override/yaml", + "js": "// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}" + }, + "newFile": { + "yaml": "New YAML", + "js": "New JS" + }, + "editInfo": { + "title": "Edit Information", + "name": "Name", + "url": "URL", + "global": "Global Enable" + }, + "editFile": { + "title": "Edit Override {{type}}", + "script": "Script", + "config": "Config" + }, + "execLog": { + "title": "Execution Log", + "close": "Close" + }, + "menuItems": { + "editInfo": "Edit Information", + "editFile": "Edit File", + "openFile": "Open File", + "execLog": "Execution Log", + "delete": "Delete" + }, + "labels": { + "global": "Global" + } + }, + "connections": { + "title": "Connections", + "upload": "Upload", + "download": "Download", + "closeAll": "Close All Connections", + "active": "Active", + "closed": "Closed", + "filter": "Filter", + "orderBy": "Order By", + "uploadAmount": "Upload Amount", + "downloadAmount": "Download Amount", + "uploadSpeed": "Upload Speed", + "downloadSpeed": "Download Speed", + "detail": { + "title": "Connection Details", + "establishTime": "Establish Time", + "rule": "Rule", + "proxyChain": "Proxy Chain", + "connectionType": "Connection Type", + "host": "Host", + "sniffHost": "Sniff Host", + "processName": "Process Name", + "processPath": "Process Path", + "sourceIP": "Source IP", + "sourceGeoIP": "Source GeoIP", + "sourceASN": "Source ASN", + "destinationIP": "Destination IP", + "destinationGeoIP": "Destination GeoIP", + "destinationASN": "Destination ASN", + "sourcePort": "Source Port", + "destinationPort": "Destination Port", + "inboundIP": "Inbound IP", + "inboundPort": "Inbound Port", + "copyRule": "Copy Rule", + "inboundName": "Inbound Name", + "inboundUser": "Inbound User", + "dscp": "DSCP", + "remoteDestination": "Remote Destination", + "dnsMode": "DNS Mode", + "specialProxy": "Special Proxy", + "specialRules": "Special Rules", + "close": "Close" + } + }, + "resources": { + "geoData": { + "geoip": "GeoIP Database", + "geosite": "GeoSite Database", + "mmdb": "MMDB Database", + "asn": "ASN Database", + "mode": "GeoData Mode", + "autoUpdate": "Auto Update", + "updateInterval": "Update Interval (hours)", + "updateSuccess": "GeoData Update Successful" + } + }, + "logs": { + "title": "Real-time Logs", + "filter": "Filter logs", + "clear": "Clear logs", + "autoScroll": "Auto scroll" + }, + "tray": { + "showWindow": "Show Window", + "showFloatingWindow": "Show Floating Window", + "hideFloatingWindow": "Hide Floating Window", + "ruleMode": "Rule Mode", + "globalMode": "Global Mode", + "directMode": "Direct Mode", + "systemProxy": "System Proxy", + "tun": "TUN", + "profiles": "Profiles", + "openDirectories": { + "title": "Open Directories", + "appDir": "App Directory", + "workDir": "Work Directory", + "coreDir": "Core Directory", + "logDir": "Log Directory" + }, + "copyEnv": "Copy Environment Variables" + }, + "guide": { + "welcome": { + "title": "Welcome to Mihomo Party", + "description": "This is an interactive tutorial. If you are already familiar with the software, you can click the close button in the top right corner. You can always open this tutorial again from the settings." + }, + "sider": { + "title": "Navigation Bar", + "description": "The left side is the application's navigation bar, which also serves as a dashboard. Here you can switch between different pages and get an overview of commonly used status information." + }, + "card": { + "title": "Cards", + "description": "Click on the navigation bar cards to jump to the corresponding page. You can also drag and drop the cards to arrange them freely." + }, + "main": { + "title": "Main Area", + "description": "The right side is the main area of the application, displaying the content of the selected page from the navigation bar." + }, + "profile": { + "title": "Profile Management", + "description": "The profile management card shows information about the currently running subscription configuration. Click to enter the profile management page where you can manage your subscription configurations." + }, + "import": { + "title": "Import Subscription", + "description": "Mihomo Party supports various subscription import methods. Enter your subscription link here and click import to import your subscription configuration. If your subscription requires a proxy to update, check the 'Proxy' option before importing (this requires having a working subscription first)." + }, + "substore": { + "title": "Sub-Store", + "description": "Mihomo Party has deep integration with Sub-Store. You can click this button to enter Sub-Store or directly import your subscriptions managed through Sub-Store. Mihomo Party uses its built-in Sub-Store backend by default. If you have your own Sub-Store backend, you can configure it in the settings page. If you don't use Sub-Store, you can also disable it in the settings." + }, + "localProfile": { + "title": "Local Profile", + "description": "Click '+' to import a local file or create a new empty configuration for editing." + }, + "sysproxy": { + "title": "System Proxy", + "description": "After importing a subscription, the core is running and listening on the specified port. You can now use the proxy through the specified port. If you want most applications to automatically use this proxy port, you need to turn on the system proxy switch." + }, + "sysproxySetting": { + "title": "System Proxy Settings", + "description": "Here you can configure system proxy-related settings and choose the proxy mode. For Windows applications that don't follow system proxy, you can use the 'UWP Tool' to remove local loopback restrictions. For the difference between 'Manual Proxy Mode' and 'PAC Proxy Mode', please search online." + }, + "tun": { + "title": "Virtual Network Card", + "description": "Virtual network card, commonly known as 'Tun Mode' in similar software, allows you to let the core take control of all traffic for applications that don't follow system proxy settings." + }, + "tunSetting": { + "title": "Virtual Network Card Settings", + "description": "Here you can modify virtual network card settings. Mihomo Party has theoretically solved all permission issues. If your virtual network card is still not working, try resetting the firewall (Windows) or manually authorizing the core (MacOS/Linux) and then restart the core." + }, + "override": { + "title": "Override", + "description": "Mihomo Party provides powerful override functionality to customize your imported subscription configurations, such as adding rules and customizing proxy groups. You can directly import override files written by others or write your own. Remember to enable the override file on the subscription you want to override. For override file syntax, please refer to the official documentation." + }, + "dns": { + "title": "DNS", + "description": "The software takes control of the core's DNS settings by default. If you need to use the DNS settings from your subscription configuration, you can disable 'Control DNS Settings' in the application settings. The same applies to domain sniffing." + }, + "end": { + "title": "Tutorial Complete", + "description": "Now that you understand the basic usage of the software, import your subscription and start using it. Enjoy!\nYou can also join our official Telegram group for the latest news." + } + }, + "main": { + "error": { + "adminRequired": "Please run with administrator privileges for first launch", + "initFailed": "Application initialization failed", + "coreStartFailed": "Core startup error", + "importFailed": "Subscription import failed", + "urlParamMissing": "Missing parameter: url" + }, + "notification": { + "importSuccess": "Subscription imported successfully" + } + } +} \ No newline at end of file diff --git a/src/renderer/src/locales/zh-CN.json b/src/renderer/src/locales/zh-CN.json new file mode 100644 index 0000000..2070642 --- /dev/null +++ b/src/renderer/src/locales/zh-CN.json @@ -0,0 +1,723 @@ +{ + "common": { + "settings": "设置", + "profiles": "配置", + "proxies": "代理", + "connections": "连接", + "dns": "DNS", + "tun": "TUN", + "save": "保存", + "cancel": "取消", + "edit": "编辑", + "delete": "删除", + "seconds": "秒", + "confirm": "确认", + "auto": "自动", + "default": "默认", + "close": "关闭", + "pinWindow": "窗口置顶", + "enterRootPassword": "请输入root密码", + "next": "下一步", + "prev": "上一步", + "done": "完成", + "notification": { + "restartRequired": "需要重启应用以使更改生效" + }, + "error": { + "appCrash": "应用崩溃了 :( 请将以下信息提交给开发者以排查错误", + "copyErrorMessage": "复制报错信息", + "invalidCron": "无效的 Cron 表达式", + "getBackupListFailed": "获取备份列表失败:{{error}}", + "restoreFailed": "恢复失败:{{error}}", + "deleteFailed": "删除失败:{{error}}", + "shortcutRegistrationFailed": "快捷键注册失败", + "shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}" + }, + "updater": { + "versionReady": "v{{version}} 版本就绪", + "goToDownload": "前往下载", + "update": "更新" + } + }, + "settings": { + "general": "通用设置", + "mihomo": "Mihomo 设置", + "language": "语言", + "theme": "主题", + "darkMode": "深色模式", + "lightMode": "浅色模式", + "autoStart": "开机自启", + "autoCheckUpdate": "自动检查更新", + "silentStart": "静默启动", + "autoQuitWithoutCore": "自动进入轻量模式", + "autoQuitWithoutCoreTooltip": "关闭窗口后指定时间自动进入轻量模式", + "autoQuitWithoutCoreDelay": "轻量模式自动启用延迟", + "envType": "环境变量类型", + "showFloatingWindow": "显示悬浮窗", + "spinFloatingIcon": "根据网速旋转悬浮窗图标", + "disableTray": "禁用托盘图标", + "proxyInTray": "在托盘菜单显示代理信息", + "showTraffic_windows": "在任务栏显示网速", + "showTraffic_mac": "在状态栏显示网速", + "showDockIcon": "显示 Dock 图标", + "useWindowFrame": "使用系统标题栏", + "backgroundColor": "背景颜色", + "backgroundAuto": "自动", + "backgroundDark": "深色", + "backgroundLight": "浅色", + "fetchTheme": "获取主题", + "importTheme": "导入主题", + "editTheme": "编辑主题", + "selectTheme": "选择主题", + "links": { + "docs": "官方文档", + "github": "GitHub 仓库", + "telegram": "Telegram 群组" + }, + "title": "应用设置" + }, + "mihomo": { + "title": "内核设置", + "restart": "重启内核", + "memory": "内存使用", + "userAgent": "订阅 User Agent", + "userAgentPlaceholder": "默认:clash.meta", + "delayTest": { + "url": "延迟测试 URL", + "urlPlaceholder": "默认:https://www.gstatic.com/generate_204", + "concurrency": "延迟测试并发数", + "concurrencyPlaceholder": "默认:50", + "timeout": "延迟测试超时", + "timeoutPlaceholder": "默认:5000" + }, + "gist": { + "title": "同步运行时配置到 Gist", + "copyUrl": "复制 Gist URL", + "token": "GitHub Token" + }, + "proxyColumns": { + "title": "代理显示列数", + "auto": "自动", + "one": "一列", + "two": "两列", + "three": "三列", + "four": "四列" + }, + "cpuPriority": { + "title": "核心进程优先级", + "realtime": "实时", + "high": "高", + "aboveNormal": "高于正常", + "normal": "正常", + "belowNormal": "低于正常", + "low": "低" + }, + "workDir": { + "title": "不同订阅使用独立工作目录", + "tooltip": "启用后可避免不同订阅中存在相同名称的代理组时发生冲突" + }, + "controlDns": "控制 DNS 设置", + "controlSniff": "控制域名嗅探", + "autoCloseConnection": "自动关闭连接", + "pauseSSID": { + "title": "指定 WiFi SSID 直连", + "placeholder": "输入 SSID" + }, + "coreVersion": "内核版本", + "upgradeCore": "升级内核", + "coreAuthLost": "内核权限丢失", + "coreUpgradeSuccess": "内核升级成功,若要使用虚拟网卡(Tun),请到虚拟网卡页面重新手动授权内核", + "alreadyLatestVersion": "已经是最新版本", + "selectCoreVersion": "选择内核版本", + "stableVersion": "稳定版", + "alphaVersion": "预览版", + "mixedPort": "混合端口", + "confirm": "确认", + "socksPort": "Socks 端口", + "httpPort": "Http 端口", + "redirPort": "Redir 端口", + "externalController": "外部控制地址", + "externalControllerSecret": "外部控制访问密钥", + "ipv6": "IPv6", + "allowLanConnection": "允许局域网连接", + "allowedIpSegments": "允许连接的 IP 段", + "disallowedIpSegments": "禁止连接的 IP 段", + "userVerification": "用户验证", + "skipAuthPrefixes": "允许跳过验证的 IP 段", + "useRttDelayTest": "使用 RTT 延迟测试", + "tcpConcurrent": "TCP 并发", + "storeSelectedNode": "存储选择节点", + "storeFakeIp": "存储 FakeIP", + "logRetentionDays": "日志保留天数", + "logLevel": "日志等级", + "selectLogLevel": "选择日志等级", + "silent": "静默", + "error": "错误", + "warning": "警告", + "info": "信息", + "debug": "调试", + "findProcess": "查找进程", + "selectFindProcessMode": "选择进程查找模式", + "strict": "自动", + "off": "关闭", + "always": "开启", + "username": { + "placeholder": "用户名" + }, + "password": { + "placeholder": "密码" + }, + "ipSegment": { + "placeholder": "IP 段" + }, + "interface": { + "title": "网络信息" + } + }, + "substore": { + "title": "Sub-Store", + "openInBrowser": "在浏览器中打开", + "enable": "启用 Sub-Store", + "allowLan": "允许局域网访问", + "useCustomBackend": "使用自定义 Sub-Store 后端", + "customBackendUrl": { + "title": "自定义 Sub-Store 后端 URL", + "placeholder": "必须包含协议" + }, + "useProxy": "为所有 Sub-Store 请求启用代理", + "sync": { + "title": "定时同步订阅/文件", + "placeholder": "Cron 表达式" + }, + "restore": { + "title": "定时恢复配置", + "placeholder": "Cron 表达式" + }, + "backup": { + "title": "定时备份配置", + "placeholder": "Cron 表达式" + } + }, + "webdav": { + "title": "WebDAV 备份", + "url": "WebDAV URL", + "dir": "WebDAV 备份目录", + "username": "WebDAV 用户名", + "password": "WebDAV 密码", + "backup": "备份", + "restore": { + "title": "恢复备份", + "noBackups": "还没有备份" + }, + "notification": { + "backupSuccess": { + "title": "备份成功", + "body": "备份文件已上传到 WebDAV" + } + } + }, + "shortcuts": { + "title": "快捷键设置", + "toggleWindow": "打开/关闭窗口", + "toggleFloatingWindow": "打开/关闭悬浮窗", + "toggleSystemProxy": "打开/关闭系统代理", + "toggleTun": "打开/关闭 TUN", + "toggleRuleMode": "切换规则模式", + "toggleGlobalMode": "切换全局模式", + "toggleDirectMode": "切换直连模式", + "toggleLightMode": "切换轻量模式", + "restartApp": "重启应用", + "input": { + "placeholder": "点击输入快捷键" + } + }, + "sider": { + "title": "侧边栏设置", + "cards": { + "systemProxy": "系统代理", + "tun": "虚拟网卡", + "profiles": "订阅管理", + "proxies": "代理组", + "rules": "规则", + "resources": "外部资源", + "override": "覆写", + "connections": "连接", + "core": "内核设置", + "dns": "DNS", + "sniff": "域名嗅探", + "logs": "日志", + "substore": "Sub-Store", + "config": "运行时配置", + "emptyProfile": "空白配置", + "viewRuntimeConfig": "查看运行时配置", + "remote": "远程", + "local": "本地", + "trafficUsage": "流量使用进度", + "neverExpire": "长期有效", + "outbound": { + "title": "出站模式", + "rule": "规则", + "global": "全局", + "direct": "直连" + } + }, + "size": { + "large": "大", + "small": "小", + "hidden": "隐藏" + } + }, + "actions": { + "guide": { + "title": "打开引导页面", + "button": "打开引导页面" + }, + "update": { + "title": "检查更新", + "button": "检查更新", + "upToDate": { + "title": "当前已是最新版本", + "body": "无需更新" + } + }, + "reset": { + "title": "重置软件", + "button": "重置软件", + "tooltip": "删除所有配置,将软件恢复初始状态" + }, + "heapSnapshot": { + "title": "创建堆快照", + "button": "创建堆快照", + "tooltip": "创建主进程堆快照,用于排查内存问题" + }, + "lightMode": { + "title": "轻量模式", + "button": "轻量模式", + "tooltip": "完全退出软件,只保留内核进程" + }, + "restartApp": "重启应用", + "quit": { + "title": "退出应用", + "button": "退出应用" + }, + "version": { + "title": "应用版本" + } + }, + "theme": { + "editor": { + "title": "编辑主题" + } + }, + "proxies": { + "card": { + "title": "代理组" + }, + "delay": { + "test": "测试", + "timeout": "超时" + }, + "unpin": "取消固定", + "order": { + "default": "默认", + "delay": "延迟", + "name": "名称" + }, + "mode": { + "full": "详细信息", + "simple": "简洁信息", + "direct": "直连模式" + }, + "search": { + "placeholder": "搜索节点" + }, + "locate": "定位到当前节点" + }, + "sniffer": { + "title": "域名嗅探设置", + "parsePureIP": "对未映射 IP 地址嗅探", + "forceDNSMapping": "对真实 IP 映射嗅探", + "overrideDestination": "覆盖连接地址", + "sniff": { + "title": "HTTP 端口嗅探", + "tls": "TLS 端口嗅探", + "quic": "QUIC 端口嗅探", + "ports": { + "placeholder": "端口号,使用英文逗号分割" + } + }, + "skipDomain": { + "title": "跳过域名嗅探", + "placeholder": "例:+.push.apple.com" + }, + "forceDomain": { + "title": "强制域名嗅探", + "placeholder": "例:v2ex.com" + }, + "skipDstAddress": { + "title": "跳过目标地址嗅探", + "placeholder": "例:1.1.1.1/32" + }, + "skipSrcAddress": { + "title": "跳过来源地址嗅探", + "placeholder": "例:192.168.1.1/24" + } + }, + "sysproxy": { + "title": "系统代理", + "host": { + "title": "代理主机", + "placeholder": "默认 127.0.0.1 若无特殊需求请勿修改" + }, + "mode": { + "title": "代理模式", + "manual": "手动", + "pac": "PAC" + }, + "uwp": { + "title": "UWP 工具", + "open": "打开 UWP 工具" + }, + "pac": { + "edit": "编辑 PAC 脚本" + }, + "bypass": { + "title": "代理绕过", + "addDefault": "添加默认代理绕过", + "placeholder": "例: *.baidu.com" + } + }, + "tun": { + "title": "虚拟网卡", + "firewall": { + "title": "重设防火墙", + "reset": "重设防火墙" + }, + "core": { + "title": "手动授权内核", + "auth": "手动授权内核" + }, + "dns": { + "autoSet": "自动设置系统DNS" + }, + "stack": { + "title": "Tun 模式堆栈" + }, + "device": { + "title": "Tun 网卡名称" + }, + "strictRoute": "严格路由", + "autoRoute": "自动设置全局路由", + "autoRedirect": "自动设置TCP重定向", + "autoDetectInterface": "自动选择流量出口接口", + "dnsHijack": "DNS 劫持", + "excludeAddress": { + "title": "排除自定义网段", + "placeholder": "例: 172.20.0.0/16" + }, + "notifications": { + "coreAuthSuccess": "内核授权成功", + "firewallResetSuccess": "防火墙重设成功" + } + }, + "dns": { + "title": "DNS 设置", + "enhancedMode": { + "title": "域名映射模式", + "fakeIp": "虚假 IP", + "redirHost": "真实 IP", + "normal": "取消映射" + }, + "fakeIp": { + "range": "回应范围", + "rangePlaceholder": "例:198.18.0.1/16", + "filter": "真实 IP 回应", + "filterPlaceholder": "例:+.lan" + }, + "respectRules": "遵守规则", + "defaultNameserver": "DNS 服务器域名解析", + "defaultNameserverPlaceholder": "例:223.5.5.5,仅支持 IP", + "proxyServerNameserver": "代理服务器域名解析", + "proxyServerNameserverPlaceholder": "例:tls://dns.alidns.com", + "nameserver": "默认解析服务器", + "nameserverPlaceholder": "例:tls://dns.alidns.com", + "directNameserver": "直连解析服务器", + "directNameserverPlaceholder": "例:tls://dns.alidns.com", + "nameserverPolicy": { + "title": "覆盖 DNS 策略", + "list": "DNS 策略列表", + "domainPlaceholder": "域名", + "serverPlaceholder": "DNS 服务器" + }, + "systemHosts": { + "title": "使用系统 Hosts" + }, + "customHosts": { + "title": "自定义 Hosts", + "list": "Hosts 列表", + "domainPlaceholder": "域名", + "valuePlaceholder": "域名或 IP" + } + }, + "profiles": { + "title": "订阅管理", + "updateAll": "更新全部订阅", + "useProxy": "代理", + "import": "导入", + "open": "打开", + "new": "新建", + "newProfile": "新建订阅", + "substore": { + "visit": "访问 Sub-Store" + }, + "error": { + "unsupportedFileType": "不支持的文件类型" + }, + "emptyProfile": "空白订阅", + "viewRuntimeConfig": "查看当前运行时配置", + "neverExpire": "长期有效", + "remote": "远程", + "local": "本地", + "trafficUsage": "流量使用进度", + "traffic": { + "usage": "{{used}}/{{total}}", + "unlimited": "无限制", + "expired": "已过期", + "remainingDays": "剩余 {{days}} 天", + "lastUpdate": "最后更新:{{time}}" + }, + "editInfo": { + "title": "编辑信息", + "name": "名称", + "url": "订阅地址", + "useProxy": "使用代理更新", + "interval": "更新间隔(分钟)", + "override": { + "title": "覆写", + "global": "全局", + "noAvailable": "没有可用的覆写", + "add": "添加覆写" + } + }, + "editFile": { + "title": "编辑订阅", + "notice": "注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用", + "override": "覆写", + "feature": "功能" + }, + "openFile": "打开文件", + "home": "主页" + }, + "outbound": { + "title": "出站模式", + "modes": { + "rule": "规则", + "global": "全局", + "direct": "直连" + } + }, + "rules": { + "title": "分流规则", + "filter": "筛选过滤" + }, + "override": { + "title": "覆写", + "import": "导入", + "docs": "使用文档", + "repository": "常用覆写仓库", + "unsupportedFileType": "不支持的文件类型", + "actions": { + "open": "打开", + "newYaml": "新建 YAML", + "newJs": "新建 JavaScript" + }, + "defaultContent": { + "yaml": "# https://mihomo.party/docs/guide/override/yaml", + "js": "// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}" + }, + "newFile": { + "yaml": "新建YAML", + "js": "新建JS" + }, + "editInfo": { + "title": "编辑信息", + "name": "名称", + "url": "地址", + "global": "全局启用" + }, + "editFile": { + "title": "编辑覆写{{type}}", + "script": "脚本", + "config": "配置" + }, + "execLog": { + "title": "执行日志", + "close": "关闭" + }, + "menuItems": { + "editInfo": "编辑信息", + "editFile": "编辑文件", + "openFile": "打开文件", + "execLog": "执行日志", + "delete": "删除" + }, + "labels": { + "global": "全局" + } + }, + "connections": { + "title": "连接", + "upload": "上传", + "download": "下载", + "closeAll": "关闭全部连接", + "active": "活动中", + "closed": "已关闭", + "filter": "筛选过滤", + "orderBy": "连接排序方式", + "uploadAmount": "上传量", + "downloadAmount": "下载量", + "uploadSpeed": "上传速度", + "downloadSpeed": "下载速度", + "detail": { + "title": "连接详情", + "establishTime": "连接建立时间", + "rule": "规则", + "proxyChain": "代理链", + "connectionType": "连接类型", + "host": "主机", + "sniffHost": "嗅探主机", + "processName": "进程名", + "processPath": "进程路径", + "sourceIP": "来源IP", + "sourceGeoIP": "来源GeoIP", + "sourceASN": "来源ASN", + "destinationIP": "目标IP", + "destinationGeoIP": "目标GeoIP", + "destinationASN": "目标ASN", + "sourcePort": "来源端口", + "destinationPort": "目标端口", + "inboundIP": "入站IP", + "inboundPort": "入站端口", + "copyRule": "复制规则", + "inboundName": "入站名称", + "inboundUser": "入站用户", + "dscp": "DSCP", + "remoteDestination": "远程目标", + "dnsMode": "DNS模式", + "specialProxy": "特殊代理", + "specialRules": "特殊规则", + "close": "关闭" + } + }, + "resources": { + "geoData": { + "geoip": "GeoIP 数据库", + "geosite": "GeoSite 数据库", + "mmdb": "MMDB 数据库", + "asn": "ASN 数据库", + "mode": "GeoData 数据模式", + "autoUpdate": "自动更新", + "updateInterval": "更新间隔(小时)", + "updateSuccess": "GeoData 更新成功" + } + }, + "logs": { + "title": "实时日志", + "filter": "筛选过滤", + "clear": "清空日志", + "autoScroll": "自动滚动" + }, + "tray": { + "showWindow": "显示窗口", + "showFloatingWindow": "显示悬浮窗", + "hideFloatingWindow": "关闭悬浮窗", + "ruleMode": "规则模式", + "globalMode": "全局模式", + "directMode": "直连模式", + "systemProxy": "系统代理", + "tun": "虚拟网卡", + "profiles": "订阅配置", + "openDirectories": { + "title": "打开目录", + "appDir": "应用目录", + "workDir": "工作目录", + "coreDir": "内核目录", + "logDir": "日志目录" + }, + "copyEnv": "复制环境变量" + }, + "guide": { + "welcome": { + "title": "欢迎使用 Mihomo Party", + "description": "这是一份交互式使用教程,如果您已经完全熟悉本软件的操作,可以直接点击右上角关闭按钮,后续您可以随时从设置中打开本教程" + }, + "sider": { + "title": "导航栏", + "description": "左侧是应用的导航栏,兼顾仪表盘功能,在这里可以切换不同页面,也可以概览常用的状态信息" + }, + "card": { + "title": "卡片", + "description": "点击导航栏卡片可以跳转到对应页面,拖动导航栏卡片可以自由排列卡片顺序" + }, + "main": { + "title": "主要区域", + "description": "右侧是应用的主要区域,展示了导航栏所选页面的内容" + }, + "profile": { + "title": "订阅管理", + "description": "订阅管理卡片展示当前运行的订阅配置信息,点击进入订阅管理页面可以在这里管理订阅配置" + }, + "import": { + "title": "订阅导入", + "description": "Mihomo Party 支持多种订阅导入方式,在此输入订阅链接,点击导入即可导入您的订阅配置,如果您的订阅需要代理才能更新,请勾选\"代理\"再点击导入,当然这需要已经有一个可以正常使用的订阅才可以" + }, + "substore": { + "title": "Sub-Store", + "description": "Mihomo Party 深度集成了 Sub-Store,您可以点击该按钮进入 Sub-Store 或直接导入您通过 Sub-Store 管理的订阅,Mihomo Party 默认使用内置的 Sub-Store 后端,如果您有自建的 Sub-Store 后端,可以在设置页面中配置,如果您不使用 Sub-Store 也可以在设置页面中关闭" + }, + "localProfile": { + "title": "本地订阅", + "description": "点击\"+\"可以选择本地文件进行导入或者直接新建空白配置进行编辑" + }, + "sysproxy": { + "title": "系统代理", + "description": "导入订阅之后,内核已经开始运行并监听指定端口,此时您已经可以通过指定代理端口来使用代理了,如果您要使大部分应用自动使用该端口的代理,您还需要打开系统代理开关" + }, + "sysproxySetting": { + "title": "系统代理设置", + "description": "在此您可以进行系统代理相关设置,选择代理模式,如果某些 Windows 应用不遵循系统代理,还可以使用\"UWP 工具\"解除本地回环限制,对于\"手动代理模式\"和\"PAC 代理模式\"的区别,请自行百度" + }, + "tun": { + "title": "虚拟网卡", + "description": "虚拟网卡,即同类软件中常见的\"Tun 模式\",对于某些不遵循系统代理的应用,您可以打开虚拟网卡以让内核接管所有流量" + }, + "tunSetting": { + "title": "虚拟网卡设置", + "description": "这里可以更改虚拟网卡相关设置,Mihomo Party 理论上已经完全解决权限问题,如果您的虚拟网卡仍然不可用,可以尝试重设防火墙(Windows)或手动授权内核(MacOS/Linux)后重启内核" + }, + "override": { + "title": "覆写", + "description": "Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,编辑好覆写文件一定要记得在需要覆写的订阅上启用,覆写文件的语法请参考 官方文档" + }, + "dns": { + "title": "DNS", + "description": "软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭\"接管 DNS 设置\",域名嗅探同理" + }, + "end": { + "title": "教程结束", + "description": "现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 Telegram 群组 获取最新资讯" + } + }, + "main": { + "error": { + "adminRequired": "首次启动请以管理员权限运行", + "initFailed": "应用初始化失败", + "coreStartFailed": "内核启动出错", + "importFailed": "订阅导入失败", + "urlParamMissing": "缺少参数 url" + }, + "notification": { + "importSuccess": "订阅导入成功" + } + } +} \ No newline at end of file diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 2be14e3..f303fa7 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -14,6 +14,7 @@ import { OverrideConfigProvider } from './hooks/use-override-config' import { ProfileConfigProvider } from './hooks/use-profile-config' import { RulesProvider } from './hooks/use-rules' import { GroupsProvider } from './hooks/use-groups' +import './i18n' let F12Count = 0 diff --git a/src/renderer/src/pages/connections.tsx b/src/renderer/src/pages/connections.tsx index ae0b5c7..2a89f6e 100644 --- a/src/renderer/src/pages/connections.tsx +++ b/src/renderer/src/pages/connections.tsx @@ -5,17 +5,19 @@ import { Badge, Button, Divider, Input, Select, SelectItem, Tab, Tabs } from '@n import { calcTraffic } from '@renderer/utils/calc' import ConnectionItem from '@renderer/components/connections/connection-item' import { Virtuoso } from 'react-virtuoso' -import dayjs from 'dayjs' +import dayjs from '@renderer/utils/dayjs' import ConnectionDetailModal from '@renderer/components/connections/connection-detail-modal' import { CgClose, CgTrash } from 'react-icons/cg' import { useAppConfig } from '@renderer/hooks/use-app-config' import { HiSortAscending, HiSortDescending } from 'react-icons/hi' import { includesIgnoreCase } from '@renderer/utils/includes' import { differenceWith, unionWith } from 'lodash' +import { useTranslation } from 'react-i18next' let cachedConnections: IMihomoConnectionDetail[] = [] const Connections: React.FC = () => { + const { t } = useTranslation() const [filter, setFilter] = useState('') const { appConfig, patchAppConfig } = useAppConfig() const { connectionDirection = 'asc', connectionOrderBy = 'time' } = appConfig || {} @@ -133,7 +135,7 @@ const Connections: React.FC = () => { return (
@@ -153,7 +155,7 @@ const Connections: React.FC = () => { > ) } > - + setValues({ ...values, enhancedMode: key as DnsMode })} > - - - + + + {values.enhancedMode === 'fake-ip' ? ( <> - + { setValues({ ...values, fakeIPRange: v }) }} />
-

真实 IP 回应

- {renderListInputs('fakeIPFilter', '例:+.lan')} +

{t('dns.fakeIp.filter')}

+ {renderListInputs('fakeIPFilter', t('dns.fakeIp.filterPlaceholder'))}
@@ -215,7 +218,7 @@ const DNS: React.FC = () => { }} />
- + {
-

DNS 服务器域名解析

- {renderListInputs('defaultNameserver', '例:223.5.5.5,仅支持 IP')} +

{t('dns.defaultNameserver')}

+ {renderListInputs('defaultNameserver', t('dns.defaultNameserverPlaceholder'))}
-

代理服务器域名解析

- {renderListInputs('proxyServerNameserver', '例:tls://dns.alidns.com')} +

{t('dns.proxyServerNameserver')}

+ {renderListInputs('proxyServerNameserver', t('dns.proxyServerNameserverPlaceholder'))}
-

默认解析服务器

- {renderListInputs('nameserver', '例:tls://dns.alidns.com')} +

{t('dns.nameserver')}

+ {renderListInputs('nameserver', t('dns.nameserverPlaceholder'))}
-

直连解析服务器

- {renderListInputs('directNameserver', '例:tls://dns.alidns.com')} +

{t('dns.directNameserver')}

+ {renderListInputs('directNameserver', t('dns.directNameserverPlaceholder'))}
- + { {values.useNameserverPolicy && (
-

+

{t('dns.nameserverPolicy.list')}

{[...values.nameserverPolicy, { domain: '', value: '' }].map( ({ domain, value }, index) => (
@@ -265,7 +268,7 @@ const DNS: React.FC = () => { handleSubkeyChange( @@ -282,7 +285,7 @@ const DNS: React.FC = () => { handleSubkeyChange('nameserverPolicy', domain, v, index) @@ -306,7 +309,7 @@ const DNS: React.FC = () => {
)} - + { }} /> - + { {values.useHosts && (
-

+

{t('dns.customHosts.list')}

{[...values.hosts, { domain: '', value: '' }].map(({ domain, value }, index) => (
handleSubkeyChange( @@ -350,7 +353,7 @@ const DNS: React.FC = () => { handleSubkeyChange('hosts', domain, v, index)} /> diff --git a/src/renderer/src/pages/logs.tsx b/src/renderer/src/pages/logs.tsx index 4716e4a..784dbab 100644 --- a/src/renderer/src/pages/logs.tsx +++ b/src/renderer/src/pages/logs.tsx @@ -5,6 +5,7 @@ import { Button, Divider, Input } from '@nextui-org/react' import { Virtuoso, VirtuosoHandle } from 'react-virtuoso' import { IoLocationSharp } from 'react-icons/io5' import { CgTrash } from 'react-icons/cg' +import { useTranslation } from 'react-i18next' import { includesIgnoreCase } from '@renderer/utils/includes' @@ -35,6 +36,7 @@ window.electron.ipcRenderer.on('mihomoLogs', (_e, log: IMihomoLogInfo) => { }) const Logs: React.FC = () => { + const { t } = useTranslation() const [logs, setLogs] = useState(cachedLogs.log) const [filter, setFilter] = useState('') const [trace, setTrace] = useState(true) @@ -68,13 +70,13 @@ const Logs: React.FC = () => { }, []) return ( - +
@@ -84,6 +86,7 @@ const Logs: React.FC = () => { className="ml-2" color={trace ? 'primary' : 'default'} variant={trace ? 'solid' : 'bordered'} + title={t('logs.autoScroll')} onPress={() => { setTrace((prev) => !prev) }} @@ -93,7 +96,7 @@ const Logs: React.FC = () => { )} @@ -163,7 +165,7 @@ const Mihomo: React.FC = () => { />
- +
{socksPortInput !== socksPort && ( )} @@ -191,7 +193,7 @@ const Mihomo: React.FC = () => { />
- +
{httpPortInput !== httpPort && ( )} @@ -220,7 +222,7 @@ const Mihomo: React.FC = () => {
{platform !== 'win32' && ( - +
{redirPortInput !== redirPort && ( )} @@ -261,7 +263,7 @@ const Mihomo: React.FC = () => { onChangeNeedRestart({ 'tproxy-port': tproxyPortInput }) }} > - 确认 + {t('mihomo.confirm')} )} @@ -279,7 +281,7 @@ const Mihomo: React.FC = () => {
)} - +
{externalControllerInput !== externalController && ( )} @@ -306,7 +308,7 @@ const Mihomo: React.FC = () => { />
- +
{secretInput !== secret && ( )} @@ -332,7 +334,7 @@ const Mihomo: React.FC = () => { />
- + { /> { {allowLan && ( <> - + {lanAllowedIpsInput.join('') !== lanAllowedIps.join('') && ( )} @@ -417,7 +419,7 @@ const Mihomo: React.FC = () => { })}
- + {lanDisallowedIpsInput.join('') !== lanDisallowedIps.join('') && ( )} @@ -471,7 +473,7 @@ const Mihomo: React.FC = () => { )} - + {authenticationInput.join('') !== authentication.join('') && ( )} @@ -493,7 +495,7 @@ const Mihomo: React.FC = () => { { if (index === authenticationInput.length) { @@ -513,7 +515,7 @@ const Mihomo: React.FC = () => { { if (index === authenticationInput.length) { @@ -546,7 +548,7 @@ const Mihomo: React.FC = () => { })}
- + {skipAuthPrefixesInput.join('') !== skipAuthPrefixes.join('') && ( )} @@ -567,7 +569,7 @@ const Mihomo: React.FC = () => { disabled={index === 0} size="sm" fullWidth - placeholder="IP 段" + placeholder={t('mihomo.ipSegment.placeholder')} value={ipcidr || ''} onValueChange={(v) => { if (index === skipAuthPrefixesInput.length) { @@ -599,7 +601,7 @@ const Mihomo: React.FC = () => { })}
- + { }} /> - + { }} /> - + { }} /> - + { }} /> - + { }} /> - + - + diff --git a/src/renderer/src/pages/override.tsx b/src/renderer/src/pages/override.tsx index fe0e6f7..f7b42ff 100644 --- a/src/renderer/src/pages/override.tsx +++ b/src/renderer/src/pages/override.tsx @@ -25,8 +25,10 @@ import OverrideItem from '@renderer/components/override/override-item' import { FaPlus } from 'react-icons/fa6' import { HiOutlineDocumentText } from 'react-icons/hi' import { RiArchiveLine } from 'react-icons/ri' +import { useTranslation } from 'react-i18next' const Override: React.FC = () => { + const { t } = useTranslation() const { overrideConfig, setOverrideConfig, @@ -102,7 +104,7 @@ const Override: React.FC = () => { setFileOver(false) } } else { - alert('不支持的文件类型') + alert(t('override.unsupportedFileType')) } } setFileOver(false) @@ -121,13 +123,13 @@ const Override: React.FC = () => { return ( @@ -208,24 +210,24 @@ const Override: React.FC = () => { } } else if (key === 'new-yaml') { await addOverrideItem({ - name: '新建YAML', + name: t('override.newFile.yaml'), type: 'local', - file: '# https://mihomo.party/docs/guide/override/yaml', + file: t('override.defaultContent.yaml'), ext: 'yaml' }) } else if (key === 'new-js') { await addOverrideItem({ - name: '新建JS', + name: t('override.newFile.js'), type: 'local', - file: '// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}', + file: t('override.defaultContent.js'), ext: 'js' }) } }} > - 打开 - 新建 YAML - 新建 JavaScript + {t('override.actions.open')} + {t('override.actions.newYaml')} + {t('override.actions.newJs')}
diff --git a/src/renderer/src/pages/profiles.tsx b/src/renderer/src/pages/profiles.tsx index d8b89b6..7594311 100644 --- a/src/renderer/src/pages/profiles.tsx +++ b/src/renderer/src/pages/profiles.tsx @@ -31,8 +31,10 @@ import { IoMdRefresh } from 'react-icons/io' import SubStoreIcon from '@renderer/components/base/substore-icon' import useSWR from 'swr' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' const Profiles: React.FC = () => { + const { t } = useTranslation() const { profileConfig, setProfileConfig, @@ -67,7 +69,7 @@ const Profiles: React.FC = () => { const items: { icon?: ReactNode; key: string; children: ReactNode; divider: boolean }[] = [ { key: 'open-substore', - children: '访问 Sub-Store', + children: t('profiles.substore.visit'), icon: , divider: (Boolean(subs) && subs.length > 0) || (Boolean(collections) && collections.length > 0) @@ -177,7 +179,7 @@ const Profiles: React.FC = () => { alert(e) } } else { - alert('不支持的文件类型') + alert(t('profiles.error.unsupportedFileType')) } } setFileOver(false) @@ -196,11 +198,11 @@ const Profiles: React.FC = () => { return ( { checked={useProxy} onValueChange={setUseProxy} > - 代理 + {t('profiles.useProxy')} } @@ -262,7 +264,7 @@ const Profiles: React.FC = () => { isLoading={importing} onPress={handleImport} > - 导入 + {t('profiles.import')} {useSubStore && ( { } } else if (key === 'new') { await addProfileItem({ - name: '新建订阅', + name: t('profiles.newProfile'), type: 'local', file: 'proxies: []\nproxy-groups: []\nrules: []' }) } }} > - 打开 - 新建 + {t('profiles.open')} + {t('profiles.new')}
diff --git a/src/renderer/src/pages/proxies.tsx b/src/renderer/src/pages/proxies.tsx index 4f194f8..3ebc017 100644 --- a/src/renderer/src/pages/proxies.tsx +++ b/src/renderer/src/pages/proxies.tsx @@ -20,6 +20,7 @@ import { useGroups } from '@renderer/hooks/use-groups' import CollapseInput from '@renderer/components/base/collapse-input' import { includesIgnoreCase } from '@renderer/utils/includes' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' +import { useTranslation } from 'react-i18next' const SCROLL_POSITION_KEY = 'proxy_scroll_position' const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state' @@ -112,6 +113,7 @@ const useProxyState = (groups: IMihomoMixedGroup[]) => { } const Proxies: React.FC = () => { + const { t } = useTranslation() const { controledMihomoConfig } = useControledMihomoConfig() const { mode = 'rule' } = controledMihomoConfig || {} const { groups = [], mutate } = useGroups() @@ -250,7 +252,7 @@ const Proxies: React.FC = () => { return ( @@ -301,7 +303,7 @@ const Proxies: React.FC = () => {
-

直连模式

+

{t('proxies.mode.direct')}

) : ( @@ -383,7 +385,7 @@ const Proxies: React.FC = () => { )} { setSearchValue((prev) => { @@ -394,7 +396,7 @@ const Proxies: React.FC = () => { }} /> ) } > - + { }} /> - + { }} /> - + { }} /> - + handleSniffPortChange('HTTP', v)} /> - + handleSniffPortChange('TLS', v)} /> - + handleSniffPortChange('QUIC', v)} />
-

跳过域名嗅探

- {renderListInputs('skipDomain', '例:+.push.apple.com')} +

{t('sniffer.skipDomain.title')}

+ {renderListInputs('skipDomain', t('sniffer.skipDomain.placeholder'))}
-

强制域名嗅探

- {renderListInputs('forceDomain', '例:v2ex.com')} +

{t('sniffer.forceDomain.title')}

+ {renderListInputs('forceDomain', t('sniffer.forceDomain.placeholder'))}
-

跳过目标地址嗅探

- {renderListInputs('skipDstAddress', '例:1.1.1.1/32')} +

{t('sniffer.skipDstAddress.title')}

+ {renderListInputs('skipDstAddress', t('sniffer.skipDstAddress.placeholder'))}
-

跳过来源地址嗅探

- {renderListInputs('skipSrcAddress', '例:192.168.1.1/24')} +

{t('sniffer.skipSrcAddress.title')}

+ {renderListInputs('skipSrcAddress', t('sniffer.skipSrcAddress.placeholder'))}
diff --git a/src/renderer/src/pages/substore.tsx b/src/renderer/src/pages/substore.tsx index 69622c3..1729839 100644 --- a/src/renderer/src/pages/substore.tsx +++ b/src/renderer/src/pages/substore.tsx @@ -4,8 +4,10 @@ import { useAppConfig } from '@renderer/hooks/use-app-config' import { subStoreFrontendPort, subStorePort } from '@renderer/utils/ipc' import React, { useEffect, useState } from 'react' import { HiExternalLink } from 'react-icons/hi' +import { useTranslation } from 'react-i18next' const SubStore: React.FC = () => { + const { t } = useTranslation() const { appConfig } = useAppConfig() const { useCustomSubStore, customSubStoreUrl } = appConfig || {} const [backendPort, setBackendPort] = useState() @@ -23,10 +25,10 @@ const SubStore: React.FC = () => { return ( <> { + const { t } = useTranslation() const { appConfig, patchAppConfig } = useAppConfig() const { sysProxy } = appConfig || ({ sysProxy: { enable: false } } as IAppConfig) const [changed, setChanged] = useState(false) @@ -104,11 +106,11 @@ const Sysproxy: React.FC = () => { return ( - 保存 + {t('common.save')} ) } @@ -124,68 +126,68 @@ const Sysproxy: React.FC = () => { /> )} - + { setValues({ ...values, host: v }) }} /> - + setValues({ ...values, mode: key as SysProxyMode })} > - - + + {platform === 'win32' && ( - + )} {values.mode === 'auto' && ( - + )} {values.mode === 'manual' && ( <> - +
-

代理绕过

+

{t('sysproxy.bypass.title')}

{[...values.bypass, ''].map((domain, index) => (
handleBypassChange(v, index)} /> diff --git a/src/renderer/src/pages/tun.tsx b/src/renderer/src/pages/tun.tsx index 8526fab..eb5890c 100644 --- a/src/renderer/src/pages/tun.tsx +++ b/src/renderer/src/pages/tun.tsx @@ -9,8 +9,10 @@ import React, { Key, useState } from 'react' import BasePasswordModal from '@renderer/components/base/base-password-modal' import { useAppConfig } from '@renderer/hooks/use-app-config' import { MdDeleteForever } from 'react-icons/md' +import { useTranslation } from 'react-i18next' const Tun: React.FC = () => { + const { t } = useTranslation() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() const { appConfig, patchAppConfig } = useAppConfig() const { autoSetDNS = true } = appConfig || {} @@ -75,7 +77,7 @@ const Tun: React.FC = () => { onConfirm={async (password: string) => { try { await manualGrantCorePermition(password) - new Notification('内核授权成功') + new Notification(t('tun.notifications.coreAuthSuccess')) await restartCore() setOpenPasswordModal(false) } catch (e) { @@ -85,7 +87,7 @@ const Tun: React.FC = () => { /> )} { }) } > - 保存 + {t('common.save')} ) } > {platform === 'win32' && ( - + )} {platform !== 'win32' && ( - + )} {platform === 'darwin' && ( - + { )} - + { {platform !== 'darwin' && ( - + { )} - + { }} /> - + { /> {platform === 'linux' && ( - + { /> )} - + { }} /> - + { />
-

排除自定义网段

+

{t('tun.excludeAddress.title')}

{[...values.routeExcludeAddress, ''].map((address, index) => (
handleExcludeAddressChange(v, index)} /> diff --git a/src/renderer/src/utils/dayjs.ts b/src/renderer/src/utils/dayjs.ts new file mode 100644 index 0000000..1910a06 --- /dev/null +++ b/src/renderer/src/utils/dayjs.ts @@ -0,0 +1,22 @@ +import dayjs from 'dayjs' +import 'dayjs/locale/zh-cn' +import 'dayjs/locale/en' +import relativeTime from 'dayjs/plugin/relativeTime' +import i18n from '@renderer/i18n' + +// 加载相对时间插件 +dayjs.extend(relativeTime) + +// 根据当前语言设置 dayjs 语言 +const updateDayjsLocale = (): void => { + const currentLanguage = i18n.language + dayjs.locale(currentLanguage === 'zh-CN' ? 'zh-cn' : 'en') +} + +// 初始设置语言 +updateDayjsLocale() + +// 监听语言变化 +i18n.on('languageChanged', updateDayjsLocale) + +export default dayjs \ No newline at end of file diff --git a/src/shared/i18n.ts b/src/shared/i18n.ts new file mode 100644 index 0000000..8734a7f --- /dev/null +++ b/src/shared/i18n.ts @@ -0,0 +1,31 @@ +import i18next from 'i18next' +import enUS from '../renderer/src/locales/en-US.json' +import zhCN from '../renderer/src/locales/zh-CN.json' + +export const resources = { + 'en-US': { + translation: enUS + }, + 'zh-CN': { + translation: zhCN + } +} + +export const defaultConfig = { + resources, + lng: 'zh-CN', + fallbackLng: 'en-US', + interpolation: { + escapeValue: false + } +} + +export const initI18n = async (options = {}): Promise => { + await i18next.init({ + ...defaultConfig, + ...options + }) + return i18next +} + +export default i18next \ No newline at end of file diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index e307489..431f215 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -291,6 +291,7 @@ interface IAppConfig { directModeShortcut?: string restartAppShortcut?: string quitWithoutCoreShortcut?: string + language?: 'zh-CN' | 'en-US' } interface IMihomoTunConfig { diff --git a/tsconfig.node.json b/tsconfig.node.json index 8d0edf2..883f8a4 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,6 +1,6 @@ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", - "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*.d.ts"], + "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"], "compilerOptions": { "composite": true, "types": ["electron-vite/node"] diff --git a/tsconfig.web.json b/tsconfig.web.json index f8ae6ae..b7ddbba 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -1,11 +1,12 @@ { "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", "include": [ + "src/renderer/**/*", + "src/shared/**/*", "src/renderer/src/utils/env.d.ts", "src/renderer/src/**/*", "src/renderer/src/**/*.tsx", - "src/preload/*.d.ts", - "src/shared/*.d.ts" + "src/preload/*.d.ts" ], "compilerOptions": { "composite": true,