Skip to content
This repository has been archived by the owner on Feb 5, 2025. It is now read-only.

Commit

Permalink
feat: add i18n support with English translations
Browse files Browse the repository at this point in the history
  • Loading branch information
pompurin404 committed Feb 3, 2025
1 parent fa3c412 commit ec6dfae
Show file tree
Hide file tree
Showing 70 changed files with 2,138 additions and 554 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
55 changes: 55 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import path from 'path'
import { startMonitor } from './resolve/trafficMonitor'
import { showFloatingWindow } from './resolve/floatingWindow'
import iconv from 'iconv-lite'
import { initI18n } from '../shared/i18n'

let quitTimeout: NodeJS.Timeout | null = null
export let mainWindow: BrowserWindow | null = null
Expand Down Expand Up @@ -122,6 +123,8 @@ app.whenReady().then(async () => {
// Set app user model id for windows
electronApp.setAppUserModelId('party.mihomo.app')
try {
const appConfig = await getAppConfig()
await initI18n({ lng: appConfig.language }) // 从配置中读取语言设置
await initPromise
} catch (e) {
dialog.showErrorBox('应用初始化失败', `${e}`)
Expand Down
42 changes: 24 additions & 18 deletions src/main/resolve/tray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -86,7 +92,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
{
id: 'show',
accelerator: showWindowShortcut,
label: '显示窗口',
label: t('tray.showWindow'),
type: 'normal',
click: (): void => {
showMainWindow()
Expand All @@ -95,15 +101,15 @@ 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()
}
},
{
id: 'rule',
label: '规则模式',
label: t('tray.ruleMode'),
accelerator: ruleModeShortcut,
type: 'radio',
checked: mode === 'rule',
Expand All @@ -117,7 +123,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
},
{
id: 'global',
label: '全局模式',
label: t('tray.globalMode'),
accelerator: globalModeShortcut,
type: 'radio',
checked: mode === 'global',
Expand All @@ -131,7 +137,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
},
{
id: 'direct',
label: '直连模式',
label: t('tray.directMode'),
accelerator: directModeShortcut,
type: 'radio',
checked: mode === 'direct',
Expand All @@ -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> => {
Expand All @@ -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> => {
Expand All @@ -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',
Expand All @@ -208,34 +214,34 @@ 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())
}
]
},
envType.length > 1
? {
type: 'submenu',
label: '复制环境变量',
label: t('tray.copyEnv'),
submenu: envType.map((type) => {
return {
id: type,
Expand All @@ -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])
Expand All @@ -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 => {
Expand All @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions src/main/utils/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
})
}
7 changes: 5 additions & 2 deletions src/renderer/src/components/base/base-error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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>
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/src/components/base/base-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading

0 comments on commit ec6dfae

Please sign in to comment.