feat(monitor): add dashboard i18n infrastructure

Add the Monitor dashboard i18n provider, supported language metadata, message catalogs, fallback behavior, and the initial language selector/wiring for the AppImage UI.
This commit is contained in:
Codex
2026-08-04 17:01:02 +02:00
parent 04a028245b
commit 2109cf2508
23 changed files with 686 additions and 136 deletions
+61 -59
View File
@@ -51,6 +51,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { useT } from "../lib/i18n/provider"
interface SystemStatus {
status: "healthy" | "warning" | "critical"
@@ -80,6 +81,7 @@ interface FlaskSystemInfo {
}
export function ProxmoxDashboard() {
const t = useT()
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
status: "healthy",
uptime: "Loading...",
@@ -168,7 +170,7 @@ export function ProxmoxDashboard() {
const data: FlaskSystemInfo = await fetchApi("/api/system-info")
const uptimeValue =
data.uptime && typeof data.uptime === "string" && data.uptime.trim() !== "" ? data.uptime : "N/A"
data.uptime && typeof data.uptime === "string" && data.uptime.trim() !== "" ? data.uptime : t("app.notAvailable")
const backendStatus = data.health?.status?.toUpperCase() || "OK"
let healthStatus: "healthy" | "warning" | "critical"
@@ -185,8 +187,8 @@ export function ProxmoxDashboard() {
status: healthStatus,
uptime: uptimeValue,
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
serverName: data.hostname || "Unknown",
nodeId: data.node_id || "Unknown",
serverName: data.hostname || t("app.unknown"),
nodeId: data.node_id || t("app.unknown"),
})
setIsServerConnected(true)
} catch (error) {
@@ -196,13 +198,13 @@ export function ProxmoxDashboard() {
setSystemStatus((prev) => ({
...prev,
status: "critical",
serverName: "Server Offline",
nodeId: "Server Offline",
uptime: "N/A",
serverName: t("app.serverOffline"),
nodeId: t("app.serverOffline"),
uptime: t("app.notAvailable"),
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
}))
}
}, [])
}, [t])
useEffect(() => {
// Siempre fetch inicial
@@ -362,19 +364,19 @@ export function ProxmoxDashboard() {
const getActiveTabLabel = () => {
switch (activeTab) {
case "overview": return "Overview"
case "vms": return "VMs & LXCs"
case "storage": return "Storage"
case "network": return "Network"
case "hardware": return "Hardware"
case "backup": return "Backup"
case "terminal": return "Terminal"
case "logs": return "System Logs"
case "security": return "Security"
case "settings": return "Settings"
case "about": return "About"
case "profile": return "Profile"
default: return "Navigation Menu"
case "overview": return t("navigation.overview")
case "vms": return t("navigation.virtualMachines")
case "storage": return t("navigation.storage")
case "network": return t("navigation.network")
case "hardware": return t("navigation.hardware")
case "backup": return t("navigation.backup")
case "terminal": return t("navigation.terminal")
case "logs": return t("navigation.systemLogs")
case "security": return t("navigation.security")
case "settings": return t("navigation.settings")
case "about": return t("navigation.about")
case "profile": return t("navigation.profile")
default: return t("navigation.menu")
}
}
@@ -388,13 +390,13 @@ export function ProxmoxDashboard() {
<div className="container mx-auto">
<div className="flex items-center space-x-2 text-red-500 mb-2">
<XCircle className="h-5 w-5" />
<span className="font-medium">ProxMenux Server Connection Failed</span>
<span className="font-medium">{t("status.connectionFailed")}</span>
</div>
<div className="text-sm text-red-500/80 space-y-1 ml-7">
<p> Check that the monitor.service is running correctly.</p>
<p> The ProxMenux server should start automatically on port 8008</p>
<p>&bull; {t("status.checkService")}</p>
<p>&bull; {t("status.serverPort")}</p>
<p>
Try accessing:{" "}
&bull; {t("status.tryAccessing")}{" "}
<a href={getApiUrl("/api/health")} target="_blank" rel="noopener noreferrer" className="underline">
{getApiUrl("/api/health")}
</a>
@@ -433,11 +435,11 @@ export function ProxmoxDashboard() {
<Server className="h-8 w-8 md:h-6 md:w-6 text-primary absolute fallback-icon hidden" />
</div>
<div className="min-w-0">
<h1 className="text-lg md:text-xl font-semibold text-foreground truncate">ProxMenux Monitor</h1>
<p className="text-xs md:text-sm text-muted-foreground">Proxmox System Dashboard</p>
<h1 className="text-lg md:text-xl font-semibold text-foreground truncate">{t("app.title")}</h1>
<p className="text-xs md:text-sm text-muted-foreground">{t("app.description")}</p>
<div className="lg:hidden flex items-center gap-1 text-xs text-muted-foreground mt-0.5">
<Server className="h-3 w-3" />
<span className="truncate">Node: {systemStatus.serverName}</span>
<span className="truncate">{t("status.node", { node: systemStatus.serverName })}</span>
</div>
</div>
</div>
@@ -447,14 +449,14 @@ export function ProxmoxDashboard() {
<div className="flex items-center space-x-2">
<Server className="h-4 w-4 text-muted-foreground" />
<div className="text-sm">
<div className="font-medium text-foreground">Node: {systemStatus.serverName}</div>
<div className="font-medium text-foreground">{t("status.node", { node: systemStatus.serverName })}</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className={statusColor}>
{statusIcon}
<span className="ml-1 capitalize">{systemStatus.status}</span>
<span className="ml-1">{t(`status.${systemStatus.status}`)}</span>
</Badge>
{systemStatus.status === "healthy" && infoCount > 0 && (
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20">
@@ -465,7 +467,7 @@ export function ProxmoxDashboard() {
</div>
<div className="text-sm text-muted-foreground whitespace-nowrap">
Uptime: {systemStatus.uptime || "N/A"}
{t("status.uptime", { uptime: systemStatus.uptime || t("app.notAvailable") })}
</div>
<Button
@@ -479,7 +481,7 @@ export function ProxmoxDashboard() {
className="border-border/50 bg-transparent hover:bg-secondary"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
Refresh
{t("actions.refresh")}
</Button>
<div onClick={(e) => e.stopPropagation()}>
@@ -513,7 +515,7 @@ export function ProxmoxDashboard() {
}}
disabled={isRefreshing}
className="h-8 w-8 p-0 border-border/50 bg-transparent hover:bg-secondary"
aria-label="Refresh"
aria-label={t("actions.refresh")}
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
</Button>
@@ -541,7 +543,7 @@ export function ProxmoxDashboard() {
<div className="flex items-center gap-1.5">
<Badge variant="outline" className={`${statusColor} text-xs px-2`}>
{statusIcon}
<span className="ml-1 capitalize">{systemStatus.status}</span>
<span className="ml-1">{t(`status.${systemStatus.status}`)}</span>
</Badge>
{systemStatus.status === "healthy" && infoCount > 0 && (
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-xs px-2">
@@ -551,7 +553,7 @@ export function ProxmoxDashboard() {
)}
</div>
<span className="text-xs text-muted-foreground whitespace-nowrap">
Uptime: {systemStatus.uptime || "N/A"}
{t("status.uptime", { uptime: systemStatus.uptime || t("app.notAvailable") })}
</span>
</div>
</div>
@@ -583,15 +585,15 @@ export function ProxmoxDashboard() {
// crumb shows where you are, the chevron tells you the
// siblings are one click away.
const NODE_ITEMS = [
{ value: "storage", label: "Storage", Icon: HardDrive, default: false },
{ value: "network", label: "Network", Icon: NetworkIcon, default: false },
{ value: "hardware", label: "Hardware", Icon: Cpu, default: false },
{ value: "storage", label: t("navigation.storage"), Icon: HardDrive, default: false },
{ value: "network", label: t("navigation.network"), Icon: NetworkIcon, default: false },
{ value: "hardware", label: t("navigation.hardware"), Icon: Cpu, default: false },
]
const ADMIN_ITEMS = [
{ value: "logs", label: "System Logs", Icon: ScrollText, default: false },
{ value: "security", label: "Security", Icon: ShieldCheck, default: false },
{ value: "settings", label: "Settings", Icon: SettingsIcon, default: false },
{ value: "about", label: "About", Icon: Info, default: false },
{ value: "logs", label: t("navigation.systemLogs"), Icon: ScrollText, default: false },
{ value: "security", label: t("navigation.security"), Icon: ShieldCheck, default: false },
{ value: "settings", label: t("navigation.settings"), Icon: SettingsIcon, default: false },
{ value: "about", label: t("navigation.about"), Icon: Info, default: false },
]
const activeNodeItem = NODE_ITEMS.find(i => i.value === activeTab)
const activeAdminItem = ADMIN_ITEMS.find(i => i.value === activeTab)
@@ -600,9 +602,9 @@ export function ProxmoxDashboard() {
// The trigger label + icon shown on the bar. When a child
// is active we surface IT; otherwise the group default.
const NodeTriggerIcon = activeNodeItem ? activeNodeItem.Icon : Server
const NodeTriggerLabel = activeNodeItem ? activeNodeItem.label : "Node"
const NodeTriggerLabel = activeNodeItem ? activeNodeItem.label : t("navigation.node")
const AdminTriggerIcon = activeAdminItem ? activeAdminItem.Icon : Settings2
const AdminTriggerLabel = activeAdminItem ? activeAdminItem.label : "Admin"
const AdminTriggerLabel = activeAdminItem ? activeAdminItem.label : t("navigation.admin")
// Dropdown trigger styling: parity with TabsTrigger so the
// parent visibly carries the "I'm the selected section"
// signal when any of its children is the active tab —
@@ -621,14 +623,14 @@ export function ProxmoxDashboard() {
{/* Direct: Overview */}
<TabsTrigger value="overview" className={triggerActiveClass}>
<LayoutDashboard className="mr-2 h-4 w-4" />
Overview
{t("navigation.overview")}
</TabsTrigger>
{/* Direct: VMs & LXCs — first-class because Proxmox IS
a hypervisor; workloads belong at top level. */}
<TabsTrigger value="vms" className={triggerActiveClass}>
<Boxes className="mr-2 h-4 w-4" />
VMs &amp; LXCs
{t("navigation.virtualMachines")}
</TabsTrigger>
{/* Dropdown: Node (Storage / Network / Hardware) */}
@@ -656,13 +658,13 @@ export function ProxmoxDashboard() {
backup ships this becomes a dropdown. */}
<TabsTrigger value="backup" className={triggerActiveClass}>
<DatabaseBackup className="mr-2 h-4 w-4" />
Backup
{t("navigation.backup")}
</TabsTrigger>
{/* Direct: Terminal */}
<TabsTrigger value="terminal" className={triggerActiveClass}>
<Terminal className="mr-2 h-4 w-4" />
Terminal
{t("navigation.terminal")}
</TabsTrigger>
{/* Dropdown: Admin (System Logs / Security / Settings / About) */}
@@ -727,47 +729,47 @@ export function ProxmoxDashboard() {
<div className="flex flex-col gap-1 mt-4">
<Button variant="ghost" onClick={() => select("overview")} className={itemClass(activeTab === "overview")}>
<LayoutDashboard className="h-5 w-5" />
<span>Overview</span>
<span>{t("navigation.overview")}</span>
</Button>
<Button variant="ghost" onClick={() => select("vms")} className={itemClass(activeTab === "vms")}>
<Boxes className="h-5 w-5" />
<span>VMs &amp; LXCs</span>
<span>{t("navigation.virtualMachines")}</span>
</Button>
<Button variant="ghost" onClick={() => select("storage")} className={itemClass(activeTab === "storage")}>
<HardDrive className="h-5 w-5" />
<span>Storage</span>
<span>{t("navigation.storage")}</span>
</Button>
<Button variant="ghost" onClick={() => select("network")} className={itemClass(activeTab === "network")}>
<NetworkIcon className="h-5 w-5" />
<span>Network</span>
<span>{t("navigation.network")}</span>
</Button>
<Button variant="ghost" onClick={() => select("hardware")} className={itemClass(activeTab === "hardware")}>
<Cpu className="h-5 w-5" />
<span>Hardware</span>
<span>{t("navigation.hardware")}</span>
</Button>
<Button variant="ghost" onClick={() => select("backup")} className={itemClass(activeTab === "backup")}>
<DatabaseBackup className="h-5 w-5" />
<span>Backup</span>
<span>{t("navigation.backup")}</span>
</Button>
<Button variant="ghost" onClick={() => select("terminal")} className={itemClass(activeTab === "terminal")}>
<Terminal className="h-5 w-5" />
<span>Terminal</span>
<span>{t("navigation.terminal")}</span>
</Button>
<Button variant="ghost" onClick={() => select("logs")} className={itemClass(activeTab === "logs")}>
<ScrollText className="h-5 w-5" />
<span>System Logs</span>
<span>{t("navigation.systemLogs")}</span>
</Button>
<Button variant="ghost" onClick={() => select("security")} className={itemClass(activeTab === "security")}>
<ShieldCheck className="h-5 w-5" />
<span>Security</span>
<span>{t("navigation.security")}</span>
</Button>
<Button variant="ghost" onClick={() => select("settings")} className={itemClass(activeTab === "settings")}>
<SettingsIcon className="h-5 w-5" />
<span>Settings</span>
<span>{t("navigation.settings")}</span>
</Button>
<Button variant="ghost" onClick={() => select("about")} className={itemClass(activeTab === "about")}>
<Info className="h-5 w-5" />
<span>About</span>
<span>{t("navigation.about")}</span>
</Button>
</div>
)
@@ -844,7 +846,7 @@ export function ProxmoxDashboard() {
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-600 hover:underline transition-colors"
>
Support and contribute to the project
{t("app.supportProject")}
</a>
</p>
</footer>