feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection

- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru
- Auto-detect system language via navigator.languages; fallback to English
- Add BCP47 locale mapping for date formatting and sorting
- Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each)
- Update language selector with native language names
- Extend locale key parity test to cover all 7 languages (70 assertions)
- Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features
- Add credential encryption, host metrics, session backup crypto, and zmodem bridge
- Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
This commit is contained in:
Logan.Li committed 2026-08-12 00:07:48 +08:00
1 parent fea6d8db04
commit b0d0e3a5f3
301 files changed
+28002 -2705

No files matched your search

+124 -28
View File
@@ -11,9 +11,12 @@
* <ConfirmDialogComponent />
*/
import { useState, useCallback, useRef } from 'react'
import { useState, useCallback, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from './Modal'
import Button from './Button'
import { AlertTriangle } from 'lucide-react'
import i18n from '../../i18n'
export interface ConfirmOptions {
/** 对话框标题 */
@@ -28,11 +31,25 @@ export interface ConfirmOptions {
cancelText?: string
}
/** Agent 高风险确认:Reject / Once / Always(Always 仅会话内存) */
export type AgentRiskChoice = 'reject' | 'once' | 'always'
export interface AgentRiskConfirmOptions {
title: string
message: string
/** 高风险时 Once/Always 用危险样式 */
danger?: boolean
}
interface ConfirmState extends ConfirmOptions {
/** 是否显示 */
isOpen: boolean
/** resolve 回调 */
resolver: ((value: boolean) => void) | null
}
interface AgentRiskState {
isOpen: boolean
title: string
message: string
danger: boolean
}
const initialState: ConfirmState = {
@@ -40,9 +57,15 @@ const initialState: ConfirmState = {
title: '',
message: '',
danger: false,
confirmText: '确认',
cancelText: '取消',
resolver: null,
confirmText: i18n.t('common:action.confirm'),
cancelText: i18n.t('common:action.cancel'),
}
const initialAgentRisk: AgentRiskState = {
isOpen: false,
title: '',
message: '',
danger: true,
}
/**
@@ -51,8 +74,11 @@ const initialState: ConfirmState = {
* 返回 confirm 函数和需要渲染的组件
*/
export function useConfirmDialog() {
const { t } = useTranslation(['common', 'ai'])
const [state, setState] = useState<ConfirmState>(initialState)
const [agentRisk, setAgentRisk] = useState<AgentRiskState>(initialAgentRisk)
const resolverRef = useRef<((value: boolean) => void) | null>(null)
const agentResolverRef = useRef<((value: AgentRiskChoice) => void) | null>(null)
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
return new Promise<boolean>((resolve) => {
@@ -61,9 +87,20 @@ export function useConfirmDialog() {
...options,
isOpen: true,
danger: options.danger ?? false,
confirmText: options.confirmText ?? '确认',
cancelText: options.cancelText ?? '取消',
resolver: null,
confirmText: options.confirmText ?? i18n.t('common:action.confirm'),
cancelText: options.cancelText ?? i18n.t('common:action.cancel'),
})
})
}, [])
const confirmAgentRisk = useCallback((options: AgentRiskConfirmOptions): Promise<AgentRiskChoice> => {
return new Promise<AgentRiskChoice>((resolve) => {
agentResolverRef.current = resolve
setAgentRisk({
isOpen: true,
title: options.title,
message: options.message,
danger: options.danger ?? true,
})
})
}, [])
@@ -80,27 +117,86 @@ export function useConfirmDialog() {
resolverRef.current = null
}, [])
const ConfirmDialogComponent = (
<Modal
isOpen={state.isOpen}
title={state.title}
onConfirm={handleConfirm}
onClose={handleClose}
confirmText={state.confirmText}
cancelText={state.cancelText}
isDanger={state.danger}
size="sm"
icon={
state.danger ? (
<AlertTriangle className="w-5 h-5 text-red-400" />
) : undefined
const resolveAgentRisk = useCallback((choice: AgentRiskChoice) => {
setAgentRisk((prev) => ({ ...prev, isOpen: false }))
agentResolverRef.current?.(choice)
agentResolverRef.current = null
}, [])
/** 停止 Agent 时关闭待确认对话框(等同 Reject,不计入用户决策) */
const dismissAgentRisk = useCallback(() => {
if (!agentResolverRef.current) return
resolveAgentRisk('reject')
}, [resolveAgentRisk])
useEffect(() => {
if (!agentRisk.isOpen) return
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault()
resolveAgentRisk('reject')
} else if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
event.preventDefault()
resolveAgentRisk('once')
}
>
<p className="text-sm text-content-secondary leading-relaxed">{state.message}</p>
</Modal>
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [agentRisk.isOpen, resolveAgentRisk])
const ConfirmDialogComponent = (
<>
<Modal
isOpen={state.isOpen}
title={state.title}
onConfirm={handleConfirm}
onClose={handleClose}
confirmText={state.confirmText}
cancelText={state.cancelText}
isDanger={state.danger}
size="sm"
icon={
state.danger ? (
<AlertTriangle className="w-5 h-5 text-red-400" />
) : undefined
}
>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-content-secondary">{state.message}</p>
</Modal>
<Modal
isOpen={agentRisk.isOpen}
title={agentRisk.title}
onClose={() => resolveAgentRisk('reject')}
showConfirm={false}
size="sm"
disableBackdropClose
icon={<AlertTriangle className="w-5 h-5 text-red-400" />}
footer={
<div className="flex w-full flex-wrap items-center justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => resolveAgentRisk('reject')}>
{t('ai:confirm.reject')}
</Button>
<Button
variant={agentRisk.danger ? 'danger' : 'primary'}
size="sm"
onClick={() => resolveAgentRisk('once')}
>
{t('ai:confirm.once')}
</Button>
<Button variant="secondary" size="sm" onClick={() => resolveAgentRisk('always')}>
{t('ai:confirm.always')}
</Button>
</div>
}
>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-content-secondary">{agentRisk.message}</p>
<p className="mt-3 text-[11px] text-txt-faint">{t('ai:confirm.hint')}</p>
</Modal>
</>
)
return { confirm, ConfirmDialogComponent }
return { confirm, confirmAgentRisk, dismissAgentRisk, ConfirmDialogComponent }
}
export default useConfirmDialog