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:
1 parent
fea6d8db04
commit
b0d0e3a5f3
301 files changed
+28002
-2705
No files matched your search
@@ -1,9 +1,13 @@
|
||||
/**
|
||||
* TabItem — local shell shows zsh/bash badge like professional terminal UIs
|
||||
* TabItem — local shell uses the same Terminal icon as the launcher (+)
|
||||
*/
|
||||
|
||||
import { X, Terminal, FolderSync, Cable, Crown, TerminalSquare } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Tab } from '../../../stores/tabStore'
|
||||
import { useSessionStore } from '../../../stores/sessionStore'
|
||||
import HostDistroIcon from '../../common/HostDistroIcon'
|
||||
import { resolveTabTitle } from '../../../utils/tabTitle'
|
||||
|
||||
interface TabItemProps {
|
||||
tab: Tab
|
||||
@@ -20,45 +24,6 @@ interface TabItemProps {
|
||||
onDragEnd: () => void
|
||||
}
|
||||
|
||||
function LocalShellBadge({ kind }: { kind?: string }) {
|
||||
const k = (kind || 'unknown').toLowerCase()
|
||||
if (k === 'zsh') {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-teal-800/90 text-[8px] font-bold text-teal-100 flex-shrink-0 border border-teal-600/40"
|
||||
title="Zsh"
|
||||
>
|
||||
zsh
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (k === 'bash') {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-dark-600 text-emerald-400 flex-shrink-0 border border-dark-500"
|
||||
title="Bash"
|
||||
>
|
||||
<span className="text-[11px] font-bold leading-none">$_</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (k === 'fish') {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-violet-800 text-[8px] font-bold text-violet-100 flex-shrink-0"
|
||||
title="Fish"
|
||||
>
|
||||
fi
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-dark-600 text-[9px] font-bold text-content-secondary flex-shrink-0">
|
||||
sh
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function TabItem({
|
||||
tab,
|
||||
index,
|
||||
@@ -73,23 +38,47 @@ export function TabItem({
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
}: TabItemProps) {
|
||||
const { t } = useTranslation('terminal')
|
||||
const displayTitle = resolveTabTitle(tab)
|
||||
const isLocalTerm = tab.type === 'terminal' && tab.isLocal
|
||||
const session = useSessionStore((state) =>
|
||||
tab.sessionId ? state.sessions.find((item) => item.id === tab.sessionId) : undefined
|
||||
)
|
||||
|
||||
const getIcon = () => {
|
||||
if (isLocalTerm) {
|
||||
return <LocalShellBadge kind={tab.localShellKind} />
|
||||
return (
|
||||
<Terminal
|
||||
className={`h-3.5 w-3.5 flex-shrink-0 ${isActive ? 'text-emerald-400' : 'text-emerald-500/80'}`}
|
||||
aria-label={t('toolbar.labelLocal')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (tab.type === 'rdp' || tab.type === 'vnc' || (tab.type === 'terminal' && session)) {
|
||||
return (
|
||||
<HostDistroIcon
|
||||
distro={
|
||||
session?.distro ??
|
||||
(tab.type === 'rdp' ? 'windows' : tab.type === 'vnc' ? 'vnc' : undefined)
|
||||
}
|
||||
protocol={
|
||||
tab.type === 'rdp' ? 'rdp' : tab.type === 'vnc' ? 'vnc' : session?.protocol ?? 'ssh'
|
||||
}
|
||||
size="xs"
|
||||
/>
|
||||
)
|
||||
}
|
||||
switch (tab.type) {
|
||||
case 'terminal':
|
||||
return <Terminal className={`w-3.5 h-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
||||
return <Terminal className={`h-3.5 w-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
||||
case 'sftp':
|
||||
return <FolderSync className={`w-3.5 h-3.5 ${isActive ? 'text-accent-purple' : ''}`} />
|
||||
return <FolderSync className={`h-3.5 w-3.5 ${isActive ? 'text-accent-purple' : ''}`} />
|
||||
case 'serial':
|
||||
return <Cable className={`w-3.5 h-3.5 ${isActive ? 'text-accent-green' : ''}`} />
|
||||
return <Cable className={`h-3.5 w-3.5 ${isActive ? 'text-accent-green' : ''}`} />
|
||||
case 'upgrade':
|
||||
return <Crown className={`w-3.5 h-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
||||
return <Crown className={`h-3.5 w-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
||||
case 'batch':
|
||||
return <TerminalSquare className={`w-3.5 h-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
||||
return <TerminalSquare className={`h-3.5 w-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -98,6 +87,15 @@ export function TabItem({
|
||||
const showLeftIndicator = dragOverIndex === index && !isDragging
|
||||
const showRightIndicator = dragOverIndex === index + 1 && !isDragging
|
||||
|
||||
const statusDot =
|
||||
tab.connectionStatus === 'connecting' ? (
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 animate-pulse rounded-full bg-yellow-400" />
|
||||
) : tab.connectionStatus === 'connected' ? (
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500" />
|
||||
) : tab.connectionStatus === 'error' ? (
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-red-500" />
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tab-item
|
||||
@@ -109,69 +107,45 @@ export function TabItem({
|
||||
onDrop={(e) => onDrop(e, index)}
|
||||
onDragEnd={onDragEnd}
|
||||
className={`
|
||||
group flex items-center gap-2 px-3 py-2 min-w-[120px] max-w-[220px]
|
||||
border-r border-dark-700/20 cursor-pointer select-none
|
||||
transition-all duration-150 relative titlebar-no-drag
|
||||
${isActive
|
||||
? 'bg-dark-900 text-content-primary'
|
||||
: 'bg-dark-800 text-txt-muted hover:bg-dark-700 hover:text-txt-muted'
|
||||
group relative flex min-w-[120px] max-w-[220px] cursor-pointer select-none items-center gap-2
|
||||
border-r border-dark-700/20 px-3 py-2 titlebar-no-drag transition-all duration-150
|
||||
${
|
||||
isActive
|
||||
? 'bg-dark-900 text-content-primary'
|
||||
: 'bg-dark-800 text-txt-muted hover:bg-dark-700 hover:text-txt-muted'
|
||||
}
|
||||
${isDragging ? 'opacity-50' : 'opacity-100'}
|
||||
`}
|
||||
>
|
||||
{showLeftIndicator && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-accent-blue z-10" />
|
||||
<div className="absolute bottom-0 left-0 top-0 z-10 w-0.5 bg-accent-blue" />
|
||||
)}
|
||||
{showRightIndicator && (
|
||||
<div className="absolute right-0 top-0 bottom-0 w-0.5 bg-accent-blue z-10" />
|
||||
<div className="absolute bottom-0 right-0 top-0 z-10 w-0.5 bg-accent-blue" />
|
||||
)}
|
||||
|
||||
{isLocalTerm ? (
|
||||
<>
|
||||
{getIcon()}
|
||||
{tab.connectionStatus === 'connected' && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />
|
||||
)}
|
||||
{tab.connectionStatus === 'connecting' && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse flex-shrink-0" />
|
||||
)}
|
||||
{tab.connectionStatus === 'error' && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-500 flex-shrink-0" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{tab.connectionStatus === 'connecting' && (
|
||||
<span className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" />
|
||||
)}
|
||||
{tab.connectionStatus === 'connected' && (
|
||||
<span className="w-2 h-2 rounded-full bg-green-500" />
|
||||
)}
|
||||
{tab.connectionStatus === 'error' && (
|
||||
<span className="w-2 h-2 rounded-full bg-red-500" />
|
||||
)}
|
||||
{getIcon()}
|
||||
</>
|
||||
)}
|
||||
{statusDot}
|
||||
{getIcon()}
|
||||
|
||||
<span className="flex-1 truncate text-sm font-medium">{tab.title}</span>
|
||||
<span className="flex-1 truncate text-sm font-medium" title={displayTitle}>
|
||||
{displayTitle}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`
|
||||
p-0.5 rounded opacity-0 group-hover:opacity-100
|
||||
hover:bg-dark-600 transition-opacity
|
||||
rounded p-0.5 opacity-0 transition-opacity hover:bg-dark-600 group-hover:opacity-100
|
||||
${isActive ? 'opacity-100' : ''}
|
||||
`}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-0 left-0 right-0 h-0.5 bg-accent-blue
|
||||
transition-[opacity,transform] duration-150 origin-left
|
||||
${isActive ? 'opacity-100 scale-x-100' : 'opacity-0 scale-x-0'}
|
||||
absolute bottom-0 left-0 right-0 h-0.5 origin-left bg-accent-blue
|
||||
transition-[opacity,transform] duration-150
|
||||
${isActive ? 'scale-x-100 opacity-100' : 'scale-x-0 opacity-0'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user