mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-07 22:24:42 +08:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 Let’s 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',
|
||||
'月流量上限 (GB,0=不限制)': 'Monthly traffic limit (GB, 0=unlimited)',
|
||||
'入站上限 (GB,0=不限制)': 'Inbound limit (GB, 0=unlimited)',
|
||||
'出站上限 (GB,0=不限制)': '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',
|
||||
'网络速率 (Mbps,0=不限制)': 'Network speed (Mbps, 0=unlimited)',
|
||||
'IO 速度 (MB/s,0=不限制)': '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, ' ')
|
||||
}
|
||||
Reference in New Issue
Block a user