feat: remove hardcoded Chinese strings and improve i18n coverage

This commit is contained in:
xmk23333 2025-12-28 12:34:13 +08:00
parent 573be5501e
commit 2467306903
18 changed files with 155 additions and 25 deletions

View File

@ -13,6 +13,8 @@ import { join } from 'path'
import { app } from 'electron'
import { mihomoUpgradeConfig } from '../core/mihomoApi'
import i18next from 'i18next'
let profileConfig: IProfileConfig // profile.yaml
// 最终选中订阅ID
let targetProfileId: string | null = null
@ -33,7 +35,8 @@ export async function setProfileConfig(config: IProfileConfig): Promise<void> {
export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> {
const { items } = await getProfileConfig()
if (!id || id === 'default') return { id: 'default', type: 'local', name: '空白订阅' }
if (!id || id === 'default')
return { id: 'default', type: 'local', name: i18next.t('profiles.emptyProfile') }
return items.find((item) => item.id === id)
}
@ -126,7 +129,13 @@ export async function removeProfileItem(id: string): Promise<void> {
export async function getCurrentProfileItem(): Promise<IProfileItem> {
const { current } = await getProfileConfig()
return (await getProfileItem(current)) || { id: 'default', type: 'local', name: '空白订阅' }
return (
(await getProfileItem(current)) || {
id: 'default',
type: 'local',
name: i18next.t('profiles.emptyProfile')
}
)
}
interface FetchOptions {

View File

@ -11,6 +11,7 @@ import { exec, execSync, spawn } from 'child_process'
import { promisify } from 'util'
import { appLogger } from '../utils/logger'
import { checkAdminPrivileges } from '../core/manager'
import i18next from 'i18next'
export async function checkUpdate(): Promise<IAppVersion | undefined> {
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
@ -68,7 +69,7 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
file = file.replace('-setup.exe', '-portable.7z')
}
if (!file) {
throw new Error('不支持自动更新,请手动下载更新')
throw new Error(i18next.t('common.error.autoUpdateNotSupported'))
}
if (process.platform === 'win32' && parseInt(os.release()) < 10) {
file = file.replace('windows', 'win7')

View File

@ -11,10 +11,11 @@ import {
profilePath,
resourcesDir
} from '../utils/dirs'
import i18next from 'i18next'
export function getFilePath(ext: string[]): string[] | undefined {
return dialog.showOpenDialogSync({
title: '选择订阅文件',
title: i18next.t('common.dialog.selectSubscriptionFile'),
filters: [{ name: `${ext} file`, extensions: ext }],
properties: ['openFile']
})

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState, useCallback } from 'react'
import { createPortal } from 'react-dom'
import { IoCheckmark, IoClose, IoAlertSharp, IoInformationSharp, IoCopy } from 'react-icons/io5'
import i18next from 'i18next'
type ToastType = 'success' | 'error' | 'warning' | 'info'
@ -125,7 +126,9 @@ const ToastItem: React.FC<{
>
{icon}
</div>
<p className="text-base font-semibold text-foreground">{data.title || '错误'}</p>
<p className="text-base font-semibold text-foreground">
{data.title || i18next.t('common.error.default')}
</p>
</div>
<div className="relative" style={{ zIndex: 99999 }}>
<button
@ -145,7 +148,7 @@ const ToastItem: React.FC<{
className={`absolute top-full mt-1 left-1/2 -translate-x-1/2 px-2 py-1 text-xs text-foreground bg-content2 border border-default-200 rounded shadow-md whitespace-nowrap transition-all duration-200 ${copied ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-1 pointer-events-none'}`}
style={{ zIndex: 99999 }}
>
{i18next.t('common.copied')}
</div>
</div>
</div>
@ -158,7 +161,7 @@ const ToastItem: React.FC<{
onClick={handleClose}
className="self-end px-4 py-1.5 text-sm font-medium text-white bg-danger rounded-lg hover:bg-danger/90 transition-colors"
>
{i18next.t('common.ok')}
</button>
</div>
)

View File

@ -1,4 +1,5 @@
import React, { createContext, useContext, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { showError } from '@renderer/utils/error-display'
import useSWR from 'swr'
import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc'
@ -12,13 +13,14 @@ interface AppConfigContextType {
const AppConfigContext = createContext<AppConfigContextType | undefined>(undefined)
export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { t } = useTranslation()
const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig())
const patchAppConfig = async (value: Partial<IAppConfig>): Promise<void> => {
try {
await patch(value)
} catch (e) {
await showError(e, '更新应用配置失败')
await showError(e, t('common.error.updateAppConfigFailed'))
} finally {
mutateAppConfig()
}

View File

@ -1,4 +1,5 @@
import React, { createContext, useContext, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { showError } from '@renderer/utils/error-display'
import useSWR from 'swr'
import { getControledMihomoConfig, patchControledMihomoConfig as patch } from '@renderer/utils/ipc'
@ -14,6 +15,7 @@ const ControledMihomoConfigContext = createContext<ControledMihomoConfigContextT
)
export const ControledMihomoConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { t } = useTranslation()
const { data: controledMihomoConfig, mutate: mutateControledMihomoConfig } = useSWR(
'getControledMihomoConfig',
() => getControledMihomoConfig()
@ -23,7 +25,7 @@ export const ControledMihomoConfigProvider: React.FC<{ children: ReactNode }> =
try {
await patch(value)
} catch (e) {
await showError(e, '更新内核配置失败')
await showError(e, t('common.error.updateCoreConfigFailed'))
} finally {
mutateControledMihomoConfig()
}

View File

@ -1,4 +1,5 @@
import React, { createContext, useContext, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { showError } from '@renderer/utils/error-display'
import useSWR from 'swr'
import {
@ -21,6 +22,7 @@ interface OverrideConfigContextType {
const OverrideConfigContext = createContext<OverrideConfigContextType | undefined>(undefined)
export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { t } = useTranslation()
const { data: overrideConfig, mutate: mutateOverrideConfig } = useSWR('getOverrideConfig', () =>
getOverrideConfig()
)
@ -29,7 +31,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
try {
await set(config)
} catch (e) {
await showError(e, '保存覆写配置失败')
await showError(e, t('common.error.saveOverrideConfigFailed'))
} finally {
mutateOverrideConfig()
}
@ -39,7 +41,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
try {
await add(item)
} catch (e) {
await showError(e, '添加覆写失败')
await showError(e, t('common.error.addOverrideFailed'))
} finally {
mutateOverrideConfig()
}
@ -49,7 +51,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
try {
await remove(id)
} catch (e) {
await showError(e, '删除覆写失败')
await showError(e, t('common.error.deleteOverrideFailed'))
} finally {
mutateOverrideConfig()
}
@ -59,7 +61,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
try {
await update(item)
} catch (e) {
await showError(e, '更新覆写失败')
await showError(e, t('common.error.updateOverrideFailed'))
} finally {
mutateOverrideConfig()
}

View File

@ -1,4 +1,5 @@
import React, { createContext, ReactNode, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { showError } from '@renderer/utils/error-display'
import useSWR from 'swr'
import {
@ -23,6 +24,7 @@ interface ProfileConfigContextType {
const ProfileConfigContext = createContext<ProfileConfigContextType | undefined>(undefined)
export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { t } = useTranslation()
const { data: profileConfig, mutate: mutateProfileConfig } = useSWR('getProfileConfig', () =>
getProfileConfig()
)
@ -33,7 +35,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
try {
await set(config)
} catch (e) {
await showError(e, '保存配置失败')
await showError(e, t('common.error.saveProfileConfigFailed'))
} finally {
mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu')
@ -44,7 +46,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
try {
await add(item)
} catch (e) {
await showError(e, '添加配置失败')
await showError(e, t('common.error.addProfileFailed'))
} finally {
mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu')
@ -55,7 +57,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
try {
await remove(id)
} catch (e) {
await showError(e, '删除配置失败')
await showError(e, t('common.error.deleteProfileFailed'))
} finally {
mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu')
@ -66,7 +68,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
try {
await update(item)
} catch (e) {
await showError(e, '更新配置失败')
await showError(e, t('common.error.updateProfileFailed'))
} finally {
mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu')
@ -108,7 +110,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
if (errorMsg.includes('reply was never sent')) {
setTimeout(() => mutateProfileConfig(), 1000)
} else {
await showError(errorMsg, '切换配置失败')
await showError(errorMsg, t('common.error.switchProfileFailed'))
mutateProfileConfig()
}
} finally {

View File

@ -39,6 +39,27 @@
"common.error.shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}",
"common.error.adminRequired": "Please run with administrator privileges for first launch",
"common.error.initFailed": "Application initialization failed",
"common.error.default": "Error",
"common.error.updateAppConfigFailed": "Failed to update app config",
"common.error.updateCoreConfigFailed": "Failed to update core config",
"common.error.saveProfileConfigFailed": "Failed to save profile config",
"common.error.addProfileFailed": "Failed to add profile",
"common.error.deleteProfileFailed": "Failed to delete profile",
"common.error.updateProfileFailed": "Failed to update profile",
"common.error.switchProfileFailed": "Failed to switch profile",
"common.error.saveOverrideConfigFailed": "Failed to save override config",
"common.error.addOverrideFailed": "Failed to add override",
"common.error.deleteOverrideFailed": "Failed to delete override",
"common.error.updateOverrideFailed": "Failed to update override",
"common.error.firewallSetupFailed": "Failed to setup firewall",
"common.error.coreAuthFailed": "Failed to authorize core",
"common.error.sysproxySetupFailed": "Failed to setup system proxy",
"common.error.snifferConfigSaveFailed": "Failed to save sniffer config",
"common.error.dnsConfigSaveFailed": "Failed to save DNS config",
"common.copied": "Copied",
"common.ok": "OK",
"common.error.autoUpdateNotSupported": "Auto update not supported, please download manually",
"common.dialog.selectSubscriptionFile": "Select Subscription File",
"core.highPrivilege.title": "High Privilege Core Detected",
"core.highPrivilege.message": "A high-privilege core is detected. The application needs to restart in administrator mode to match permissions. Restart now?",
"common.updater.versionReady": "v{{version}} Version Ready",

View File

@ -39,6 +39,27 @@
"common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}",
"common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید",
"common.error.initFailed": "راه‌اندازی برنامه با خطا مواجه شد",
"common.error.default": "خطا",
"common.error.updateAppConfigFailed": "به‌روزرسانی تنظیمات برنامه با خطا مواجه شد",
"common.error.updateCoreConfigFailed": "به‌روزرسانی تنظیمات هسته با خطا مواجه شد",
"common.error.saveProfileConfigFailed": "ذخیره تنظیمات پروفایل با خطا مواجه شد",
"common.error.addProfileFailed": "افزودن پروفایل با خطا مواجه شد",
"common.error.deleteProfileFailed": "حذف پروفایل با خطا مواجه شد",
"common.error.updateProfileFailed": "به‌روزرسانی پروفایل با خطا مواجه شد",
"common.error.switchProfileFailed": "تغییر پروفایل با خطا مواجه شد",
"common.error.saveOverrideConfigFailed": "ذخیره تنظیمات بازنویسی با خطا مواجه شد",
"common.error.addOverrideFailed": "افزودن بازنویسی با خطا مواجه شد",
"common.error.deleteOverrideFailed": "حذف بازنویسی با خطا مواجه شد",
"common.error.updateOverrideFailed": "به‌روزرسانی بازنویسی با خطا مواجه شد",
"common.error.firewallSetupFailed": "تنظیم فایروال با خطا مواجه شد",
"common.error.coreAuthFailed": "احراز هویت هسته با خطا مواجه شد",
"common.error.sysproxySetupFailed": "تنظیم پروکسی سیستم با خطا مواجه شد",
"common.error.snifferConfigSaveFailed": "ذخیره تنظیمات اسنیفر با خطا مواجه شد",
"common.error.dnsConfigSaveFailed": "ذخیره تنظیمات DNS با خطا مواجه شد",
"common.copied": "کپی شد",
"common.ok": "تأیید",
"common.error.autoUpdateNotSupported": "به‌روزرسانی خودکار پشتیبانی نمی‌شود، لطفاً به صورت دستی دانلود کنید",
"common.dialog.selectSubscriptionFile": "انتخاب فایل اشتراک",
"core.highPrivilege.title": "هسته با سطح دسترسی بالا شناسایی شد",
"core.highPrivilege.message": "هسته‌ای با سطح دسترسی بالا شناسایی شد. برنامه باید در حالت مدیر سیستم برای تطابق سطح دسترسی‌ها دوباره راه‌اندازی شود. آیا می‌خواهید اکنون راه‌اندازی مجدد شود؟",
"common.updater.versionReady": "نسخه v{{version}} آماده است",

View File

@ -39,6 +39,27 @@
"common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}",
"common.error.adminRequired": "Для первого запуска требуются права администратора",
"common.error.initFailed": "Не удалось инициализировать приложение",
"common.error.default": "Ошибка",
"common.error.updateAppConfigFailed": "Не удалось обновить конфигурацию приложения",
"common.error.updateCoreConfigFailed": "Не удалось обновить конфигурацию ядра",
"common.error.saveProfileConfigFailed": "Не удалось сохранить конфигурацию профиля",
"common.error.addProfileFailed": "Не удалось добавить профиль",
"common.error.deleteProfileFailed": "Не удалось удалить профиль",
"common.error.updateProfileFailed": "Не удалось обновить профиль",
"common.error.switchProfileFailed": "Не удалось переключить профиль",
"common.error.saveOverrideConfigFailed": "Не удалось сохранить конфигурацию переопределения",
"common.error.addOverrideFailed": "Не удалось добавить переопределение",
"common.error.deleteOverrideFailed": "Не удалось удалить переопределение",
"common.error.updateOverrideFailed": "Не удалось обновить переопределение",
"common.error.firewallSetupFailed": "Не удалось настроить брандмауэр",
"common.error.coreAuthFailed": "Не удалось авторизовать ядро",
"common.error.sysproxySetupFailed": "Не удалось настроить системный прокси",
"common.error.snifferConfigSaveFailed": "Не удалось сохранить конфигурацию сниффера",
"common.error.dnsConfigSaveFailed": "Не удалось сохранить конфигурацию DNS",
"common.copied": "Скопировано",
"common.ok": "ОК",
"common.error.autoUpdateNotSupported": "Автообновление не поддерживается, пожалуйста, скачайте вручную",
"common.dialog.selectSubscriptionFile": "Выберите файл подписки",
"core.highPrivilege.title": "High Privilege Core Detected",
"core.highPrivilege.message": "Обнаружено ядро с повышенными привилегиями. Приложение необходимо перезапустить в режиме администратора для согласования прав. Перезапустить сейчас?",
"common.updater.versionReady": "Обнаружено ядро с повышенными привилегиями",

View File

@ -39,6 +39,27 @@
"common.error.shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}",
"common.error.adminRequired": "首次启动请以管理员权限运行",
"common.error.initFailed": "应用初始化失败",
"common.error.default": "错误",
"common.error.updateAppConfigFailed": "更新应用配置失败",
"common.error.updateCoreConfigFailed": "更新内核配置失败",
"common.error.saveProfileConfigFailed": "保存配置失败",
"common.error.addProfileFailed": "添加配置失败",
"common.error.deleteProfileFailed": "删除配置失败",
"common.error.updateProfileFailed": "更新配置失败",
"common.error.switchProfileFailed": "切换配置失败",
"common.error.saveOverrideConfigFailed": "保存覆写配置失败",
"common.error.addOverrideFailed": "添加覆写失败",
"common.error.deleteOverrideFailed": "删除覆写失败",
"common.error.updateOverrideFailed": "更新覆写失败",
"common.error.firewallSetupFailed": "防火墙设置失败",
"common.error.coreAuthFailed": "内核授权失败",
"common.error.sysproxySetupFailed": "系统代理设置失败",
"common.error.snifferConfigSaveFailed": "嗅探配置保存失败",
"common.error.dnsConfigSaveFailed": "DNS 配置保存失败",
"common.copied": "已复制",
"common.ok": "确定",
"common.error.autoUpdateNotSupported": "不支持自动更新,请手动下载更新",
"common.dialog.selectSubscriptionFile": "选择订阅文件",
"core.highPrivilege.title": "检测到高权限内核",
"core.highPrivilege.message": "检测到运行中的高权限内核,需以管理员模式重启应用以匹配权限,确定重启?",
"common.updater.versionReady": "v{{version}} 版本就绪",

View File

@ -39,6 +39,27 @@
"common.error.shortcutRegistrationFailedWithError": "快捷鍵註冊失敗:{{error}}",
"common.error.adminRequired": "首次啟動請以系統管理員身分執行",
"common.error.initFailed": "應用初始化失敗",
"common.error.default": "錯誤",
"common.error.updateAppConfigFailed": "更新應用配置失敗",
"common.error.updateCoreConfigFailed": "更新內核配置失敗",
"common.error.saveProfileConfigFailed": "保存配置失敗",
"common.error.addProfileFailed": "添加配置失敗",
"common.error.deleteProfileFailed": "刪除配置失敗",
"common.error.updateProfileFailed": "更新配置失敗",
"common.error.switchProfileFailed": "切換配置失敗",
"common.error.saveOverrideConfigFailed": "保存覆寫配置失敗",
"common.error.addOverrideFailed": "添加覆寫失敗",
"common.error.deleteOverrideFailed": "刪除覆寫失敗",
"common.error.updateOverrideFailed": "更新覆寫失敗",
"common.error.firewallSetupFailed": "防火牆設置失敗",
"common.error.coreAuthFailed": "內核授權失敗",
"common.error.sysproxySetupFailed": "系統代理設置失敗",
"common.error.snifferConfigSaveFailed": "嗅探配置保存失敗",
"common.error.dnsConfigSaveFailed": "DNS 配置保存失敗",
"common.copied": "已複製",
"common.ok": "確定",
"common.error.autoUpdateNotSupported": "不支持自動更新,請手動下載更新",
"common.dialog.selectSubscriptionFile": "選擇訂閱文件",
"core.highPrivilege.title": "檢測到高權限內核",
"core.highPrivilege.message": "檢測到運行中的高權限內核,需以系統管理員模式重新啟動應用程式以匹配權限,確定重新啟動?",
"common.updater.versionReady": "v{{version}} 版本就緒",

View File

@ -146,7 +146,7 @@ const DNS: React.FC = () => {
await restartCore()
}
} catch (e) {
showErrorSync(e, 'DNS 配置保存失败')
showErrorSync(e, t('common.error.dnsConfigSaveFailed'))
}
}

View File

@ -71,7 +71,7 @@ const Sniffer: React.FC = () => {
await restartCore()
}
} catch (e) {
showErrorSync(e, '嵅探配置保存失败')
showErrorSync(e, t('common.error.snifferConfigSaveFailed'))
}
}

View File

@ -106,7 +106,7 @@ const Sysproxy: React.FC = () => {
} catch (e) {
setValues({ ...values, enable: previousState })
setChanged(true)
showErrorSync(e, '系统代理设置失败')
showErrorSync(e, t('common.error.sysproxySetupFailed'))
await patchAppConfig({ sysProxy: { enable: false } })
}

View File

@ -113,7 +113,7 @@ const Tun: React.FC = () => {
new Notification(t('tun.notifications.firewallResetSuccess'))
await restartCore()
} catch (e) {
showErrorSync(e, '防火墙设置失败')
showErrorSync(e, t('common.error.firewallSetupFailed'))
} finally {
setLoading(false)
}
@ -134,7 +134,7 @@ const Tun: React.FC = () => {
new Notification(t('tun.notifications.coreAuthSuccess'))
await restartCore()
} catch (e) {
showErrorSync(e, '内核授权失败')
showErrorSync(e, t('common.error.coreAuthFailed'))
}
}}
>

View File

@ -1,4 +1,5 @@
import { toast } from '@renderer/components/base/toast'
import i18next from 'i18next'
const DETAILED_ERROR_KEYWORDS = [
'yaml',
@ -33,9 +34,10 @@ function shouldShowDetailedError(message: string): boolean {
export async function showError(error: unknown, title?: string): Promise<void> {
const message = error instanceof Error ? error.message : String(error)
const defaultTitle = i18next.t('common.error.default')
if (shouldShowDetailedError(message)) {
toast.detailedError(message, title || '错误')
toast.detailedError(message, title || defaultTitle)
} else {
toast.error(message, title)
}
@ -43,9 +45,10 @@ export async function showError(error: unknown, title?: string): Promise<void> {
export function showErrorSync(error: unknown, title?: string): void {
const message = error instanceof Error ? error.message : String(error)
const defaultTitle = i18next.t('common.error.default')
if (shouldShowDetailedError(message)) {
toast.detailedError(message, title || '错误')
toast.detailedError(message, title || defaultTitle)
} else {
toast.error(message, title)
}