mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 11:02:08 +08:00
chore: update i18n
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function ErrorFallback({ error }: FallbackProps) {
|
||||
const { t } = useTranslation()
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorStack = error instanceof Error ? error.stack : undefined
|
||||
|
||||
return (
|
||||
<div role="alert" style={{ padding: 16 }}>
|
||||
<h4>Something went wrong:(</h4>
|
||||
<h4>{t('shared.feedback.errors.unexpected')}</h4>
|
||||
|
||||
<pre>{errorMessage}</pre>
|
||||
|
||||
<details title="Error Stack">
|
||||
<summary>Error Stack</summary>
|
||||
<details title={t('shared.feedback.errors.stack')}>
|
||||
<summary>{t('shared.feedback.errors.stack')}</summary>
|
||||
<pre>{errorStack}</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -92,26 +92,61 @@ function convertDelayColor(
|
||||
}
|
||||
}
|
||||
|
||||
function getSignalIcon(delay: number): {
|
||||
function getSignalIcon(
|
||||
delay: number,
|
||||
translate: (key: string) => string,
|
||||
): {
|
||||
icon: React.ReactElement
|
||||
text: string
|
||||
color: string
|
||||
} {
|
||||
if (delay === -2)
|
||||
return { icon: <SignalNone />, text: '测试中', color: 'text.secondary' }
|
||||
return {
|
||||
icon: <SignalNone />,
|
||||
text: translate('home.components.currentProxy.status.testing'),
|
||||
color: 'text.secondary',
|
||||
}
|
||||
if (delay === -1)
|
||||
return { icon: <SignalNone />, text: '未测试', color: 'text.secondary' }
|
||||
return {
|
||||
icon: <SignalNone />,
|
||||
text: translate('home.components.currentProxy.status.untested'),
|
||||
color: 'text.secondary',
|
||||
}
|
||||
if (delay > 1e5)
|
||||
return { icon: <SignalError />, text: '错误', color: 'error.main' }
|
||||
return {
|
||||
icon: <SignalError />,
|
||||
text: translate('home.components.currentProxy.status.error'),
|
||||
color: 'error.main',
|
||||
}
|
||||
if (delay === 0 || delay >= 10000)
|
||||
return { icon: <SignalError />, text: '超时', color: 'error.main' }
|
||||
return {
|
||||
icon: <SignalError />,
|
||||
text: translate('home.components.currentProxy.status.timeout'),
|
||||
color: 'error.main',
|
||||
}
|
||||
if (delay >= 500)
|
||||
return { icon: <SignalWeak />, text: '延迟较高', color: 'error.main' }
|
||||
return {
|
||||
icon: <SignalWeak />,
|
||||
text: translate('home.components.currentProxy.status.latencyHigh'),
|
||||
color: 'error.main',
|
||||
}
|
||||
if (delay >= 300)
|
||||
return { icon: <SignalMedium />, text: '延迟中等', color: 'warning.main' }
|
||||
return {
|
||||
icon: <SignalMedium />,
|
||||
text: translate('home.components.currentProxy.status.latencyMedium'),
|
||||
color: 'warning.main',
|
||||
}
|
||||
if (delay >= 200)
|
||||
return { icon: <SignalGood />, text: '延迟良好', color: 'info.main' }
|
||||
return { icon: <SignalStrong />, text: '延迟极佳', color: 'success.main' }
|
||||
return {
|
||||
icon: <SignalGood />,
|
||||
text: translate('home.components.currentProxy.status.latencyGood'),
|
||||
color: 'info.main',
|
||||
}
|
||||
return {
|
||||
icon: <SignalStrong />,
|
||||
text: translate('home.components.currentProxy.status.latencyExcellent'),
|
||||
color: 'success.main',
|
||||
}
|
||||
}
|
||||
|
||||
export const CurrentProxyCard = () => {
|
||||
@@ -378,8 +413,12 @@ export const CurrentProxyCard = () => {
|
||||
// 信号图标(增加非空校验)
|
||||
const signalInfo =
|
||||
currentProxy && selectedGroupName
|
||||
? getSignalIcon(currentDelay)
|
||||
: { icon: <SignalNone />, text: '未初始化', color: 'text.secondary' }
|
||||
? getSignalIcon(currentDelay, t)
|
||||
: {
|
||||
icon: <SignalNone />,
|
||||
text: t('home.components.currentProxy.status.uninitialized'),
|
||||
color: 'text.secondary',
|
||||
}
|
||||
|
||||
const checkCurrentProxyDelay = useCallback(async () => {
|
||||
if (autoCheckInProgressRef.current) return
|
||||
@@ -617,7 +656,7 @@ export const CurrentProxyCard = () => {
|
||||
title={
|
||||
currentProxy
|
||||
? `${signalInfo.text}: ${delayManager.formatDelay(currentDelay)}`
|
||||
: '无代理节点'
|
||||
: t('home.components.currentProxy.status.noProxyNode')
|
||||
}
|
||||
>
|
||||
<Box sx={{ color: signalInfo.color }}>
|
||||
|
||||
@@ -380,7 +380,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
// 格式化时间戳
|
||||
const timeStr = dataPoint.timestamp
|
||||
? formatTrafficName(dataPoint.timestamp)
|
||||
: '未知时间'
|
||||
: t('home.components.traffic.unknownTime')
|
||||
|
||||
// 计算数据点对应的Y坐标位置(用于高亮)
|
||||
const { topValue: tvH, bottomValue: bvH } = yScale
|
||||
@@ -420,7 +420,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
})
|
||||
})
|
||||
},
|
||||
[displayData, calculateY, yScale],
|
||||
[displayData, calculateY, yScale, t],
|
||||
)
|
||||
|
||||
// 鼠标离开处理
|
||||
@@ -1205,7 +1205,11 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
{chartStyle === 'bezier' ? 'Smooth' : 'Linear'}
|
||||
{t(
|
||||
chartStyle === 'bezier'
|
||||
? 'home.components.traffic.chartStyles.smooth'
|
||||
: 'home.components.traffic.chartStyles.linear',
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 数据统计指示器(左下角) */}
|
||||
@@ -1220,8 +1224,11 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Points: {displayData.length} | Compressed:{' '}
|
||||
{samplerStats.compressedBufferSize} | FPS: {currentFPS}
|
||||
{t('home.components.traffic.diagnostics', {
|
||||
points: displayData.length,
|
||||
compressed: samplerStats.compressedBufferSize,
|
||||
fps: currentFPS,
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* 悬浮提示框 */}
|
||||
|
||||
@@ -86,7 +86,10 @@ export const ServiceMigrationDialog = () => {
|
||||
}
|
||||
actionSucceeded = true
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
showNotice.error(
|
||||
'layout.components.serviceMigration.errors.actionFailed',
|
||||
error,
|
||||
)
|
||||
}
|
||||
|
||||
let initialRefreshSucceeded = false
|
||||
@@ -94,7 +97,10 @@ export const ServiceMigrationDialog = () => {
|
||||
await refreshInstallState()
|
||||
initialRefreshSucceeded = true
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
showNotice.error(
|
||||
'layout.components.serviceMigration.errors.stateRefreshFailed',
|
||||
error,
|
||||
)
|
||||
}
|
||||
if (!actionSucceeded || !initialRefreshSucceeded) {
|
||||
setLoading(false)
|
||||
@@ -106,7 +112,10 @@ export const ServiceMigrationDialog = () => {
|
||||
await restartCore()
|
||||
restartSucceeded = true
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
showNotice.error(
|
||||
'layout.components.serviceMigration.errors.restartFailed',
|
||||
error,
|
||||
)
|
||||
}
|
||||
|
||||
let finalRefreshSucceeded = false
|
||||
@@ -114,14 +123,20 @@ export const ServiceMigrationDialog = () => {
|
||||
await refreshInstallState()
|
||||
finalRefreshSucceeded = true
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
showNotice.error(
|
||||
'layout.components.serviceMigration.errors.stateRefreshFailed',
|
||||
error,
|
||||
)
|
||||
}
|
||||
let revalidationSucceeded = false
|
||||
try {
|
||||
await refreshSystemAndRunning()
|
||||
revalidationSucceeded = true
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
showNotice.error(
|
||||
'layout.components.serviceMigration.errors.revalidationFailed',
|
||||
error,
|
||||
)
|
||||
}
|
||||
if (restartSucceeded && finalRefreshSucceeded && revalidationSucceeded) {
|
||||
setWorkflowIncomplete(false)
|
||||
@@ -145,17 +160,26 @@ export const ServiceMigrationDialog = () => {
|
||||
await refreshInstallState()
|
||||
installRefreshSucceeded = true
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
showNotice.error(
|
||||
'layout.components.serviceMigration.errors.stateRefreshFailed',
|
||||
error,
|
||||
)
|
||||
}
|
||||
let revalidationSucceeded = false
|
||||
try {
|
||||
await refreshSystemAndRunning()
|
||||
revalidationSucceeded = true
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
showNotice.error(
|
||||
'layout.components.serviceMigration.errors.revalidationFailed',
|
||||
error,
|
||||
)
|
||||
}
|
||||
if (startupError) {
|
||||
showNotice.error(startupError)
|
||||
showNotice.error(
|
||||
'layout.components.serviceMigration.errors.sidecarFailed',
|
||||
startupError,
|
||||
)
|
||||
} else if (installRefreshSucceeded && revalidationSucceeded) {
|
||||
setWorkflowIncomplete(false)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from '@mui/material'
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DialogRef } from '@/components/base'
|
||||
import { useUpdate } from '@/hooks/use-update'
|
||||
@@ -12,6 +13,7 @@ interface Props {
|
||||
|
||||
export const UpdateButton = (props: Props) => {
|
||||
const { className } = props
|
||||
const { t } = useTranslation()
|
||||
const viewerRef = useRef<DialogRef>(null)
|
||||
|
||||
const { updateInfo } = useUpdate()
|
||||
@@ -29,7 +31,7 @@ export const UpdateButton = (props: Props) => {
|
||||
className={className}
|
||||
onClick={() => viewerRef.current?.open()}
|
||||
>
|
||||
New
|
||||
{t('shared.actions.new')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ export const FileInput = (props: Props) => {
|
||||
/>
|
||||
|
||||
<Typography noWrap sx={{ ml: 1 }}>
|
||||
{loading ? 'Loading...' : fileName}
|
||||
{loading ? t('shared.statuses.loading') : fileName}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -91,9 +91,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 基本验证
|
||||
if (!form.type) throw new Error('`Type` should not be null')
|
||||
if (!form.type) {
|
||||
throw new Error(t('profiles.modals.profileForm.errors.typeRequired'))
|
||||
}
|
||||
if (form.type === 'remote' && !form.url) {
|
||||
throw new Error('The URL should not be null')
|
||||
throw new Error(t('profiles.modals.profileForm.errors.urlRequired'))
|
||||
}
|
||||
|
||||
// 处理表单数据
|
||||
@@ -131,7 +133,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
if (openType === 'new') {
|
||||
await createProfile(item, fileDataRef.current)
|
||||
} else {
|
||||
if (!form.uid) throw new Error('UID not found')
|
||||
if (!form.uid) {
|
||||
throw new Error(
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await patchProfile(form.uid, item)
|
||||
}
|
||||
} else {
|
||||
@@ -141,7 +147,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
if (openType === 'new') {
|
||||
await createProfile(item, fileDataRef.current)
|
||||
} else {
|
||||
if (!form.uid) throw new Error('UID not found')
|
||||
if (!form.uid) {
|
||||
throw new Error(
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await patchProfile(form.uid, item)
|
||||
}
|
||||
} catch {
|
||||
@@ -164,7 +174,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
if (openType === 'new') {
|
||||
await createProfile(retryItem, fileDataRef.current)
|
||||
} else {
|
||||
if (!form.uid) throw new Error('UID not found')
|
||||
if (!form.uid) {
|
||||
throw new Error(
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await patchProfile(form.uid, retryItem)
|
||||
|
||||
// 编辑模式下恢复原始代理设置
|
||||
@@ -187,7 +201,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
onChange(isActivating)
|
||||
}, 0)
|
||||
} catch (err) {
|
||||
showNotice.error(err)
|
||||
showNotice.error('profiles.modals.profileForm.errors.saveFailed', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -246,8 +260,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
autoFocus
|
||||
label={t('profiles.modals.profileForm.fields.type')}
|
||||
>
|
||||
<MenuItem value="remote">Remote</MenuItem>
|
||||
<MenuItem value="local">Local</MenuItem>
|
||||
<MenuItem value="remote">
|
||||
{t('profiles.modals.profileForm.types.remote')}
|
||||
</MenuItem>
|
||||
<MenuItem value="local">
|
||||
{t('profiles.modals.profileForm.types.local')}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
@@ -305,7 +323,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
{...text}
|
||||
{...field}
|
||||
placeholder={`clash-verge/v${version}`}
|
||||
label="User Agent"
|
||||
label={t('profiles.modals.profileForm.fields.userAgent')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -214,9 +214,7 @@ const SortableItem = ({
|
||||
{proxy.delay !== undefined && (
|
||||
<Chip
|
||||
label={
|
||||
proxy.delay > 0
|
||||
? `${proxy.delay}ms`
|
||||
: t('shared.labels.timeout') || '超时'
|
||||
proxy.delay > 0 ? `${proxy.delay}ms` : t('shared.labels.timeout')
|
||||
}
|
||||
size="small"
|
||||
color={
|
||||
@@ -402,7 +400,7 @@ export const ProxyChain = ({
|
||||
onUpdateChain([])
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect from proxy chain:', error)
|
||||
alert(t('proxies.page.chain.disconnectFailed') || '断开链式代理失败')
|
||||
alert(t('proxies.page.chain.disconnectFailed'))
|
||||
} finally {
|
||||
setIsConnecting(false)
|
||||
}
|
||||
@@ -410,7 +408,7 @@ export const ProxyChain = ({
|
||||
}
|
||||
|
||||
if (mode === 'global' && proxyView?.global === null) {
|
||||
alert(t('proxies.page.chain.connectFailed') || '连接链式代理失败')
|
||||
alert(t('proxies.page.chain.connectFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -418,7 +416,7 @@ export const ProxyChain = ({
|
||||
currentProxyChain.length < 2 ||
|
||||
currentProxyChain.some(({ recordId }) => !recordId)
|
||||
) {
|
||||
alert(t('proxies.page.chain.minimumNodes') || '链式代理至少需要2个节点')
|
||||
alert(t('proxies.page.chain.minimumNodes'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -450,7 +448,7 @@ export const ProxyChain = ({
|
||||
debugLog('Successfully connected to proxy chain')
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to proxy chain:', error)
|
||||
alert(t('proxies.page.chain.connectFailed') || '连接链式代理失败')
|
||||
alert(t('proxies.page.chain.connectFailed'))
|
||||
} finally {
|
||||
setIsConnecting(false)
|
||||
}
|
||||
@@ -526,9 +524,7 @@ export const ProxyChain = ({
|
||||
backgroundColor: theme.palette.error.light + '20',
|
||||
},
|
||||
}}
|
||||
title={
|
||||
t('proxies.page.actions.clearChainConfig') || '删除链式配置'
|
||||
}
|
||||
title={t('proxies.page.actions.clearChainConfig')}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
@@ -554,16 +550,15 @@ export const ProxyChain = ({
|
||||
}}
|
||||
title={
|
||||
!isConnected && currentProxyChain.length < 2
|
||||
? t('proxies.page.chain.minimumNodes') ||
|
||||
'链式代理至少需要2个节点'
|
||||
? t('proxies.page.chain.minimumNodes')
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isConnecting
|
||||
? t('proxies.page.actions.connecting') || '连接中...'
|
||||
? t('proxies.page.actions.connecting')
|
||||
: isConnected
|
||||
? t('proxies.page.actions.disconnect') || '断开'
|
||||
: t('proxies.page.actions.connect') || '连接'}
|
||||
? t('proxies.page.actions.disconnect')
|
||||
: t('proxies.page.actions.connect')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -573,10 +568,8 @@ export const ProxyChain = ({
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{currentProxyChain.length === 1
|
||||
? t('proxies.page.chain.minimumNodesHint') ||
|
||||
'链式代理至少需要2个节点,请再添加一个节点。'
|
||||
: t('proxies.page.chain.instruction') ||
|
||||
'按顺序点击节点添加到代理链中'}
|
||||
? t('proxies.page.chain.minimumNodesHint')
|
||||
: t('proxies.page.chain.instruction')}
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
||||
|
||||
@@ -63,6 +63,7 @@ interface GroupSelectMenuProps {
|
||||
groups: ProxyGroupOption[]
|
||||
selectedGroup: string | null
|
||||
emptyText: string
|
||||
nodeCountLabel: (count: number) => string
|
||||
onClose: () => void
|
||||
onSelect: (groupName: string) => void
|
||||
}
|
||||
@@ -162,6 +163,7 @@ function GroupSelectMenu({
|
||||
groups,
|
||||
selectedGroup,
|
||||
emptyText,
|
||||
nodeCountLabel,
|
||||
onClose,
|
||||
onSelect,
|
||||
}: GroupSelectMenuProps) {
|
||||
@@ -197,7 +199,7 @@ function GroupSelectMenu({
|
||||
{group.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{group.type} · {group.members.length} 节点
|
||||
{group.type} · {nodeCountLabel(group.members.length)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
@@ -521,7 +523,10 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
anchorEl={ruleMenuAnchor}
|
||||
groups={availableGroups}
|
||||
selectedGroup={activeSelectedGroup}
|
||||
emptyText="暂无可用代理组"
|
||||
emptyText={t('proxies.page.empty.noAvailableGroups')}
|
||||
nodeCountLabel={(count) =>
|
||||
t('proxies.page.labels.nodeCount', { count })
|
||||
}
|
||||
onClose={handleGroupMenuClose}
|
||||
onSelect={handleGroupSelect}
|
||||
/>
|
||||
|
||||
@@ -183,7 +183,7 @@ export const ProxyItemMini = (props: Props) => {
|
||||
':hover': { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
Check
|
||||
{t('shared.actions.check')}
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type SxProps,
|
||||
type Theme,
|
||||
} from '@mui/material'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseLoading } from '@/components/base'
|
||||
import { useProxyDelayState } from '@/hooks/use-proxy-delay-state'
|
||||
@@ -49,6 +50,7 @@ const TypeBox = styled('span')(({ theme }) => ({
|
||||
}))
|
||||
|
||||
export const ProxyItem = (props: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { group, member, selected, showType = true, sx, onClick } = props
|
||||
const details = memberDetails(member)
|
||||
const unresolved = member.kind === 'unresolved'
|
||||
@@ -160,7 +162,7 @@ export const ProxyItem = (props: Props) => {
|
||||
':hover': { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
Check
|
||||
{t('shared.actions.check')}
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
|
||||
@@ -262,7 +262,9 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
|
||||
}}
|
||||
>
|
||||
<InboxRounded sx={{ fontSize: '2.5em', color: 'inherit' }} />
|
||||
<Typography sx={{ color: 'inherit' }}>No Proxies</Typography>
|
||||
<Typography sx={{ color: 'inherit' }}>
|
||||
{t('proxies.page.empty.noProxies')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -583,7 +583,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
setOpen(false)
|
||||
showNotice.success('settings.modals.dns.messages.saved')
|
||||
} catch (err) {
|
||||
showNotice.error(err)
|
||||
showNotice.error('settings.modals.dns.messages.configError', err)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -30,11 +30,26 @@ type MarkdownNode = {
|
||||
}
|
||||
|
||||
const GITHUB_ALERTS = {
|
||||
note: { label: 'Note', color: '#0969da' },
|
||||
tip: { label: 'Tip', color: '#1a7f37' },
|
||||
important: { label: 'Important', color: '#8250df' },
|
||||
warning: { label: 'Warning', color: '#9a6700' },
|
||||
caution: { label: 'Caution', color: '#cf222e' },
|
||||
note: {
|
||||
labelKey: 'settings.modals.update.alerts.note',
|
||||
color: '#0969da',
|
||||
},
|
||||
tip: {
|
||||
labelKey: 'settings.modals.update.alerts.tip',
|
||||
color: '#1a7f37',
|
||||
},
|
||||
important: {
|
||||
labelKey: 'settings.modals.update.alerts.important',
|
||||
color: '#8250df',
|
||||
},
|
||||
warning: {
|
||||
labelKey: 'settings.modals.update.alerts.warning',
|
||||
color: '#9a6700',
|
||||
},
|
||||
caution: {
|
||||
labelKey: 'settings.modals.update.alerts.caution',
|
||||
color: '#cf222e',
|
||||
},
|
||||
} as const
|
||||
|
||||
type GitHubAlertType = keyof typeof GITHUB_ALERTS
|
||||
@@ -76,7 +91,7 @@ const findFirstTextNode = (node: MarkdownNode): MarkdownNode | null => {
|
||||
return null
|
||||
}
|
||||
|
||||
const remarkGitHubAlerts = () => {
|
||||
const remarkGitHubAlerts = (labels: Record<GitHubAlertType, string>) => {
|
||||
const visit = (node: MarkdownNode) => {
|
||||
for (const child of node.children ?? []) {
|
||||
visit(child)
|
||||
@@ -111,7 +126,7 @@ const remarkGitHubAlerts = () => {
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: GITHUB_ALERTS[alertType].label,
|
||||
value: labels[alertType],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -144,12 +159,27 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
close: () => setOpen(false),
|
||||
}))
|
||||
|
||||
const githubAlertLabels = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
Object.entries(GITHUB_ALERTS).map(([type, alert]) => [
|
||||
type,
|
||||
t(alert.labelKey),
|
||||
]),
|
||||
) as Record<GitHubAlertType, string>,
|
||||
[t],
|
||||
)
|
||||
const remarkGitHubAlertsPlugin = useMemo(
|
||||
() => () => remarkGitHubAlerts(githubAlertLabels),
|
||||
[githubAlertLabels],
|
||||
)
|
||||
|
||||
const markdownContent = useMemo(() => {
|
||||
if (!updateInfo?.body) {
|
||||
return 'New Version is available'
|
||||
return t('settings.modals.update.messages.available')
|
||||
}
|
||||
return updateInfo?.body
|
||||
}, [updateInfo])
|
||||
}, [t, updateInfo])
|
||||
|
||||
const breakChangeFlag = useMemo(() => {
|
||||
if (!updateInfo?.body) {
|
||||
@@ -382,7 +412,7 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{open && (
|
||||
<Suspense fallback={<LinearProgress />}>
|
||||
<LazyReactMarkdown
|
||||
remarkPlugins={[remarkGitHubAlerts]}
|
||||
remarkPlugins={[remarkGitHubAlertsPlugin]}
|
||||
components={{
|
||||
a: ({ ...props }) => {
|
||||
const { children } = props
|
||||
|
||||
@@ -74,9 +74,17 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
let url = value.trim().replaceAll('%host', '127.0.0.1')
|
||||
|
||||
if (url.includes('%port') || url.includes('%secret')) {
|
||||
if (!clashInfo) throw new Error('failed to get clash info')
|
||||
if (!clashInfo) {
|
||||
throw new Error(
|
||||
t('settings.modals.webUI.errors.clashInfoUnavailable'),
|
||||
)
|
||||
}
|
||||
if (!clashInfo.server?.includes(':')) {
|
||||
throw new Error(`failed to parse the server "${clashInfo.server}"`)
|
||||
throw new Error(
|
||||
t('settings.modals.webUI.errors.invalidServer', {
|
||||
server: clashInfo.server,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const port = clashInfo.server
|
||||
@@ -92,7 +100,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
await openWebUrl(url)
|
||||
} catch (e: any) {
|
||||
showNotice.error(e)
|
||||
showNotice.error('settings.modals.webUI.errors.openFailed', e)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -189,8 +189,10 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
|
||||
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Error:</strong>{' '}
|
||||
{error instanceof Error ? error.message : 'Unknown error'}
|
||||
<strong>{t('shared.feedback.errors.label')}:</strong>{' '}
|
||||
{error instanceof Error
|
||||
? error.message
|
||||
: t('shared.feedback.errors.unknown')}
|
||||
</Typography>
|
||||
{retryCount > 0 && (
|
||||
<Typography variant="caption" sx={{ mt: 1, display: 'block' }}>
|
||||
@@ -239,7 +241,7 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Error Details:
|
||||
{t('shared.feedback.errors.details')}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
@@ -258,7 +260,7 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
{errorInfo?.componentStack && (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
|
||||
Component Stack:
|
||||
{t('shared.feedback.errors.componentStack')}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
@@ -288,6 +290,8 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
export const LightweightTrafficErrorBoundary: React.FC<{
|
||||
children: ReactNode
|
||||
}> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<TrafficErrorBoundary
|
||||
fallbackComponent={
|
||||
@@ -304,7 +308,9 @@ export const LightweightTrafficErrorBoundary: React.FC<{
|
||||
}}
|
||||
>
|
||||
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
|
||||
<Typography variant="caption">Traffic data unavailable</Typography>
|
||||
<Typography variant="caption">
|
||||
{t('shared.feedback.errors.trafficUnavailable')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -70,8 +70,8 @@ export const TestItem = ({
|
||||
})
|
||||
|
||||
const menu = [
|
||||
{ label: 'Edit', handler: onEditTest },
|
||||
{ label: 'Delete', handler: onDelete },
|
||||
{ label: t('shared.actions.edit'), handler: onEditTest },
|
||||
{ label: t('shared.actions.delete'), handler: onDelete },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
@@ -220,7 +220,7 @@ export const TestItem = ({
|
||||
sx={{ minWidth: 120 }}
|
||||
dense
|
||||
>
|
||||
{t(item.label)}
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
|
||||
@@ -68,8 +68,12 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
if (!form.name) throw new Error('`Name` should not be null')
|
||||
if (!form.url) throw new Error('`Url` should not be null')
|
||||
if (!form.name) {
|
||||
throw new Error(t('tests.modals.test.errors.nameRequired'))
|
||||
}
|
||||
if (!form.url) {
|
||||
throw new Error(t('tests.modals.test.errors.urlRequired'))
|
||||
}
|
||||
|
||||
let newList
|
||||
let uid
|
||||
@@ -84,7 +88,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
|
||||
'image/svg+xml',
|
||||
)
|
||||
if (doc.querySelector('parsererror')) {
|
||||
throw new Error('`Icon`svg format error')
|
||||
throw new Error(t('tests.modals.test.errors.invalidSvg'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +99,9 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
|
||||
await patchVerge({ test_list: newList })
|
||||
onChange(uid)
|
||||
} else {
|
||||
if (!form.uid) throw new Error('UID not found')
|
||||
if (!form.uid) {
|
||||
throw new Error(t('tests.modals.test.errors.uidMissing'))
|
||||
}
|
||||
uid = form.uid
|
||||
|
||||
await patchTestList(uid, form)
|
||||
@@ -105,7 +111,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
|
||||
setLoading(false)
|
||||
setTimeout(() => formIns.reset(), 500)
|
||||
} catch (err: any) {
|
||||
showNotice.error(err)
|
||||
showNotice.error('tests.modals.test.errors.saveFailed', err)
|
||||
setLoading(false)
|
||||
}
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user