feat: add i18n support with English translations

This commit is contained in:
pompurin404 2025-02-04 07:38:53 +08:00
parent fa3c412146
commit 8210b477ab
72 changed files with 2500 additions and 756 deletions

View File

@ -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"

55
pnpm-lock.yaml generated
View File

@ -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:

View File

@ -19,6 +19,8 @@ import path from 'path'
import { startMonitor } from './resolve/trafficMonitor'
import { showFloatingWindow } from './resolve/floatingWindow'
import iconv from 'iconv-lite'
import { initI18n } from '../shared/i18n'
import i18next from 'i18next'
let quitTimeout: NodeJS.Timeout | null = null
export let mainWindow: BrowserWindow | null = null
@ -48,8 +50,8 @@ if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')
// ignore
}
dialog.showErrorBox(
'首次启动请以管理员权限运行',
`首次启动请以管理员权限运行\n${createErrorStr}\n${eStr}`
i18next.t('main.error.adminRequired'),
`${i18next.t('main.error.adminRequired')}\n${createErrorStr}\n${eStr}`
)
} finally {
app.exit()
@ -122,9 +124,11 @@ app.whenReady().then(async () => {
// Set app user model id for windows
electronApp.setAppUserModelId('party.mihomo.app')
try {
const appConfig = await getAppConfig()
await initI18n({ lng: appConfig.language })
await initPromise
} catch (e) {
dialog.showErrorBox('应用初始化失败', `${e}`)
dialog.showErrorBox(i18next.t('main.error.initFailed'), `${e}`)
app.quit()
}
try {
@ -133,7 +137,7 @@ app.whenReady().then(async () => {
await initProfileUpdater()
})
} catch (e) {
dialog.showErrorBox('内核启动出错', `${e}`)
dialog.showErrorBox(i18next.t('main.error.coreStartFailed'), `${e}`)
}
try {
await startMonitor()
@ -174,7 +178,7 @@ async function handleDeepLink(url: string): Promise<void> {
const profileUrl = urlObj.searchParams.get('url')
const profileName = urlObj.searchParams.get('name')
if (!profileUrl) {
throw new Error('缺少参数 url')
throw new Error(i18next.t('main.error.urlParamMissing'))
}
await addProfileItem({
type: 'remote',
@ -182,10 +186,10 @@ async function handleDeepLink(url: string): Promise<void> {
url: profileUrl
})
mainWindow?.webContents.send('profileConfigUpdated')
new Notification({ title: '订阅导入成功' }).show()
new Notification({ title: i18next.t('main.notification.importSuccess') }).show()
break
} catch (e) {
dialog.showErrorBox('订阅导入失败', `${url}\n${e}`)
dialog.showErrorBox(i18next.t('main.error.importFailed'), `${url}\n${e}`)
}
}
}

View File

@ -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<Menu> => {
// 添加调试日志
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<Menu> => {
{
id: 'show',
accelerator: showWindowShortcut,
label: '显示窗口',
label: t('tray.showWindow'),
type: 'normal',
click: (): void => {
showMainWindow()
@ -95,7 +101,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
{
id: 'show-floating',
accelerator: showFloatingWindowShortcut,
label: floatingWindow?.isVisible() ? '关闭悬浮窗' : '显示悬浮窗',
label: floatingWindow?.isVisible() ? t('tray.hideFloatingWindow') : t('tray.showFloatingWindow'),
type: 'normal',
click: async (): Promise<void> => {
await triggerFloatingWindow()
@ -103,7 +109,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
},
{
id: 'rule',
label: '规则模式',
label: t('tray.ruleMode'),
accelerator: ruleModeShortcut,
type: 'radio',
checked: mode === 'rule',
@ -117,7 +123,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
},
{
id: 'global',
label: '全局模式',
label: t('tray.globalMode'),
accelerator: globalModeShortcut,
type: 'radio',
checked: mode === 'global',
@ -131,7 +137,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
},
{
id: 'direct',
label: '直连模式',
label: t('tray.directMode'),
accelerator: directModeShortcut,
type: 'radio',
checked: mode === 'direct',
@ -146,7 +152,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
{ type: 'separator' },
{
type: 'checkbox',
label: '系统代理',
label: t('tray.systemProxy'),
accelerator: triggerSysProxyShortcut,
checked: sysProxy.enable,
click: async (item): Promise<void> => {
@ -165,7 +171,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
},
{
type: 'checkbox',
label: '虚拟网卡',
label: t('tray.tun'),
accelerator: triggerTunShortcut,
checked: tun?.enable ?? false,
click: async (item): Promise<void> => {
@ -190,7 +196,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
{ 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<Menu> => {
{ type: 'separator' },
{
type: 'submenu',
label: '打开目录',
label: t('tray.openDirectories.title'),
submenu: [
{
type: 'normal',
label: '应用目录',
label: t('tray.openDirectories.appDir'),
click: (): Promise<string> => shell.openPath(dataDir())
},
{
type: 'normal',
label: '工作目录',
label: t('tray.openDirectories.workDir'),
click: (): Promise<string> => shell.openPath(mihomoWorkDir())
},
{
type: 'normal',
label: '内核目录',
label: t('tray.openDirectories.coreDir'),
click: (): Promise<string> => shell.openPath(mihomoCoreDir())
},
{
type: 'normal',
label: '日志目录',
label: t('tray.openDirectories.logDir'),
click: (): Promise<string> => shell.openPath(logDir())
}
]
@ -235,7 +241,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
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<Menu> => {
}
: {
id: 'copyenv',
label: '复制环境变量',
label: t('tray.copyEnv'),
type: 'normal',
click: async (): Promise<void> => {
await copyEnv(envType[0])
@ -258,14 +264,14 @@ export const buildContextMenu = async (): Promise<Menu> => {
{ 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<Menu> => {
},
{
id: 'quit',
label: '退出应用',
label: t('actions.quit.button'),
type: 'normal',
accelerator: 'CommandOrControl+Q',
click: (): void => app.quit()

View File

@ -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<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
fn: (...args: any[]) => Promise<T> // 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')
})
}

View File

@ -35,10 +35,17 @@ import SubStoreCard from '@renderer/components/sider/substore-card'
import MihomoIcon from './components/base/mihomo-icon'
import { driver } from 'driver.js'
import 'driver.js/dist/driver.css'
import { useTranslation } from 'react-i18next'
let navigate: NavigateFunction
let driverInstance: ReturnType<typeof driver> | null = null
export function getDriver(): ReturnType<typeof driver> | null {
return driverInstance
}
const App: React.FC = () => {
const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig()
const {
appTheme = 'system',
@ -96,12 +103,188 @@ const App: React.FC = () => {
}, [siderWidthValue, resizing])
useEffect(() => {
driverInstance = driver({
showProgress: true,
nextBtnText: t('common.next'),
prevBtnText: t('common.prev'),
doneBtnText: t('common.done'),
progressText: '{{current}} / {{total}}',
overlayOpacity: 0.9,
steps: [
{
element: 'none',
popover: {
title: t('guide.welcome.title'),
description: t('guide.welcome.description'),
side: 'over',
align: 'center'
}
},
{
element: '.side',
popover: {
title: t('guide.sider.title'),
description: t('guide.sider.description'),
side: 'right',
align: 'center'
}
},
{
element: '.sysproxy-card',
popover: {
title: t('guide.card.title'),
description: t('guide.card.description'),
side: 'right',
align: 'start'
}
},
{
element: '.main',
popover: {
title: t('guide.main.title'),
description: t('guide.main.description'),
side: 'left',
align: 'center'
}
},
{
element: '.profile-card',
popover: {
title: t('guide.profile.title'),
description: t('guide.profile.description'),
side: 'right',
align: 'start',
onNextClick: async (): Promise<void> => {
navigate('/profiles')
setTimeout(() => {
driverInstance?.moveNext()
}, 0)
}
}
},
{
element: '.profiles-sticky',
popover: {
title: t('guide.import.title'),
description: t('guide.import.description'),
side: 'bottom',
align: 'start'
}
},
{
element: '.substore-import',
popover: {
title: t('guide.substore.title'),
description: t('guide.substore.description'),
side: 'bottom',
align: 'start'
}
},
{
element: '.new-profile',
popover: {
title: t('guide.localProfile.title'),
description: t('guide.localProfile.description'),
side: 'bottom',
align: 'start'
}
},
{
element: '.sysproxy-card',
popover: {
title: t('guide.sysproxy.title'),
description: t('guide.sysproxy.description'),
side: 'right',
align: 'start',
onNextClick: async (): Promise<void> => {
navigate('/sysproxy')
setTimeout(() => {
driverInstance?.moveNext()
}, 0)
}
}
},
{
element: '.sysproxy-settings',
popover: {
title: t('guide.sysproxySetting.title'),
description: t('guide.sysproxySetting.description'),
side: 'top',
align: 'start'
}
},
{
element: '.tun-card',
popover: {
title: t('guide.tun.title'),
description: t('guide.tun.description'),
side: 'right',
align: 'start',
onNextClick: async (): Promise<void> => {
navigate('/tun')
setTimeout(() => {
driverInstance?.moveNext()
}, 0)
}
}
},
{
element: '.tun-settings',
popover: {
title: t('guide.tunSetting.title'),
description: t('guide.tunSetting.description'),
side: 'bottom',
align: 'start'
}
},
{
element: '.override-card',
popover: {
title: t('guide.override.title'),
description: t('guide.override.description'),
side: 'right',
align: 'center'
}
},
{
element: '.dns-card',
popover: {
title: t('guide.dns.title'),
description: t('guide.dns.description'),
side: 'right',
align: 'center',
onNextClick: async (): Promise<void> => {
navigate('/profiles')
setTimeout(() => {
driverInstance?.moveNext()
}, 0)
}
}
},
{
element: 'none',
popover: {
title: t('guide.end.title'),
description: t('guide.end.description'),
side: 'top',
align: 'center',
onNextClick: async (): Promise<void> => {
navigate('/profiles')
setTimeout(() => {
driverInstance?.destroy()
}, 0)
}
}
}
]
})
const tourShown = window.localStorage.getItem('tourShown')
if (!tourShown) {
window.localStorage.setItem('tourShown', 'true')
firstDriver.drive()
driverInstance.drive()
}
}, [])
}, [t])
useEffect(() => {
setNativeTheme(appTheme)
@ -295,191 +478,3 @@ const App: React.FC = () => {
}
export default App
export const firstDriver = driver({
showProgress: true,
nextBtnText: '下一步',
prevBtnText: '上一步',
doneBtnText: '完成',
progressText: '{{current}} / {{total}}',
overlayOpacity: 0.9,
steps: [
{
element: 'none',
popover: {
title: '欢迎使用 Mihomo Party',
description:
'这是一份交互式使用教程,如果您已经完全熟悉本软件的操作,可以直接点击右上角关闭按钮,后续您可以随时从设置中打开本教程',
side: 'over',
align: 'center'
}
},
{
element: '.side',
popover: {
title: '导航栏',
description:
'左侧是应用的导航栏,兼顾仪表盘功能,在这里可以切换不同页面,也可以概览常用的状态信息',
side: 'right',
align: 'center'
}
},
{
element: '.sysproxy-card',
popover: {
title: '卡片',
description: '点击导航栏卡片可以跳转到对应页面,拖动导航栏卡片可以自由排列卡片顺序',
side: 'right',
align: 'start'
}
},
{
element: '.main',
popover: {
title: '主要区域',
description: '右侧是应用的主要区域,展示了导航栏所选页面的内容',
side: 'left',
align: 'center'
}
},
{
element: '.profile-card',
popover: {
title: '订阅管理',
description:
'订阅管理卡片展示当前运行的订阅配置信息,点击进入订阅管理页面可以在这里管理订阅配置',
side: 'right',
align: 'start',
onNextClick: async (): Promise<void> => {
navigate('/profiles')
setTimeout(() => {
firstDriver.moveNext()
}, 0)
}
}
},
{
element: '.profiles-sticky',
popover: {
title: '订阅导入',
description:
'Mihomo Party 支持多种订阅导入方式,在此输入订阅链接,点击导入即可导入您的订阅配置,如果您的订阅需要代理才能更新,请勾选“代理”再点击导入,当然这需要已经有一个可以正常使用的订阅才可以',
side: 'bottom',
align: 'start'
}
},
{
element: '.substore-import',
popover: {
title: 'Sub-Store',
description:
'Mihomo Party 深度集成了 Sub-Store您可以点击该按钮进入 Sub-Store 或直接导入您通过 Sub-Store 管理的订阅Mihomo Party 默认使用内置的 Sub-Store 后端,如果您有自建的 Sub-Store 后端,可以在设置页面中配置,如果您不使用 Sub-Store 也可以在设置页面中关闭',
side: 'bottom',
align: 'start'
}
},
{
element: '.new-profile',
popover: {
title: '本地订阅',
description: '点击“+”可以选择本地文件进行导入或者直接新建空白配置进行编辑',
side: 'bottom',
align: 'start'
}
},
{
element: '.sysproxy-card',
popover: {
title: '系统代理',
description:
'导入订阅之后,内核已经开始运行并监听指定端口,此时您已经可以通过指定代理端口来使用代理了,如果您要使大部分应用自动使用该端口的代理,您还需要打开系统代理开关',
side: 'right',
align: 'start',
onNextClick: async (): Promise<void> => {
navigate('/sysproxy')
setTimeout(() => {
firstDriver.moveNext()
}, 0)
}
}
},
{
element: '.sysproxy-settings',
popover: {
title: '系统代理设置',
description:
'在此您可以进行系统代理相关设置,选择代理模式,如果某些 Windows 应用不遵循系统代理还可以使用“UWP 工具”解除本地回环限制对于“手动代理模式”和“PAC 代理模式”的区别,请自行百度',
side: 'top',
align: 'start'
}
},
{
element: '.tun-card',
popover: {
title: '虚拟网卡',
description:
'虚拟网卡即同类软件中常见的“Tun 模式”,对于某些不遵循系统代理的应用,您可以打开虚拟网卡以让内核接管所有流量',
side: 'right',
align: 'start',
onNextClick: async (): Promise<void> => {
navigate('/tun')
setTimeout(() => {
firstDriver.moveNext()
}, 0)
}
}
},
{
element: '.tun-settings',
popover: {
title: '虚拟网卡设置',
description:
'这里可以更改虚拟网卡相关设置Mihomo Party 理论上已经完全解决权限问题如果您的虚拟网卡仍然不可用可以尝试重设防火墙Windows或手动授权内核MacOS/Linux后重启内核',
side: 'bottom',
align: 'start'
}
},
{
element: '.override-card',
popover: {
title: '覆写',
description:
'Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,<b>编辑好覆写文件一定要记得在需要覆写的订阅上启用</b>,覆写文件的语法请参考 <a href="https://mihomo.party/docs/guide/override" target="_blank">官方文档</a>',
side: 'right',
align: 'center'
}
},
{
element: '.dns-card',
popover: {
title: 'DNS',
description:
'软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭“接管 DNS 设置”,域名嗅探同理',
side: 'right',
align: 'center',
onNextClick: async (): Promise<void> => {
navigate('/profiles')
setTimeout(() => {
firstDriver.moveNext()
}, 0)
}
}
},
{
element: 'none',
popover: {
title: '教程结束',
description:
'现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href="https://t.me/mihomo_party_group" target="_blank">Telegram 群组</a> 获取最新资讯',
side: 'top',
align: 'center',
onNextClick: async (): Promise<void> => {
navigate('/profiles')
setTimeout(() => {
firstDriver.destroy()
}, 0)
}
}
}
]
})

View File

@ -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 (
<div className="p-4">
<h2 className="my-2 text-lg font-bold">
{'应用崩溃了 :( 请将以下信息提交给开发者以排查错误'}
{t('common.error.appCrash')}
</h2>
<Button
@ -35,7 +38,7 @@ const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
navigator.clipboard.writeText('```\n' + error.message + '\n' + error.stack + '\n```')
}
>
{t('common.error.copyErrorMessage')}
</Button>
<p className="my-2">{error.message}</p>

View File

@ -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<HTMLDivElement, Props>((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<HTMLDivElement, Props>((props, ref) => {
size="sm"
className="app-nodrag"
isIconOnly
title="窗口置顶"
title={t('common.pinWindow')}
variant="light"
color={onTop ? 'primary' : 'default'}
onPress={async () => {

View File

@ -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> = (props) => {
const { t } = useTranslation()
const { onCancel, onConfirm } = props
const [password, setPassword] = useState('')
return (
<Modal backdrop="blur" classNames={{ backdrop: 'top-[48px]' }} hideCloseButton isOpen={true}>
<ModalContent>
<ModalHeader className="flex app-drag">root密码</ModalHeader>
<ModalHeader className="flex app-drag">{t('common.enterRootPassword')}</ModalHeader>
<ModalBody>
<Input fullWidth type="password" value={password} onValueChange={setPassword} />
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onCancel}>
{t('common.cancel')}
</Button>
<Button size="sm" color="primary" onPress={() => onConfirm(password)}>
{t('common.confirm')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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={
<Dropdown>
<DropdownTrigger>
<Button title="复制规则" isIconOnly size="sm" variant="light">
<Button title={t('connections.detail.copyRule')} isIconOnly size="sm" variant="light">
<BiCopy className="text-lg" />
</Button>
</DropdownTrigger>
@ -120,6 +122,8 @@ const CopyableSettingItem: React.FC<{
const ConnectionDetailModal: React.FC<Props> = (props) => {
const { connection, onClose } = props
const { t } = useTranslation()
return (
<Modal
backdrop="blur"
@ -131,41 +135,41 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent className="flag-emoji break-all">
<ModalHeader className="flex app-drag"></ModalHeader>
<ModalHeader className="flex app-drag">{t('connections.detail.title')}</ModalHeader>
<ModalBody>
<SettingItem title="连接建立时间">{dayjs(connection.start).fromNow()}</SettingItem>
<SettingItem title="规则">
<SettingItem title={t('connections.detail.establishTime')}>{dayjs(connection.start).fromNow()}</SettingItem>
<SettingItem title={t('connections.detail.rule')}>
{connection.rule}
{connection.rulePayload ? `(${connection.rulePayload})` : ''}
</SettingItem>
<SettingItem title="代理链">{[...connection.chains].reverse().join('>>')}</SettingItem>
<SettingItem title="上传速度">{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
<SettingItem title="下载速度">{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
<SettingItem title="上传量">{calcTraffic(connection.upload)}</SettingItem>
<SettingItem title="下载量">{calcTraffic(connection.download)}</SettingItem>
<SettingItem title={t('connections.detail.proxyChain')}>{[...connection.chains].reverse().join('>>')}</SettingItem>
<SettingItem title={t('connections.uploadSpeed')}>{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
<SettingItem title={t('connections.downloadSpeed')}>{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
<SettingItem title={t('connections.uploadAmount')}>{calcTraffic(connection.upload)}</SettingItem>
<SettingItem title={t('connections.downloadAmount')}>{calcTraffic(connection.download)}</SettingItem>
<CopyableSettingItem
title="连接类型"
title={t('connections.detail.connectionType')}
value={[connection.metadata.type, connection.metadata.network]}
displayName={`${connection.metadata.type}(${connection.metadata.network})`}
prefix={['IN-TYPE', 'NETWORK']}
/>
{connection.metadata.host && (
<CopyableSettingItem
title="主机"
title={t('connections.detail.host')}
value={connection.metadata.host}
prefix={['DOMAIN', 'DOMAIN-SUFFIX']}
/>
)}
{connection.metadata.sniffHost && (
<CopyableSettingItem
title="嗅探主机"
title={t('connections.detail.sniffHost')}
value={connection.metadata.sniffHost}
prefix={['DOMAIN', 'DOMAIN-SUFFIX']}
/>
)}
{connection.metadata.process && (
<CopyableSettingItem
title="进程名"
title={t('connections.detail.processName')}
value={[
connection.metadata.process,
...(connection.metadata.uid ? [connection.metadata.uid.toString()] : [])
@ -176,35 +180,35 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
)}
{connection.metadata.processPath && (
<CopyableSettingItem
title="进程路径"
title={t('connections.detail.processPath')}
value={connection.metadata.processPath}
prefix={['PROCESS-PATH']}
/>
)}
{connection.metadata.sourceIP && (
<CopyableSettingItem
title="来源IP"
title={t('connections.detail.sourceIP')}
value={connection.metadata.sourceIP}
prefix={['SRC-IP-CIDR']}
/>
)}
{connection.metadata.sourceGeoIP && connection.metadata.sourceGeoIP.length > 0 && (
<CopyableSettingItem
title="来源GeoIP"
title={t('connections.detail.sourceGeoIP')}
value={connection.metadata.sourceGeoIP}
prefix={['SRC-GEOIP']}
/>
)}
{connection.metadata.sourceIPASN && (
<CopyableSettingItem
title="来源ASN"
title={t('connections.detail.sourceASN')}
value={connection.metadata.sourceIPASN}
prefix={['SRC-IP-ASN']}
/>
)}
{connection.metadata.destinationIP && (
<CopyableSettingItem
title="目标IP"
title={t('connections.detail.destinationIP')}
value={connection.metadata.destinationIP}
prefix={['IP-CIDR']}
/>
@ -212,83 +216,83 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
{connection.metadata.destinationGeoIP &&
connection.metadata.destinationGeoIP.length > 0 && (
<CopyableSettingItem
title="目标GeoIP"
title={t('connections.detail.destinationGeoIP')}
value={connection.metadata.destinationGeoIP}
prefix={['GEOIP']}
/>
)}
{connection.metadata.destinationIPASN && (
<CopyableSettingItem
title="目标ASN"
title={t('connections.detail.destinationASN')}
value={connection.metadata.destinationIPASN}
prefix={['IP-ASN']}
/>
)}
{connection.metadata.sourcePort && (
<CopyableSettingItem
title="来源端口"
title={t('connections.detail.sourcePort')}
value={connection.metadata.sourcePort}
prefix={['SRC-PORT']}
/>
)}
{connection.metadata.destinationPort && (
<CopyableSettingItem
title="目标端口"
title={t('connections.detail.destinationPort')}
value={connection.metadata.destinationPort}
prefix={['DST-PORT']}
/>
)}
{connection.metadata.inboundIP && (
<CopyableSettingItem
title="入站IP"
title={t('connections.detail.inboundIP')}
value={connection.metadata.inboundIP}
prefix={['SRC-IP-CIDR']}
/>
)}
{connection.metadata.inboundPort && (
<CopyableSettingItem
title="入站端口"
title={t('connections.detail.inboundPort')}
value={connection.metadata.inboundPort}
prefix={['SRC-PORT']}
prefix={['IN-PORT']}
/>
)}
{connection.metadata.inboundName && (
<CopyableSettingItem
title="入站名称"
title={t('connections.detail.inboundName')}
value={connection.metadata.inboundName}
prefix={['IN-NAME']}
/>
)}
{connection.metadata.inboundUser && (
<CopyableSettingItem
title="入站用户"
title={t('connections.detail.inboundUser')}
value={connection.metadata.inboundUser}
prefix={['IN-USER']}
/>
)}
<CopyableSettingItem
title="DSCP"
title={t('connections.detail.dscp')}
value={connection.metadata.dscp.toString()}
prefix={['DSCP']}
/>
{connection.metadata.remoteDestination && (
<SettingItem title="远程目标">{connection.metadata.remoteDestination}</SettingItem>
<SettingItem title={t('connections.detail.remoteDestination')}>{connection.metadata.remoteDestination}</SettingItem>
)}
{connection.metadata.dnsMode && (
<SettingItem title="DNS模式">{connection.metadata.dnsMode}</SettingItem>
<SettingItem title={t('connections.detail.dnsMode')}>{connection.metadata.dnsMode}</SettingItem>
)}
{connection.metadata.specialProxy && (
<SettingItem title="特殊代理">{connection.metadata.specialProxy}</SettingItem>
<SettingItem title={t('connections.detail.specialProxy')}>{connection.metadata.specialProxy}</SettingItem>
)}
{connection.metadata.specialRules && (
<SettingItem title="特殊规则">{connection.metadata.specialRules}</SettingItem>
<SettingItem title={t('connections.detail.specialRules')}>{connection.metadata.specialRules}</SettingItem>
)}
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onClose}>
{t('connections.detail.close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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'

View File

@ -9,11 +9,15 @@ import {
} from '@nextui-org/react'
import React, { useEffect, useState } from 'react'
import { getInterfaces } from '@renderer/utils/ipc'
import { useTranslation } from 'react-i18next'
interface Props {
onClose: () => void
}
const InterfaceModal: React.FC<Props> = (props) => {
const { onClose } = props
const { t } = useTranslation()
const [info, setInfo] = useState<Record<string, NetworkInterfaceInfo[]>>({})
const getInfo = async (): Promise<void> => {
setInfo(await getInterfaces())
@ -33,7 +37,7 @@ const InterfaceModal: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent>
<ModalHeader className="flex app-drag"></ModalHeader>
<ModalHeader className="flex app-drag">{t('mihomo.interface.title')}</ModalHeader>
<ModalBody>
{Object.entries(info).map(([key, value]) => {
return (
@ -57,7 +61,7 @@ const InterfaceModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onClose}>
{t('common.close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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> = (props) => {
const { id, language, onClose } = props
const [currData, setCurrData] = useState('')
const { t } = useTranslation()
const getContent = async (): Promise<void> => {
setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml'))
@ -31,7 +34,9 @@ const EditFileModal: React.FC<Props> = (props) => {
>
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag">
{language === 'javascript' ? '脚本' : '配置'}
{t('override.editFile.title', {
type: language === 'javascript' ? t('override.editFile.script') : t('override.editFile.config')
})}
</ModalHeader>
<ModalBody className="h-full">
<BaseEditor
@ -42,7 +47,7 @@ const EditFileModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter className="pt-0">
<Button size="sm" variant="light" onPress={onClose}>
{t('common.cancel')}
</Button>
<Button
size="sm"
@ -57,7 +62,7 @@ const EditFileModal: React.FC<Props> = (props) => {
}
}}
>
{t('common.confirm')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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<void>
@ -19,6 +21,7 @@ interface Props {
const EditInfoModal: React.FC<Props> = (props) => {
const { item, updateOverrideItem, onClose } = props
const [values, setValues] = useState(item)
const { t } = useTranslation()
const onSave = async (): Promise<void> => {
await updateOverrideItem(values)
@ -36,9 +39,9 @@ const EditInfoModal: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent>
<ModalHeader className="flex app-drag"></ModalHeader>
<ModalHeader className="flex app-drag">{t('override.editInfo.title')}</ModalHeader>
<ModalBody>
<SettingItem title="名称">
<SettingItem title={t('override.editInfo.name')}>
<Input
size="sm"
className="w-[200px]"
@ -49,7 +52,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
/>
</SettingItem>
{values.type === 'remote' && (
<SettingItem title="地址">
<SettingItem title={t('override.editInfo.url')}>
<Input
size="sm"
className="w-[200px]"
@ -60,7 +63,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
/>
</SettingItem>
)}
<SettingItem title="全局启用">
<SettingItem title={t('override.editInfo.global')}>
<Switch
size="sm"
isSelected={values.global}
@ -72,10 +75,10 @@ const EditInfoModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onClose}>
{t('common.cancel')}
</Button>
<Button size="sm" color="primary" onPress={onSave}>
{t('common.save')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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> = (props) => {
const { id, onClose } = props
const [logs, setLogs] = useState<string[]>([])
const { t } = useTranslation()
const getLog = async (): Promise<void> => {
setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean))
@ -35,7 +38,7 @@ const ExecLogModal: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent>
<ModalHeader className="flex app-drag"></ModalHeader>
<ModalHeader className="flex app-drag">{t('override.execLog.title')}</ModalHeader>
<ModalBody>
{logs.map((log) => {
return (
@ -48,7 +51,7 @@ const ExecLogModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onClose}>
{t('override.execLog.close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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> = (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> = (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> = (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> = (props) => {
<div className={`mt-2 flex justify-start`}>
{info.global && (
<Chip size="sm" variant="dot" color="primary" className="mr-2">
{t('override.labels.global')}
</Chip>
)}
<Chip size="sm" variant="bordered">

View File

@ -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> = (props) => {
const { id, onClose } = props
const [currData, setCurrData] = useState('')
const navigate = useNavigate()
const { t } = useTranslation()
const getContent = async (): Promise<void> => {
setCurrData(await getProfileStr(id))
@ -33,9 +37,9 @@ const EditFileModal: React.FC<Props> = (props) => {
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag">
<div className="flex justify-start">
<div className="flex items-center"></div>
<div className="flex items-center">{t('profiles.editFile.title')}</div>
<small className="ml-2 text-foreground-500">
使
{t('profiles.editFile.notice')}
<Button
size="sm"
color="primary"
@ -45,9 +49,9 @@ const EditFileModal: React.FC<Props> = (props) => {
navigate('/override')
}}
>
{t('profiles.editFile.override')}
</Button>
{t('profiles.editFile.feature')}
</small>
</div>
</ModalHeader>
@ -56,7 +60,7 @@ const EditFileModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter className="pt-0">
<Button size="sm" variant="light" onPress={onClose}>
{t('common.cancel')}
</Button>
<Button
size="sm"
@ -66,7 +70,7 @@ const EditFileModal: React.FC<Props> = (props) => {
onClose()
}}
>
{t('common.confirm')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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> = (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<void> => {
try {
@ -62,9 +64,9 @@ const EditInfoModal: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent>
<ModalHeader className="flex app-drag"></ModalHeader>
<ModalHeader className="flex app-drag">{t('profiles.editInfo.title')}</ModalHeader>
<ModalBody>
<SettingItem title="名称">
<SettingItem title={t('profiles.editInfo.name')}>
<Input
size="sm"
className={cn(inputWidth)}
@ -76,7 +78,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
</SettingItem>
{values.type === 'remote' && (
<>
<SettingItem title="订阅地址">
<SettingItem title={t('profiles.editInfo.url')}>
<Input
size="sm"
className={cn(inputWidth)}
@ -86,7 +88,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
}}
/>
</SettingItem>
<SettingItem title="使用代理更新">
<SettingItem title={t('profiles.editInfo.useProxy')}>
<Switch
size="sm"
isSelected={values.useProxy ?? false}
@ -95,7 +97,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
}}
/>
</SettingItem>
<SettingItem title="更新间隔(分钟)">
<SettingItem title={t('profiles.editInfo.interval')}>
<Input
size="sm"
type="number"
@ -108,7 +110,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
</SettingItem>
</>
)}
<SettingItem title="覆写">
<SettingItem title={t('profiles.editInfo.override.title')}>
<div>
{overrideItems
.filter((i) => i.global)
@ -116,7 +118,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
return (
<div className="flex mb-2" key={i.id}>
<Button disabled fullWidth variant="flat" size="sm">
{i.name} ()
{i.name} ({t('profiles.editInfo.override.global')})
</Button>
</div>
)
@ -153,7 +155,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
</Button>
</DropdownTrigger>
<DropdownMenu
emptyContent="没有可用的覆写"
emptyContent={t('profiles.editInfo.override.noAvailable')}
onAction={(key) => {
setValues({
...values,
@ -173,10 +175,10 @@ const EditInfoModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onClose}>
{t('common.cancel')}
</Button>
<Button size="sm" color="primary" onPress={onSave}>
{t('common.save')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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> = (props) => {
const { t } = useTranslation()
const {
info,
addProfileItem,
@ -75,28 +77,28 @@ const ProfileItem: React.FC<Props> = (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> = (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<void> => {
switch (key) {
@ -282,7 +284,7 @@ const ProfileItem: React.FC<Props> = (props) => {
variant="bordered"
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
>
{t('profiles.remote')}
</Chip>
<small>{dayjs(info.updated).fromNow()}</small>
</div>
@ -296,14 +298,14 @@ const ProfileItem: React.FC<Props> = (props) => {
variant="bordered"
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
>
{t('profiles.local')}
</Chip>
</div>
)}
{extra && (
<Progress
className="w-full"
aria-label="流量使用进度"
aria-label={t('profiles.trafficUsage')}
classNames={{
indicator: isCurrent ? 'bg-primary-foreground' : 'bg-foreground'
}}

View File

@ -2,6 +2,7 @@ import { Button, Card, CardBody } from '@nextui-org/react'
import { mihomoUnfixedProxy } from '@renderer/utils/ipc'
import React, { useMemo, useState } from 'react'
import { FaMapPin } from 'react-icons/fa6'
import { useTranslation } from 'react-i18next'
interface Props {
mutateProxies: () => void
@ -14,6 +15,7 @@ interface Props {
}
const ProxyItem: React.FC<Props> = (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> = (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> = (props) => {
{fixed && (
<Button
isIconOnly
title="取消固定"
title={t('proxies.unpin')}
color="danger"
onPress={async () => {
await mihomoUnfixedProxy(group.name)
@ -124,7 +126,7 @@ const ProxyItem: React.FC<Props> = (props) => {
{fixed && (
<Button
isIconOnly
title="取消固定"
title={t('proxies.unpin')}
color="danger"
onPress={async () => {
await mihomoUnfixedProxy(group.name)

View File

@ -5,8 +5,10 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import { mihomoUpgradeGeo } from '@renderer/utils/ipc'
import { useState } from 'react'
import { IoMdRefresh } from 'react-icons/io'
import { useTranslation } from 'react-i18next'
const GeoData: React.FC = () => {
const { t } = useTranslation()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const {
'geox-url': geoxUrl = {
@ -27,7 +29,7 @@ const GeoData: React.FC = () => {
return (
<SettingCard>
<SettingItem title="GeoIP 数据库" divider>
<SettingItem title={t('resources.geoData.geoip')} divider>
<div className="flex w-[70%]">
{geoipInput !== geoxUrl.geoip && (
<Button
@ -38,13 +40,13 @@ const GeoData: React.FC = () => {
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, geoip: geoipInput } })
}}
>
{t('common.confirm')}
</Button>
)}
<Input size="sm" value={geoipInput} onValueChange={setGeoIpInput} />
</div>
</SettingItem>
<SettingItem title="GeoSite 数据库" divider>
<SettingItem title={t('resources.geoData.geosite')} divider>
<div className="flex w-[70%]">
{geositeInput !== geoxUrl.geosite && (
<Button
@ -55,13 +57,13 @@ const GeoData: React.FC = () => {
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, geosite: geositeInput } })
}}
>
{t('common.confirm')}
</Button>
)}
<Input size="sm" value={geositeInput} onValueChange={setGeositeInput} />
</div>
</SettingItem>
<SettingItem title="MMDB 数据库" divider>
<SettingItem title={t('resources.geoData.mmdb')} divider>
<div className="flex w-[70%]">
{mmdbInput !== geoxUrl.mmdb && (
<Button
@ -72,13 +74,13 @@ const GeoData: React.FC = () => {
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, mmdb: mmdbInput } })
}}
>
{t('common.confirm')}
</Button>
)}
<Input size="sm" value={mmdbInput} onValueChange={setMmdbInput} />
</div>
</SettingItem>
<SettingItem title="ASN 数据库" divider>
<SettingItem title={t('resources.geoData.asn')} divider>
<div className="flex w-[70%]">
{asnInput !== geoxUrl.asn && (
<Button
@ -89,13 +91,13 @@ const GeoData: React.FC = () => {
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, asn: asnInput } })
}}
>
{t('common.confirm')}
</Button>
)}
<Input size="sm" value={asnInput} onValueChange={setAsnInput} />
</div>
</SettingItem>
<SettingItem title="GeoIP 数据模式" divider>
<SettingItem title={t('resources.geoData.mode')} divider>
<Tabs
size="sm"
color="primary"
@ -109,7 +111,7 @@ const GeoData: React.FC = () => {
</Tabs>
</SettingItem>
<SettingItem
title="自动更新 Geo 数据库"
title={t('resources.geoData.autoUpdate')}
actions={
<Button
size="sm"
@ -119,7 +121,7 @@ const GeoData: React.FC = () => {
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 = () => {
/>
</SettingItem>
{geoAutoUpdate && (
<SettingItem title="更新间隔(小时)">
<SettingItem title={t('resources.geoData.updateInterval')}>
<Input
size="sm"
type="number"

View File

@ -12,7 +12,7 @@ import { Button, Chip } from '@nextui-org/react'
import { IoMdRefresh } from 'react-icons/io'
import { CgLoadbarDoc } from 'react-icons/cg'
import { MdEditDocument } from 'react-icons/md'
import dayjs from 'dayjs'
import dayjs from '@renderer/utils/dayjs'
import { calcTraffic } from '@renderer/utils/calc'
import { getHash } from '@renderer/utils/hash'

View File

@ -13,7 +13,7 @@ import { Button, Chip } from '@nextui-org/react'
import { IoMdRefresh } from 'react-icons/io'
import { CgLoadbarDoc } from 'react-icons/cg'
import { MdEditDocument } from 'react-icons/md'
import dayjs from 'dayjs'
import dayjs from '@renderer/utils/dayjs'
const RuleProvider: React.FC = () => {
const [showDetails, setShowDetails] = useState({

View File

@ -12,9 +12,11 @@ import { useState } from 'react'
import UpdaterModal from '../updater/updater-modal'
import { version } from '@renderer/utils/init'
import { IoIosHelpCircle } from 'react-icons/io'
import { firstDriver } from '@renderer/App'
import { getDriver } from '@renderer/App'
import { useTranslation } from 'react-i18next'
const Actions: React.FC = () => {
const { t } = useTranslation()
const [newVersion, setNewVersion] = useState('')
const [changelog, setChangelog] = useState('')
const [openUpdate, setOpenUpdate] = useState(false)
@ -30,12 +32,12 @@ const Actions: React.FC = () => {
/>
)}
<SettingCard>
<SettingItem title="打开引导页面" divider>
<Button size="sm" onPress={() => firstDriver.drive()}>
<SettingItem title={t('actions.guide.title')} divider>
<Button size="sm" onPress={() => getDriver()?.drive()}>
{t('actions.guide.button')}
</Button>
</SettingItem>
<SettingItem title="检查更新" divider>
<SettingItem title={t('actions.update.title')} divider>
<Button
size="sm"
isLoading={checkingUpdate}
@ -48,7 +50,9 @@ const Actions: React.FC = () => {
setChangelog(version.changelog)
setOpenUpdate(true)
} else {
new window.Notification('当前已是最新版本', { body: '无需更新' })
new window.Notification(t('actions.update.upToDate.title'), {
body: t('actions.update.upToDate.body')
})
}
} catch (e) {
alert(e)
@ -57,13 +61,13 @@ const Actions: React.FC = () => {
}
}}
>
{t('actions.update.button')}
</Button>
</SettingItem>
<SettingItem
title="重置软件"
title={t('actions.reset.title')}
actions={
<Tooltip content="删除所有配置,将软件恢复初始状态">
<Tooltip content={t('actions.reset.tooltip')}>
<Button isIconOnly size="sm" variant="light">
<IoIosHelpCircle className="text-lg" />
</Button>
@ -72,13 +76,13 @@ const Actions: React.FC = () => {
divider
>
<Button size="sm" onPress={resetAppConfig}>
{t('actions.reset.button')}
</Button>
</SettingItem>
<SettingItem
title="创建堆快照"
title={t('actions.heapSnapshot.title')}
actions={
<Tooltip content="创建主进程堆快照,用于排查内存问题">
<Tooltip content={t('actions.heapSnapshot.tooltip')}>
<Button isIconOnly size="sm" variant="light">
<IoIosHelpCircle className="text-lg" />
</Button>
@ -87,13 +91,13 @@ const Actions: React.FC = () => {
divider
>
<Button size="sm" onPress={createHeapSnapshot}>
{t('actions.heapSnapshot.button')}
</Button>
</SettingItem>
<SettingItem
title="轻量模式"
title={t('actions.lightMode.title')}
actions={
<Tooltip content="完全退出软件,只保留内核进程">
<Tooltip content={t('actions.lightMode.tooltip')}>
<Button isIconOnly size="sm" variant="light">
<IoIosHelpCircle className="text-lg" />
</Button>
@ -102,15 +106,15 @@ const Actions: React.FC = () => {
divider
>
<Button size="sm" onPress={quitWithoutCore}>
{t('actions.lightMode.button')}
</Button>
</SettingItem>
<SettingItem title="退出应用" divider>
<SettingItem title={t('actions.quit.title')} divider>
<Button size="sm" onPress={quitApp}>
退
{t('actions.quit.button')}
</Button>
</SettingItem>
<SettingItem title="应用版本">
<SettingItem title={t('actions.version.title')}>
<div>v{version}</div>
</SettingItem>
</SettingCard>

View File

@ -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> = (props) => {
const { t } = useTranslation()
const { theme, onCancel, onConfirm } = props
const [currData, setCurrData] = useState('')
@ -30,7 +34,7 @@ const CSSEditorModal: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag"></ModalHeader>
<ModalHeader className="flex pb-0 app-drag">{t('theme.editor.title')}</ModalHeader>
<ModalBody className="h-full">
<BaseEditor
language="css"
@ -40,10 +44,10 @@ const CSSEditorModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter className="pt-0">
<Button size="sm" variant="light" onPress={onCancel}>
{t('common.cancel')}
</Button>
<Button size="sm" color="primary" onPress={() => onConfirm(currData)}>
{t('common.confirm')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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 = () => {
/>
)}
<SettingCard>
<SettingItem title="开机自启" divider>
<SettingItem title={t('settings.language')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[150px]"
size="sm"
selectedKeys={[language]}
aria-label={t('settings.language')}
onSelectionChange={async (v) => {
const newLang = Array.from(v)[0] as 'zh-CN' | 'en-US'
await patchAppConfig({ language: newLang })
i18n.changeLanguage(newLang)
}}
>
<SelectItem key="zh-CN"></SelectItem>
<SelectItem key="en-US">English</SelectItem>
</Select>
</SettingItem>
<SettingItem title={t('settings.autoStart')} divider>
<Switch
size="sm"
isSelected={enable}
@ -94,7 +114,7 @@ const GeneralConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="自动检查更新" divider>
<SettingItem title={t('settings.autoCheckUpdate')} divider>
<Switch
size="sm"
isSelected={autoCheckUpdate}
@ -103,7 +123,7 @@ const GeneralConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="静默启动" divider>
<SettingItem title={t('settings.silentStart')} divider>
<Switch
size="sm"
isSelected={silentStart}
@ -113,9 +133,9 @@ const GeneralConfig: React.FC = () => {
/>
</SettingItem>
<SettingItem
title="自动开启轻量模式"
title={t('settings.autoQuitWithoutCore')}
actions={
<Tooltip content="关闭窗口指定时间后自动进入轻量模式">
<Tooltip content={t('settings.autoQuitWithoutCoreTooltip')}>
<Button isIconOnly size="sm" variant="light">
<IoIosHelpCircle className="text-lg" />
</Button>
@ -132,12 +152,12 @@ const GeneralConfig: React.FC = () => {
/>
</SettingItem>
{autoQuitWithoutCore && (
<SettingItem title="自动开启轻量模式延时" divider>
<SettingItem title={t('settings.autoQuitWithoutCoreDelay')} divider>
<Input
size="sm"
className="w-[100px]"
type="number"
endContent="秒"
endContent={t('common.seconds')}
value={autoQuitWithoutCoreDelay.toString()}
onValueChange={async (v: string) => {
let num = parseInt(v)
@ -149,7 +169,7 @@ const GeneralConfig: React.FC = () => {
</SettingItem>
)}
<SettingItem
title="复制环境变量类型"
title={t('settings.envType')}
actions={envType.map((type) => (
<Button
key={type}
@ -170,7 +190,7 @@ const GeneralConfig: React.FC = () => {
size="sm"
selectionMode="multiple"
selectedKeys={new Set(envType)}
aria-label="选择环境变量类型"
aria-label={t('settings.envType')}
onSelectionChange={async (v) => {
try {
await patchAppConfig({
@ -186,7 +206,7 @@ const GeneralConfig: React.FC = () => {
<SelectItem key="powershell">PowerShell</SelectItem>
</Select>
</SettingItem>
<SettingItem title="显示悬浮窗" divider>
<SettingItem title={t('settings.showFloatingWindow')} divider>
<Switch
size="sm"
isSelected={showFloating}
@ -203,7 +223,7 @@ const GeneralConfig: React.FC = () => {
{showFloating && (
<>
<SettingItem title="根据网速旋转悬浮窗图标" divider>
<SettingItem title={t('settings.spinFloatingIcon')} divider>
<Switch
size="sm"
isSelected={spinFloatingIcon}
@ -213,7 +233,7 @@ const GeneralConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="禁用托盘图标" divider>
<SettingItem title={t('settings.disableTray')} divider>
<Switch
size="sm"
isSelected={disableTray}
@ -231,7 +251,7 @@ const GeneralConfig: React.FC = () => {
)}
{platform !== 'linux' && (
<>
<SettingItem title="托盘菜单显示节点信息" divider>
<SettingItem title={t('settings.proxyInTray')} divider>
<Switch
size="sm"
isSelected={proxyInTray}
@ -241,7 +261,9 @@ const GeneralConfig: React.FC = () => {
/>
</SettingItem>
<SettingItem
title={`${platform === 'win32' ? '任务栏' : '状态栏'}显示网速信息`}
title={t('settings.showTraffic', {
context: platform === 'win32' ? 'windows' : 'mac'
})}
divider
>
<Switch
@ -257,7 +279,7 @@ const GeneralConfig: React.FC = () => {
)}
{platform === 'darwin' && (
<>
<SettingItem title="显示 Dock 图标" divider>
<SettingItem title={t('settings.showDockIcon')} divider>
<Switch
size="sm"
isSelected={useDockIcon}
@ -269,7 +291,7 @@ const GeneralConfig: React.FC = () => {
</>
)}
<SettingItem title="使用系统标题栏" divider>
<SettingItem title={t('settings.useWindowFrame')} divider>
<Switch
size="sm"
isSelected={useWindowFrame}
@ -287,7 +309,7 @@ const GeneralConfig: React.FC = () => {
}, 1000)}
/>
</SettingItem>
<SettingItem title="背景色" divider>
<SettingItem title={t('settings.backgroundColor')} divider>
<Tabs
size="sm"
color="primary"
@ -297,20 +319,20 @@ const GeneralConfig: React.FC = () => {
patchAppConfig({ appTheme: key as AppTheme })
}}
>
<Tab key="system" title="自动" />
<Tab key="dark" title="深色" />
<Tab key="light" title="浅色" />
<Tab key="system" title={t('settings.backgroundAuto')} />
<Tab key="dark" title={t('settings.backgroundDark')} />
<Tab key="light" title={t('settings.backgroundLight')} />
</Tabs>
</SettingItem>
<SettingItem
title="主题"
title={t('settings.theme')}
actions={
<>
<Button
size="sm"
isLoading={fetching}
isIconOnly
title="拉取主题"
title={t('settings.fetchTheme')}
variant="light"
onPress={async () => {
setFetching(true)
@ -329,7 +351,7 @@ const GeneralConfig: React.FC = () => {
<Button
size="sm"
isIconOnly
title="导入主题"
title={t('settings.importTheme')}
variant="light"
onPress={async () => {
const files = await getFilePath(['css'])
@ -347,7 +369,7 @@ const GeneralConfig: React.FC = () => {
<Button
size="sm"
isIconOnly
title="编辑主题"
title={t('settings.editTheme')}
variant="light"
onPress={async () => {
setOpenCSSEditor(true)
@ -364,7 +386,7 @@ const GeneralConfig: React.FC = () => {
className="w-[60%]"
size="sm"
selectedKeys={new Set([customTheme])}
aria-label="选择主题"
aria-label={t('settings.selectTheme')}
onSelectionChange={async (v) => {
try {
await patchAppConfig({ customTheme: v.currentKey as string })

View File

@ -9,8 +9,10 @@ import { MdDeleteForever } from 'react-icons/md'
import { BiCopy } from 'react-icons/bi'
import { IoIosHelpCircle } from 'react-icons/io'
import { platform } from '@renderer/utils/init'
import { useTranslation } from 'react-i18next'
const MihomoConfig: React.FC = () => {
const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig()
const {
diffWorkDir = false,
@ -37,59 +39,59 @@ const MihomoConfig: React.FC = () => {
}, 500)
return (
<SettingCard>
<SettingItem title="订阅拉取 UA" divider>
<SettingItem title={t('mihomo.userAgent')} divider>
<Input
size="sm"
className="w-[60%]"
value={ua}
placeholder="默认 clash.meta"
placeholder={t('mihomo.userAgentPlaceholder')}
onValueChange={(v) => {
setUa(v)
setUaDebounce(v)
}}
></Input>
</SettingItem>
<SettingItem title="延迟测试地址" divider>
<SettingItem title={t('mihomo.delayTest.url')} divider>
<Input
size="sm"
className="w-[60%]"
value={url}
placeholder="默认 https://www.gstatic.com/generate_204"
placeholder={t('mihomo.delayTest.urlPlaceholder')}
onValueChange={(v) => {
setUrl(v)
setUrlDebounce(v)
}}
></Input>
</SettingItem>
<SettingItem title="延迟测试并发数量" divider>
<SettingItem title={t('mihomo.delayTest.concurrency')} divider>
<Input
type="number"
size="sm"
className="w-[60%]"
value={delayTestConcurrency?.toString()}
placeholder="默认 50"
placeholder={t('mihomo.delayTest.concurrencyPlaceholder')}
onValueChange={(v) => {
patchAppConfig({ delayTestConcurrency: parseInt(v) })
}}
/>
</SettingItem>
<SettingItem title="延迟测试超时时间" divider>
<SettingItem title={t('mihomo.delayTest.timeout')} divider>
<Input
type="number"
size="sm"
className="w-[60%]"
value={delayTestTimeout?.toString()}
placeholder="默认 5000"
placeholder={t('mihomo.delayTest.timeoutPlaceholder')}
onValueChange={(v) => {
patchAppConfig({ delayTestTimeout: parseInt(v) })
}}
/>
</SettingItem>
<SettingItem
title="同步运行时配置到 Gist"
title={t('mihomo.gist.title')}
actions={
<Button
title="复制 Gist URL"
title={t('mihomo.gist.copyUrl')}
isIconOnly
size="sm"
variant="light"
@ -114,32 +116,32 @@ const MihomoConfig: React.FC = () => {
size="sm"
className="w-[60%]"
value={githubToken}
placeholder="GitHub Token"
placeholder={t('mihomo.gist.token')}
onValueChange={(v) => {
patchAppConfig({ githubToken: v })
}}
/>
</SettingItem>
<SettingItem title="代理节点展示列数" divider>
<SettingItem title={t('mihomo.proxyColumns.title')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[150px]"
size="sm"
selectedKeys={new Set([proxyCols])}
aria-label="选择代理节点展示列数"
aria-label={t('mihomo.proxyColumns.title')}
onSelectionChange={async (v) => {
await patchAppConfig({ proxyCols: v.currentKey as 'auto' | '1' | '2' | '3' | '4' })
}}
>
<SelectItem key="auto"></SelectItem>
<SelectItem key="1"></SelectItem>
<SelectItem key="2"></SelectItem>
<SelectItem key="3"></SelectItem>
<SelectItem key="4"></SelectItem>
<SelectItem key="auto">{t('mihomo.proxyColumns.auto')}</SelectItem>
<SelectItem key="1">{t('mihomo.proxyColumns.one')}</SelectItem>
<SelectItem key="2">{t('mihomo.proxyColumns.two')}</SelectItem>
<SelectItem key="3">{t('mihomo.proxyColumns.three')}</SelectItem>
<SelectItem key="4">{t('mihomo.proxyColumns.four')}</SelectItem>
</Select>
</SettingItem>
{platform === 'win32' && (
<SettingItem title="内核进程优先级" divider>
<SettingItem title={t('mihomo.cpuPriority.title')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[150px]"
@ -156,19 +158,19 @@ const MihomoConfig: React.FC = () => {
}
}}
>
<SelectItem key="PRIORITY_HIGHEST"></SelectItem>
<SelectItem key="PRIORITY_HIGH"></SelectItem>
<SelectItem key="PRIORITY_ABOVE_NORMAL"></SelectItem>
<SelectItem key="PRIORITY_NORMAL"></SelectItem>
<SelectItem key="PRIORITY_BELOW_NORMAL"></SelectItem>
<SelectItem key="PRIORITY_LOW"></SelectItem>
<SelectItem key="PRIORITY_HIGHEST">{t('mihomo.cpuPriority.realtime')}</SelectItem>
<SelectItem key="PRIORITY_HIGH">{t('mihomo.cpuPriority.high')}</SelectItem>
<SelectItem key="PRIORITY_ABOVE_NORMAL">{t('mihomo.cpuPriority.aboveNormal')}</SelectItem>
<SelectItem key="PRIORITY_NORMAL">{t('mihomo.cpuPriority.normal')}</SelectItem>
<SelectItem key="PRIORITY_BELOW_NORMAL">{t('mihomo.cpuPriority.belowNormal')}</SelectItem>
<SelectItem key="PRIORITY_LOW">{t('mihomo.cpuPriority.low')}</SelectItem>
</Select>
</SettingItem>
)}
<SettingItem
title="为不同订阅分别指定工作目录"
title={t('mihomo.workDir.title')}
actions={
<Tooltip content="开启后可以避免不同订阅中存在相同代理组名时无法分别保存选择的节点">
<Tooltip content={t('mihomo.workDir.tooltip')}>
<Button isIconOnly size="sm" variant="light">
<IoIosHelpCircle className="text-lg" />
</Button>
@ -189,7 +191,7 @@ const MihomoConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="接管 DNS 设置" divider>
<SettingItem title={t('mihomo.controlDns')} divider>
<Switch
size="sm"
isSelected={controlDns}
@ -204,7 +206,7 @@ const MihomoConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="接管域名嗅探设置" divider>
<SettingItem title={t('mihomo.controlSniff')} divider>
<Switch
size="sm"
isSelected={controlSniff}
@ -219,7 +221,7 @@ const MihomoConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="自动断开连接" divider>
<SettingItem title={t('mihomo.autoCloseConnection')} divider>
<Switch
size="sm"
isSelected={autoCloseConnection}
@ -228,7 +230,7 @@ const MihomoConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="在特定的 WiFi SSID 下直连">
<SettingItem title={t('mihomo.pauseSSID.title')}>
{pauseSSIDInput.join('') !== pauseSSID.join('') && (
<Button
size="sm"
@ -237,7 +239,7 @@ const MihomoConfig: React.FC = () => {
patchAppConfig({ pauseSSID: pauseSSIDInput })
}}
>
{t('common.confirm')}
</Button>
)}
</SettingItem>

View File

@ -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 (
<SettingCard title="快捷键设置">
<SettingItem title="打开/关闭窗口" divider>
<SettingCard title={t('shortcuts.title')}>
<SettingItem title={t('shortcuts.toggleWindow')} divider>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={showWindowShortcut}
@ -64,7 +66,7 @@ const ShortcutConfig: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="打开/关闭悬浮窗" divider>
<SettingItem title={t('shortcuts.toggleFloatingWindow')} divider>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={showFloatingWindowShortcut}
@ -73,7 +75,7 @@ const ShortcutConfig: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="打开/关闭系统代理" divider>
<SettingItem title={t('shortcuts.toggleSystemProxy')} divider>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={triggerSysProxyShortcut}
@ -82,7 +84,7 @@ const ShortcutConfig: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="打开/关闭虚拟网卡" divider>
<SettingItem title={t('shortcuts.toggleTun')} divider>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={triggerTunShortcut}
@ -91,7 +93,7 @@ const ShortcutConfig: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="切换规则模式" divider>
<SettingItem title={t('shortcuts.toggleRuleMode')} divider>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={ruleModeShortcut}
@ -100,7 +102,7 @@ const ShortcutConfig: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="切换全局模式" divider>
<SettingItem title={t('shortcuts.toggleGlobalMode')} divider>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={globalModeShortcut}
@ -109,7 +111,7 @@ const ShortcutConfig: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="切换直连模式" divider>
<SettingItem title={t('shortcuts.toggleDirectMode')} divider>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={directModeShortcut}
@ -118,7 +120,7 @@ const ShortcutConfig: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="轻量模式" divider>
<SettingItem title={t('shortcuts.toggleLightMode')} divider>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={quitWithoutCoreShortcut}
@ -127,7 +129,7 @@ const ShortcutConfig: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="重启应用">
<SettingItem title={t('shortcuts.restartApp')}>
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={restartAppShortcut}
@ -145,6 +147,7 @@ const ShortcutInput: React.FC<{
action: string
patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
}> = (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')}
</Button>
)}
<Input
placeholder="点击输入快捷键"
placeholder={t('shortcuts.input.placeholder')}
onKeyDown={(e: KeyboardEvent): void => {
parseShortcut(e, setInputValue)
}}

View File

@ -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<string, string> = {
sysproxyCardStatus: 'sider.cards.systemProxy',
tunCardStatus: 'sider.cards.tun',
profileCardStatus: 'sider.cards.profiles',
proxyCardStatus: 'sider.cards.proxies',
ruleCardStatus: 'sider.cards.rules',
resourceCardStatus: 'sider.cards.resources',
overrideCardStatus: 'sider.cards.override',
connectionCardStatus: 'sider.cards.connections',
mihomoCoreCardStatus: 'sider.cards.core',
dnsCardStatus: 'sider.cards.dns',
sniffCardStatus: 'sider.cards.sniff',
logCardStatus: 'sider.cards.logs',
substoreCardStatus: 'sider.cards.substore'
}
const SiderConfig: React.FC = () => {
const sizeMap: Record<string, string> = {
'col-span-2': 'sider.size.large',
'col-span-1': 'sider.size.small',
hidden: 'sider.size.hidden'
}
const SiderConfig: FC = () => {
const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig()
const {
sysproxyCardStatus = 'col-span-1',
tunCardStatus = 'col-span-1',
profileCardStatus = 'col-span-2',
proxyCardStatus = 'col-span-1',
ruleCardStatus = 'col-span-1',
resourceCardStatus = 'col-span-1',
overrideCardStatus = 'col-span-1',
connectionCardStatus = 'col-span-2',
mihomoCoreCardStatus = 'col-span-2',
dnsCardStatus = 'col-span-1',
sniffCardStatus = 'col-span-1',
logCardStatus = 'col-span-1',
substoreCardStatus = 'col-span-1'
} = appConfig || {}
const cardStatus = {
sysproxyCardStatus,
tunCardStatus,
profileCardStatus,
proxyCardStatus,
ruleCardStatus,
resourceCardStatus,
overrideCardStatus,
connectionCardStatus,
mihomoCoreCardStatus,
dnsCardStatus,
sniffCardStatus,
logCardStatus,
substoreCardStatus
sysproxyCardStatus: appConfig?.sysproxyCardStatus || 'col-span-1',
tunCardStatus: appConfig?.tunCardStatus || 'col-span-1',
profileCardStatus: appConfig?.profileCardStatus || 'col-span-2',
proxyCardStatus: appConfig?.proxyCardStatus || 'col-span-1',
ruleCardStatus: appConfig?.ruleCardStatus || 'col-span-1',
resourceCardStatus: appConfig?.resourceCardStatus || 'col-span-1',
overrideCardStatus: appConfig?.overrideCardStatus || 'col-span-1',
connectionCardStatus: appConfig?.connectionCardStatus || 'col-span-2',
mihomoCoreCardStatus: appConfig?.mihomoCoreCardStatus || 'col-span-2',
dnsCardStatus: appConfig?.dnsCardStatus || 'col-span-1',
sniffCardStatus: appConfig?.sniffCardStatus || 'col-span-1',
logCardStatus: appConfig?.logCardStatus || 'col-span-1',
substoreCardStatus: appConfig?.substoreCardStatus || 'col-span-1'
}
return (
<SettingCard title="侧边栏设置">
{Object.keys(cardStatus).map((key, index, array) => {
return (
<SettingItem title={titleMap[key]} key={key} divider={index !== array.length - 1}>
<RadioGroup
orientation="horizontal"
value={cardStatus[key]}
onValueChange={(v) => {
patchAppConfig({ [key]: v as CardStatus })
}}
>
<Radio value="col-span-2"></Radio>
<Radio value="col-span-1"></Radio>
<Radio value="hidden"></Radio>
</RadioGroup>
</SettingItem>
)
})}
<SettingCard title={t('sider.title')}>
{Object.entries(cardStatus).map(([key, value]) => (
<SettingItem key={key} title={t(titleMap[key])}>
<RadioGroup
orientation="horizontal"
value={value}
onValueChange={(v: string) => {
if (v === 'col-span-1' || v === 'col-span-2' || v === 'hidden') {
patchAppConfig({ [key]: v })
}
}}
>
{Object.entries(sizeMap).map(([size, label]) => (
<Radio key={size} value={size}>
{t(label)}
</Radio>
))}
</RadioGroup>
</SettingItem>
))}
</SettingCard>
)
}

View File

@ -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 (
<SettingCard title="Sub-Store 设置">
<SettingItem title="启用 Sub-Store" divider={useSubStore}>
<SettingCard title={t('substore.title')}>
<SettingItem title={t('substore.enable')} divider={useSubStore}>
<Switch
size="sm"
isSelected={useSubStore}
@ -60,7 +62,7 @@ const SubStoreConfig: React.FC = () => {
</SettingItem>
{useSubStore && (
<>
<SettingItem title="允许局域网连接" divider>
<SettingItem title={t('substore.allowLan')} divider>
<Switch
size="sm"
isSelected={subStoreHost === '0.0.0.0'}
@ -79,7 +81,7 @@ const SubStoreConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="使用自建 Sub-Store 后端" divider>
<SettingItem title={t('substore.useCustomBackend')} divider>
<Switch
size="sm"
isSelected={useCustomSubStore}
@ -98,12 +100,12 @@ const SubStoreConfig: React.FC = () => {
/>
</SettingItem>
{useCustomSubStore ? (
<SettingItem title="自建 Sub-Store 后端地址">
<SettingItem title={t('substore.customBackendUrl.title')}>
<Input
size="sm"
className="w-[60%]"
value={customSubStoreUrlValue}
placeholder="必须包含协议头"
placeholder={t('substore.customBackendUrl.placeholder')}
onValueChange={(v: string) => {
setCustomSubStoreUrlValue(v)
setCustomSubStoreUrl(v)
@ -112,7 +114,7 @@ const SubStoreConfig: React.FC = () => {
</SettingItem>
) : (
<>
<SettingItem title="为 Sub-Store 内所有请求启用代理" divider>
<SettingItem title={t('substore.useProxy')} divider>
<Switch
size="sm"
isSelected={useProxyInSubStore}
@ -126,7 +128,7 @@ const SubStoreConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="定时同步订阅/文件" divider>
<SettingItem title={t('substore.sync.title')} divider>
<div className="flex w-[60%] gap-2">
{subStoreBackendSyncCronValue !== subStoreBackendSyncCron && (
<Button
@ -140,26 +142,26 @@ const SubStoreConfig: React.FC = () => {
await patchAppConfig({
subStoreBackendSyncCron: subStoreBackendSyncCronValue
})
new Notification('重启应用生效')
new Notification(t('common.notification.restartRequired'))
} else {
alert('Cron 表达式无效')
alert(t('common.error.invalidCron'))
}
}}
>
{t('common.confirm')}
</Button>
)}
<Input
size="sm"
value={subStoreBackendSyncCronValue}
placeholder="Cron 表达式"
placeholder={t('substore.sync.placeholder')}
onValueChange={(v: string) => {
setSubStoreBackendSyncCronValue(v)
}}
/>
</div>
</SettingItem>
<SettingItem title="定时恢复配置" divider>
<SettingItem title={t('substore.restore.title')} divider>
<div className="flex w-[60%] gap-2">
{subStoreBackendDownloadCronValue !== subStoreBackendDownloadCron && (
<Button
@ -173,26 +175,26 @@ const SubStoreConfig: React.FC = () => {
await patchAppConfig({
subStoreBackendDownloadCron: subStoreBackendDownloadCronValue
})
new Notification('重启应用生效')
new Notification(t('common.notification.restartRequired'))
} else {
alert('Cron 表达式无效')
alert(t('common.error.invalidCron'))
}
}}
>
{t('common.confirm')}
</Button>
)}
<Input
size="sm"
value={subStoreBackendDownloadCronValue}
placeholder="Cron 表达式"
placeholder={t('substore.restore.placeholder')}
onValueChange={(v: string) => {
setSubStoreBackendDownloadCronValue(v)
}}
/>
</div>
</SettingItem>
<SettingItem title="定时备份配置">
<SettingItem title={t('substore.backup.title')}>
<div className="flex w-[60%] gap-2">
{subStoreBackendUploadCronValue !== subStoreBackendUploadCron && (
<Button
@ -206,19 +208,19 @@ const SubStoreConfig: React.FC = () => {
await patchAppConfig({
subStoreBackendUploadCron: subStoreBackendUploadCronValue
})
new Notification('重启应用生效')
new Notification(t('common.notification.restartRequired'))
} else {
alert('Cron 表达式无效')
alert(t('common.error.invalidCron'))
}
}}
>
{t('common.confirm')}
</Button>
)}
<Input
size="sm"
value={subStoreBackendUploadCronValue}
placeholder="Cron 表达式"
placeholder={t('substore.backup.placeholder')}
onValueChange={(v: string) => {
setSubStoreBackendUploadCronValue(v)
}}

View File

@ -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 && (
<WebdavRestoreModal filenames={filenames} onClose={() => setRestoreOpen(false)} />
)}
<SettingCard title="WebDAV 备份">
<SettingItem title="WebDAV 地址" divider>
<SettingCard title={t('webdav.title')}>
<SettingItem title={t('webdav.url')} divider>
<Input
size="sm"
className="w-[60%]"
@ -60,7 +64,7 @@ const WebdavConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="WebDAV 备份目录" divider>
<SettingItem title={t('webdav.dir')} divider>
<Input
size="sm"
className="w-[60%]"
@ -71,7 +75,7 @@ const WebdavConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="WebDAV 用户名" divider>
<SettingItem title={t('webdav.username')} divider>
<Input
size="sm"
className="w-[60%]"
@ -82,7 +86,7 @@ const WebdavConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="WebDAV 密码" divider>
<SettingItem title={t('webdav.password')} divider>
<Input
size="sm"
className="w-[60%]"
@ -96,7 +100,7 @@ const WebdavConfig: React.FC = () => {
</SettingItem>
<div className="flex justify0between">
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
{t('webdav.backup')}
</Button>
<Button
isLoading={restoring}
@ -105,7 +109,7 @@ const WebdavConfig: React.FC = () => {
className="ml-1"
onPress={handleRestore}
>
{t('webdav.restore')}
</Button>
</div>
</SettingCard>

View File

@ -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> = (props) => {
const { t } = useTranslation()
const { filenames: names, onClose } = props
const [filenames, setFilenames] = useState<string[]>(names)
const [restoring, setRestoring] = useState(false)
@ -21,10 +25,10 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent>
<ModalHeader className="flex app-drag"></ModalHeader>
<ModalHeader className="flex app-drag">{t('webdav.restore.title')}</ModalHeader>
<ModalBody>
{filenames.length === 0 ? (
<div className="flex justify-center"></div>
<div className="flex justify-center">{t('webdav.restore.noBackups')}</div>
) : (
filenames.map((filename) => (
<div className="flex" key={filename}>
@ -39,7 +43,7 @@ const WebdavRestoreModal: React.FC<Props> = (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> = (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> = (props) => {
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onClose}>
{t('common.close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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> = (props) => {
const { t } = useTranslation()
const { onClose } = props
const [currData, setCurrData] = useState('')
@ -28,13 +31,13 @@ const ConfigViewer: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag"></ModalHeader>
<ModalHeader className="flex pb-0 app-drag">{t('sider.cards.config')}</ModalHeader>
<ModalBody className="h-full">
<BaseEditor language="yaml" value={currData} readOnly={true} />
</ModalBody>
<ModalFooter className="pt-0">
<Button size="sm" variant="light" onPress={onClose}>
{t('common.close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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> = (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> = (props) => {
if (iconOnly) {
return (
<div className={`${connectionCardStatus} flex justify-center`}>
<Tooltip content="连接" placement="right">
<Tooltip content={t('sider.cards.connections')} placement="right">
<Button
size="sm"
isIconOnly
@ -162,7 +164,7 @@ const ConnCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.connections')}
</h3>
</CardFooter>
</Card>
@ -218,7 +220,7 @@ const ConnCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.connections')}
</h3>
</CardFooter>
</Card>

View File

@ -8,11 +8,13 @@ import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import React from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const DNSCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {}
@ -41,7 +43,7 @@ const DNSCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${dnsCardStatus} ${!controlDns ? 'hidden' : ''} flex justify-center`}>
<Tooltip content="DNS" placement="right">
<Tooltip content={t('sider.cards.dns')} placement="right">
<Button
size="sm"
isIconOnly
@ -99,7 +101,7 @@ const DNSCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
DNS
{t('sider.cards.dns')}
</h3>
</CardFooter>
</Card>

View File

@ -5,12 +5,14 @@ import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import React from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const LogCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { logCardStatus = 'col-span-1' } = appConfig || {}
@ -32,7 +34,7 @@ const LogCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${logCardStatus} flex justify-center`}>
<Tooltip content="日志" placement="right">
<Tooltip content={t('sider.cards.logs')} placement="right">
<Button
size="sm"
isIconOnly
@ -84,7 +86,7 @@ const LogCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.logs')}
</h3>
</CardFooter>
</Card>

View File

@ -10,6 +10,7 @@ import PubSub from 'pubsub-js'
import useSWR from 'swr'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { LuCpu } from 'react-icons/lu'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
@ -35,6 +36,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
})
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const [mem, setMem] = useState(0)
const { t } = useTranslation()
useEffect(() => {
const token = PubSub.subscribe('mihomo-core-changed', () => {
@ -52,7 +54,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${mihomoCoreCardStatus} flex justify-center`}>
<Tooltip content="内核设置" placement="right">
<Tooltip content={t('sider.cards.core')} placement="right">
<Button
size="sm"
isIconOnly
@ -125,7 +127,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
<div
className={`flex justify-between w-full text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
<h4></h4>
<h4>{t('sider.cards.core')}</h4>
<h4>{calcTraffic(mem)}</h4>
</div>
</CardFooter>
@ -157,7 +159,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.core')}
</h3>
</CardFooter>
</Card>

View File

@ -4,8 +4,10 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import { useGroups } from '@renderer/hooks/use-groups'
import { mihomoCloseAllConnections, patchMihomoConfig } from '@renderer/utils/ipc'
import { Key } from 'react'
import { useTranslation } from 'react-i18next'
const OutboundModeSwitcher: React.FC = () => {
const { t } = useTranslation()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { mutate: mutateGroups } = useGroups()
const { appConfig } = useAppConfig()
@ -32,9 +34,9 @@ const OutboundModeSwitcher: React.FC = () => {
}}
onSelectionChange={(key: Key) => onChangeMode(key as OutboundMode)}
>
<Tab className={`${mode === 'rule' ? 'font-bold' : ''}`} key="rule" title="规则" />
<Tab className={`${mode === 'global' ? 'font-bold' : ''}`} key="global" title="全局" />
<Tab className={`${mode === 'direct' ? 'font-bold' : ''}`} key="direct" title="直连" />
<Tab className={`${mode === 'rule' ? 'font-bold' : ''}`} key="rule" title={t('sider.cards.outbound.rule')} />
<Tab className={`${mode === 'global' ? 'font-bold' : ''}`} key="global" title={t('sider.cards.outbound.global')} />
<Tab className={`${mode === 'direct' ? 'font-bold' : ''}`} key="direct" title={t('sider.cards.outbound.direct')} />
</Tabs>
)
}

View File

@ -5,12 +5,14 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const OverrideCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { overrideCardStatus = 'col-span-1' } = appConfig || {}
@ -31,7 +33,7 @@ const OverrideCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${overrideCardStatus} flex justify-center`}>
<Tooltip content="覆写" placement="right">
<Tooltip content={t('sider.cards.override')} placement="right">
<Button
size="sm"
isIconOnly
@ -83,7 +85,7 @@ const OverrideCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.override')}
</h3>
</CardFooter>
</Card>

View File

@ -8,11 +8,12 @@ import relativeTime from 'dayjs/plugin/relativeTime'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import 'dayjs/locale/zh-cn'
import dayjs from 'dayjs'
import dayjs from '@renderer/utils/dayjs'
import React, { useState } from 'react'
import ConfigViewer from './config-viewer'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { TiFolder } from 'react-icons/ti'
import { useTranslation } from 'react-i18next'
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')
@ -22,6 +23,7 @@ interface Props {
}
const ProfileCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig()
const { iconOnly } = props
const { profileCardStatus = 'col-span-2', profileDisplayDate = 'expire' } = appConfig || {}
@ -46,7 +48,7 @@ const ProfileCard: React.FC<Props> = (props) => {
const info = items?.find((item) => item.id === current) ?? {
id: 'default',
type: 'local',
name: '空白订阅'
name: t('sider.cards.emptyProfile')
}
const extra = info?.extra
@ -56,7 +58,7 @@ const ProfileCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${profileCardStatus} flex justify-center`}>
<Tooltip content="订阅管理" placement="right">
<Tooltip content={t('sider.cards.profiles')} placement="right">
<Button
size="sm"
isIconOnly
@ -109,7 +111,7 @@ const ProfileCard: React.FC<Props> = (props) => {
<Button
isIconOnly
size="sm"
title="查看当前运行时配置"
title={t('sider.cards.viewRuntimeConfig')}
variant="light"
color="default"
onPress={() => {
@ -156,7 +158,7 @@ const ProfileCard: React.FC<Props> = (props) => {
await patchAppConfig({ profileDisplayDate: 'update' })
}}
>
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : '长期有效'}
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : t('sider.cards.neverExpire')}
</Button>
) : (
<Button
@ -183,7 +185,7 @@ const ProfileCard: React.FC<Props> = (props) => {
variant="bordered"
className={`${match ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
>
{t('sider.cards.remote')}
</Chip>
<small>{dayjs(info.updated).fromNow()}</small>
</div>
@ -197,14 +199,14 @@ const ProfileCard: React.FC<Props> = (props) => {
variant="bordered"
className={`${match ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
>
{t('sider.cards.local')}
</Chip>
</div>
)}
{extra && (
<Progress
className="w-full"
aria-label="流量使用进度"
aria-label={t('sider.cards.trafficUsage')}
classNames={{ indicator: match ? 'bg-primary-foreground' : 'bg-foreground' }}
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
/>
@ -238,7 +240,7 @@ const ProfileCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.profiles')}
</h3>
</CardFooter>
</Card>

View File

@ -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> = (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> = (props) => {
if (iconOnly) {
return (
<div className={`${proxyCardStatus} flex justify-center`}>
<Tooltip content="代理组" placement="right">
<Tooltip content={t('proxies.card.title')} placement="right">
<Button
size="sm"
isIconOnly
@ -103,7 +105,7 @@ const ProxyCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('proxies.card.title')}
</h3>
</CardFooter>
</Card>

View File

@ -5,12 +5,14 @@ import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { IoLayersOutline } from 'react-icons/io5'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const ResourceCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { resourceCardStatus = 'col-span-1' } = appConfig || {}
@ -32,7 +34,7 @@ const ResourceCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${resourceCardStatus} flex justify-center`}>
<Tooltip content="外部资源" placement="right">
<Tooltip content={t('sider.cards.resources')} placement="right">
<Button
size="sm"
isIconOnly
@ -84,7 +86,7 @@ const ResourceCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.resources')}
</h3>
</CardFooter>
</Card>

View File

@ -6,12 +6,14 @@ import { CSS } from '@dnd-kit/utilities'
import { useRules } from '@renderer/hooks/use-rules'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import React from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const RuleCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { ruleCardStatus = 'col-span-1' } = appConfig || {}
@ -34,7 +36,7 @@ const RuleCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${ruleCardStatus} flex justify-center`}>
<Tooltip content="规则" placement="right">
<Tooltip content={t('sider.cards.rules')} placement="right">
<Button
size="sm"
isIconOnly
@ -104,7 +106,7 @@ const RuleCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.rules')}
</h3>
</CardFooter>
</Card>

View File

@ -8,11 +8,13 @@ import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import React from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const SniffCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { sniffCardStatus = 'col-span-1', controlSniff = true } = appConfig || {}
@ -41,7 +43,7 @@ const SniffCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${sniffCardStatus} ${!controlSniff ? 'hidden' : ''} flex justify-center`}>
<Tooltip content="域名嗅探" placement="right">
<Tooltip content={t('sider.cards.sniff')} placement="right">
<Button
size="sm"
isIconOnly
@ -99,7 +101,7 @@ const SniffCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.sniff')}
</h3>
</CardFooter>
</Card>

View File

@ -5,12 +5,14 @@ import { CSS } from '@dnd-kit/utilities'
import SubStoreIcon from '../base/substore-icon'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import React from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const SubStoreCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { substoreCardStatus = 'col-span-1', useSubStore = true } = appConfig || {}
@ -32,7 +34,7 @@ const SubStoreCard: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${substoreCardStatus} ${!useSubStore ? 'hidden' : ''} flex justify-center`}>
<Tooltip content="Sub-Store" placement="right">
<Tooltip content={t('sider.cards.substore')} placement="right">
<Button
size="sm"
isIconOnly
@ -84,7 +86,7 @@ const SubStoreCard: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
Sub-Store
{t('sider.cards.substore')}
</h3>
</CardFooter>
</Card>

View File

@ -7,12 +7,14 @@ import { AiOutlineGlobal } from 'react-icons/ai'
import React from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const SysproxySwitcher: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { iconOnly } = props
const location = useLocation()
const navigate = useNavigate()
@ -45,7 +47,7 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${sysproxyCardStatus} flex justify-center`}>
<Tooltip content="系统代理" placement="right">
<Tooltip content={t('sider.cards.systemProxy')} placement="right">
<Button
size="sm"
isIconOnly
@ -102,7 +104,7 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.systemProxy')}
</h3>
</CardFooter>
</Card>

View File

@ -8,12 +8,14 @@ import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import React from 'react'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { useTranslation } from 'react-i18next'
interface Props {
iconOnly?: boolean
}
const TunSwitcher: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { iconOnly } = props
const location = useLocation()
const navigate = useNavigate()
@ -48,7 +50,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
if (iconOnly) {
return (
<div className={`${tunCardStatus} flex justify-center`}>
<Tooltip content="虚拟网卡" placement="right">
<Tooltip content={t('sider.cards.tun')} placement="right">
<Button
size="sm"
isIconOnly
@ -105,7 +107,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
<h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
>
{t('sider.cards.tun')}
</h3>
</CardFooter>
</Card>

View File

@ -1,14 +1,18 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
import { BaseEditor } from '@renderer/components/base/base-editor'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
script: string
onCancel: () => void
onConfirm: (script: string) => void
}
const PacEditorModal: React.FC<Props> = (props) => {
const { script, onCancel, onConfirm } = props
const [currData, setCurrData] = useState(script)
const { t } = useTranslation()
return (
<Modal
@ -21,7 +25,7 @@ const PacEditorModal: React.FC<Props> = (props) => {
scrollBehavior="inside"
>
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag">PAC脚本</ModalHeader>
<ModalHeader className="flex pb-0 app-drag">{t('sysproxy.pacEditor.title')}</ModalHeader>
<ModalBody className="h-full">
<BaseEditor
language="javascript"
@ -31,10 +35,10 @@ const PacEditorModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter className="pt-0">
<Button size="sm" variant="light" onPress={onCancel}>
{t('common.cancel')}
</Button>
<Button size="sm" color="primary" onPress={() => onConfirm(currData)}>
{t('common.confirm')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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> = (props) => {
const { version, changelog, onClose } = props
const [downloading, setDownloading] = useState(false)
const { t } = useTranslation()
const onUpdate = async (): Promise<void> => {
try {
await downloadAndInstallUpdate(version)
@ -38,7 +42,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
>
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex justify-between app-drag">
<div>v{version} </div>
<div>{t('common.updater.versionReady', { version })}</div>
<Button
color="primary"
size="sm"
@ -47,7 +51,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
open(`https://github.com/mihomo-party-org/mihomo-party/releases/tag/v${version}`)
}}
>
{t('common.updater.goToDownload')}
</Button>
</ModalHeader>
<ModalBody className="h-full">
@ -65,7 +69,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onClose}>
{t('common.cancel')}
</Button>
<Button
size="sm"
@ -83,7 +87,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
}
}}
>
{t('common.updater.update')}
</Button>
</ModalFooter>
</ModalContent>

18
src/renderer/src/i18n.ts Normal file
View File

@ -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

View File

@ -0,0 +1,723 @@
{
"common": {
"settings": "Settings",
"profiles": "Profiles",
"proxies": "Proxies",
"connections": "Connections",
"dns": "DNS",
"tun": "TUN",
"save": "Save",
"cancel": "Cancel",
"edit": "Edit",
"delete": "Delete",
"seconds": "seconds",
"confirm": "Confirm",
"auto": "Auto",
"default": "Default",
"close": "Close",
"pinWindow": "Pin Window",
"enterRootPassword": "Please enter root password",
"next": "Next",
"prev": "Previous",
"done": "Done",
"notification": {
"restartRequired": "Restart required for changes to take effect"
},
"error": {
"appCrash": "Application crashed :( Please submit the following information to the developer to troubleshoot",
"copyErrorMessage": "Copy Error Message",
"invalidCron": "Invalid Cron expression",
"getBackupListFailed": "Failed to get backup list: {{error}}",
"restoreFailed": "Failed to restore: {{error}}",
"deleteFailed": "Failed to delete: {{error}}",
"shortcutRegistrationFailed": "Failed to register shortcut",
"shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}"
},
"updater": {
"versionReady": "v{{version}} Version Ready",
"goToDownload": "Go to Download",
"update": "Update"
}
},
"settings": {
"general": "General Settings",
"mihomo": "Mihomo Settings",
"language": "Language",
"theme": "Theme",
"darkMode": "Dark Mode",
"lightMode": "Light Mode",
"autoStart": "Auto Start",
"autoCheckUpdate": "Auto Check Update",
"silentStart": "Silent Start",
"autoQuitWithoutCore": "Auto Enable Light Mode",
"autoQuitWithoutCoreTooltip": "Automatically enter light mode after closing window for specified time",
"autoQuitWithoutCoreDelay": "Light Mode Auto Enable Delay",
"envType": "Environment Variable Type",
"showFloatingWindow": "Show Floating Window",
"spinFloatingIcon": "Spin Floating Icon Based on Network Speed",
"disableTray": "Disable Tray Icon",
"proxyInTray": "Show Proxy Info in Tray Menu",
"showTraffic_windows": "Show Network Speed in Taskbar",
"showTraffic_mac": "Show Network Speed in Status Bar",
"showDockIcon": "Show Dock Icon",
"useWindowFrame": "Use System Title Bar",
"backgroundColor": "Background Color",
"backgroundAuto": "Auto",
"backgroundDark": "Dark",
"backgroundLight": "Light",
"fetchTheme": "Fetch Theme",
"importTheme": "Import Theme",
"editTheme": "Edit Theme",
"selectTheme": "Select Theme",
"links": {
"docs": "Documentation",
"github": "GitHub Repository",
"telegram": "Telegram Group"
},
"title": "Application Settings"
},
"mihomo": {
"userAgent": "Subscription User Agent",
"userAgentPlaceholder": "Default: clash.meta",
"delayTest": {
"url": "Delay Test URL",
"urlPlaceholder": "Default: https://www.gstatic.com/generate_204",
"concurrency": "Delay Test Concurrency",
"concurrencyPlaceholder": "Default: 50",
"timeout": "Delay Test Timeout",
"timeoutPlaceholder": "Default: 5000"
},
"gist": {
"title": "Sync Runtime Config to Gist",
"copyUrl": "Copy Gist URL",
"token": "GitHub Token"
},
"proxyColumns": {
"title": "Proxy Display Columns",
"auto": "Auto",
"one": "One Column",
"two": "Two Columns",
"three": "Three Columns",
"four": "Four Columns"
},
"cpuPriority": {
"title": "Core Process Priority",
"realtime": "Realtime",
"high": "High",
"aboveNormal": "Above Normal",
"normal": "Normal",
"belowNormal": "Below Normal",
"low": "Low"
},
"workDir": {
"title": "Separate Work Directory for Different Subscriptions",
"tooltip": "Enable to avoid conflicts when different subscriptions have proxy groups with the same name"
},
"controlDns": "Control DNS Settings",
"controlSniff": "Control Domain Sniffing",
"autoCloseConnection": "Auto Close Connection",
"pauseSSID": {
"title": "Direct Connection for Specific WiFi SSIDs",
"placeholder": "Enter SSID"
},
"title": "Core Settings",
"restart": "Restart Core",
"memory": "Memory Usage",
"coreVersion": "Core Version",
"upgradeCore": "Upgrade Core",
"CoreAuthLost": "Core Authorization Lost",
"coreUpgradeSuccess": "Core upgrade successful. If you want to use virtual network card (Tun), please go to the virtual network card page to manually authorize the Core again",
"alreadyLatestVersion": "Already Latest Version",
"selectCoreVersion": "Select Core Version",
"stableVersion": "Stable Version",
"alphaVersion": "Alpha Version",
"mixedPort": "Mixed Port",
"confirm": "Confirm",
"socksPort": "Socks Port",
"httpPort": "Http Port",
"redirPort": "Redir Port",
"externalController": "External Controller Address",
"externalControllerSecret": "External Controller Secret",
"ipv6": "IPv6",
"allowLanConnection": "Allow LAN Connection",
"allowedIpSegments": "Allowed IP Segments",
"disallowedIpSegments": "Disallowed IP Segments",
"userVerification": "User Verification",
"skipAuthPrefixes": "Skip Auth IP Segments",
"useRttDelayTest": "Use RTT Delay Test",
"tcpConcurrent": "TCP Concurrent",
"storeSelectedNode": "Store Selected Node",
"storeFakeIp": "Store Fake IP",
"logRetentionDays": "Log Retention Days",
"logLevel": "Log Level",
"selectLogLevel": "Select Log Level",
"silent": "Silent",
"error": "Error",
"warning": "Warning",
"info": "Info",
"debug": "Debug",
"findProcess": "Find Process",
"selectFindProcessMode": "Select Process Find Mode",
"strict": "Auto",
"off": "Off",
"always": "Always",
"username": {
"placeholder": "Username"
},
"password": {
"placeholder": "Password"
},
"ipSegment": {
"placeholder": "IP Segment"
},
"interface": {
"title": "Network Information"
}
},
"substore": {
"title": "Sub-Store",
"openInBrowser": "Open in Browser",
"enable": "Enable Sub-Store",
"allowLan": "Allow LAN Access",
"useCustomBackend": "Use Custom Sub-Store Backend",
"customBackendUrl": {
"title": "Custom Sub-Store Backend URL",
"placeholder": "Must include protocol"
},
"useProxy": "Enable Proxy for All Sub-Store Requests",
"sync": {
"title": "Schedule Subscription/File Sync",
"placeholder": "Cron expression"
},
"restore": {
"title": "Schedule Config Restore",
"placeholder": "Cron expression"
},
"backup": {
"title": "Schedule Config Backup",
"placeholder": "Cron expression"
}
},
"webdav": {
"title": "WebDAV Backup",
"url": "WebDAV URL",
"dir": "WebDAV Backup Directory",
"username": "WebDAV Username",
"password": "WebDAV Password",
"backup": "Backup",
"restore": {
"title": "Restore Backup",
"noBackups": "No backups available"
},
"notification": {
"backupSuccess": {
"title": "Backup Successful",
"body": "Backup file has been uploaded to WebDAV"
}
}
},
"shortcuts": {
"title": "Keyboard Shortcuts",
"toggleWindow": "Toggle Window",
"toggleFloatingWindow": "Toggle Floating Window",
"toggleSystemProxy": "Toggle System Proxy",
"toggleTun": "Toggle TUN",
"toggleRuleMode": "Toggle Rule Mode",
"toggleGlobalMode": "Toggle Global Mode",
"toggleDirectMode": "Toggle Direct Mode",
"toggleLightMode": "Toggle Light Mode",
"restartApp": "Restart App",
"input": {
"placeholder": "Click to input shortcut"
}
},
"sider": {
"title": "Sidebar Settings",
"cards": {
"systemProxy": "Sys Proxy",
"tun": "TUN",
"profiles": "Profiles",
"proxies": "Proxy Groups",
"rules": "Rules",
"resources": "Ext. Res.",
"override": "Override",
"connections": "Connections",
"core": "Core Settings",
"dns": "DNS",
"sniff": "Sniffing",
"logs": "Logs",
"substore": "Sub-Store",
"config": "Runtime Config",
"emptyProfile": "Empty Profile",
"viewRuntimeConfig": "View Runtime Config",
"remote": "Remote",
"local": "Local",
"trafficUsage": "Traffic Usage Progress",
"neverExpire": "Never Expire",
"outbound": {
"title": "Outbound Mode",
"rule": "Rule",
"global": "Global",
"direct": "Direct"
}
},
"size": {
"large": "Large",
"small": "Small",
"hidden": "Hidden"
}
},
"actions": {
"guide": {
"title": "Open Guide",
"button": "Open Guide"
},
"update": {
"title": "Check for Updates",
"button": "Check for Updates",
"upToDate": {
"title": "Up to Date",
"body": "No updates available"
}
},
"reset": {
"title": "Reset App",
"button": "Reset App",
"tooltip": "Delete all configurations and restore the app to its initial state"
},
"heapSnapshot": {
"title": "Create Heap Snapshot",
"button": "Create Heap Snapshot",
"tooltip": "Create a heap snapshot of the main process for memory issue debugging"
},
"lightMode": {
"title": "Light Mode",
"button": "Light Mode",
"tooltip": "Completely exit the app while keeping only the core process"
},
"restartApp": "Restart App",
"quit": {
"title": "Quit App",
"button": "Quit App"
},
"version": {
"title": "App Version"
}
},
"theme": {
"editor": {
"title": "Edit Theme"
}
},
"proxies": {
"card": {
"title": "ProxyGrp"
},
"delay": {
"test": "Test",
"timeout": "Timeout"
},
"unpin": "Unpin",
"order": {
"default": "Default",
"delay": "Delay",
"name": "Name"
},
"mode": {
"full": "Detailed Info",
"simple": "Simple Info",
"direct": "Direct Mode"
},
"search": {
"placeholder": "Search Proxies"
},
"locate": "Locate Current Proxy"
},
"sniffer": {
"title": "Domain Sniffing Settings",
"parsePureIP": "Sniff Unmapped IP Addresses",
"forceDNSMapping": "Sniff Real IP Mappings",
"overrideDestination": "Override Connection Address",
"sniff": {
"title": "HTTP Port Sniffing",
"tls": "TLS Port Sniffing",
"quic": "QUIC Port Sniffing",
"ports": {
"placeholder": "Port numbers, separated by commas"
}
},
"skipDomain": {
"title": "Skip Domain Sniffing",
"placeholder": "Example: +.push.apple.com"
},
"forceDomain": {
"title": "Force Domain Sniffing",
"placeholder": "Example: v2ex.com"
},
"skipDstAddress": {
"title": "Skip Destination Address Sniffing",
"placeholder": "Example: 1.1.1.1/32"
},
"skipSrcAddress": {
"title": "Skip Source Address Sniffing",
"placeholder": "Example: 192.168.1.1/24"
}
},
"sysproxy": {
"title": "System Proxy",
"host": {
"title": "Proxy Host",
"placeholder": "Default 127.0.0.1, do not modify unless necessary"
},
"mode": {
"title": "Proxy Mode",
"manual": "Manual",
"pac": "PAC"
},
"uwp": {
"title": "UWP Tool",
"open": "Open UWP Tool"
},
"pac": {
"edit": "Edit PAC Script"
},
"bypass": {
"title": "Proxy Bypass",
"addDefault": "Add Default Bypass",
"placeholder": "Example: *.baidu.com"
}
},
"tun": {
"title": "TUN",
"firewall": {
"title": "Reset Firewall",
"reset": "Reset Firewall"
},
"core": {
"title": "Manual Authorization",
"auth": "Authorize Core"
},
"dns": {
"autoSet": "Auto Set System DNS"
},
"stack": {
"title": "Tun Mode Stack"
},
"device": {
"title": "Tun Device Name"
},
"strictRoute": "Strict Route",
"autoRoute": "Auto Set Global Route",
"autoRedirect": "Auto Set TCP Redirect",
"autoDetectInterface": "Auto Detect Interface",
"dnsHijack": "DNS Hijack",
"excludeAddress": {
"title": "Exclude Custom Networks",
"placeholder": "Example: 172.20.0.0/16"
},
"notifications": {
"coreAuthSuccess": "Core Authorization Successful",
"firewallResetSuccess": "Firewall Reset Successful"
}
},
"dns": {
"title": "DNS Settings",
"enhancedMode": {
"title": "Domain Mapping Mode",
"fakeIp": "Fake IP",
"redirHost": "Real IP",
"normal": "No Mapping"
},
"fakeIp": {
"range": "Response Range",
"rangePlaceholder": "Example: 198.18.0.1/16",
"filter": "Real IP Response",
"filterPlaceholder": "Example: +.lan"
},
"respectRules": "Respect Rules",
"defaultNameserver": "DNS Server Domain Resolution",
"defaultNameserverPlaceholder": "Example: 223.5.5.5, IP only",
"proxyServerNameserver": "Proxy Server Domain Resolution",
"proxyServerNameserverPlaceholder": "Example: tls://dns.alidns.com",
"nameserver": "Default Resolution Server",
"nameserverPlaceholder": "Example: tls://dns.alidns.com",
"directNameserver": "Direct Resolution Server",
"directNameserverPlaceholder": "Example: tls://dns.alidns.com",
"nameserverPolicy": {
"title": "Override DNS Policy",
"list": "DNS Policy List",
"domainPlaceholder": "Domain",
"serverPlaceholder": "DNS Server"
},
"systemHosts": {
"title": "Use System Hosts"
},
"customHosts": {
"title": "Custom Hosts",
"list": "Hosts List",
"domainPlaceholder": "Domain",
"valuePlaceholder": "Domain or IP"
}
},
"profiles": {
"title": "Profile Management",
"updateAll": "Update All Profiles",
"useProxy": "Proxy",
"import": "Import",
"open": "Open",
"new": "New",
"newProfile": "New Profile",
"substore": {
"visit": "Visit Sub-Store"
},
"error": {
"unsupportedFileType": "Unsupported file type"
},
"emptyProfile": "Empty Profile",
"viewRuntimeConfig": "View Current Runtime Config",
"neverExpire": "Never Expire",
"remote": "Remote",
"local": "Local",
"trafficUsage": "Traffic Usage Progress",
"editInfo": {
"title": "Edit Information",
"name": "Name",
"url": "Subscription URL",
"useProxy": "Use Proxy to Update",
"interval": "Upd. Interval (min)",
"override": {
"title": "Override",
"global": "Global",
"noAvailable": "No available overrides",
"add": "Add Override"
}
},
"editFile": {
"title": "Edit Profile",
"notice": "Note: Changes made here will be reset after profile update. For custom configurations, please use",
"override": "Override",
"feature": "feature"
},
"openFile": "Open File",
"home": "Home",
"traffic": {
"usage": "{{used}}/{{total}}",
"unlimited": "Unlimited",
"expired": "Expired",
"remainingDays": "{{days}} days",
"lastUpdate": "Last updated: {{time}}"
}
},
"outbound": {
"title": "Outbound Mode",
"modes": {
"rule": "Rule",
"global": "Global",
"direct": "Direct"
}
},
"rules": {
"title": "Rules",
"filter": "Filter Rules"
},
"override": {
"title": "Override",
"import": "Import",
"docs": "Documentation",
"repository": "Override Repository",
"unsupportedFileType": "Unsupported file type",
"actions": {
"open": "Open",
"newYaml": "New YAML",
"newJs": "New JavaScript"
},
"defaultContent": {
"yaml": "# https://mihomo.party/docs/guide/override/yaml",
"js": "// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}"
},
"newFile": {
"yaml": "New YAML",
"js": "New JS"
},
"editInfo": {
"title": "Edit Information",
"name": "Name",
"url": "URL",
"global": "Global Enable"
},
"editFile": {
"title": "Edit Override {{type}}",
"script": "Script",
"config": "Config"
},
"execLog": {
"title": "Execution Log",
"close": "Close"
},
"menuItems": {
"editInfo": "Edit Information",
"editFile": "Edit File",
"openFile": "Open File",
"execLog": "Execution Log",
"delete": "Delete"
},
"labels": {
"global": "Global"
}
},
"connections": {
"title": "Connections",
"upload": "Upload",
"download": "Download",
"closeAll": "Close All Connections",
"active": "Active",
"closed": "Closed",
"filter": "Filter",
"orderBy": "Order By",
"uploadAmount": "Upload Amount",
"downloadAmount": "Download Amount",
"uploadSpeed": "Upload Speed",
"downloadSpeed": "Download Speed",
"detail": {
"title": "Connection Details",
"establishTime": "Establish Time",
"rule": "Rule",
"proxyChain": "Proxy Chain",
"connectionType": "Connection Type",
"host": "Host",
"sniffHost": "Sniff Host",
"processName": "Process Name",
"processPath": "Process Path",
"sourceIP": "Source IP",
"sourceGeoIP": "Source GeoIP",
"sourceASN": "Source ASN",
"destinationIP": "Destination IP",
"destinationGeoIP": "Destination GeoIP",
"destinationASN": "Destination ASN",
"sourcePort": "Source Port",
"destinationPort": "Destination Port",
"inboundIP": "Inbound IP",
"inboundPort": "Inbound Port",
"copyRule": "Copy Rule",
"inboundName": "Inbound Name",
"inboundUser": "Inbound User",
"dscp": "DSCP",
"remoteDestination": "Remote Destination",
"dnsMode": "DNS Mode",
"specialProxy": "Special Proxy",
"specialRules": "Special Rules",
"close": "Close"
}
},
"resources": {
"geoData": {
"geoip": "GeoIP Database",
"geosite": "GeoSite Database",
"mmdb": "MMDB Database",
"asn": "ASN Database",
"mode": "GeoData Mode",
"autoUpdate": "Auto Update",
"updateInterval": "Update Interval (hours)",
"updateSuccess": "GeoData Update Successful"
}
},
"logs": {
"title": "Real-time Logs",
"filter": "Filter logs",
"clear": "Clear logs",
"autoScroll": "Auto scroll"
},
"tray": {
"showWindow": "Show Window",
"showFloatingWindow": "Show Floating Window",
"hideFloatingWindow": "Hide Floating Window",
"ruleMode": "Rule Mode",
"globalMode": "Global Mode",
"directMode": "Direct Mode",
"systemProxy": "System Proxy",
"tun": "TUN",
"profiles": "Profiles",
"openDirectories": {
"title": "Open Directories",
"appDir": "App Directory",
"workDir": "Work Directory",
"coreDir": "Core Directory",
"logDir": "Log Directory"
},
"copyEnv": "Copy Environment Variables"
},
"guide": {
"welcome": {
"title": "Welcome to Mihomo Party",
"description": "This is an interactive tutorial. If you are already familiar with the software, you can click the close button in the top right corner. You can always open this tutorial again from the settings."
},
"sider": {
"title": "Navigation Bar",
"description": "The left side is the application's navigation bar, which also serves as a dashboard. Here you can switch between different pages and get an overview of commonly used status information."
},
"card": {
"title": "Cards",
"description": "Click on the navigation bar cards to jump to the corresponding page. You can also drag and drop the cards to arrange them freely."
},
"main": {
"title": "Main Area",
"description": "The right side is the main area of the application, displaying the content of the selected page from the navigation bar."
},
"profile": {
"title": "Profile Management",
"description": "The profile management card shows information about the currently running subscription configuration. Click to enter the profile management page where you can manage your subscription configurations."
},
"import": {
"title": "Import Subscription",
"description": "Mihomo Party supports various subscription import methods. Enter your subscription link here and click import to import your subscription configuration. If your subscription requires a proxy to update, check the 'Proxy' option before importing (this requires having a working subscription first)."
},
"substore": {
"title": "Sub-Store",
"description": "Mihomo Party has deep integration with Sub-Store. You can click this button to enter Sub-Store or directly import your subscriptions managed through Sub-Store. Mihomo Party uses its built-in Sub-Store backend by default. If you have your own Sub-Store backend, you can configure it in the settings page. If you don't use Sub-Store, you can also disable it in the settings."
},
"localProfile": {
"title": "Local Profile",
"description": "Click '+' to import a local file or create a new empty configuration for editing."
},
"sysproxy": {
"title": "System Proxy",
"description": "After importing a subscription, the core is running and listening on the specified port. You can now use the proxy through the specified port. If you want most applications to automatically use this proxy port, you need to turn on the system proxy switch."
},
"sysproxySetting": {
"title": "System Proxy Settings",
"description": "Here you can configure system proxy-related settings and choose the proxy mode. For Windows applications that don't follow system proxy, you can use the 'UWP Tool' to remove local loopback restrictions. For the difference between 'Manual Proxy Mode' and 'PAC Proxy Mode', please search online."
},
"tun": {
"title": "Virtual Network Card",
"description": "Virtual network card, commonly known as 'Tun Mode' in similar software, allows you to let the core take control of all traffic for applications that don't follow system proxy settings."
},
"tunSetting": {
"title": "Virtual Network Card Settings",
"description": "Here you can modify virtual network card settings. Mihomo Party has theoretically solved all permission issues. If your virtual network card is still not working, try resetting the firewall (Windows) or manually authorizing the core (MacOS/Linux) and then restart the core."
},
"override": {
"title": "Override",
"description": "Mihomo Party provides powerful override functionality to customize your imported subscription configurations, such as adding rules and customizing proxy groups. You can directly import override files written by others or write your own. <b>Remember to enable the override file on the subscription you want to override</b>. For override file syntax, please refer to the <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">official documentation</a>."
},
"dns": {
"title": "DNS",
"description": "The software takes control of the core's DNS settings by default. If you need to use the DNS settings from your subscription configuration, you can disable 'Control DNS Settings' in the application settings. The same applies to domain sniffing."
},
"end": {
"title": "Tutorial Complete",
"description": "Now that you understand the basic usage of the software, import your subscription and start using it. Enjoy!\nYou can also join our official <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram group</a> for the latest news."
}
},
"main": {
"error": {
"adminRequired": "Please run with administrator privileges for first launch",
"initFailed": "Application initialization failed",
"coreStartFailed": "Core startup error",
"importFailed": "Subscription import failed",
"urlParamMissing": "Missing parameter: url"
},
"notification": {
"importSuccess": "Subscription imported successfully"
}
}
}

View File

@ -0,0 +1,723 @@
{
"common": {
"settings": "设置",
"profiles": "配置",
"proxies": "代理",
"connections": "连接",
"dns": "DNS",
"tun": "TUN",
"save": "保存",
"cancel": "取消",
"edit": "编辑",
"delete": "删除",
"seconds": "秒",
"confirm": "确认",
"auto": "自动",
"default": "默认",
"close": "关闭",
"pinWindow": "窗口置顶",
"enterRootPassword": "请输入root密码",
"next": "下一步",
"prev": "上一步",
"done": "完成",
"notification": {
"restartRequired": "需要重启应用以使更改生效"
},
"error": {
"appCrash": "应用崩溃了 :( 请将以下信息提交给开发者以排查错误",
"copyErrorMessage": "复制报错信息",
"invalidCron": "无效的 Cron 表达式",
"getBackupListFailed": "获取备份列表失败:{{error}}",
"restoreFailed": "恢复失败:{{error}}",
"deleteFailed": "删除失败:{{error}}",
"shortcutRegistrationFailed": "快捷键注册失败",
"shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}"
},
"updater": {
"versionReady": "v{{version}} 版本就绪",
"goToDownload": "前往下载",
"update": "更新"
}
},
"settings": {
"general": "通用设置",
"mihomo": "Mihomo 设置",
"language": "语言",
"theme": "主题",
"darkMode": "深色模式",
"lightMode": "浅色模式",
"autoStart": "开机自启",
"autoCheckUpdate": "自动检查更新",
"silentStart": "静默启动",
"autoQuitWithoutCore": "自动进入轻量模式",
"autoQuitWithoutCoreTooltip": "关闭窗口后指定时间自动进入轻量模式",
"autoQuitWithoutCoreDelay": "轻量模式自动启用延迟",
"envType": "环境变量类型",
"showFloatingWindow": "显示悬浮窗",
"spinFloatingIcon": "根据网速旋转悬浮窗图标",
"disableTray": "禁用托盘图标",
"proxyInTray": "在托盘菜单显示代理信息",
"showTraffic_windows": "在任务栏显示网速",
"showTraffic_mac": "在状态栏显示网速",
"showDockIcon": "显示 Dock 图标",
"useWindowFrame": "使用系统标题栏",
"backgroundColor": "背景颜色",
"backgroundAuto": "自动",
"backgroundDark": "深色",
"backgroundLight": "浅色",
"fetchTheme": "获取主题",
"importTheme": "导入主题",
"editTheme": "编辑主题",
"selectTheme": "选择主题",
"links": {
"docs": "官方文档",
"github": "GitHub 仓库",
"telegram": "Telegram 群组"
},
"title": "应用设置"
},
"mihomo": {
"title": "内核设置",
"restart": "重启内核",
"memory": "内存使用",
"userAgent": "订阅 User Agent",
"userAgentPlaceholder": "默认clash.meta",
"delayTest": {
"url": "延迟测试 URL",
"urlPlaceholder": "默认https://www.gstatic.com/generate_204",
"concurrency": "延迟测试并发数",
"concurrencyPlaceholder": "默认50",
"timeout": "延迟测试超时",
"timeoutPlaceholder": "默认5000"
},
"gist": {
"title": "同步运行时配置到 Gist",
"copyUrl": "复制 Gist URL",
"token": "GitHub Token"
},
"proxyColumns": {
"title": "代理显示列数",
"auto": "自动",
"one": "一列",
"two": "两列",
"three": "三列",
"four": "四列"
},
"cpuPriority": {
"title": "核心进程优先级",
"realtime": "实时",
"high": "高",
"aboveNormal": "高于正常",
"normal": "正常",
"belowNormal": "低于正常",
"low": "低"
},
"workDir": {
"title": "不同订阅使用独立工作目录",
"tooltip": "启用后可避免不同订阅中存在相同名称的代理组时发生冲突"
},
"controlDns": "控制 DNS 设置",
"controlSniff": "控制域名嗅探",
"autoCloseConnection": "自动关闭连接",
"pauseSSID": {
"title": "指定 WiFi SSID 直连",
"placeholder": "输入 SSID"
},
"coreVersion": "内核版本",
"upgradeCore": "升级内核",
"coreAuthLost": "内核权限丢失",
"coreUpgradeSuccess": "内核升级成功若要使用虚拟网卡Tun请到虚拟网卡页面重新手动授权内核",
"alreadyLatestVersion": "已经是最新版本",
"selectCoreVersion": "选择内核版本",
"stableVersion": "稳定版",
"alphaVersion": "预览版",
"mixedPort": "混合端口",
"confirm": "确认",
"socksPort": "Socks 端口",
"httpPort": "Http 端口",
"redirPort": "Redir 端口",
"externalController": "外部控制地址",
"externalControllerSecret": "外部控制访问密钥",
"ipv6": "IPv6",
"allowLanConnection": "允许局域网连接",
"allowedIpSegments": "允许连接的 IP 段",
"disallowedIpSegments": "禁止连接的 IP 段",
"userVerification": "用户验证",
"skipAuthPrefixes": "允许跳过验证的 IP 段",
"useRttDelayTest": "使用 RTT 延迟测试",
"tcpConcurrent": "TCP 并发",
"storeSelectedNode": "存储选择节点",
"storeFakeIp": "存储 FakeIP",
"logRetentionDays": "日志保留天数",
"logLevel": "日志等级",
"selectLogLevel": "选择日志等级",
"silent": "静默",
"error": "错误",
"warning": "警告",
"info": "信息",
"debug": "调试",
"findProcess": "查找进程",
"selectFindProcessMode": "选择进程查找模式",
"strict": "自动",
"off": "关闭",
"always": "开启",
"username": {
"placeholder": "用户名"
},
"password": {
"placeholder": "密码"
},
"ipSegment": {
"placeholder": "IP 段"
},
"interface": {
"title": "网络信息"
}
},
"substore": {
"title": "Sub-Store",
"openInBrowser": "在浏览器中打开",
"enable": "启用 Sub-Store",
"allowLan": "允许局域网访问",
"useCustomBackend": "使用自定义 Sub-Store 后端",
"customBackendUrl": {
"title": "自定义 Sub-Store 后端 URL",
"placeholder": "必须包含协议"
},
"useProxy": "为所有 Sub-Store 请求启用代理",
"sync": {
"title": "定时同步订阅/文件",
"placeholder": "Cron 表达式"
},
"restore": {
"title": "定时恢复配置",
"placeholder": "Cron 表达式"
},
"backup": {
"title": "定时备份配置",
"placeholder": "Cron 表达式"
}
},
"webdav": {
"title": "WebDAV 备份",
"url": "WebDAV URL",
"dir": "WebDAV 备份目录",
"username": "WebDAV 用户名",
"password": "WebDAV 密码",
"backup": "备份",
"restore": {
"title": "恢复备份",
"noBackups": "还没有备份"
},
"notification": {
"backupSuccess": {
"title": "备份成功",
"body": "备份文件已上传到 WebDAV"
}
}
},
"shortcuts": {
"title": "快捷键设置",
"toggleWindow": "打开/关闭窗口",
"toggleFloatingWindow": "打开/关闭悬浮窗",
"toggleSystemProxy": "打开/关闭系统代理",
"toggleTun": "打开/关闭 TUN",
"toggleRuleMode": "切换规则模式",
"toggleGlobalMode": "切换全局模式",
"toggleDirectMode": "切换直连模式",
"toggleLightMode": "切换轻量模式",
"restartApp": "重启应用",
"input": {
"placeholder": "点击输入快捷键"
}
},
"sider": {
"title": "侧边栏设置",
"cards": {
"systemProxy": "系统代理",
"tun": "虚拟网卡",
"profiles": "订阅管理",
"proxies": "代理组",
"rules": "规则",
"resources": "外部资源",
"override": "覆写",
"connections": "连接",
"core": "内核设置",
"dns": "DNS",
"sniff": "域名嗅探",
"logs": "日志",
"substore": "Sub-Store",
"config": "运行时配置",
"emptyProfile": "空白配置",
"viewRuntimeConfig": "查看运行时配置",
"remote": "远程",
"local": "本地",
"trafficUsage": "流量使用进度",
"neverExpire": "长期有效",
"outbound": {
"title": "出站模式",
"rule": "规则",
"global": "全局",
"direct": "直连"
}
},
"size": {
"large": "大",
"small": "小",
"hidden": "隐藏"
}
},
"actions": {
"guide": {
"title": "打开引导页面",
"button": "打开引导页面"
},
"update": {
"title": "检查更新",
"button": "检查更新",
"upToDate": {
"title": "当前已是最新版本",
"body": "无需更新"
}
},
"reset": {
"title": "重置软件",
"button": "重置软件",
"tooltip": "删除所有配置,将软件恢复初始状态"
},
"heapSnapshot": {
"title": "创建堆快照",
"button": "创建堆快照",
"tooltip": "创建主进程堆快照,用于排查内存问题"
},
"lightMode": {
"title": "轻量模式",
"button": "轻量模式",
"tooltip": "完全退出软件,只保留内核进程"
},
"restartApp": "重启应用",
"quit": {
"title": "退出应用",
"button": "退出应用"
},
"version": {
"title": "应用版本"
}
},
"theme": {
"editor": {
"title": "编辑主题"
}
},
"proxies": {
"card": {
"title": "代理组"
},
"delay": {
"test": "测试",
"timeout": "超时"
},
"unpin": "取消固定",
"order": {
"default": "默认",
"delay": "延迟",
"name": "名称"
},
"mode": {
"full": "详细信息",
"simple": "简洁信息",
"direct": "直连模式"
},
"search": {
"placeholder": "搜索节点"
},
"locate": "定位到当前节点"
},
"sniffer": {
"title": "域名嗅探设置",
"parsePureIP": "对未映射 IP 地址嗅探",
"forceDNSMapping": "对真实 IP 映射嗅探",
"overrideDestination": "覆盖连接地址",
"sniff": {
"title": "HTTP 端口嗅探",
"tls": "TLS 端口嗅探",
"quic": "QUIC 端口嗅探",
"ports": {
"placeholder": "端口号,使用英文逗号分割"
}
},
"skipDomain": {
"title": "跳过域名嗅探",
"placeholder": "例:+.push.apple.com"
},
"forceDomain": {
"title": "强制域名嗅探",
"placeholder": "例v2ex.com"
},
"skipDstAddress": {
"title": "跳过目标地址嗅探",
"placeholder": "例1.1.1.1/32"
},
"skipSrcAddress": {
"title": "跳过来源地址嗅探",
"placeholder": "例192.168.1.1/24"
}
},
"sysproxy": {
"title": "系统代理",
"host": {
"title": "代理主机",
"placeholder": "默认 127.0.0.1 若无特殊需求请勿修改"
},
"mode": {
"title": "代理模式",
"manual": "手动",
"pac": "PAC"
},
"uwp": {
"title": "UWP 工具",
"open": "打开 UWP 工具"
},
"pac": {
"edit": "编辑 PAC 脚本"
},
"bypass": {
"title": "代理绕过",
"addDefault": "添加默认代理绕过",
"placeholder": "例: *.baidu.com"
}
},
"tun": {
"title": "虚拟网卡",
"firewall": {
"title": "重设防火墙",
"reset": "重设防火墙"
},
"core": {
"title": "手动授权内核",
"auth": "手动授权内核"
},
"dns": {
"autoSet": "自动设置系统DNS"
},
"stack": {
"title": "Tun 模式堆栈"
},
"device": {
"title": "Tun 网卡名称"
},
"strictRoute": "严格路由",
"autoRoute": "自动设置全局路由",
"autoRedirect": "自动设置TCP重定向",
"autoDetectInterface": "自动选择流量出口接口",
"dnsHijack": "DNS 劫持",
"excludeAddress": {
"title": "排除自定义网段",
"placeholder": "例: 172.20.0.0/16"
},
"notifications": {
"coreAuthSuccess": "内核授权成功",
"firewallResetSuccess": "防火墙重设成功"
}
},
"dns": {
"title": "DNS 设置",
"enhancedMode": {
"title": "域名映射模式",
"fakeIp": "虚假 IP",
"redirHost": "真实 IP",
"normal": "取消映射"
},
"fakeIp": {
"range": "回应范围",
"rangePlaceholder": "例198.18.0.1/16",
"filter": "真实 IP 回应",
"filterPlaceholder": "例:+.lan"
},
"respectRules": "遵守规则",
"defaultNameserver": "DNS 服务器域名解析",
"defaultNameserverPlaceholder": "例223.5.5.5,仅支持 IP",
"proxyServerNameserver": "代理服务器域名解析",
"proxyServerNameserverPlaceholder": "例tls://dns.alidns.com",
"nameserver": "默认解析服务器",
"nameserverPlaceholder": "例tls://dns.alidns.com",
"directNameserver": "直连解析服务器",
"directNameserverPlaceholder": "例tls://dns.alidns.com",
"nameserverPolicy": {
"title": "覆盖 DNS 策略",
"list": "DNS 策略列表",
"domainPlaceholder": "域名",
"serverPlaceholder": "DNS 服务器"
},
"systemHosts": {
"title": "使用系统 Hosts"
},
"customHosts": {
"title": "自定义 Hosts",
"list": "Hosts 列表",
"domainPlaceholder": "域名",
"valuePlaceholder": "域名或 IP"
}
},
"profiles": {
"title": "订阅管理",
"updateAll": "更新全部订阅",
"useProxy": "代理",
"import": "导入",
"open": "打开",
"new": "新建",
"newProfile": "新建订阅",
"substore": {
"visit": "访问 Sub-Store"
},
"error": {
"unsupportedFileType": "不支持的文件类型"
},
"emptyProfile": "空白订阅",
"viewRuntimeConfig": "查看当前运行时配置",
"neverExpire": "长期有效",
"remote": "远程",
"local": "本地",
"trafficUsage": "流量使用进度",
"traffic": {
"usage": "{{used}}/{{total}}",
"unlimited": "无限制",
"expired": "已过期",
"remainingDays": "剩余 {{days}} 天",
"lastUpdate": "最后更新:{{time}}"
},
"editInfo": {
"title": "编辑信息",
"name": "名称",
"url": "订阅地址",
"useProxy": "使用代理更新",
"interval": "更新间隔(分钟)",
"override": {
"title": "覆写",
"global": "全局",
"noAvailable": "没有可用的覆写",
"add": "添加覆写"
}
},
"editFile": {
"title": "编辑订阅",
"notice": "注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用",
"override": "覆写",
"feature": "功能"
},
"openFile": "打开文件",
"home": "主页"
},
"outbound": {
"title": "出站模式",
"modes": {
"rule": "规则",
"global": "全局",
"direct": "直连"
}
},
"rules": {
"title": "分流规则",
"filter": "筛选过滤"
},
"override": {
"title": "覆写",
"import": "导入",
"docs": "使用文档",
"repository": "常用覆写仓库",
"unsupportedFileType": "不支持的文件类型",
"actions": {
"open": "打开",
"newYaml": "新建 YAML",
"newJs": "新建 JavaScript"
},
"defaultContent": {
"yaml": "# https://mihomo.party/docs/guide/override/yaml",
"js": "// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}"
},
"newFile": {
"yaml": "新建YAML",
"js": "新建JS"
},
"editInfo": {
"title": "编辑信息",
"name": "名称",
"url": "地址",
"global": "全局启用"
},
"editFile": {
"title": "编辑覆写{{type}}",
"script": "脚本",
"config": "配置"
},
"execLog": {
"title": "执行日志",
"close": "关闭"
},
"menuItems": {
"editInfo": "编辑信息",
"editFile": "编辑文件",
"openFile": "打开文件",
"execLog": "执行日志",
"delete": "删除"
},
"labels": {
"global": "全局"
}
},
"connections": {
"title": "连接",
"upload": "上传",
"download": "下载",
"closeAll": "关闭全部连接",
"active": "活动中",
"closed": "已关闭",
"filter": "筛选过滤",
"orderBy": "连接排序方式",
"uploadAmount": "上传量",
"downloadAmount": "下载量",
"uploadSpeed": "上传速度",
"downloadSpeed": "下载速度",
"detail": {
"title": "连接详情",
"establishTime": "连接建立时间",
"rule": "规则",
"proxyChain": "代理链",
"connectionType": "连接类型",
"host": "主机",
"sniffHost": "嗅探主机",
"processName": "进程名",
"processPath": "进程路径",
"sourceIP": "来源IP",
"sourceGeoIP": "来源GeoIP",
"sourceASN": "来源ASN",
"destinationIP": "目标IP",
"destinationGeoIP": "目标GeoIP",
"destinationASN": "目标ASN",
"sourcePort": "来源端口",
"destinationPort": "目标端口",
"inboundIP": "入站IP",
"inboundPort": "入站端口",
"copyRule": "复制规则",
"inboundName": "入站名称",
"inboundUser": "入站用户",
"dscp": "DSCP",
"remoteDestination": "远程目标",
"dnsMode": "DNS模式",
"specialProxy": "特殊代理",
"specialRules": "特殊规则",
"close": "关闭"
}
},
"resources": {
"geoData": {
"geoip": "GeoIP 数据库",
"geosite": "GeoSite 数据库",
"mmdb": "MMDB 数据库",
"asn": "ASN 数据库",
"mode": "GeoData 数据模式",
"autoUpdate": "自动更新",
"updateInterval": "更新间隔(小时)",
"updateSuccess": "GeoData 更新成功"
}
},
"logs": {
"title": "实时日志",
"filter": "筛选过滤",
"clear": "清空日志",
"autoScroll": "自动滚动"
},
"tray": {
"showWindow": "显示窗口",
"showFloatingWindow": "显示悬浮窗",
"hideFloatingWindow": "关闭悬浮窗",
"ruleMode": "规则模式",
"globalMode": "全局模式",
"directMode": "直连模式",
"systemProxy": "系统代理",
"tun": "虚拟网卡",
"profiles": "订阅配置",
"openDirectories": {
"title": "打开目录",
"appDir": "应用目录",
"workDir": "工作目录",
"coreDir": "内核目录",
"logDir": "日志目录"
},
"copyEnv": "复制环境变量"
},
"guide": {
"welcome": {
"title": "欢迎使用 Mihomo Party",
"description": "这是一份交互式使用教程,如果您已经完全熟悉本软件的操作,可以直接点击右上角关闭按钮,后续您可以随时从设置中打开本教程"
},
"sider": {
"title": "导航栏",
"description": "左侧是应用的导航栏,兼顾仪表盘功能,在这里可以切换不同页面,也可以概览常用的状态信息"
},
"card": {
"title": "卡片",
"description": "点击导航栏卡片可以跳转到对应页面,拖动导航栏卡片可以自由排列卡片顺序"
},
"main": {
"title": "主要区域",
"description": "右侧是应用的主要区域,展示了导航栏所选页面的内容"
},
"profile": {
"title": "订阅管理",
"description": "订阅管理卡片展示当前运行的订阅配置信息,点击进入订阅管理页面可以在这里管理订阅配置"
},
"import": {
"title": "订阅导入",
"description": "Mihomo Party 支持多种订阅导入方式,在此输入订阅链接,点击导入即可导入您的订阅配置,如果您的订阅需要代理才能更新,请勾选\"代理\"再点击导入,当然这需要已经有一个可以正常使用的订阅才可以"
},
"substore": {
"title": "Sub-Store",
"description": "Mihomo Party 深度集成了 Sub-Store您可以点击该按钮进入 Sub-Store 或直接导入您通过 Sub-Store 管理的订阅Mihomo Party 默认使用内置的 Sub-Store 后端,如果您有自建的 Sub-Store 后端,可以在设置页面中配置,如果您不使用 Sub-Store 也可以在设置页面中关闭"
},
"localProfile": {
"title": "本地订阅",
"description": "点击\"+\"可以选择本地文件进行导入或者直接新建空白配置进行编辑"
},
"sysproxy": {
"title": "系统代理",
"description": "导入订阅之后,内核已经开始运行并监听指定端口,此时您已经可以通过指定代理端口来使用代理了,如果您要使大部分应用自动使用该端口的代理,您还需要打开系统代理开关"
},
"sysproxySetting": {
"title": "系统代理设置",
"description": "在此您可以进行系统代理相关设置,选择代理模式,如果某些 Windows 应用不遵循系统代理,还可以使用\"UWP 工具\"解除本地回环限制,对于\"手动代理模式\"和\"PAC 代理模式\"的区别,请自行百度"
},
"tun": {
"title": "虚拟网卡",
"description": "虚拟网卡,即同类软件中常见的\"Tun 模式\",对于某些不遵循系统代理的应用,您可以打开虚拟网卡以让内核接管所有流量"
},
"tunSetting": {
"title": "虚拟网卡设置",
"description": "这里可以更改虚拟网卡相关设置Mihomo Party 理论上已经完全解决权限问题如果您的虚拟网卡仍然不可用可以尝试重设防火墙Windows或手动授权内核MacOS/Linux后重启内核"
},
"override": {
"title": "覆写",
"description": "Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,<b>编辑好覆写文件一定要记得在需要覆写的订阅上启用</b>,覆写文件的语法请参考 <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">官方文档</a>"
},
"dns": {
"title": "DNS",
"description": "软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭\"接管 DNS 设置\",域名嗅探同理"
},
"end": {
"title": "教程结束",
"description": "现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram 群组</a> 获取最新资讯"
}
},
"main": {
"error": {
"adminRequired": "首次启动请以管理员权限运行",
"initFailed": "应用初始化失败",
"coreStartFailed": "内核启动出错",
"importFailed": "订阅导入失败",
"urlParamMissing": "缺少参数 url"
},
"notification": {
"importSuccess": "订阅导入成功"
}
}
}

View File

@ -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

View File

@ -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 (
<BasePage
title="连接"
title={t('connections.title')}
header={
<div className="flex">
<div className="flex items-center">
@ -153,7 +155,7 @@ const Connections: React.FC = () => {
>
<Button
className="app-nodrag ml-1"
title="关闭全部连接"
title={t('connections.closeAll')}
isIconOnly
size="sm"
variant="light"
@ -199,7 +201,7 @@ const Connections: React.FC = () => {
content={activeConnections.length}
showOutline={false}
>
<span className="p-1"></span>
<span className="p-1">{t('connections.active')}</span>
</Badge>
}
/>
@ -214,7 +216,7 @@ const Connections: React.FC = () => {
content={closedConnections.length}
showOutline={false}
>
<span className="p-1"></span>
<span className="p-1">{t('connections.closed')}</span>
</Badge>
}
/>
@ -223,7 +225,7 @@ const Connections: React.FC = () => {
variant="flat"
size="sm"
value={filter}
placeholder="筛选过滤"
placeholder={t('connections.filter')}
isClearable
onValueChange={setFilter}
/>
@ -232,7 +234,7 @@ const Connections: React.FC = () => {
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
size="sm"
className="w-[180px] min-w-[120px]"
aria-label="连接排序方式"
aria-label={t('connections.orderBy')}
selectedKeys={new Set([connectionOrderBy])}
onSelectionChange={async (v) => {
await patchAppConfig({
@ -245,11 +247,10 @@ const Connections: React.FC = () => {
})
}}
>
<SelectItem key="upload"></SelectItem>
<SelectItem key="download"></SelectItem>
<SelectItem key="uploadSpeed"></SelectItem>
<SelectItem key="downloadSpeed"></SelectItem>
<SelectItem key="time"></SelectItem>
<SelectItem key="upload">{t('connections.uploadAmount')}</SelectItem>
<SelectItem key="download">{t('connections.downloadAmount')}</SelectItem>
<SelectItem key="uploadSpeed">{t('connections.uploadSpeed')}</SelectItem>
<SelectItem key="downloadSpeed">{t('connections.downloadSpeed')}</SelectItem>
</Select>
<Button
size="sm"

View File

@ -7,8 +7,10 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { restartCore } from '@renderer/utils/ipc'
import React, { Key, ReactNode, useState } from 'react'
import { useTranslation } from 'react-i18next'
const DNS: React.FC = () => {
const { t } = useTranslation()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { appConfig, patchAppConfig } = useAppConfig()
const { nameserverPolicy, useNameserverPolicy } = appConfig || {}
@ -132,7 +134,7 @@ const DNS: React.FC = () => {
return (
<BasePage
title="DNS 设置"
title={t('dns.title')}
header={
changed && (
<Button
@ -169,39 +171,40 @@ const DNS: React.FC = () => {
})
}}
>
{t('common.save')}
</Button>
)
}
>
<SettingCard>
<SettingItem title="域名映射模式" divider>
<SettingItem title={t('dns.enhancedMode.title')} divider>
<Tabs
size="sm"
color="primary"
selectedKey={values.enhancedMode}
onSelectionChange={(key: Key) => setValues({ ...values, enhancedMode: key as DnsMode })}
>
<Tab key="fake-ip" title="虚假 IP" />
<Tab key="redir-host" title="真实 IP" />
<Tab key="normal" title="取消映射" />
<Tab key="fake-ip" title={t('dns.enhancedMode.fakeIp')} />
<Tab key="redir-host" title={t('dns.enhancedMode.redirHost')} />
<Tab key="normal" title={t('dns.enhancedMode.normal')} />
</Tabs>
</SettingItem>
{values.enhancedMode === 'fake-ip' ? (
<>
<SettingItem title="回应范围" divider>
<SettingItem title={t('dns.fakeIp.range')} divider>
<Input
size="sm"
className="w-[50%]"
value={values.fakeIPRange}
placeholder={t('dns.fakeIp.rangePlaceholder')}
onValueChange={(v) => {
setValues({ ...values, fakeIPRange: v })
}}
/>
</SettingItem>
<div className="flex flex-col items-stretch">
<h3> IP </h3>
{renderListInputs('fakeIPFilter', '例:+.lan')}
<h3>{t('dns.fakeIp.filter')}</h3>
{renderListInputs('fakeIPFilter', t('dns.fakeIp.filterPlaceholder'))}
</div>
<Divider className="my-2" />
</>
@ -215,7 +218,7 @@ const DNS: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="遵守规则" divider>
<SettingItem title={t('dns.respectRules')} divider>
<Switch
size="sm"
isSelected={values.respectRules}
@ -226,26 +229,26 @@ const DNS: React.FC = () => {
</SettingItem>
<div className="flex flex-col items-stretch">
<h3>DNS </h3>
{renderListInputs('defaultNameserver', '例223.5.5.5,仅支持 IP')}
<h3>{t('dns.defaultNameserver')}</h3>
{renderListInputs('defaultNameserver', t('dns.defaultNameserverPlaceholder'))}
</div>
<Divider className="my-2" />
<div className="flex flex-col items-stretch">
<h3></h3>
{renderListInputs('proxyServerNameserver', '例tls://dns.alidns.com')}
<h3>{t('dns.proxyServerNameserver')}</h3>
{renderListInputs('proxyServerNameserver', t('dns.proxyServerNameserverPlaceholder'))}
</div>
<Divider className="my-2" />
<div className="flex flex-col items-stretch">
<h3></h3>
{renderListInputs('nameserver', '例tls://dns.alidns.com')}
<h3>{t('dns.nameserver')}</h3>
{renderListInputs('nameserver', t('dns.nameserverPlaceholder'))}
</div>
<Divider className="my-2" />
<div className="flex flex-col items-stretch">
<h3></h3>
{renderListInputs('directNameserver', '例tls://dns.alidns.com')}
<h3>{t('dns.directNameserver')}</h3>
{renderListInputs('directNameserver', t('dns.directNameserverPlaceholder'))}
</div>
<Divider className="my-2" />
<SettingItem title="覆盖DNS策略" divider>
<SettingItem title={t('dns.nameserverPolicy.title')} divider>
<Switch
size="sm"
isSelected={values.useNameserverPolicy}
@ -257,7 +260,7 @@ const DNS: React.FC = () => {
{values.useNameserverPolicy && (
<div className="flex flex-col items-stretch">
<div className="flex flex-col items-stretch">
<h3 className="mb-2"></h3>
<h3 className="mb-2">{t('dns.nameserverPolicy.list')}</h3>
{[...values.nameserverPolicy, { domain: '', value: '' }].map(
({ domain, value }, index) => (
<div key={index} className="flex mb-2">
@ -265,7 +268,7 @@ const DNS: React.FC = () => {
<Input
size="sm"
fullWidth
placeholder="域名"
placeholder={t('dns.nameserverPolicy.domainPlaceholder')}
value={domain}
onValueChange={(v) =>
handleSubkeyChange(
@ -282,7 +285,7 @@ const DNS: React.FC = () => {
<Input
size="sm"
fullWidth
placeholder="DNS 服务器"
placeholder={t('dns.nameserverPolicy.serverPlaceholder')}
value={Array.isArray(value) ? value.join(',') : value}
onValueChange={(v) =>
handleSubkeyChange('nameserverPolicy', domain, v, index)
@ -306,7 +309,7 @@ const DNS: React.FC = () => {
</div>
</div>
)}
<SettingItem title="使用系统 Hosts" divider>
<SettingItem title={t('dns.systemHosts.title')} divider>
<Switch
size="sm"
isSelected={values.useSystemHosts}
@ -315,7 +318,7 @@ const DNS: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="自定义 Hosts">
<SettingItem title={t('dns.customHosts.title')}>
<Switch
size="sm"
isSelected={values.useHosts}
@ -326,14 +329,14 @@ const DNS: React.FC = () => {
</SettingItem>
{values.useHosts && (
<div className="flex flex-col items-stretch">
<h3 className="mb-2"></h3>
<h3 className="mb-2">{t('dns.customHosts.list')}</h3>
{[...values.hosts, { domain: '', value: '' }].map(({ domain, value }, index) => (
<div key={index} className="flex mb-2">
<div className="flex-[4]">
<Input
size="sm"
fullWidth
placeholder="域名"
placeholder={t('dns.customHosts.domainPlaceholder')}
value={domain}
onValueChange={(v) =>
handleSubkeyChange(
@ -350,7 +353,7 @@ const DNS: React.FC = () => {
<Input
size="sm"
fullWidth
placeholder="域名或 IP"
placeholder={t('dns.customHosts.valuePlaceholder')}
value={Array.isArray(value) ? value.join(',') : value}
onValueChange={(v) => handleSubkeyChange('hosts', domain, v, index)}
/>

View File

@ -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<IMihomoLogInfo[]>(cachedLogs.log)
const [filter, setFilter] = useState('')
const [trace, setTrace] = useState(true)
@ -68,13 +70,13 @@ const Logs: React.FC = () => {
}, [])
return (
<BasePage title="实时日志">
<BasePage title={t('logs.title')}>
<div className="sticky top-0 z-40">
<div className="w-full flex p-2">
<Input
size="sm"
value={filter}
placeholder="筛选过滤"
placeholder={t('logs.filter')}
isClearable
onValueChange={setFilter}
/>
@ -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 = () => {
<Button
size="sm"
isIconOnly
title="清空日志"
title={t('logs.clear')}
className="ml-2"
variant="light"
color="danger"

View File

@ -17,13 +17,15 @@ import {
import React, { useState } from 'react'
import InterfaceModal from '@renderer/components/mihomo/interface-modal'
import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next'
const CoreMap = {
mihomo: '稳定版',
'mihomo-alpha': '预览版'
mihomo: 'mihomo.stableVersion',
'mihomo-alpha': 'mihomo.alphaVersion'
}
const Mihomo: React.FC = () => {
const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig()
const { core = 'mihomo', maxLogDays = 7, sysProxy } = appConfig || {}
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
@ -71,15 +73,15 @@ const Mihomo: React.FC = () => {
return (
<>
{lanOpen && <InterfaceModal onClose={() => setLanOpen(false)} />}
<BasePage title="内核设置">
<BasePage title={t('mihomo.title')}>
<SettingCard>
<SettingItem
title="内核版本"
title={t('mihomo.coreVersion')}
actions={
<Button
size="sm"
isIconOnly
title="升级内核"
title={t('mihomo.upgradeCore')}
variant="light"
isLoading={upgrading}
onPress={async () => {
@ -90,13 +92,13 @@ const Mihomo: React.FC = () => {
PubSub.publish('mihomo-core-changed')
}, 2000)
if (platform !== 'win32') {
new Notification('内核权限丢失', {
body: '内核升级成功若要使用虚拟网卡Tun请到虚拟网卡页面重新手动授权内核'
new Notification(t('mihomo.coreAuthLost'), {
body: t('mihomo.coreUpgradeSuccess')
})
}
} catch (e) {
if (typeof e === 'string' && e.includes('already using latest version')) {
new Notification('已经是最新版本')
new Notification(t('mihomo.alreadyLatestVersion'))
} else {
alert(e)
}
@ -114,7 +116,7 @@ const Mihomo: React.FC = () => {
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[100px]"
size="sm"
aria-label="选择内核版本"
aria-label={t('mihomo.selectCoreVersion')}
selectedKeys={new Set([core])}
onSelectionChange={async (v) => {
try {
@ -127,11 +129,11 @@ const Mihomo: React.FC = () => {
}
}}
>
<SelectItem key="mihomo">{CoreMap['mihomo']}</SelectItem>
<SelectItem key="mihomo-alpha">{CoreMap['mihomo-alpha']}</SelectItem>
<SelectItem key="mihomo">{t(CoreMap['mihomo'])}</SelectItem>
<SelectItem key="mihomo-alpha">{t(CoreMap['mihomo-alpha'])}</SelectItem>
</Select>
</SettingItem>
<SettingItem title="混合端口" divider>
<SettingItem title={t('mihomo.mixedPort')} divider>
<div className="flex">
{mixedPortInput !== mixedPort && (
<Button
@ -146,7 +148,7 @@ const Mihomo: React.FC = () => {
}
}}
>
{t('mihomo.confirm')}
</Button>
)}
@ -163,7 +165,7 @@ const Mihomo: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="Socks 端口" divider>
<SettingItem title={t('mihomo.socksPort')} divider>
<div className="flex">
{socksPortInput !== socksPort && (
<Button
@ -174,7 +176,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ 'socks-port': socksPortInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
@ -191,7 +193,7 @@ const Mihomo: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="Http 端口" divider>
<SettingItem title={t('mihomo.httpPort')} divider>
<div className="flex">
{httpPortInput !== httpPort && (
<Button
@ -202,7 +204,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ port: httpPortInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
@ -220,7 +222,7 @@ const Mihomo: React.FC = () => {
</div>
</SettingItem>
{platform !== 'win32' && (
<SettingItem title="Redir 端口" divider>
<SettingItem title={t('mihomo.redirPort')} divider>
<div className="flex">
{redirPortInput !== redirPort && (
<Button
@ -231,7 +233,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ 'redir-port': redirPortInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
@ -261,7 +263,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ 'tproxy-port': tproxyPortInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
@ -279,7 +281,7 @@ const Mihomo: React.FC = () => {
</div>
</SettingItem>
)}
<SettingItem title="外部控制地址" divider>
<SettingItem title={t('mihomo.externalController')} divider>
<div className="flex">
{externalControllerInput !== externalController && (
<Button
@ -292,7 +294,7 @@ const Mihomo: React.FC = () => {
})
}}
>
{t('mihomo.confirm')}
</Button>
)}
@ -306,7 +308,7 @@ const Mihomo: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="外部控制访问密钥" divider>
<SettingItem title={t('mihomo.externalControllerSecret')} divider>
<div className="flex">
{secretInput !== secret && (
<Button
@ -317,7 +319,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ secret: secretInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
@ -332,7 +334,7 @@ const Mihomo: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title="IPv6" divider>
<SettingItem title={t('mihomo.ipv6')} divider>
<Switch
size="sm"
isSelected={ipv6}
@ -342,7 +344,7 @@ const Mihomo: React.FC = () => {
/>
</SettingItem>
<SettingItem
title="允许局域网连接"
title={t('mihomo.allowLanConnection')}
actions={
<Button
size="sm"
@ -367,7 +369,7 @@ const Mihomo: React.FC = () => {
</SettingItem>
{allowLan && (
<>
<SettingItem title="允许连接的 IP 段">
<SettingItem title={t('mihomo.allowedIpSegments')}>
{lanAllowedIpsInput.join('') !== lanAllowedIps.join('') && (
<Button
size="sm"
@ -376,7 +378,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ 'lan-allowed-ips': lanAllowedIpsInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
</SettingItem>
@ -417,7 +419,7 @@ const Mihomo: React.FC = () => {
})}
</div>
<Divider className="mb-2" />
<SettingItem title="禁止连接的 IP 段">
<SettingItem title={t('mihomo.disallowedIpSegments')}>
{lanDisallowedIpsInput.join('') !== lanDisallowedIps.join('') && (
<Button
size="sm"
@ -426,7 +428,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ 'lan-disallowed-ips': lanDisallowedIpsInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
</SettingItem>
@ -471,7 +473,7 @@ const Mihomo: React.FC = () => {
<Divider className="mb-2" />
</>
)}
<SettingItem title="用户验证">
<SettingItem title={t('mihomo.userVerification')}>
{authenticationInput.join('') !== authentication.join('') && (
<Button
size="sm"
@ -480,7 +482,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ authentication: authenticationInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
</SettingItem>
@ -493,7 +495,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
fullWidth
placeholder="用户名"
placeholder={t('mihomo.username.placeholder')}
value={user || ''}
onValueChange={(v) => {
if (index === authenticationInput.length) {
@ -513,7 +515,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
fullWidth
placeholder="密码"
placeholder={t('mihomo.password.placeholder')}
value={pass || ''}
onValueChange={(v) => {
if (index === authenticationInput.length) {
@ -546,7 +548,7 @@ const Mihomo: React.FC = () => {
})}
</div>
<Divider className="mb-2" />
<SettingItem title="允许跳过验证的 IP 段">
<SettingItem title={t('mihomo.skipAuthPrefixes')}>
{skipAuthPrefixesInput.join('') !== skipAuthPrefixes.join('') && (
<Button
size="sm"
@ -555,7 +557,7 @@ const Mihomo: React.FC = () => {
onChangeNeedRestart({ 'skip-auth-prefixes': skipAuthPrefixesInput })
}}
>
{t('mihomo.confirm')}
</Button>
)}
</SettingItem>
@ -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 = () => {
})}
</div>
<Divider className="mb-2" />
<SettingItem title="使用 RTT 延迟测试" divider>
<SettingItem title={t('mihomo.useRttDelayTest')} divider>
<Switch
size="sm"
isSelected={unifiedDelay}
@ -608,7 +610,7 @@ const Mihomo: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="TCP 并发" divider>
<SettingItem title={t('mihomo.tcpConcurrent')} divider>
<Switch
size="sm"
isSelected={tcpConcurrent}
@ -617,7 +619,7 @@ const Mihomo: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="存储选择节点" divider>
<SettingItem title={t('mihomo.storeSelectedNode')} divider>
<Switch
size="sm"
isSelected={storeSelected}
@ -626,7 +628,7 @@ const Mihomo: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="存储 FakeIP" divider>
<SettingItem title={t('mihomo.storeFakeIp')} divider>
<Switch
size="sm"
isSelected={storeFakeIp}
@ -635,7 +637,7 @@ const Mihomo: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="日志保留天数" divider>
<SettingItem title={t('mihomo.logRetentionDays')} divider>
<Input
size="sm"
type="number"
@ -646,38 +648,38 @@ const Mihomo: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="日志等级" divider>
<SettingItem title={t('mihomo.logLevel')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[100px]"
size="sm"
aria-label="选择日志等级"
aria-label={t('mihomo.selectLogLevel')}
selectedKeys={new Set([logLevel])}
onSelectionChange={(v) => {
onChangeNeedRestart({ 'log-level': v.currentKey as LogLevel })
}}
>
<SelectItem key="silent"></SelectItem>
<SelectItem key="error"></SelectItem>
<SelectItem key="warning"></SelectItem>
<SelectItem key="info"></SelectItem>
<SelectItem key="debug"></SelectItem>
<SelectItem key="silent">{t('mihomo.silent')}</SelectItem>
<SelectItem key="error">{t('mihomo.error')}</SelectItem>
<SelectItem key="warning">{t('mihomo.warning')}</SelectItem>
<SelectItem key="info">{t('mihomo.info')}</SelectItem>
<SelectItem key="debug">{t('mihomo.debug')}</SelectItem>
</Select>
</SettingItem>
<SettingItem title="查找进程">
<SettingItem title={t('mihomo.findProcess')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[100px]"
size="sm"
aria-label="选择进程查找模式"
aria-label={t('mihomo.selectFindProcessMode')}
selectedKeys={new Set([findProcessMode])}
onSelectionChange={(v) => {
onChangeNeedRestart({ 'find-process-mode': v.currentKey as FindProcessMode })
}}
>
<SelectItem key="strict"></SelectItem>
<SelectItem key="off"></SelectItem>
<SelectItem key="always"></SelectItem>
<SelectItem key="strict">{t('mihomo.strict')}</SelectItem>
<SelectItem key="off">{t('mihomo.off')}</SelectItem>
<SelectItem key="always">{t('mihomo.always')}</SelectItem>
</Select>
</SettingItem>
</SettingCard>

View File

@ -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 (
<BasePage
ref={pageRef}
title="覆写"
title={t('override.title')}
header={
<>
<Button
size="sm"
variant="light"
title="使用文档"
title={t('override.docs')}
isIconOnly
className="app-nodrag"
onPress={() => {
@ -138,7 +140,7 @@ const Override: React.FC = () => {
</Button>
<Button
className="app-nodrag"
title="常用覆写仓库"
title={t('override.repository')}
isIconOnly
variant="light"
size="sm"
@ -180,7 +182,7 @@ const Override: React.FC = () => {
isLoading={importing}
onPress={handleImport}
>
{t('override.import')}
</Button>
<Dropdown>
<DropdownTrigger>
@ -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'
})
}
}}
>
<DropdownItem key="open"></DropdownItem>
<DropdownItem key="new-yaml"> YAML</DropdownItem>
<DropdownItem key="new-js"> JavaScript</DropdownItem>
<DropdownItem key="open">{t('override.actions.open')}</DropdownItem>
<DropdownItem key="new-yaml">{t('override.actions.newYaml')}</DropdownItem>
<DropdownItem key="new-js">{t('override.actions.newJs')}</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>

View File

@ -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: <SubStoreIcon className="text-lg" />,
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 (
<BasePage
ref={pageRef}
title="订阅管理"
title={t('profiles.title')}
header={
<Button
size="sm"
title="更新全部订阅"
title={t('profiles.updateAll')}
className="app-nodrag"
variant="light"
isIconOnly
@ -248,7 +250,7 @@ const Profiles: React.FC = () => {
checked={useProxy}
onValueChange={setUseProxy}
>
{t('profiles.useProxy')}
</Checkbox>
</>
}
@ -262,7 +264,7 @@ const Profiles: React.FC = () => {
isLoading={importing}
onPress={handleImport}
>
{t('profiles.import')}
</Button>
{useSubStore && (
<Dropdown
@ -361,15 +363,15 @@ const Profiles: React.FC = () => {
}
} else if (key === 'new') {
await addProfileItem({
name: '新建订阅',
name: t('profiles.newProfile'),
type: 'local',
file: 'proxies: []\nproxy-groups: []\nrules: []'
})
}
}}
>
<DropdownItem key="open"></DropdownItem>
<DropdownItem key="new"></DropdownItem>
<DropdownItem key="open">{t('profiles.open')}</DropdownItem>
<DropdownItem key="new">{t('profiles.new')}</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>

View File

@ -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 (
<BasePage
title="代理组"
title={t('proxies.card.title')}
header={
<>
<Button
@ -270,11 +272,11 @@ const Proxies: React.FC = () => {
}}
>
{proxyDisplayOrder === 'default' ? (
<TbCircleLetterD className="text-lg" title="默认" />
<TbCircleLetterD className="text-lg" title={t('proxies.order.default')} />
) : proxyDisplayOrder === 'delay' ? (
<MdOutlineSpeed className="text-lg" title="延迟" />
<MdOutlineSpeed className="text-lg" title={t('proxies.order.delay')} />
) : (
<RxLetterCaseCapitalize className="text-lg" title="名称" />
<RxLetterCaseCapitalize className="text-lg" title={t('proxies.order.name')} />
)}
</Button>
<Button
@ -289,9 +291,9 @@ const Proxies: React.FC = () => {
}}
>
{proxyDisplayMode === 'full' ? (
<CgDetailsMore className="text-lg" title="详细信息" />
<CgDetailsMore className="text-lg" title={t('proxies.mode.full')} />
) : (
<CgDetailsLess className="text-lg" title="简洁信息" />
<CgDetailsLess className="text-lg" title={t('proxies.mode.simple')} />
)}
</Button>
</>
@ -301,7 +303,7 @@ const Proxies: React.FC = () => {
<div className="h-full w-full flex justify-center items-center">
<div className="flex flex-col items-center">
<MdDoubleArrow className="text-foreground-500 text-[100px]" />
<h2 className="text-foreground-500 text-[20px]"></h2>
<h2 className="text-foreground-500 text-[20px]">{t('proxies.mode.direct')}</h2>
</div>
</div>
) : (
@ -383,7 +385,7 @@ const Proxies: React.FC = () => {
</Chip>
)}
<CollapseInput
title="搜索节点"
title={t('proxies.search.placeholder')}
value={searchValue[index]}
onValueChange={(v) => {
setSearchValue((prev) => {
@ -394,7 +396,7 @@ const Proxies: React.FC = () => {
}}
/>
<Button
title="定位到当前节点"
title={t('proxies.locate')}
variant="light"
size="sm"
isIconOnly
@ -424,7 +426,7 @@ const Proxies: React.FC = () => {
<FaLocationCrosshairs className="text-lg text-foreground-500" />
</Button>
<Button
title="延迟测试"
title={t('proxies.delay.test')}
variant="light"
isLoading={delaying[index]}
size="sm"

View File

@ -2,9 +2,13 @@ import BasePage from '@renderer/components/base/base-page'
import GeoData from '@renderer/components/resources/geo-data'
import ProxyProvider from '@renderer/components/resources/proxy-provider'
import RuleProvider from '@renderer/components/resources/rule-provider'
import { useTranslation } from 'react-i18next'
const Resources: React.FC = () => {
const { t } = useTranslation()
return (
<BasePage title="外部资源">
<BasePage title={t('sider.cards.resources')}>
<GeoData />
<ProxyProvider />
<RuleProvider />

View File

@ -5,10 +5,12 @@ import { useMemo, useState } from 'react'
import { Divider, Input } from '@nextui-org/react'
import { useRules } from '@renderer/hooks/use-rules'
import { includesIgnoreCase } from '@renderer/utils/includes'
import { useTranslation } from 'react-i18next'
const Rules: React.FC = () => {
const { rules } = useRules()
const [filter, setFilter] = useState('')
const { t } = useTranslation()
const filteredRules = useMemo(() => {
if (!rules) return []
@ -23,13 +25,13 @@ const Rules: React.FC = () => {
}, [rules, filter])
return (
<BasePage title="分流规则">
<BasePage title={t('rules.title')}>
<div className="sticky top-0 z-40">
<div className="flex p-2">
<Input
size="sm"
value={filter}
placeholder="筛选过滤"
placeholder={t('rules.filter')}
isClearable
onValueChange={setFilter}
/>

View File

@ -10,18 +10,21 @@ import ShortcutConfig from '@renderer/components/settings/shortcut-config'
import { FaTelegramPlane } from 'react-icons/fa'
import SiderConfig from '@renderer/components/settings/sider-config'
import SubStoreConfig from '@renderer/components/settings/substore-config'
import { useTranslation } from 'react-i18next'
const Settings: React.FC = () => {
const { t } = useTranslation()
return (
<BasePage
title="应用设置"
title={t('settings.title')}
header={
<>
<Button
isIconOnly
size="sm"
variant="light"
title="官方文档"
title={t('settings.links.docs')}
className="app-nodrag"
onPress={() => {
window.open('https://mihomo.party')
@ -34,7 +37,7 @@ const Settings: React.FC = () => {
size="sm"
variant="light"
className="app-nodrag"
title="GitHub 仓库"
title={t('settings.links.github')}
onPress={() => {
window.open('https://github.com/mihomo-party-org/mihomo-party')
}}
@ -46,7 +49,7 @@ const Settings: React.FC = () => {
size="sm"
variant="light"
className="app-nodrag"
title="Telegram 群组"
title={t('settings.links.telegram')}
onPress={() => {
window.open('https://t.me/mihomo_party_group')
}}

View File

@ -6,8 +6,10 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import { restartCore } from '@renderer/utils/ipc'
import React, { ReactNode, useState } from 'react'
import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next'
const Sniffer: React.FC = () => {
const { t } = useTranslation()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { sniffer } = controledMihomoConfig || {}
const {
@ -118,7 +120,7 @@ const Sniffer: React.FC = () => {
return (
<BasePage
title="域名嗅探设置"
title={t('sniffer.title')}
header={
changed && (
<Button
@ -138,13 +140,13 @@ const Sniffer: React.FC = () => {
})
}
>
{t('common.save')}
</Button>
)
}
>
<SettingCard>
<SettingItem title="覆盖连接地址" divider>
<SettingItem title={t('sniffer.overrideDestination')} divider>
<Switch
size="sm"
isSelected={values.overrideDestination}
@ -164,7 +166,7 @@ const Sniffer: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="对真实 IP 映射嗅探" divider>
<SettingItem title={t('sniffer.forceDNSMapping')} divider>
<Switch
size="sm"
isSelected={values.forceDNSMapping}
@ -173,7 +175,7 @@ const Sniffer: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="对未映射 IP 地址嗅探" divider>
<SettingItem title={t('sniffer.parsePureIP')} divider>
<Switch
size="sm"
isSelected={values.parsePureIP}
@ -182,51 +184,51 @@ const Sniffer: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="HTTP 端口嗅探" divider>
<SettingItem title={t('sniffer.sniff.title')} divider>
<Input
size="sm"
className="w-[50%]"
placeholder="端口号,使用英文逗号分割"
placeholder={t('sniffer.sniff.ports.placeholder')}
value={values.sniff.HTTP?.ports.join(',')}
onValueChange={(v) => handleSniffPortChange('HTTP', v)}
/>
</SettingItem>
<SettingItem title="TLS 端口嗅探" divider>
<SettingItem title={t('sniffer.sniff.tls')} divider>
<Input
size="sm"
className="w-[50%]"
placeholder="端口号,使用英文逗号分割"
placeholder={t('sniffer.sniff.ports.placeholder')}
value={values.sniff.TLS?.ports.join(',')}
onValueChange={(v) => handleSniffPortChange('TLS', v)}
/>
</SettingItem>
<SettingItem title="QUIC 端口嗅探" divider>
<SettingItem title={t('sniffer.sniff.quic')} divider>
<Input
size="sm"
className="w-[50%]"
placeholder="端口号,使用英文逗号分割"
placeholder={t('sniffer.sniff.ports.placeholder')}
value={values.sniff.QUIC?.ports.join(',')}
onValueChange={(v) => handleSniffPortChange('QUIC', v)}
/>
</SettingItem>
<div className="flex flex-col items-stretch">
<h3></h3>
{renderListInputs('skipDomain', '例:+.push.apple.com')}
<h3>{t('sniffer.skipDomain.title')}</h3>
{renderListInputs('skipDomain', t('sniffer.skipDomain.placeholder'))}
</div>
<Divider className="my-2" />
<div className="flex flex-col items-stretch">
<h3 className="mb-2"></h3>
{renderListInputs('forceDomain', '例v2ex.com')}
<h3 className="mb-2">{t('sniffer.forceDomain.title')}</h3>
{renderListInputs('forceDomain', t('sniffer.forceDomain.placeholder'))}
</div>
<Divider className="my-2" />
<div className="flex flex-col items-stretch">
<h3 className="mb-2"></h3>
{renderListInputs('skipDstAddress', '例1.1.1.1/32')}
<h3 className="mb-2">{t('sniffer.skipDstAddress.title')}</h3>
{renderListInputs('skipDstAddress', t('sniffer.skipDstAddress.placeholder'))}
</div>
<Divider className="my-2" />
<div className="flex flex-col items-stretch">
<h3 className="mb-2"></h3>
{renderListInputs('skipSrcAddress', '例192.168.1.1/24')}
<h3 className="mb-2">{t('sniffer.skipSrcAddress.title')}</h3>
{renderListInputs('skipSrcAddress', t('sniffer.skipSrcAddress.placeholder'))}
</div>
</SettingCard>
</BasePage>

View File

@ -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<number | undefined>()
@ -23,10 +25,10 @@ const SubStore: React.FC = () => {
return (
<>
<BasePage
title="Sub-Store"
title={t('substore.title')}
header={
<Button
title="在浏览器中打开"
title={t('substore.openInBrowser')}
isIconOnly
size="sm"
className="app-nodrag"

View File

@ -9,6 +9,7 @@ import { openUWPTool, triggerSysProxy } from '@renderer/utils/ipc'
import { Key, useState } from 'react'
import React from 'react'
import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next'
const defaultBypass: string[] =
platform === 'linux'
@ -55,6 +56,7 @@ function FindProxyForURL(url, host) {
`
const Sysproxy: React.FC = () => {
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 (
<BasePage
title="系统代理设置"
title={t('sysproxy.title')}
header={
changed && (
<Button color="primary" className="app-nodrag" size="sm" onPress={onSave}>
{t('common.save')}
</Button>
)
}
@ -124,68 +126,68 @@ const Sysproxy: React.FC = () => {
/>
)}
<SettingCard className="sysproxy-settings">
<SettingItem title="代理主机" divider>
<SettingItem title={t('sysproxy.host.title')} divider>
<Input
size="sm"
className="w-[50%]"
value={values.host}
placeholder="默认 127.0.0.1 若无特殊需求请勿修改"
placeholder={t('sysproxy.host.placeholder')}
onValueChange={(v) => {
setValues({ ...values, host: v })
}}
/>
</SettingItem>
<SettingItem title="代理模式" divider>
<SettingItem title={t('sysproxy.mode.title')} divider>
<Tabs
size="sm"
color="primary"
selectedKey={values.mode}
onSelectionChange={(key: Key) => setValues({ ...values, mode: key as SysProxyMode })}
>
<Tab key="manual" title="手动" />
<Tab key="auto" title="PAC" />
<Tab key="manual" title={t('sysproxy.mode.manual')} />
<Tab key="auto" title={t('sysproxy.mode.pac')} />
</Tabs>
</SettingItem>
{platform === 'win32' && (
<SettingItem title="UWP 工具" divider>
<SettingItem title={t('sysproxy.uwp.title')} divider>
<Button
size="sm"
onPress={async () => {
await openUWPTool()
}}
>
UWP
{t('sysproxy.uwp.open')}
</Button>
</SettingItem>
)}
{values.mode === 'auto' && (
<SettingItem title="代理模式">
<SettingItem title={t('sysproxy.mode.title')}>
<Button size="sm" onPress={() => setOpenPacEditor(true)} variant="bordered">
PAC
{t('sysproxy.pac.edit')}
</Button>
</SettingItem>
)}
{values.mode === 'manual' && (
<>
<SettingItem title="添加默认代理绕过" divider>
<SettingItem title={t('sysproxy.bypass.addDefault')} divider>
<Button
size="sm"
onPress={() => {
setValues({ ...values, bypass: defaultBypass.concat(values.bypass) })
}}
>
{t('sysproxy.bypass.addDefault')}
</Button>
</SettingItem>
<div className="flex flex-col items-stretch">
<h3 className="mb-2"></h3>
<h3 className="mb-2">{t('sysproxy.bypass.title')}</h3>
{[...values.bypass, ''].map((domain, index) => (
<div key={index} className="mb-2 flex">
<Input
fullWidth
size="sm"
placeholder="例: *.baidu.com"
placeholder={t('sysproxy.bypass.placeholder')}
value={domain}
onValueChange={(v) => handleBypassChange(v, index)}
/>

View File

@ -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 = () => {
/>
)}
<BasePage
title="虚拟网卡设置"
title={t('tun.title')}
header={
changed && (
<Button
@ -108,14 +110,14 @@ const Tun: React.FC = () => {
})
}
>
{t('common.save')}
</Button>
)
}
>
<SettingCard className="tun-settings">
{platform === 'win32' && (
<SettingItem title="重设防火墙" divider>
<SettingItem title={t('tun.firewall.title')} divider>
<Button
size="sm"
color="primary"
@ -124,7 +126,7 @@ const Tun: React.FC = () => {
setLoading(true)
try {
await setupFirewall()
new Notification('防火墙重设成功')
new Notification(t('tun.notifications.firewallResetSuccess'))
await restartCore()
} catch (e) {
alert(e)
@ -133,12 +135,12 @@ const Tun: React.FC = () => {
}
}}
>
{t('tun.firewall.reset')}
</Button>
</SettingItem>
)}
{platform !== 'win32' && (
<SettingItem title="手动授权内核" divider>
<SettingItem title={t('tun.core.title')} divider>
<Button
size="sm"
color="primary"
@ -146,7 +148,7 @@ const Tun: React.FC = () => {
if (platform === 'darwin') {
try {
await manualGrantCorePermition()
new Notification('内核授权成功')
new Notification(t('tun.notifications.coreAuthSuccess'))
await restartCore()
} catch (e) {
alert(e)
@ -156,12 +158,12 @@ const Tun: React.FC = () => {
}
}}
>
{t('tun.core.auth')}
</Button>
</SettingItem>
)}
{platform === 'darwin' && (
<SettingItem title="自动设置系统DNS" divider>
<SettingItem title={t('tun.dns.autoSet')} divider>
<Switch
size="sm"
isSelected={autoSetDNS}
@ -172,7 +174,7 @@ const Tun: React.FC = () => {
</SettingItem>
)}
<SettingItem title="Tun 模式堆栈" divider>
<SettingItem title={t('tun.stack.title')} divider>
<Tabs
size="sm"
color="primary"
@ -185,7 +187,7 @@ const Tun: React.FC = () => {
</Tabs>
</SettingItem>
{platform !== 'darwin' && (
<SettingItem title="Tun 网卡名称" divider>
<SettingItem title={t('tun.device.title')} divider>
<Input
size="sm"
className="w-[100px]"
@ -197,7 +199,7 @@ const Tun: React.FC = () => {
</SettingItem>
)}
<SettingItem title="严格路由" divider>
<SettingItem title={t('tun.strictRoute')} divider>
<Switch
size="sm"
isSelected={values.strictRoute}
@ -206,7 +208,7 @@ const Tun: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="自动设置全局路由" divider>
<SettingItem title={t('tun.autoRoute')} divider>
<Switch
size="sm"
isSelected={values.autoRoute}
@ -216,7 +218,7 @@ const Tun: React.FC = () => {
/>
</SettingItem>
{platform === 'linux' && (
<SettingItem title="自动设置TCP重定向" divider>
<SettingItem title={t('tun.autoRedirect')} divider>
<Switch
size="sm"
isSelected={values.autoRedirect}
@ -226,7 +228,7 @@ const Tun: React.FC = () => {
/>
</SettingItem>
)}
<SettingItem title="自动选择流量出口接口" divider>
<SettingItem title={t('tun.autoDetectInterface')} divider>
<Switch
size="sm"
isSelected={values.autoDetectInterface}
@ -246,7 +248,7 @@ const Tun: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title="DNS 劫持" divider>
<SettingItem title={t('tun.dnsHijack')} divider>
<Input
size="sm"
className="w-[50%]"
@ -258,13 +260,13 @@ const Tun: React.FC = () => {
/>
</SettingItem>
<div className="flex flex-col items-stretch">
<h3 className="mb-2"></h3>
<h3 className="mb-2">{t('tun.excludeAddress.title')}</h3>
{[...values.routeExcludeAddress, ''].map((address, index) => (
<div key={index} className="mb-2 flex">
<Input
fullWidth
size="sm"
placeholder="例: 172.20.0.0/16"
placeholder={t('tun.excludeAddress.placeholder')}
value={address}
onValueChange={(v) => handleExcludeAddressChange(v, index)}
/>

View File

@ -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

31
src/shared/i18n.ts Normal file
View File

@ -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<typeof i18next> => {
await i18next.init({
...defaultConfig,
...options
})
return i18next
}
export default i18next

View File

@ -291,6 +291,7 @@ interface IAppConfig {
directModeShortcut?: string
restartAppShortcut?: string
quitWithoutCoreShortcut?: string
language?: 'zh-CN' | 'en-US'
}
interface IMihomoTunConfig {

View File

@ -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"]

View File

@ -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,