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

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

No files matched your search

@@ -0,0 +1,185 @@
/**
* Port-forward traffic diagram.
* Solid = path to firewall; dashed = SSH tunnel (explicit stroke — Tailwind stroke-* is unreliable on SVG).
*/
import { useTranslation } from 'react-i18next'
import appIcon from '../../assets/opsxshell-app-icon.png'
import firewallIcon from '../../assets/diagram/network-firewall.png'
import serverIcon from '../../assets/diagram/ec2.png'
import cloudGcp from '../../assets/diagram/cloud-gcp.svg'
import cloudCf from '../../assets/diagram/cloud-cf.svg'
import type { ForwardRuleType } from './forwardRules'
interface ForwardArchitectureDiagramProps {
type: ForwardRuleType
}
const SOLID = '#fb7185'
const DASH = '#60a5fa'
function AppBadge() {
return (
<div className="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-accent-blue/40 bg-[#0b1220] shadow-lg shadow-accent-blue/15">
<img src={appIcon} alt="" className="h-10 w-10 object-cover" draggable={false} />
</div>
)
}
function IconImg({
src,
className,
rounded,
}: {
src: string
className?: string
rounded?: boolean
}) {
return (
<img
src={src}
alt=""
draggable={false}
className={`object-contain drop-shadow-md ${rounded ? 'rounded-xl' : ''} ${className || ''}`}
/>
)
}
function SolidPath({ d }: { d: string }) {
return (
<path
d={d}
fill="none"
stroke={SOLID}
strokeWidth="2.4"
strokeLinecap="round"
/>
)
}
function DashPath({ d }: { d: string }) {
return (
<path
d={d}
fill="none"
stroke={DASH}
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="6 5"
opacity={0.95}
/>
)
}
export default function ForwardArchitectureDiagram({ type }: ForwardArchitectureDiagramProps) {
const { t } = useTranslation('common')
const caption =
type === 'local'
? t('portForward.diagram.localCaption')
: type === 'remote'
? t('portForward.diagram.remoteCaption')
: t('portForward.diagram.dynamicCaption')
return (
<div className="rounded-xl border border-dark-700/60 bg-dark-900/35 px-3 py-3">
<div className="mb-2 text-[11px] font-medium text-content-secondary">
{t('portForward.diagram.title')}
</div>
<div className="relative mx-auto h-[160px] w-full max-w-[360px] overflow-hidden rounded-lg border border-dark-700/40 bg-gradient-to-b from-dark-900/50 to-dark-950/70">
{/* Lines under icons */}
<svg
className="pointer-events-none absolute inset-0 h-full w-full"
viewBox="0 0 360 160"
preserveAspectRatio="none"
aria-hidden
>
{type === 'local' && (
<>
{/* App → firewall (solid) */}
<SolidPath d="M64 48 H155" />
{/* App → SSH host → remote targets (dashed) */}
<DashPath d="M56 58 C100 95, 150 118, 180 122" />
<DashPath d="M180 122 C235 115, 275 70, 300 48" />
<DashPath d="M180 122 C245 110, 285 75, 318 52" />
</>
)}
{type === 'remote' && (
<>
{/* Same span as Local/Dynamic: ~App→Firewall */}
<SolidPath d="M64 48 H155" />
<DashPath d="M56 58 C100 95, 150 118, 180 122" />
<DashPath d="M304 58 C260 95, 210 118, 180 122" />
</>
)}
{type === 'dynamic' && (
<>
<SolidPath d="M64 48 H155" />
<DashPath d="M56 58 C100 95, 150 118, 180 122" />
<DashPath d="M180 122 C245 110, 285 70, 318 42" />
<DashPath d="M180 122 C245 120, 285 100, 320 88" />
<DashPath d="M180 122 C235 132, 275 138, 318 132" />
</>
)}
</svg>
{type === 'local' && (
<>
<div className="absolute left-3 top-[26px]">
<AppBadge />
</div>
{/* Firewall + SSH host: same column, vertically centered as a pair */}
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-3">
<IconImg src={firewallIcon} rounded className="h-11 w-11" />
<IconImg src={serverIcon} className="h-10 w-10" />
</div>
<div className="absolute right-3 top-[26px] flex items-center gap-1.5 rounded-xl border border-dark-600/60 bg-dark-800/40 px-2 py-1.5">
<IconImg src={serverIcon} className="h-9 w-9" />
<IconImg src={serverIcon} className="h-9 w-9" />
</div>
</>
)}
{type === 'remote' && (
<>
{/* Match Local/Dynamic left inset so solid gap equals App→Firewall */}
<div className="absolute left-3 top-[26px]">
<IconImg src={serverIcon} className="h-11 w-11" />
</div>
<div className="absolute left-1/2 top-[26px] -translate-x-1/2">
<IconImg src={firewallIcon} rounded className="h-11 w-11" />
</div>
<div className="absolute right-3 top-[26px]">
<IconImg src={serverIcon} className="h-11 w-11" />
</div>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2">
<AppBadge />
</div>
</>
)}
{type === 'dynamic' && (
<>
<div className="absolute left-3 top-[26px]">
<AppBadge />
</div>
{/* Firewall + SSH host stacked & vertically centered */}
<div className="absolute left-[42%] top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-3">
<IconImg src={firewallIcon} rounded className="h-11 w-11" />
<IconImg src={serverIcon} className="h-10 w-10" />
</div>
<div className="absolute right-3 top-1/2 flex -translate-y-1/2 flex-col items-center gap-2 rounded-2xl border border-dark-600/60 bg-dark-800/40 px-2.5 py-2.5">
<IconImg src={cloudGcp} className="h-7 w-7" />
<IconImg src={cloudCf} className="h-7 w-7" />
<IconImg src={cloudGcp} className="h-6 w-6 opacity-70" />
</div>
</>
)}
</div>
<p className="mt-2.5 text-[11px] leading-relaxed text-txt-muted">{caption}</p>
</div>
)
}