Refactor code structure for improved readability and maintainability

This commit is contained in:
MengMengCode
2026-06-09 15:33:17 +08:00
parent cf00d0d03d
commit e971d99070
17 changed files with 2068 additions and 178 deletions
+182
View File
@@ -0,0 +1,182 @@
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useLanguage } from '../contexts/LanguageContext'
import { shouldTranslateText, translateText } from '../utils/i18n'
const translatedTitleAttr = 'data-i18n-title-original'
const translatedPlaceholderAttr = 'data-i18n-placeholder-original'
const translatedAriaLabelAttr = 'data-i18n-aria-label-original'
const attributeNames = ['title', 'placeholder', 'aria-label'] as const
const translatedTextNodes = new Set<Text>()
const textOriginals = new WeakMap<Text, string>()
const wholeTextSelector = 'button,a,span,label,option,th,td,p,h1,h2,h3,h4,small'
export default function AutoTranslate() {
const { language } = useLanguage()
const location = useLocation()
useEffect(() => {
if (language === 'zh') {
restoreTranslatedNodes(document.body)
return
}
translateNode(document.body)
const pending = new Set<Node>()
let scheduled = false
const flush = () => {
scheduled = false
const nodes = Array.from(pending)
pending.clear()
for (const node of nodes) {
if (node.isConnected) translateNode(node)
}
}
const schedule = (node: Node) => {
pending.add(node)
if (scheduled) return
scheduled = true
window.requestAnimationFrame(flush)
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(schedule)
} else {
schedule(mutation.target)
}
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: [...attributeNames],
})
return () => observer.disconnect()
}, [language, location.pathname, location.search])
return null
}
function translateNode(root: Node) {
if (root.nodeType === Node.TEXT_NODE) {
translateTextNode(root as Text)
return
}
if (!(root instanceof Element)) return
if (shouldSkipElement(root)) return
translateWholeTextElement(root)
root.querySelectorAll<HTMLElement>(wholeTextSelector).forEach(translateWholeTextElement)
translateElementAttributes(root)
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
if (!node.textContent || !shouldTranslateText(node.textContent)) return NodeFilter.FILTER_REJECT
const parent = node.parentElement
if (!parent || shouldSkipElement(parent)) {
return NodeFilter.FILTER_REJECT
}
return NodeFilter.FILTER_ACCEPT
},
})
const nodes: Text[] = []
while (walker.nextNode()) nodes.push(walker.currentNode as Text)
for (const node of nodes) translateTextNode(node)
root.querySelectorAll<HTMLElement>('[title], [placeholder], [aria-label]').forEach(translateElementAttributes)
}
function translateTextNode(node: Text) {
const original = node.textContent || ''
if (!shouldTranslateText(original)) return
const parent = node.parentElement
if (!parent || shouldSkipElement(parent)) return
const translated = translateText(original)
if (translated === original) return
textOriginals.set(node, original)
translatedTextNodes.add(node)
node.textContent = translated
}
function translateWholeTextElement(el: Element) {
if (!(el instanceof HTMLElement) || shouldSkipElement(el) || !isSimpleTextElement(el)) return
const original = el.textContent || ''
if (!shouldTranslateText(original)) return
const translated = translateText(original)
if (translated === original) return
const textNodes = directTextNodes(el)
if (textNodes.length === 0) return
textNodes.forEach((node, index) => {
textOriginals.set(node, node.textContent || '')
translatedTextNodes.add(node)
node.textContent = index === 0 ? translated : ''
})
}
function directTextNodes(el: HTMLElement) {
return Array.from(el.childNodes).filter((node): node is Text => node.nodeType === Node.TEXT_NODE)
}
function translateElementAttributes(el: Element) {
if (!(el instanceof HTMLElement)) return
translateAttribute(el, 'title', translatedTitleAttr)
translateAttribute(el, 'placeholder', translatedPlaceholderAttr)
translateAttribute(el, 'aria-label', translatedAriaLabelAttr)
}
function restoreTranslatedNodes(root: ParentNode) {
for (const node of Array.from(translatedTextNodes)) {
if (!node.isConnected) {
translatedTextNodes.delete(node)
continue
}
if (root instanceof Document || root.contains(node)) {
node.textContent = textOriginals.get(node) || node.textContent
translatedTextNodes.delete(node)
}
}
root.querySelectorAll<HTMLElement>(`[${translatedTitleAttr}]`).forEach((el) => {
el.setAttribute('title', el.getAttribute(translatedTitleAttr) || '')
el.removeAttribute(translatedTitleAttr)
})
root.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>(`[${translatedPlaceholderAttr}]`).forEach((el) => {
el.setAttribute('placeholder', el.getAttribute(translatedPlaceholderAttr) || '')
el.removeAttribute(translatedPlaceholderAttr)
})
root.querySelectorAll<HTMLElement>(`[${translatedAriaLabelAttr}]`).forEach((el) => {
el.setAttribute('aria-label', el.getAttribute(translatedAriaLabelAttr) || '')
el.removeAttribute(translatedAriaLabelAttr)
})
}
function translateAttribute(el: HTMLElement, attr: 'title' | 'placeholder' | 'aria-label', originalAttr: string) {
const storedOriginal = el.getAttribute(originalAttr)
const original = storedOriginal || el.getAttribute(attr) || ''
if (!shouldTranslateText(original)) return
const translated = translateText(original)
if (translated === original) return
if (!storedOriginal) {
el.setAttribute(originalAttr, original)
}
if (el.getAttribute(attr) !== translated) {
el.setAttribute(attr, translated)
}
}
function shouldSkipElement(el: Element) {
return !!el.closest('script, style, code, pre, textarea, [data-no-translate]')
}
function isSimpleTextElement(el: HTMLElement) {
if (!el.matches(wholeTextSelector)) return false
if (el.querySelector('input, textarea, select, button, table, pre, code, canvas, iframe')) return false
const textNodes = directTextNodes(el)
if (textNodes.length === 0) return false
return Array.from(el.children).every((child) => child.tagName.toLowerCase() === 'svg')
}
@@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useLanguage } from '../contexts/LanguageContext'
export default function BrowserDialogTranslator() {
const { t } = useLanguage()
useEffect(() => {
const originalAlert = window.alert
const originalConfirm = window.confirm
window.alert = (message?: unknown) => originalAlert(t(String(message ?? '')))
window.confirm = (message?: string) => originalConfirm(t(String(message ?? '')))
return () => {
window.alert = originalAlert
window.confirm = originalConfirm
}
}, [t])
return null
}
+6 -4
View File
@@ -1,5 +1,6 @@
import { useState, useCallback, createContext, useContext, ReactNode } from 'react'
import { AlertTriangle, CheckCircle, X } from 'lucide-react'
import { useLanguage } from '../contexts/LanguageContext'
type DialogType = 'confirm' | 'alert'
@@ -20,6 +21,7 @@ const DialogContext = createContext<DialogContextType | undefined>(undefined)
export function DialogProvider({ children }: { children: ReactNode }) {
const [dialog, setDialog] = useState<DialogState>({ open: false, type: 'alert', title: '', message: '' })
const { t } = useLanguage()
const confirm = useCallback((title: string, message: string) => {
return new Promise<boolean>((resolve) => {
@@ -50,7 +52,7 @@ export function DialogProvider({ children }: { children: ReactNode }) {
}`}>
{dialog.type === 'confirm' ? <AlertTriangle className="w-4 h-4" /> : <CheckCircle className="w-4 h-4" />}
</div>
<h3 className="text-sm font-semibold text-black flex-1">{dialog.title}</h3>
<h3 className="text-sm font-semibold text-black flex-1">{t(dialog.title)}</h3>
{dialog.type === 'alert' && (
<button onClick={() => close(true)} className="p-1 text-gray-400 hover:text-black rounded">
<X className="w-4 h-4" />
@@ -58,7 +60,7 @@ export function DialogProvider({ children }: { children: ReactNode }) {
)}
</div>
<div className="px-5 py-4">
<p className="text-sm text-gray-600">{dialog.message}</p>
<p className="text-sm text-gray-600">{t(dialog.message)}</p>
</div>
<div className="flex justify-end gap-2 px-5 py-3 bg-gray-50 border-t border-gray-100">
{dialog.type === 'confirm' && (
@@ -66,7 +68,7 @@ export function DialogProvider({ children }: { children: ReactNode }) {
onClick={() => close(false)}
className="px-4 py-2 text-sm text-gray-700 hover:bg-gray-200 rounded-md transition-colors"
>
{t('取消')}
</button>
)}
<button
@@ -77,7 +79,7 @@ export function DialogProvider({ children }: { children: ReactNode }) {
: 'bg-black text-white hover:bg-gray-800'
}`}
>
{dialog.type === 'confirm' ? '确认' : '确定'}
{dialog.type === 'confirm' ? t('确认') : t('确定')}
</button>
</div>
</div>
+4
View File
@@ -1,12 +1,16 @@
import { Outlet } from 'react-router-dom'
import Sidebar from './Sidebar'
import { useState } from 'react'
import AutoTranslate from './AutoTranslate'
import BrowserDialogTranslator from './BrowserDialogTranslator'
export default function Layout() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
return (
<div className="min-h-screen bg-gray-50 flex dark:bg-gray-950">
<AutoTranslate />
<BrowserDialogTranslator />
<Sidebar collapsed={sidebarCollapsed} onToggle={() => setSidebarCollapsed(!sidebarCollapsed)} />
<main className={`flex-1 transition-all duration-300 ${sidebarCollapsed ? 'ml-16' : 'ml-60'}`}>
<div className="p-6">
+36 -13
View File
@@ -19,6 +19,7 @@ import {
UserCog,
} from 'lucide-react'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../contexts/LanguageContext'
import { useTheme } from '../contexts/ThemeContext'
import { getVersion } from '../services/api'
import AppIcon from './AppIcon'
@@ -45,11 +46,23 @@ function GitHubIcon({ className = '' }: { className?: string }) {
)
}
function LanguageIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path
d="M213.333333 640v85.333333a85.333333 85.333333 0 0 0 78.933334 85.12L298.666667 810.666667h128v85.333333H298.666667a170.666667 170.666667 0 0 1-170.666667-170.666667v-85.333333h85.333333z m554.666667-213.333333l187.733333 469.333333h-91.946666l-51.242667-128h-174.506667l-51.157333 128h-91.904L682.666667 426.666667h85.333333z m-42.666667 123.093333L672.128 682.666667h106.325333L725.333333 549.76zM341.333333 85.333333v85.333334h170.666667v298.666666H341.333333v128H256v-128H85.333333V170.666667h170.666667V85.333333h85.333333z m384 42.666667a170.666667 170.666667 0 0 1 170.666667 170.666667v85.333333h-85.333333V298.666667a85.333333 85.333333 0 0 0-85.333334-85.333334h-128V128h128zM256 256H170.666667v128h85.333333V256z m170.666667 0H341.333333v128h85.333334V256z"
fill="currentColor"
/>
</svg>
)
}
export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
const navigate = useNavigate()
const location = useLocation()
const { logout, isSubUser } = useAuth()
const { theme, toggleTheme } = useTheme()
const { language, toggleLanguage, t } = useLanguage()
const [version, setVersion] = useState('')
useEffect(() => {
@@ -99,7 +112,7 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
<button
onClick={onToggle}
className="p-1 rounded hover:bg-gray-100 text-gray-500 dark:hover:bg-gray-800 dark:text-gray-400"
title="切换侧边栏"
title={t('切换侧边栏')}
>
{collapsed ? (
<ChevronRight className="w-4 h-4" />
@@ -253,18 +266,28 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
<div className="border-t border-gray-200 dark:border-gray-700 p-2 space-y-1">
{/* Theme Toggle */}
<button
onClick={toggleTheme}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm text-gray-600 hover:bg-gray-100 transition-colors dark:text-gray-400 dark:hover:bg-gray-800"
title={theme === 'dark' ? '切换亮色模式' : '切换暗黑模式'}
>
{theme === 'dark' ? (
<Sun className="w-4 h-4" />
) : (
<Moon className="w-4 h-4" />
)}
{!collapsed && <span>{theme === 'dark' ? '亮色模式' : '暗黑模式'}</span>}
</button>
<div className={collapsed ? 'space-y-1' : 'flex items-center gap-1'}>
<button
onClick={toggleTheme}
className={`${collapsed ? 'w-full justify-center' : 'flex-1'} flex items-center gap-3 px-3 py-2.5 rounded-md text-sm text-gray-600 hover:bg-gray-100 transition-colors dark:text-gray-400 dark:hover:bg-gray-800`}
title={t(theme === 'dark' ? '切换亮色模式' : '切换暗黑模式')}
>
{theme === 'dark' ? (
<Sun className="w-4 h-4" />
) : (
<Moon className="w-4 h-4" />
)}
{!collapsed && <span>{theme === 'dark' ? '亮色模式' : '暗黑模式'}</span>}
</button>
<button
onClick={() => { void toggleLanguage() }}
className={`${collapsed ? 'w-full' : 'w-10'} flex items-center justify-center rounded-md px-2 py-2.5 text-sm text-gray-600 hover:bg-gray-100 transition-colors dark:text-gray-400 dark:hover:bg-gray-800`}
title={language === 'en' ? '切换中文' : 'Switch to English'}
>
<LanguageIcon className="h-4 w-4" />
</button>
</div>
{/* Version */}
{version && (
+72
View File
@@ -0,0 +1,72 @@
import { ReactNode, createContext, useContext, useEffect, useMemo, useState } from 'react'
import { translateText } from '../utils/i18n'
import { getLanguage, updateLanguage } from '../services/api'
export type Language = 'zh' | 'en'
interface LanguageContextValue {
language: Language
setLanguage: (language: Language) => void
toggleLanguage: () => Promise<void>
t: (value: string) => string
}
const LanguageContext = createContext<LanguageContextValue | undefined>(undefined)
function initialLanguage(): Language {
return 'zh'
}
export function LanguageProvider({ children }: { children: ReactNode }) {
const [language, setLanguageState] = useState<Language>(initialLanguage)
const setLanguageLocal = (next: Language) => {
setLanguageState(next)
}
const setLanguage = (next: Language) => {
setLanguageLocal(next)
updateLanguage(next).catch(() => {})
}
const value = useMemo<LanguageContextValue>(() => ({
language,
setLanguage,
toggleLanguage: async () => {
const next = language === 'zh' ? 'en' : 'zh'
setLanguageLocal(next)
try {
const res = await updateLanguage(next)
setLanguageLocal(res.data.data?.language || next)
} catch {
setLanguageLocal(language)
}
},
t: (text: string) => language === 'en' ? translateText(text) : text,
}), [language])
useEffect(() => {
getLanguage()
.then((res) => {
const serverLanguage = res.data.data?.language
if (serverLanguage === 'zh' || serverLanguage === 'en') {
setLanguageLocal(serverLanguage)
}
})
.catch(() => {})
}, [])
useEffect(() => {
document.documentElement.lang = language === 'en' ? 'en' : 'zh-CN'
document.documentElement.dataset.language = language
}, [language])
return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>
}
export function useLanguage() {
const context = useContext(LanguageContext)
if (!context) {
throw new Error('useLanguage must be used within LanguageProvider')
}
return context
}
+8 -5
View File
@@ -4,6 +4,7 @@ import { BrowserRouter } from 'react-router-dom'
import App from './App'
import { AuthProvider } from './contexts/AuthContext'
import { ThemeProvider } from './contexts/ThemeContext'
import { LanguageProvider } from './contexts/LanguageContext'
import { DialogProvider } from './components/Dialog'
import './index.css'
@@ -11,11 +12,13 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<ThemeProvider>
<AuthProvider>
<DialogProvider>
<App />
</DialogProvider>
</AuthProvider>
<LanguageProvider>
<AuthProvider>
<DialogProvider>
<App />
</DialogProvider>
</AuthProvider>
</LanguageProvider>
</ThemeProvider>
</BrowserRouter>
</React.StrictMode>,
+23 -1
View File
@@ -2,9 +2,21 @@ import { FormEvent, useState } from 'react'
import { Lock, User } from 'lucide-react'
import AppIcon from '../components/AppIcon'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../contexts/LanguageContext'
import AutoTranslate from '../components/AutoTranslate'
import BrowserDialogTranslator from '../components/BrowserDialogTranslator'
function LanguageIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M213.333333 640v85.333333a85.333333 85.333333 0 0 0 78.933334 85.12L298.666667 810.666667h128v85.333333H298.666667a170.666667 170.666667 0 0 1-170.666667-170.666667v-85.333333h85.333333z m554.666667-213.333333l187.733333 469.333333h-91.946666l-51.242667-128h-174.506667l-51.157333 128h-91.904L682.666667 426.666667h85.333333z m-42.666667 123.093333L672.128 682.666667h106.325333L725.333333 549.76zM341.333333 85.333333v85.333334h170.666667v298.666666H341.333333v128H256v-128H85.333333V170.666667h170.666667V85.333333h85.333333z m384 42.666667a170.666667 170.666667 0 0 1 170.666667 170.666667v85.333333h-85.333333V298.666667a85.333333 85.333333 0 0 0-85.333334-85.333334h-128V128h128zM256 256H170.666667v128h85.333333V256z m170.666667 0H341.333333v128h85.333334V256z" fill="currentColor" />
</svg>
)
}
export default function Login() {
const { login, accessCodeLogin } = useAuth()
const { language, toggleLanguage, t } = useLanguage()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
@@ -29,7 +41,7 @@ export default function Login() {
}
} catch (err: unknown) {
const error = err as { response?: { data?: { message?: string } } }
setError(error.response?.data?.message || '登录失败,请检查用户名和密码')
setError(error.response?.data?.message || t('登录失败,请检查用户名和密码'))
} finally {
setLoading(false)
}
@@ -37,6 +49,16 @@ export default function Login() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<AutoTranslate />
<BrowserDialogTranslator />
<button
type="button"
onClick={() => { void toggleLanguage() }}
className="absolute right-4 top-4 inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 shadow-sm hover:bg-gray-50"
>
<LanguageIcon className="h-3.5 w-3.5" />
{language === 'en' ? '中文' : 'English'}
</button>
<div className="w-full max-w-md">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-8">
<div className="flex flex-col items-center mb-8">
+9
View File
@@ -714,6 +714,15 @@ export const createWebSSHTicket = (containerName: string) =>
export const createVNCTicket = (containerName: string) =>
api.post<APIResponse<{ ticket: string }>>('/vnc-ticket', { container_name: containerName })
// Language
export type PanelLanguage = 'zh' | 'en'
export const getLanguage = () =>
api.get<APIResponse<{ language: PanelLanguage }>>('/language')
export const updateLanguage = (language: PanelLanguage) =>
api.post<APIResponse<{ language: PanelLanguage }>>('/language', { language })
// Version
export const getVersion = () =>
api.get<APIResponse<{ version: string }>>('/version')
+910
View File
@@ -0,0 +1,910 @@
const exact: Record<string, string> = {
'控制面板': 'Dashboard',
'共': 'Total',
'第': 'Page',
'页': 'page',
'个': 'items',
'条': 'records',
'核': 'cores',
'个容器': 'containers',
'条操作记录': 'audit records',
'个地址': 'addresses',
'列表': 'List',
'主机资源': 'Host Resources',
'主机资源状态': 'Host Resource Status',
'容器管理': 'Containers',
'镜像管理': 'Images',
'安全告警': 'Security Alerts',
'快照管理': 'Snapshots',
'路由管理': 'Routing',
'操作日志': 'Audit Logs',
'子用户管理': 'Sub Users',
'API 集成': 'API Integration',
'宿主机信息': 'Host Info',
'面板设置': 'Panel Settings',
'退出登录': 'Log out',
'亮色模式': 'Light Mode',
'暗黑模式': 'Dark Mode',
'切换亮色模式': 'Switch to light mode',
'切换暗黑模式': 'Switch to dark mode',
'切换侧边栏': 'Toggle sidebar',
'刷新': 'Refresh',
'搜索': 'Search',
'复制': 'Copy',
'编辑': 'Edit',
'删除': 'Delete',
'保存': 'Save',
'提交': 'Submit',
'应用': 'Apply',
'查看': 'View',
'详情': 'Details',
'返回': 'Back',
'返回列表': 'Back to list',
'取消': 'Cancel',
'确认': 'Confirm',
'确定': 'OK',
'完成': 'Done',
'失败': 'Failed',
'成功': 'Success',
'提示': 'Notice',
'警告': 'Warning',
'开机': 'Start',
'关机': 'Stop',
'重启': 'Restart',
'重装': 'Reinstall',
'创建': 'Create',
'在线': 'Online',
'离线': 'Offline',
'永久': 'Permanent',
'长期有效': 'No expiration',
'长期': 'No expiration',
'不限制': 'Unlimited',
'未设置流量限制': 'No traffic limit set',
'未设置': 'Not set',
'已选': 'Selected',
',已选': ', selected',
'筛选后': 'Filtered',
',筛选后': ', filtered',
'每页数量': 'Items per page',
'任务中': 'In task',
'1周': '1 week',
'1 周': '1 week',
'资源配置': 'Resource Configuration',
'实时状态': 'Live Status',
'连接信息': 'Connection Info',
'管理链接': 'Management Link',
'NAT 管理': 'NAT Management',
'快照': 'Snapshots',
'系统': 'System',
'全部类型': 'All types',
'全部系统': 'All systems',
'全部状态': 'All statuses',
'类型筛选': 'Type filter',
'系统筛选': 'System filter',
'状态筛选': 'Status filter',
'内网': 'Private IP',
'内网 IP': 'Private IP',
'策略封禁': 'Policy Blocked',
'已封禁': 'Blocked',
'已到期': 'Expired',
'识别码': 'Identifier',
'CPU 累计时间': 'CPU Total Time',
'创建时间': 'Created At',
'网络速率': 'Network Speed',
'IO 速度': 'IO Speed',
'月流量': 'Monthly Traffic',
'统计信息': 'Statistics',
'CPU 使用率': 'CPU Usage',
'内存使用': 'Memory Usage',
'网络流量': 'Network Traffic',
'磁盘IO': 'Disk IO',
'磁盘 IO': 'Disk IO',
'负载': 'Load',
'平均': 'Average',
'峰值': 'Peak',
'容量': 'Capacity',
'累计': 'Total',
'读': 'Read',
'写': 'Write',
'入': 'In',
'出': 'Out',
'运行中': 'Running',
'已停止': 'Stopped',
'已完成': 'Completed',
'等待中': 'Pending',
'执行中': 'Running',
'未知': 'Unknown',
'必要': 'Required',
'可选': 'Optional',
'用户名': 'Username',
'密码': 'Password',
'输入用户名': 'Enter username',
'输入密码': 'Enter password',
'登录': 'Log in',
'登录中...': 'Logging in...',
'登录失败,请检查用户名和密码': 'Login failed. Check your username and password.',
'Authentication required': 'Authentication required',
'Administrator permission required': 'Administrator permission required',
'Method not allowed': 'Method not allowed',
'Invalid request body': 'Invalid request body',
'Invalid credentials': 'Invalid credentials',
'Access denied': 'Access denied',
'Access denied to this container': 'Access denied to this container',
'Container not found': 'Container not found',
'Template not found': 'Template not found',
'Template is required': 'Template is required',
'Template is not enabled or downloaded': 'Template is not enabled or downloaded',
'Container name is required': 'Container name is required',
'Container created successfully': 'Container created successfully',
'Password changed successfully': 'Password changed successfully',
'SSL settings saved': 'SSL settings saved',
'Save SSL settings failed': 'Save SSL settings failed',
'Task deleted': 'Task deleted',
'Snapshot deleted': 'Snapshot deleted',
'Snapshot restored': 'Snapshot restored',
'Security check completed': 'Security check completed',
'当前密码不正确': 'Current password is incorrect',
'密码不正确': 'Password is incorrect',
'新密码至少 6 位': 'New password must be at least 6 characters',
'用户名至少 3 位': 'Username must be at least 3 characters',
'密码加密失败': 'Failed to hash password',
'保存配置失败': 'Failed to save configuration',
'密码修改成功': 'Password changed successfully',
'用户名修改成功': 'Username changed successfully',
'容器已到期,不允许此操作': 'Container has expired. This action is not allowed.',
'容器管理登录': 'Container Access Login',
'操作失败': 'Action failed',
'错误': 'Error',
'保存失败': 'Save failed',
'重装失败': 'Reinstall failed',
'密码重置失败': 'Password reset failed',
'端口配额已满': 'Port quota reached',
'输入错误': 'Input error',
'请输入有效的内部端口': 'Enter a valid internal port',
'密码长度必须为 8-64 位': 'Password length must be 8-64 characters',
'密码不能包含空白字符': 'Password cannot contain whitespace',
'密码至少需要包含字母': 'Password must contain at least one letter',
'密码至少需要包含数字': 'Password must contain at least one number',
'密码格式不正确': 'Invalid password format',
'策略临时封禁': 'Temporarily blocked by policy',
'虚拟机被策略临时封禁,暂不能执行操作。': 'This VM is temporarily blocked by policy and cannot perform actions.',
'确定要删除容器': 'Delete container',
'吗?此操作不可撤销。': '? This action cannot be undone.',
'容器名称不能包含空格': 'Container name cannot contain spaces',
'该容器名称已存在': 'Container name already exists',
'请填写容器名称并选择系统模板': 'Enter a container name and select a system template',
'资源配置有误': 'Invalid resource configuration',
'请按红色提示修改 vCPU、内存或磁盘配置': 'Fix the vCPU, memory, or disk fields marked in red',
'创建失败': 'Create failed',
'创建新容器': 'Create New Container',
'批量创建数量': 'Batch Count',
'虚拟化架构': 'Virtualization',
'LXC 容器': 'LXC Container',
'KVM 虚拟机': 'KVM VM',
'系统模板': 'System Template',
'搜索名称、ID、UUID、IP': 'Search name, ID, UUID, IP',
'带宽 (Mbps)': 'Bandwidth (Mbps)',
'双向统计': 'Total In+Out',
'入/出分离': 'Separate In/Out',
'GB (0=不限制)': 'GB (0=unlimited)',
'入站 (GB)': 'Inbound (GB)',
'出站 (GB)': 'Outbound (GB)',
'NAT 端口映射数量': 'NAT Port Mapping Count',
'子用户快照上限': 'Sub-user Snapshot Limit',
'到期时间': 'Expiration Time',
'不选择则长期有效;选择日期后,到期会自动关机。': 'Leave blank for no expiration. If a date is selected, the container will shut down automatically when it expires.',
'创建中...': 'Creating...',
'请输入 vCPU': 'Enter vCPU',
'内存 (MB)': 'Memory (MB)',
'磁盘 (GB)': 'Disk (GB)',
'IO 速度 (MB/s)': 'IO Speed (MB/s)',
'将创建': 'Will create',
'暂无可用的': 'No available',
'不能小于': 'Cannot be less than',
'不能大于': 'Cannot be greater than',
'KVM vCPU 必须是整数': 'KVM vCPU must be an integer',
'请输入内存': 'Enter memory',
'请输入磁盘': 'Enter disk',
'轮换失败': 'Rotation failed',
'获取操作日志失败': 'Failed to load audit logs',
'获取登录日志失败': 'Failed to load login logs',
'暂无子用户': 'No sub-users',
'容器名称': 'Container Name',
'最后登录': 'Last Login',
'从未登录': 'Never logged in',
'查看密码': 'View Password',
'查看操作日志': 'View Audit Logs',
'查看登录日志': 'View Login Logs',
'轮换密码': 'Rotate Password',
'轮换中...': 'Rotating...',
'用户': 'User',
'未保存,请轮换生成新密码': 'Not saved. Rotate to generate a new password.',
'操作时间': 'Action Time',
'登录时间': 'Login Time',
'登录 IP': 'Login IP',
'请输入当前密码以确认修改': 'Enter current password to confirm changes',
'至少填写新密码或新用户名中的一项': 'Enter at least a new password or a new username',
'用户名已修改': 'Username changed',
'用户名修改失败': 'Username change failed',
'密码已修改': 'Password changed',
'密码修改失败': 'Password change failed',
'下次登录生效': 'Takes effect at next login',
'修改失败': 'Change failed',
'账号、安全证书与登录日志': 'Account, certificates, and login logs',
'SSL 设置已保存,服务正在重启。稍后请用新的协议重新打开面板。': 'SSL settings saved. The service is restarting. Reopen the panel with the new protocol shortly.',
'SSL 设置已保存,重启 clicd 服务后生效。': 'SSL settings saved. Restart the clicd service to apply them.',
'SSL 设置保存失败': 'Failed to save SSL settings',
'纯 IP 证书需要服务器安装 Certbot 5.4+,且验证时 80 端口必须能被 Let’s Encrypt 访问。IP 证书是短有效期证书,certbot 需要保持自动续签。': 'Pure IP certificates require Certbot 5.4+ on the server, and port 80 must be reachable by Lets Encrypt during validation. IP certificates are short-lived, so certbot auto-renewal must remain enabled.',
'自签证书可以加密面板和 VNC,但浏览器会提示证书不受信任;证书快到期时系统会自动重新签发。': 'Self-signed certificates can encrypt the panel and VNC, but browsers will show an untrusted certificate warning. The system will renew them automatically before expiration.',
'上传来源还没有保存证书,请粘贴证书和私钥后保存。': 'No certificate has been saved for the uploaded source. Paste the certificate and private key, then save.',
'当前来源还没有保存证书,保存 SSL 设置时会自动生成或申请。': 'No certificate has been saved for the current source. It will be generated or requested when SSL settings are saved.',
'暂无容器': 'No containers',
'暂无快照': 'No snapshots',
'暂无操作日志': 'No audit logs',
'暂无登录日志': 'No login logs',
'暂无登录记录': 'No login records',
'暂无 NAT4 端口映射': 'No NAT4 port mappings',
'暂无 IPv6 地址分配': 'No IPv6 assignments',
'暂无镜像': 'No images',
'暂无数据': 'No data',
'容器': 'Container',
'名称': 'Name',
'状态': 'Status',
'剩余时间': 'Time Left',
'配置': 'Config',
'镜像': 'Image',
'内存': 'Memory',
'磁盘': 'Disk',
'流量': 'Traffic',
'操作': 'Actions',
'类型': 'Type',
'创建者': 'Creator',
'管理员密码': 'Admin Password',
'SSH 密码': 'SSH Password',
'SSH 地址': 'SSH Address',
'RDP 地址': 'RDP Address',
'VNC 端口': 'VNC Port',
'点击隐藏': 'Click to hide',
'点击显示': 'Click to show',
'编辑资源限制': 'Edit resource limits',
'编辑流量限制': 'Edit traffic limit',
'修改到期时间': 'Change expiration time',
'新 SSH 密码': 'New SSH Password',
'生成随机密码': 'Generate random password',
'密码已修改成功': 'Password changed successfully',
'修改中...': 'Changing...',
'确认修改': 'Confirm Change',
'容器不存在': 'Container not found',
'容器未运行,请先开机': 'Container is not running. Start it first.',
'VNC 控制台暂不可用,请确认 KVM 虚拟机已开机并刷新页面': 'VNC console is unavailable. Make sure the KVM VM is running and refresh the page.',
'虚拟机被策略临时封禁': 'VM temporarily blocked by policy',
'虚拟机被策略临时封禁,连接信息暂不可用。': 'This VM is temporarily blocked by policy. Connection info is unavailable.',
'已达到管理员分配的 NAT 端口配额。': 'The NAT port quota assigned by the administrator has been reached.',
'保存端口映射失败': 'Failed to save port mapping',
'删除端口映射失败': 'Failed to delete port mapping',
'删除映射': 'Delete Mapping',
'确定要删除这条映射规则吗?': 'Delete this mapping rule?',
'快照配额已满': 'Snapshot quota reached',
'已达到管理员设置的快照配额,请先删除旧快照。': 'The snapshot quota set by the administrator has been reached. Delete old snapshots first.',
'拍摄快照': 'Take Snapshot',
'拍摄快照需要先关机,完成后会自动重启容器': 'Taking a snapshot requires shutdown first. The container will restart automatically afterward',
'是否继续?': 'Continue?',
'创建快照失败': 'Failed to create snapshot',
'参数错误': 'Invalid parameters',
'自动快照周期最低是 1 天一次。': 'The minimum automatic snapshot interval is once per day.',
'定时快照失败': 'Scheduled snapshot failed',
'保存快照配额失败': 'Failed to save snapshot quota',
'删除快照失败': 'Failed to delete snapshot',
'恢复快照失败': 'Failed to restore snapshot',
'确定恢复到': 'Restore to',
'当前容器数据会被覆盖。': 'Current container data will be overwritten.',
'确定删除': 'Delete',
'的快照吗?': 'snapshot?',
'新建快照': 'New Snapshot',
'定时设置': 'Schedule Settings',
'定时快照': 'Scheduled Snapshot',
'处理中...': 'Processing...',
'快照数量:': 'Snapshot count:',
'子用户配额:': 'Sub-user quota:',
'定时状态:': 'Schedule status:',
'下次执行:': 'Next run:',
'未开启': 'Off',
'已开启': 'On',
'每': 'Every',
'执行': 'run',
'子用户每台容器快照上限': 'Sub-user snapshot limit per container',
'自动快照周期': 'Automatic snapshot interval',
'天': 'days',
'小时': 'hours',
'分钟': 'minutes',
'秒': 'seconds',
'大小': 'Size',
'手动': 'Manual',
'定时': 'Scheduled',
'时间': 'Time',
'设备': 'Device',
'结果': 'Result',
'地址': 'Address',
'前缀': 'Prefix',
'出口网卡': 'Uplink',
'协议': 'Protocol',
'说明': 'Description',
'端口': 'Port',
'容器端口': 'Container Port',
'宿主机端口': 'Host Port',
'容器 IPv4': 'Container IPv4',
'IPv6 地址': 'IPv6 Address',
'LXC 名称': 'LXC Name',
'快照时间': 'Snapshot Time',
'删除快照': 'Delete Snapshot',
'全局快照列表': 'Global snapshot list',
'主机名': 'Hostname',
'操作系统': 'Operating System',
'内核': 'Kernel',
'生成时间': 'Generated At',
'系统概览': 'System Overview',
'公网与路由': 'Public Network & Routing',
'内存条': 'Memory Modules',
'硬盘与健康': 'Disks & Health',
'网卡': 'Network Interfaces',
'显卡': 'GPUs',
'环境支持': 'Environment Support',
'服务管理器 systemd/OpenRC': 'Service Manager systemd/OpenRC',
'LXC 创建工具': 'LXC Create Tool',
'LXC 启动工具': 'LXC Start Tool',
'iptables 网络规则': 'iptables Network Rules',
'iproute2 网络工具': 'iproute2 Network Tool',
'conntrack 安全扫描': 'conntrack Security Scan',
'QEMU/KVM 虚拟机': 'QEMU/KVM Virtualization',
'KVM cloud-init ISO 工具': 'KVM cloud-init ISO Tool',
'ISO 备用工具': 'ISO Fallback Tool',
'硬盘健康检测': 'Disk Health Check',
'Certbot 证书工具 >= 5.4': 'Certbot Certificate Tool >= 5.4',
'/dev/kvm 硬件虚拟化': '/dev/kvm Hardware Virtualization',
'IPv4 转发': 'IPv4 Forwarding',
'lxcfs 服务': 'lxcfs Service',
'libvirt 服务': 'libvirt Service',
'正在探测宿主机环境...': 'Probing host environment...',
'暂未获取到宿主机信息': 'No host information available',
'面板资源状态与容器概览': 'Panel resource status and container overview',
'宿主机资源状态与容器概览': 'Host resource status and container overview',
'账号设置': 'Account Settings',
'当前用户名': 'Current Username',
'新用户名,留空则不修改': 'New Username, leave blank to keep unchanged',
'新密码,留空则不修改': 'New Password, leave blank to keep unchanged',
'当前密码,验证身份': 'Current Password, for verification',
'至少 3 位': 'At least 3 characters',
'至少 6 位': 'At least 6 characters',
'输入当前密码以确认修改': 'Enter current password to confirm changes',
'保存修改': 'Save Changes',
'SSL 证书': 'SSL Certificate',
'启用 HTTPS / WSS': 'Enable HTTPS / WSS',
'IP / 域名': 'IP / Domain',
'服务器公网 IP 或域名': 'Server public IP or domain',
'邮箱,可选': 'Email, optional',
'自签证书': 'Self-signed Certificate',
'上传证书': 'Uploaded Certificate',
'证书 PEM / fullchain.pem': 'Certificate PEM / fullchain.pem',
'私钥 PEM / privkey.pem': 'Private Key PEM / privkey.pem',
'保存后自动重启服务并立即生效': 'Restart service automatically after saving',
'保存中...': 'Saving...',
'保存 SSL 设置': 'Save SSL Settings',
'登录日志': 'Login Logs',
'首页': 'First',
'上一页': 'Previous',
'下一页': 'Next',
'末页': 'Last',
'搜索端口/容器...': 'Search port/container...',
'搜索地址/容器...': 'Search address/container...',
'NAT4 端口': 'NAT4 Ports',
'NAT4 端口分配': 'NAT4 Port Allocation',
'IPv6 地址分配': 'IPv6 Address Allocation',
'剩余端口 / 端口总数': 'Available Ports / Total Ports',
'剩余地址 / 地址总数': 'Available Addresses / Total Addresses',
'已分配': 'Allocated',
'充足': 'Enough',
'容器列表': 'Container List',
'刷新列表': 'Refresh list',
'创建容器': 'Create Container',
'点击"创建容器"开始': 'Click "Create Container" to start',
'创建中': 'Creating',
'批量创建': 'Batch Create',
'导入容器': 'Import Container',
'重置密码': 'Reset Password',
'WebSSH': 'WebSSH',
'WebVNC': 'WebVNC',
'发送 Ctrl+Alt+Del': 'Send Ctrl+Alt+Del',
'重新连接': 'Reconnect',
'关闭': 'Close',
'已连接': 'Connected',
'连接中...': 'Connecting...',
'已断开': 'Disconnected',
'连接失败': 'Connection Failed',
'正在连接 KVM VNC 控制台...': 'Connecting to KVM VNC console...',
'WebVNC 已断开': 'WebVNC disconnected',
'下载': 'Download',
'下载中': 'Downloading',
'启用': 'Enable',
'停用': 'Disable',
'已启用': 'Enabled',
'未启用': 'Disabled',
'系统镜像': 'System Images',
'安全检查': 'Security Check',
'告警列表': 'Alert List',
'自动关机已开': 'Auto-stop on',
'自动关机已关': 'Auto-stop off',
'暂无安全告警': 'No security alerts',
'严重': 'Critical',
'高': 'High',
'中': 'Medium',
'低': 'Low',
'管理员': 'Admin',
'子用户': 'Sub User',
'公网 IPv4': 'Public IPv4',
'IPv4 地址': 'IPv4 Addresses',
'IPv4 段': 'IPv4 Prefixes',
'IPv6 段': 'IPv6 Prefixes',
'网关': 'Gateways',
'CPU 架构': 'CPU Architecture',
'CPU 虚拟化指令': 'CPU Virtualization Flags',
'CPU 核显': 'Integrated GPU',
'运行能力': 'Runtime Capability',
'KVM 嵌套虚拟化': 'KVM Nested Virtualization',
'支持': 'Supported',
'未检测到': 'Not detected',
'检测到': 'Detected',
'有效': 'Valid',
'已过期或未生效': 'Expired or not active',
'是': 'Yes',
'否': 'No',
'开启': 'On',
'已关闭': 'Off',
'自动': 'Auto',
'默认': 'Default',
'全部': 'All',
'无': 'None',
'根目录': 'Root',
'版本': 'Version',
'当前': 'Current',
'最近': 'Recent',
'来源': 'Source',
'目标': 'Target',
'描述': 'Description',
'备注': 'Notes',
'搜索容器...': 'Search containers...',
'搜索镜像...': 'Search images...',
'搜索日志...': 'Search logs...',
'复制成功': 'Copied',
'复制失败': 'Copy failed',
'请稍后重试': 'Please try again later',
'请稍后重试。': 'Please try again later.',
'开机中...': 'Starting...',
'关机中...': 'Stopping...',
'重启中...': 'Restarting...',
'删除中...': 'Deleting...',
'重装中...': 'Reinstalling...',
'开机中': 'Starting',
'关机中': 'Stopping',
'重启中': 'Restarting',
'删除中': 'Deleting',
'重装中': 'Reinstalling',
'正在初始化': 'Initializing',
'容器总数': 'Total Containers',
'驱动/速率': 'Driver / Speed',
'支持 KVM + LXC': 'KVM + LXC supported',
'仅支持 LXC': 'LXC only',
'未满足运行环境': 'Runtime requirements not met',
'健康': 'Healthy',
'异常': 'Abnormal',
'核显': 'Integrated',
'独显': 'Discrete',
'获取镜像列表失败': 'Failed to load image list',
'下载失败': 'Download failed',
'删除失败': 'Delete failed',
'取消失败': 'Cancel failed',
'删除镜像': 'Delete Image',
'确定要删除该镜像缓存吗?删除后需要重新下载才能使用。': 'Delete this image cache? You must download it again before using it.',
'取消下载并清理临时文件': 'Cancel download and clean temporary files',
'删除镜像缓存': 'Delete image cache',
'取消中': 'Cancelling',
'取消中...': 'Cancelling...',
'下载中...': 'Downloading...',
'阶段:': 'Stage:',
'转换中': 'Converting',
'端口扫描': 'Port scan',
'横向扫描': 'Lateral scan',
'暴力破解': 'Brute force',
'DDoS/大规模扫描': 'DDoS / large-scale scan',
'垃圾邮件': 'Spam',
'恶意软件': 'Malware',
'挖矿连接': 'Mining connection',
'代理/VPN/Tor': 'Proxy / VPN / Tor',
'UDP反射放大': 'UDP reflection amplification',
'高危': 'High risk',
'中危': 'Medium risk',
'低危': 'Low risk',
'告警自动关机': 'Auto shutdown on alerts',
'相关连接记录': 'Related Connection Records',
'查看相关记录': 'View related records',
'告警原始记录': 'Raw Alert Record',
'正在加载连接记录...': 'Loading connection records...',
'暂无可用连接记录。历史告警对应的 conntrack 记录可能已经过期。': 'No connection records available. Conntrack records for historical alerts may have expired.',
'源地址': 'Source Address',
'目标地址': 'Target Address',
'源IP': 'Source IP',
'次数': 'Count',
'等级': 'Severity',
'总览与只读': 'Overview & Read-only',
'路由信息': 'Routing Info',
'IPv6 状态': 'IPv6 Status',
'镜像列表': 'Image List',
'查看容器': 'View Container',
'开关机/重启': 'Power / Restart',
'重装系统': 'Reinstall OS',
'资源/到期': 'Resources / Expiration',
'流量管理': 'Traffic Management',
'端口映射': 'Port Mappings',
'分配 IPv6': 'Assign IPv6',
'快照与终端': 'Snapshots & Terminal',
'查看快照': 'View Snapshots',
'创建快照': 'Create Snapshot',
'恢复快照': 'Restore Snapshot',
'计划/配额': 'Schedule / Quota',
'平台管理': 'Platform Management',
'下载镜像': 'Download Image',
'启停镜像': 'Enable / Disable Image',
'安全数据': 'Security Data',
'安全扫描': 'Security Scan',
'安全设置': 'Security Settings',
'Swap 信息': 'Swap Info',
'Swap 管理': 'Swap Management',
'Key 列表': 'Key List',
'创建 Key': 'Create Key',
'更新 Key': 'Update Key',
'删除 Key': 'Delete Key',
'总览': 'Overview',
'NAT/IPv6 路由': 'NAT / IPv6 Routing',
'任务队列': 'Task Queue',
'任务列表': 'Task List',
'操作记录': 'audit records',
'子用户列表': 'Sub-user List',
'创建子用户': 'Create Sub-user',
'更新子用户': 'Update Sub-user',
'管理员接口': 'Admin API',
'控制面板统计': 'Dashboard Stats',
'立即安全检查': 'Run Security Check',
'返回响应样例': 'Response Example',
'请求参数': 'Request Parameters',
'响应字段': 'Response Fields',
'接口地址': 'Endpoint',
'请求方法': 'Method',
'权限范围': 'Scopes',
'绑定容器': 'Bound Containers',
'全部容器': 'All Containers',
'全权限': 'Full Access',
'取消全权限': 'Remove Full Access',
'禁用这个 Key': 'Disable this key',
'过期时间': 'Expiration Time',
'永不过期': 'Never expires',
'IP 白名单': 'IP Whitelist',
'密钥名称': 'Key Name',
'删除任务': 'Delete Task',
'容器详情': 'Container Details',
'资源用量': 'Resource Usage',
'流量统计': 'Traffic Stats',
'重置流量': 'Reset Traffic',
'调整流量限制': 'Adjust Traffic Limit',
'调整资源限制': 'Adjust Resource Limit',
'重置 SSH 密码': 'Reset SSH Password',
'端口与快照': 'Ports & Snapshots',
'随机可用端口': 'Random Available Port',
'添加端口映射': 'Add Port Mapping',
'更新端口映射': 'Update Port Mapping',
'删除端口映射': 'Delete Port Mapping',
'快照总览': 'Snapshot Overview',
'容器快照': 'Container Snapshots',
'计划快照': 'Scheduled Snapshots',
'快照配额': 'Snapshot Quota',
'模板列表': 'Template List',
'取消镜像下载': 'Cancel Image Download',
'启用/禁用镜像': 'Enable / Disable Image',
'安全连接日志': 'Security Connection Logs',
'安全汇总': 'Security Summary',
'更新安全设置': 'Update Security Settings',
'调整 Swap': 'Adjust Swap',
'批量开关机/删除/重装': 'Batch power/delete/reinstall',
'账号与日志': 'Account & Logs',
'API Key 列表': 'API Key List',
'创建 API Key': 'Create API Key',
'更新 API Key': 'Update API Key',
'删除 API Key': 'Delete API Key',
'30分钟': '30 minutes',
'1小时': '1 hour',
'1天': '1 day',
'切换中文': 'Switch to Chinese',
'WebSSH ticket 创建失败,请重新登录后再试': 'Failed to create WebSSH ticket. Log in again and retry.',
'WebSSH ticket 为空,请重新登录后再试': 'WebSSH ticket is empty. Log in again and retry.',
'WebSSH 连接失败,请确认容器已运行且 SSH 服务可用': 'WebSSH connection failed. Make sure the container is running and SSH is available.',
'WebVNC ticket 创建失败,请重新登录后再试': 'Failed to create WebVNC ticket. Log in again and retry.',
'WebVNC ticket 为空,请重新登录后再试': 'WebVNC ticket is empty. Log in again and retry.',
'WebVNC 连接已断开,请确认虚拟机正在运行且 VNC 控制台可用': 'WebVNC disconnected. Make sure the VM is running and the VNC console is available.',
'VNC 安全协商失败': 'VNC security negotiation failed',
'当前 VNC 控制台要求密码,暂不支持自动输入': 'This VNC console requires a password. Automatic input is not supported yet.',
'删除容器': 'Delete Container',
'WebSSH 票据': 'WebSSH Ticket',
'WebVNC 票据': 'WebVNC Ticket',
'容器列表(兼容旧接口)': 'Container List (legacy-compatible API)',
'调整到期时间': 'Adjust Expiration Time',
'镜像管理列表': 'Image Management List',
'批量创建容器': 'Batch Create Containers',
'创建 WebSSH 票据': 'Create WebSSH Ticket',
'创建 WebVNC 票据': 'Create WebVNC Ticket',
'创建子用户链接': 'Create Sub-user Link',
'轮换子用户密码': 'Rotate Sub-user Password',
'子用户操作日志': 'Sub-user Audit Logs',
'子用户登录日志': 'Sub-user Login Logs',
'确定删除这个 API Key 吗?': 'Delete this API Key?',
'管理外部调用凭据、权限范围与平台 API 文档': 'Manage external credentials, permission scopes, and platform API docs',
'新的 API Key 已生成': 'New API Key generated',
'已复制': 'Copied',
'加载中...': 'Loading...',
'暂无 API Key': 'No API Keys',
'权限': 'Permissions',
'限制': 'Limits',
'最后使用': 'Last Used',
'已禁用': 'Disabled',
'不限 IP': 'Any IP',
'从未使用': 'Never used',
'API 文档': 'API Docs',
'查看使用范例': 'View examples',
'Python 使用范例': 'Python example',
'编辑 API Key': 'Edit API Key',
'CI/CD、计费系统、自动化脚本': 'CI/CD, billing systems, automation scripts',
'SWAP 已调整为 16384 MB': 'SWAP adjusted to 16384 MB',
'***60秒有效票据***': '***60-second valid ticket***',
'WebVNC 仅适用于 KVM 虚拟机;LXC 容器会返回 VNC console is only available for KVM VMs。': 'WebVNC only applies to KVM VMs; LXC containers return "VNC console is only available for KVM VMs".',
'该接口会进入任务队列,请随后调用 GET /api/v1/tasks 查看执行状态。': 'This API enters the task queue. Call GET /api/v1/tasks afterward to check execution status.',
'样例中的密钥、密码和票据已脱敏;创建类接口的完整密钥只在创建响应中出现一次。': 'Keys, passwords, and tickets in examples are masked. Full secrets from create APIs appear only once in the creation response.',
'编辑月流量限制': 'Edit Monthly Traffic Limit',
'流量统计模式': 'Traffic Accounting Mode',
'双向合并统计': 'Combined In+Out',
'入站/出站分开统计': 'Separate Inbound/Outbound',
'月流量上限 (GB0=不限制)': 'Monthly traffic limit (GB, 0=unlimited)',
'入站上限 (GB0=不限制)': 'Inbound limit (GB, 0=unlimited)',
'出站上限 (GB0=不限制)': 'Outbound limit (GB, 0=unlimited)',
'请输入 8-64 位,至少包含字母和数字': 'Enter 8-64 characters, including at least letters and numbers',
'Linux LXC/KVM 修改 root SSH 密码通常无需重启;KVM 需要虚拟机运行且 guest agent 或 SSH 可用。': 'Changing the root SSH password for Linux LXC/KVM usually does not require a restart. KVM requires the VM to be running and guest agent or SSH to be available.',
'退出全屏': 'Exit Fullscreen',
'全屏显示': 'Fullscreen',
'全屏': 'Fullscreen',
'修改': 'Change',
'关闭定时': 'Disable Schedule',
'执行时间': 'Run Time',
'NAT 端口管理': 'NAT Port Management',
'添加映射': 'Add Mapping',
'端口配额:': 'Port quota:',
'已达到管理员分配的 NAT 端口配额': 'The NAT port quota assigned by the administrator has been reached',
'修改端口映射': 'Edit Port Mapping',
'重装系统会删除容器内所有数据,请谨慎操作。': 'Reinstalling the OS will delete all data in the container. Proceed carefully.',
'选择新系统模板': 'Select New System Template',
'确认重装': 'Confirm Reinstall',
'当前:': 'Current:',
'新到期日期(留空为长期有效)': 'New expiration date (leave blank for no expiration)',
'vCPU 核数': 'vCPU Cores',
'网络速率 (Mbps0=不限制)': 'Network speed (Mbps, 0=unlimited)',
'IO 速度 (MB/s0=不限制)': 'IO speed (MB/s, 0=unlimited)',
'磁盘容量不支持动态修改。修改后运行中的容器会立即应用新的 cgroup 限制。': 'Disk capacity cannot be changed dynamically. Running containers apply the new cgroup limits immediately.',
'恢复': 'Restore',
'全部 (ALL)': 'All (ALL)',
'外部端口': 'External Port',
'默认同内部': 'Same as internal by default',
'随机空闲端口': 'Random Free Port',
'随机': 'Random',
'内部端口': 'Internal Port',
'例如 80': 'e.g. 80',
'暂无端口映射': 'No port mappings',
'默认 SSH 映射不能删除': 'Default SSH mapping cannot be deleted',
'入站 (RX)': 'Inbound (RX)',
'(不限制)': '(unlimited)',
'出站 (TX)': 'Outbound (TX)',
'已用': 'Used',
'重置': 'Reset',
'执行中...': 'Running...',
'点击': 'Click',
'开始': 'Start',
'没有匹配的容器': 'No matching containers',
'显示': 'Showing',
'初始化失败': 'Initialization failed',
'初始化完成': 'Initialization complete',
'排队等待': 'Queued',
'处理中': 'Processing',
'未知系统': 'Unknown system',
'处理失败': 'Failed',
'暂无任务': 'No tasks',
'取消任务': 'Cancel Task',
'硬件、网络、磁盘健康与运行环境探测报告': 'Hardware, network, disk health, and runtime environment report',
'运行状态': 'Runtime Status',
'未检测到内存条明细,可能缺少 dmidecode 或权限受限': 'No memory module details detected. dmidecode may be missing or permissions may be limited.',
'插槽': 'Slot',
'频率': 'Frequency',
'厂商': 'Vendor',
'型号/序列号': 'Model / Serial',
'未检测到硬盘': 'No disks detected',
'型号': 'Model',
'挂载点': 'Mount Point',
'寿命': 'Lifetime',
'通电': 'Power-on',
'读取': 'Reads',
'写入': 'Writes',
'命令数': 'Commands',
'擦写': 'Erase Count',
'未检测到网卡': 'No network interfaces detected',
'未检测到显卡': 'No GPUs detected',
'驱动': 'Driver',
'管理 LXC / KVM 系统镜像,下载后的镜像才能用于创建容器/虚拟机。': 'Manage LXC / KVM system images. Downloaded images can be used to create containers/VMs.',
'已下载': 'Downloaded',
'LXC 容器镜像': 'LXC Container Images',
'KVM 虚拟机镜像': 'KVM VM Images',
'发行版': 'Distribution',
'架构': 'Architecture',
'禁用': 'Disable',
'可用': 'Available',
'未下载': 'Not downloaded',
'中文': 'Chinese',
'宿主机分配给 LXC 的 NAT4 端口和 IPv6 地址': 'NAT4 ports and IPv6 addresses assigned to LXC by the host',
'确认删除容器': 'Delete container',
'剩余地址 / 地址总数 ·': 'Available Addresses / Total Addresses ·',
'结果 ': 'Result ',
'告警列表 (': 'Alert List (',
'当前证书:': 'Current certificate:',
'到期时间:': 'Expires:',
'证书路径:': 'Certificate path:',
'最近错误:': 'Last error:',
'1 天': '1 day',
'3 天': '3 days',
'7 天': '7 days',
'14 天': '14 days',
'10 / 页': '10 / page',
'20 / 页': '20 / page',
'50 / 页': '50 / page',
'全局快照列表,共': 'Global snapshot list, total',
'容器分配的子用户列表,共': 'Sub-user list assigned to containers, total',
}
const artifactPatterns: RegExp[] = [
/Back\s*列表/,
/SearchName、ID、UUID、IP/,
/All(Type|Status|系统)/,
/AutoStop\s*已[开关]/,
/暂\s*(None|无)\s*Security Alerts/,
/Memory\s*使用/,
/网络\s*Traffic/,
/实时\s*Status/,
/Create\s*Time/,
/长期\s*Valid/,
]
const replacements: Array<[RegExp, string]> = [
[/Back\s*列表/g, 'Back to list'],
[/Search\s*名称、ID、UUID、IP/g, 'Search name, ID, UUID, IP'],
[/All\s*类型/g, 'All types'],
[/All\s*系统/g, 'All systems'],
[/All\s*状态/g, 'All statuses'],
[/AllType/g, 'All types'],
[/All系统/g, 'All systems'],
[/AllStatus/g, 'All statuses'],
[/SearchName、ID、UUID、IP/g, 'Search name, ID, UUID, IP'],
[/AutoStop\s*已关/g, 'Auto-stop off'],
[/AutoStop\s*已开/g, 'Auto-stop on'],
[/暂\s*None\s*Security Alerts/g, 'No security alerts'],
[/暂\s*无\s*Security Alerts/g, 'No security alerts'],
[/WebVNC\s*初始化失败(.+)$/g, 'WebVNC initialization failed$1'],
[/确定要删除容器\s*(.+?)\s*吗?此操作不可撤销。/g, 'Delete container $1? This action cannot be undone.'],
[/确定要删除容器\s*(.+?)\s*吗?此操作不可撤销。/g, 'Delete container $1? This action cannot be undone.'],
[/拍摄快照需要先关机,完成后会自动重启容器\s*(.+?)。是否继续?/g, 'Taking a snapshot requires shutdown first. Container $1 will restart automatically afterward. Continue?'],
[/确定删除\s*(.+?)\s*的快照吗?/g, 'Delete snapshot $1?'],
[/确定恢复到\s*(.+?)\s*的快照吗?当前容器数据会被覆盖。/g, 'Restore to snapshot $1? Current container data will be overwritten.'],
[/旧版\s*\/api\/containers\/list\s*已兼容,但新接入请使用\s*GET\s*\/api\/v1\/containers/g, 'Legacy /api/containers/list remains compatible, but new integrations should use GET /api/v1/containers'],
[/到期\s*(.+)$/g, 'Expires $1'],
[/支持\s*\((.+?)\)/g, 'Supported ($1)'],
[/下载中\s*(.+)$/g, 'Downloading $1'],
[/结果\s*(.+)$/g, 'Result $1'],
[/磨损\s*(.+)$/g, 'Wear $1'],
[/擦写\s*(.+)$/g, 'Erase $1'],
[/启停\s*(.+)$/g, 'Power cycles $1'],
[/每\s*(.+)$/g, 'Every $1'],
[/已开启,每\s*(.+)$/g, 'Enabled, every $1'],
[/告警列表\s*\((\d+)\)/g, 'Alert List ($1)'],
[/共\s*(\d+)\s*个\s*Container/g, 'Total $1 containers'],
[/共\s*(\d+)\s*个\s*容器/g, 'Total $1 containers'],
[/共\s*(\d+)\s*条/g, 'Total $1'],
[/共\s*(\d+)\s*个/g, 'Total $1 items'],
[/,筛选后\s*(\d+)\s*个/g, ', filtered $1 items'],
[/,已选\s*(\d+)\s*个/g, ', selected $1 items'],
[/第\s*(\d+)\/(\d+)\s*页/g, 'Page $1/$2'],
[/显示\s*(\d+)-(\d+)\s*\/\s*(\d+)/g, 'Showing $1-$2 / $3'],
[/显示\s*(\d+)-(\d+),共\s*(\d+)\s*条/g, 'Showing $1-$2 of $3'],
[/搜索\s*"([^"]+)"\s*结果\s*(\d+)\s*条,/g, 'Search "$1" returned $2 results, '],
[/搜索\s*"([^"]+)"\s*结果\s*(\d+)\s*个地址/g, 'Search "$1" returned $2 addresses, '],
[/(\d+)\s*个/g, '$1 items'],
[/(\d+)\s*条/g, '$1 records'],
[/(\d+)\s*核/g, '$1 cores'],
[/(\d+)\s*线程/g, '$1 threads'],
[/已用/g, 'used'],
[/未设置\s*Traffic\s*限制/g, 'No traffic limit set'],
[/Memory\s*使用/g, 'Memory Usage'],
[/网络\s*Traffic/g, 'Network Traffic'],
[/实时\s*Status/g, 'Live Status'],
[/Expiration Time\s*长期\s*Valid/g, 'Expiration Time No expiration'],
[/长期\s*Valid/g, 'No expiration'],
[/Create\s*Time/g, 'Created At'],
[/CPU\s*累计\s*Time/g, 'CPU Total Time'],
[/(\d+(?:\.\d+)?)\s*cores\s*\/\s*(\d+)\s*核/g, '$1 cores / $2 cores'],
[/(\d+)\s*核\/(.+?)\/(\d+)\s*GB/g, '$1 cores / $2 / $3 GB'],
[/(\d+)\s*\/\s*页/g, '$1 / page'],
[/到期时间:/g, 'Expires: '],
[/证书路径:/g, 'Certificate path: '],
[/最近错误:/g, 'Last error: '],
[/当前证书:/g, 'Current certificate: '],
[/第\s*(\d+)\s*页/g, 'Page $1'],
[/入\s*([^/]+)\s*\/\s*出\s*([^]+),累计\s*(.+)$/g, 'In $1 / Out $2, total $3'],
[/读\s*([^/]+)\s*\/\s*写\s*([^]+),累计\s*([^]+),容量\s*(.+)$/g, 'Read $1 / Write $2, total $3, capacity $4'],
[/(.+?),筛选后\s*(\d+)\s*items/g, '$1, filtered $2 items'],
[/(.+?),已选\s*(\d+)\s*items/g, '$1, selected $2 items'],
[/将创建\s*(\d+)\s*个容器:(.+?)\s*至\s*(.+)$/g, 'Will create $1 containers: $2 to $3'],
[/暂无可用的\s*(KVM|LXC)\s*系统镜像,请先在「镜像管理」中下载镜像模板。/g, 'No available $1 system images. Download image templates in Images first.'],
[/不能小于\s*(.+)$/g, 'Cannot be less than $1'],
[/不能大于\s*(.+)$/g, 'Cannot be greater than $1'],
[/^(.+?)\s*-\s*操作日志$/g, '$1 - Audit Logs'],
[/^(.+?)\s*-\s*登录日志$/g, '$1 - Login Logs'],
[/^(.+?)。下次登录生效$/g, '$1. Takes effect at next login'],
[/阶段:(.+)$/g, 'Stage: $1'],
[/\$\{days\}天/g, '${days} days'],
[/\$\{hours\}小时/g, '${hours} hours'],
[/\$\{hours\}\s*小时/g, '${hours} hours'],
[/\$\{Math\.floor\(diff \/ 60000\)\}分钟/g, '${Math.floor(diff / 60000)} minutes'],
[/(\d+)分钟/g, '$1 minutes'],
[/(\d+)小时/g, '$1 hours'],
[/(\d+)\s*周/g, '$1 weeks'],
[/(\d+)天/g, '$1 days'],
[/确认删除容器\s*(.+?)\s*的快照吗?此操作不可恢复。/g, 'Delete the snapshot for container $1? This cannot be undone.'],
[/确定要删除容器\s*(.+?)\s*吗?此操作不可撤销。/g, 'Delete container $1? This action cannot be undone.'],
[/容器\s*(.+?)\s*已开机/g, 'Container $1 started'],
[/容器\s*(.+?)\s*已关机/g, 'Container $1 stopped'],
[/容器\s*(.+?)\s*已重启/g, 'Container $1 restarted'],
]
export function translateText(value: string): string {
if (!shouldTranslateText(value)) return value
const leading = value.match(/^\s*/)?.[0] || ''
const trailing = value.match(/\s*$/)?.[0] || ''
const body = value.trim()
if (!body) return value
if (exact[body]) return leading + exact[body] + trailing
let translated = body
for (const [pattern, replacement] of replacements) {
translated = translated.replace(pattern, replacement)
}
for (const [source, target] of Object.entries(exact).sort((a, b) => b[0].length - a[0].length)) {
translated = translated.split(source).join(target)
}
translated = cleanupTranslatedText(translated)
return leading + translated + trailing
}
export function shouldTranslateText(value: string): boolean {
return /[\u3400-\u9fff]/.test(value) || artifactPatterns.some((pattern) => pattern.test(value))
}
function cleanupTranslatedText(value: string): string {
return value
.replace(/Back\s*List/g, 'Back to list')
.replace(/Container\s*List/g, 'Container List')
.replace(/Snapshot\s*List/g, 'Snapshot List')
.replace(/All\s*Type/g, 'All types')
.replace(/All\s*Status/g, 'All statuses')
.replace(/All\s*System/g, 'All systems')
.replace(/AutoStop\s*Off/g, 'Auto-stop off')
.replace(/AutoStop\s*On/g, 'Auto-stop on')
.replace(/\s{2,}/g, ' ')
}