From ec6dfaef50a04e5a182d1d1acceb915d13b0a93a Mon Sep 17 00:00:00 2001 From: pompurin404 Date: Tue, 4 Feb 2025 05:37:48 +0800 Subject: [PATCH] feat: add i18n support with English translations --- package.json | 2 + pnpm-lock.yaml | 55 ++ src/main/index.ts | 3 + src/main/resolve/tray.ts | 42 +- src/main/utils/ipc.ts | 8 + .../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 +- .../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 | 38 +- .../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 | 643 ++++++++++++++++++ src/renderer/src/locales/zh-CN.json | 643 ++++++++++++++++++ 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 +- 70 files changed, 2138 insertions(+), 554 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 4e9c32bc..07156aa8 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 3605bee8..d4721624 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 67bc4301..20df430f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -19,6 +19,7 @@ import path from 'path' import { startMonitor } from './resolve/trafficMonitor' import { showFloatingWindow } from './resolve/floatingWindow' import iconv from 'iconv-lite' +import { initI18n } from '../shared/i18n' let quitTimeout: NodeJS.Timeout | null = null export let mainWindow: BrowserWindow | null = null @@ -122,6 +123,8 @@ 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}`) diff --git a/src/main/resolve/tray.ts b/src/main/resolve/tray.ts index a17544be..fbee02a6 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 304da090..1d297257 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/components/base/base-error-boundary.tsx b/src/renderer/src/components/base/base-error-boundary.tsx index 884e867b..6a1d062a 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 77d80f55..99687aa3 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 898b970a..7925a3c9 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 2fbb7f46..f12af391 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 12d374fc..725b5941 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/override/edit-file-modal.tsx b/src/renderer/src/components/override/edit-file-modal.tsx index 40296115..c885922a 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 e182abe1..2e44e030 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 9f33e046..81a0dbdb 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 a3af40ea..359bb9fa 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 4e7fc5e6..f444f3c5 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 02b54c4f..ff72f6b3 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 733a5b07..20a233d8 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 1a80925b..8c9a0904 100644 --- a/src/renderer/src/components/settings/actions.tsx +++ b/src/renderer/src/components/settings/actions.tsx @@ -13,8 +13,10 @@ import UpdaterModal from '../updater/updater-modal' import { version } from '@renderer/utils/init' import { IoIosHelpCircle } from 'react-icons/io' import { firstDriver } 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 9b18034a..4f953c1e 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 60695521..bcbb8b5d 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 25f4f9be..6e1f1d41 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 af3610a9..c550e6cd 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 sizeMap: Record = { + 'col-span-2': 'sider.size.large', + 'col-span-1': 'sider.size.small', + hidden: 'sider.size.hidden' } -const SiderConfig: React.FC = () => { + +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 7a04d55c..75785eac 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 c4197943..e189b438 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 e3c24a4b..0b8f3b4f 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 1d2c1b6c..f3798444 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 923319bb..8ae6b042 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 3a811c54..76383fd5 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 4f13c41b..935316f3 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 00000000..358a6e7b --- /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 00000000..d8d4c72a --- /dev/null +++ b/src/renderer/src/locales/en-US.json @@ -0,0 +1,643 @@ +{ + "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", + "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" + } + }, + "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" + } +} \ 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 00000000..8608f50b --- /dev/null +++ b/src/renderer/src/locales/zh-CN.json @@ -0,0 +1,643 @@ +{ + "common": { + "settings": "设置", + "profiles": "配置", + "proxies": "代理", + "connections": "连接", + "dns": "DNS", + "tun": "TUN", + "save": "保存", + "cancel": "取消", + "edit": "编辑", + "delete": "删除", + "seconds": "秒", + "confirm": "确认", + "auto": "自动", + "default": "默认", + "close": "关闭", + "pinWindow": "窗口置顶", + "enterRootPassword": "请输入root密码", + "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 段" + } + }, + "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": "复制环境变量" + } +} \ No newline at end of file diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 2be14e34..f303fa7d 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 ae0b5c75..2a89f6e7 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 4716e4a4..784dbabe 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 fe0e6f79..f7b42fff 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 d8b89b61..7594311f 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 4f194f8b..3ebc0173 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 69622c36..17298392 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 8526fab0..eb5890c2 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 00000000..1910a06a --- /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 00000000..8734a7f3 --- /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 e3074897..431f2156 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 8d0edf24..883f8a4d 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 f8ae6ae2..b7ddbba2 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,