From 2109cf250857b2a1adc862b5fbf7ee52475a1c28 Mon Sep 17 00:00:00 2001
From: Codex
Date: Tue, 4 Aug 2026 17:01:02 +0200
Subject: [PATCH] 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.
---
AppImage/app/layout.tsx | 9 +-
AppImage/app/page.tsx | 6 +-
AppImage/components/about.tsx | 4 +-
AppImage/components/auth-setup.tsx | 64 +++++----
AppImage/components/avatar-menu.tsx | 13 +-
AppImage/components/login.tsx | 38 ++---
AppImage/components/notification-settings.tsx | 1 +
AppImage/components/proxmox-dashboard.tsx | 120 ++++++++--------
AppImage/components/settings.tsx | 76 +++++++++-
AppImage/components/sidebar.tsx | 19 +--
AppImage/components/theme-toggle.tsx | 6 +-
AppImage/lib/i18n/languages.ts | 39 +++++
AppImage/lib/i18n/provider.tsx | 133 ++++++++++++++++++
AppImage/messages/README.md | 12 ++
AppImage/messages/de/common.json | 3 +
AppImage/messages/en/common.json | 132 +++++++++++++++++
AppImage/messages/es/common.json | 3 +
AppImage/messages/fr/common.json | 3 +
AppImage/messages/it/common.json | 3 +
AppImage/messages/pt/common.json | 3 +
AppImage/messages/sk/common.json | 132 +++++++++++++++++
AppImage/scripts/notification_manager.py | 2 +-
AppImage/scripts/notification_templates.py | 1 +
23 files changed, 686 insertions(+), 136 deletions(-)
create mode 100644 AppImage/lib/i18n/languages.ts
create mode 100644 AppImage/lib/i18n/provider.tsx
create mode 100644 AppImage/messages/README.md
create mode 100644 AppImage/messages/de/common.json
create mode 100644 AppImage/messages/en/common.json
create mode 100644 AppImage/messages/es/common.json
create mode 100644 AppImage/messages/fr/common.json
create mode 100644 AppImage/messages/it/common.json
create mode 100644 AppImage/messages/pt/common.json
create mode 100644 AppImage/messages/sk/common.json
diff --git a/AppImage/app/layout.tsx b/AppImage/app/layout.tsx
index 3b664966..dd48ca8c 100644
--- a/AppImage/app/layout.tsx
+++ b/AppImage/app/layout.tsx
@@ -5,6 +5,7 @@ import { GeistMono } from "geist/font/mono"
import { ThemeProvider } from "../components/theme-provider"
import { PwaRegister } from "../components/pwa-register"
import { PwaInstallPrompt } from "../components/pwa-install-prompt"
+import { I18nProvider } from "../lib/i18n/provider"
import { Suspense } from "react"
import "./globals.css"
@@ -44,9 +45,11 @@ export default function RootLayout({
Loading...}>
-
- {children}
-
+
+
+ {children}
+
+
diff --git a/AppImage/app/page.tsx b/AppImage/app/page.tsx
index 810f2766..c52df589 100644
--- a/AppImage/app/page.tsx
+++ b/AppImage/app/page.tsx
@@ -5,8 +5,10 @@ import { ProxmoxDashboard } from "../components/proxmox-dashboard"
import { Login } from "../components/login"
import { AuthSetup } from "../components/auth-setup"
import { getApiUrl } from "../lib/api-config"
+import { useT } from "../lib/i18n/provider"
export default function Home() {
+ const t = useT()
const [authStatus, setAuthStatus] = useState<{
loading: boolean
authEnabled: boolean
@@ -113,8 +115,8 @@ export default function Home() {
- Loading...
- Connecting to ProxMenux Monitor
+ {t("app.loading")}
+ {t("app.connecting")}
)
diff --git a/AppImage/components/about.tsx b/AppImage/components/about.tsx
index 86f91ef3..aa6bc4c9 100644
--- a/AppImage/components/about.tsx
+++ b/AppImage/components/about.tsx
@@ -13,6 +13,7 @@ import {
} from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { APP_VERSION } from "./release-notes-modal"
+import { useT } from "../lib/i18n/provider"
// Issue #191: a dedicated About tab. Centralises project metadata
// (version, license, author) and every external link the project
@@ -111,6 +112,7 @@ function LinkCard({ row }: { row: LinkRow }) {
}
export function About() {
+ const t = useT()
return (
{/* Hero — logo, name, version, one-line description. */}
@@ -151,7 +153,7 @@ export function About() {
const href = isPrerelease
? "https://github.com/MacRimi/ProxMenux/releases"
: "https://proxmenux.com/en/changelog"
- const label = isPrerelease ? "Release notes" : "Changelog"
+ const label = isPrerelease ? t("about.releaseNotes") : t("about.changelog")
return (
void
}
export function AuthSetup({ onComplete }: AuthSetupProps) {
+ const t = useT()
const [open, setOpen] = useState(false)
const [step, setStep] = useState<"choice" | "setup">("choice")
const [username, setUsername] = useState("")
@@ -74,7 +76,7 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
const data = await response.json()
if (!response.ok) {
- throw new Error(data.error || "Failed to skip authentication")
+ throw new Error(data.error || t("authSetup.skipFailed"))
}
if (data.auth_declined) {
@@ -86,7 +88,7 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
onComplete()
} catch (err) {
console.error("Auth skip error:", err)
- setError(err instanceof Error ? err.message : "Failed to save preference")
+ setError(err instanceof Error ? err.message : t("authSetup.savePreferenceFailed"))
} finally {
setLoading(false)
}
@@ -108,17 +110,17 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
setError("")
if (!username || !password) {
- setError("Please fill in all fields")
+ setError(t("authSetup.fillFields"))
return
}
if (password !== confirmPassword) {
- setError("Passwords do not match")
+ setError(t("authSetup.passwordMismatch"))
return
}
if (password.length < 6) {
- setError("Password must be at least 6 characters")
+ setError(t("authSetup.passwordTooShort"))
return
}
@@ -137,7 +139,7 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
const data = await response.json()
if (!response.ok) {
- throw new Error(data.error || "Failed to setup authentication")
+ throw new Error(data.error || t("authSetup.setupFailed"))
}
if (data.token) {
@@ -204,7 +206,7 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
onComplete()
} catch (err) {
console.error("Auth setup error:", err)
- setError(err instanceof Error ? err.message : "Failed to setup authentication")
+ setError(err instanceof Error ? err.message : t("authSetup.setupFailed"))
} finally {
setLoading(false)
}
@@ -214,7 +216,7 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
- {step === "choice" ? "Setup Dashboard Protection" : "Create Password"}
+ {step === "choice" ? t("authSetup.choiceTitle") : t("authSetup.passwordTitle")}
{step === "choice" ? (
@@ -222,16 +224,16 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
-
Protect Your Dashboard?
+
{t("authSetup.protectTitle")}
- Add an extra layer of security to protect your Proxmox data when accessing from non-private networks.
+ {t("authSetup.protectDescription")}
setStep("setup")} className="w-full bg-blue-500 hover:bg-blue-600" size="lg">
- Yes, Setup Password
+ {t("authSetup.setupPassword")}
- No, Continue Without Protection
+ {t("authSetup.skipProtection")}
- You can always enable this later in Settings
+ {t("authSetup.enableLater")}
) : (
@@ -252,8 +254,8 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
-
Setup Authentication
-
Create a username and password to protect your dashboard
+
{t("authSetup.setupTitle")}
+
{t("authSetup.setupDescription")}
{error && (
@@ -266,14 +268,14 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
- Username
+ {t("authSetup.username")}
setUsername(e.target.value)}
className="pl-10 text-base"
@@ -285,14 +287,14 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
- Password
+ {t("authSetup.password")}
setPassword(e.target.value)}
className="pl-10 text-base"
@@ -312,14 +314,14 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
- Confirm Password
+ {t("authSetup.confirmPassword")}
setConfirmPassword(e.target.value)}
className="pl-10 text-base"
@@ -345,19 +347,19 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
setup endpoint returns the JWT. */}
- Profile · optional
+ {t("authSetup.profileOptional")}
-
Avatar
+
{t("authSetup.avatar")}
{avatarPreviewUrl ? (
// eslint-disable-next-line @next/next/no-img-element
@@ -407,7 +409,7 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
className="h-7 text-xs"
>
- {avatarFile ? "Change" : "Choose image"}
+ {avatarFile ? t("authSetup.change") : t("authSetup.chooseImage")}
{avatarFile && (
- Clear
+ {t("authSetup.clear")}
)}
- PNG, JPEG, WebP or GIF · up to 2 MB · pre-crop square for best results.
+ {t("authSetup.avatarHint")}
@@ -434,10 +436,10 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
- {loading ? "Setting up..." : "Setup Authentication"}
+ {loading ? t("authSetup.settingUp") : t("authSetup.setupAuthentication")}
setStep("choice")} variant="ghost" className="w-full" disabled={loading}>
- Back
+ {t("authSetup.back")}
diff --git a/AppImage/components/avatar-menu.tsx b/AppImage/components/avatar-menu.tsx
index 7efdffe5..76f0574d 100644
--- a/AppImage/components/avatar-menu.tsx
+++ b/AppImage/components/avatar-menu.tsx
@@ -11,6 +11,7 @@ import {
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { fetchApi, getApiUrl, getAuthToken } from "../lib/api-config"
+import { useT } from "../lib/i18n/provider"
interface AuthStatus {
auth_enabled?: boolean
@@ -57,6 +58,8 @@ interface AvatarMenuProps {
* proper /api/auth/logout that revokes the JWT server-side too.
*/
export function AvatarMenu({ size = "lg", onOpenSecurity, onOpenProfile }: AvatarMenuProps) {
+ const t = useT()
+
// IMPORTANT — all hooks must run unconditionally on every render. The
// previous version short-circuited with `if (!auth_enabled) return null`
// BEFORE the avatar blob hooks, so the hook count changed between
@@ -201,7 +204,7 @@ export function AvatarMenu({ size = "lg", onOpenSecurity, onOpenProfile }: Avata
{username}
)}
{!profile?.display_name && (
-
Signed in
+
{t("account.signedIn")}
)}
@@ -257,13 +260,13 @@ export function AvatarMenu({ size = "lg", onOpenSecurity, onOpenProfile }: Avata
{onOpenProfile && (
- View profile
+ {t("account.viewProfile")}
)}
{onOpenSecurity && (
- Security
+ {t("account.security")}
)}
{(onOpenProfile || onOpenSecurity) &&
}
@@ -272,7 +275,7 @@ export function AvatarMenu({ size = "lg", onOpenSecurity, onOpenProfile }: Avata
className="text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
>
- Sign out
+ {t("account.signOut")}
diff --git a/AppImage/components/login.tsx b/AppImage/components/login.tsx
index 74941a46..6bc745ba 100644
--- a/AppImage/components/login.tsx
+++ b/AppImage/components/login.tsx
@@ -9,6 +9,7 @@ import { Label } from "./ui/label"
import { Checkbox } from "./ui/checkbox"
import { Lock, User, AlertCircle, Server, Shield, Eye, EyeOff } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
+import { useT } from "../lib/i18n/provider"
import Image from "next/image"
interface LoginProps {
@@ -16,6 +17,7 @@ interface LoginProps {
}
export function Login({ onLogin }: LoginProps) {
+ const t = useT()
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [totpCode, setTotpCode] = useState("")
@@ -56,12 +58,12 @@ export function Login({ onLogin }: LoginProps) {
setError("")
if (!username || !password) {
- setError("Please enter username and password")
+ setError(t("login.missingCredentials"))
return
}
if (requiresTotp && !totpCode) {
- setError("Please enter your 2FA code")
+ setError(t("login.missingTotp"))
return
}
@@ -87,7 +89,7 @@ export function Login({ onLogin }: LoginProps) {
}
if (!response.ok) {
- throw new Error(data.message || "Login failed")
+ throw new Error(data.message || t("login.loginFailed"))
}
localStorage.setItem("proxmenux-auth-token", data.token)
@@ -107,7 +109,7 @@ export function Login({ onLogin }: LoginProps) {
onLogin()
} catch (err) {
- setError(err instanceof Error ? err.message : "Login failed")
+ setError(err instanceof Error ? err.message : t("login.loginFailed"))
} finally {
setLoading(false)
}
@@ -139,8 +141,8 @@ export function Login({ onLogin }: LoginProps) {
-
ProxMenux Monitor
-
Sign in to access your dashboard
+
{t("app.title")}
+
{t("login.subtitle")}
@@ -157,14 +159,14 @@ export function Login({ onLogin }: LoginProps) {
<>
- Username
+ {t("login.username")}
setUsername(e.target.value)}
className="pl-10 text-base"
@@ -176,14 +178,14 @@ export function Login({ onLogin }: LoginProps) {
- Password
+ {t("login.password")}
setPassword(e.target.value)}
className="pl-10 pr-10 text-base"
@@ -214,7 +216,7 @@ export function Login({ onLogin }: LoginProps) {
disabled={loading}
/>
- Remember me
+ {t("login.rememberMe")}
>
@@ -223,14 +225,14 @@ export function Login({ onLogin }: LoginProps) {
-
Two-Factor Authentication
-
Enter the 6-digit code from your authentication app
+
{t("login.twoFactorTitle")}
+
{t("login.twoFactorDescription")}
@@ -260,18 +262,18 @@ export function Login({ onLogin }: LoginProps) {
}}
className="w-full"
>
- Back to login
+ {t("login.backToLogin")}
)}
- {loading ? "Signing in..." : requiresTotp ? "Verify Code" : "Sign In"}
+ {loading ? t("login.signingIn") : requiresTotp ? t("login.verifyCode") : t("login.signIn")}
-
ProxMenux Monitor v1.2.4.1-beta
+
{t("login.version")}
)
diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx
index 68cfbc14..94a48aa9 100644
--- a/AppImage/components/notification-settings.tsx
+++ b/AppImage/components/notification-settings.tsx
@@ -212,6 +212,7 @@ const AI_PROVIDERS = [
const AI_LANGUAGES = [
{ value: "en", label: "English" },
+ { value: "sk", label: "Slovenčina" },
{ value: "es", label: "Espanol" },
{ value: "fr", label: "Francais" },
{ value: "de", label: "Deutsch" },
diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx
index 1959bc58..034da150 100644
--- a/AppImage/components/proxmox-dashboard.tsx
+++ b/AppImage/components/proxmox-dashboard.tsx
@@ -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({
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() {
- ProxMenux Server Connection Failed
+ {t("status.connectionFailed")}
-
• Check that the monitor.service is running correctly.
-
• The ProxMenux server should start automatically on port 8008
+
• {t("status.checkService")}
+
• {t("status.serverPort")}
- • Try accessing:{" "}
+ • {t("status.tryAccessing")}{" "}
{getApiUrl("/api/health")}
@@ -433,11 +435,11 @@ export function ProxmoxDashboard() {
-
ProxMenux Monitor
-
Proxmox System Dashboard
+
{t("app.title")}
+
{t("app.description")}
- Node: {systemStatus.serverName}
+ {t("status.node", { node: systemStatus.serverName })}
@@ -447,14 +449,14 @@ export function ProxmoxDashboard() {
-
Node: {systemStatus.serverName}
+
{t("status.node", { node: systemStatus.serverName })}
{statusIcon}
- {systemStatus.status}
+ {t(`status.${systemStatus.status}`)}
{systemStatus.status === "healthy" && infoCount > 0 && (
@@ -465,7 +467,7 @@ export function ProxmoxDashboard() {
- Uptime: {systemStatus.uptime || "N/A"}
+ {t("status.uptime", { uptime: systemStatus.uptime || t("app.notAvailable") })}
- Refresh
+ {t("actions.refresh")}
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")}
>
@@ -541,7 +543,7 @@ export function ProxmoxDashboard() {
{statusIcon}
- {systemStatus.status}
+ {t(`status.${systemStatus.status}`)}
{systemStatus.status === "healthy" && infoCount > 0 && (
@@ -551,7 +553,7 @@ export function ProxmoxDashboard() {
)}
- Uptime: {systemStatus.uptime || "N/A"}
+ {t("status.uptime", { uptime: systemStatus.uptime || t("app.notAvailable") })}
@@ -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 */}
- Overview
+ {t("navigation.overview")}
{/* Direct: VMs & LXCs — first-class because Proxmox IS
a hypervisor; workloads belong at top level. */}
- VMs & LXCs
+ {t("navigation.virtualMachines")}
{/* Dropdown: Node (Storage / Network / Hardware) */}
@@ -656,13 +658,13 @@ export function ProxmoxDashboard() {
backup ships this becomes a dropdown. */}
- Backup
+ {t("navigation.backup")}
{/* Direct: Terminal */}
- Terminal
+ {t("navigation.terminal")}
{/* Dropdown: Admin (System Logs / Security / Settings / About) */}
@@ -727,47 +729,47 @@ export function ProxmoxDashboard() {
select("overview")} className={itemClass(activeTab === "overview")}>
- Overview
+ {t("navigation.overview")}
select("vms")} className={itemClass(activeTab === "vms")}>
- VMs & LXCs
+ {t("navigation.virtualMachines")}
select("storage")} className={itemClass(activeTab === "storage")}>
- Storage
+ {t("navigation.storage")}
select("network")} className={itemClass(activeTab === "network")}>
- Network
+ {t("navigation.network")}
select("hardware")} className={itemClass(activeTab === "hardware")}>
- Hardware
+ {t("navigation.hardware")}
select("backup")} className={itemClass(activeTab === "backup")}>
- Backup
+ {t("navigation.backup")}
select("terminal")} className={itemClass(activeTab === "terminal")}>
- Terminal
+ {t("navigation.terminal")}
select("logs")} className={itemClass(activeTab === "logs")}>
- System Logs
+ {t("navigation.systemLogs")}
select("security")} className={itemClass(activeTab === "security")}>
- Security
+ {t("navigation.security")}
select("settings")} className={itemClass(activeTab === "settings")}>
- Settings
+ {t("navigation.settings")}
select("about")} className={itemClass(activeTab === "about")}>
- About
+ {t("navigation.about")}
)
@@ -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")}
diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx
index 9da31e8b..1f5ddb1b 100644
--- a/AppImage/components/settings.tsx
+++ b/AppImage/components/settings.tsx
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
-import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff, Code, X, Copy, Sparkles, ArrowUpCircle, BellOff } from "lucide-react"
+import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff, Code, X, Copy, Sparkles, ArrowUpCircle, BellOff, Globe2 } from "lucide-react"
import { Badge } from "./ui/badge"
import { Button } from "./ui/button"
import { NotificationSettings } from "./notification-settings"
@@ -14,6 +14,8 @@ import { Switch } from "./ui/switch"
import { Input } from "./ui/input"
import { getNetworkUnit } from "../lib/format-network"
import { fetchApi } from "../lib/api-config"
+import { SUPPORTED_LANGUAGES, useI18n } from "../lib/i18n/provider"
+import type { LanguageCode, LanguageStatus } from "../lib/i18n/languages"
// GitHub Dark color palette for bash syntax highlighting
const BASH_KEYWORDS = new Set([
@@ -297,6 +299,7 @@ interface NetworkInterface {
}
export function Settings() {
+ const { language, setLanguage, t } = useI18n()
const [proxmenuxTools, setProxmenuxTools] = useState([])
const [updatesAvailableCount, setUpdatesAvailableCount] = useState(0)
const [loadingTools, setLoadingTools] = useState(true)
@@ -899,21 +902,82 @@ export function Settings() {
k => pendingChanges[k] !== -2
)
+ const getLanguageStatusLabel = (status: LanguageStatus) => {
+ switch (status) {
+ case "complete":
+ return t("settings.interfaceLanguage.statusComplete")
+ case "partial":
+ return t("settings.interfaceLanguage.statusPartial")
+ case "needs-translation":
+ return t("settings.interfaceLanguage.statusNeedsTranslation")
+ }
+ }
+
return (
-
Settings
-
Manage your dashboard preferences
+
{t("settings.title")}
+
{t("settings.description")}
+ {/* Interface Language Settings */}
+
+
+
+
+ {t("settings.interfaceLanguage.title")}
+
+ {t("settings.interfaceLanguage.description")}
+
+
+
+
+
{t("settings.interfaceLanguage.label")}
+
{t("settings.interfaceLanguage.fallbackNote")}
+
+
setLanguage(value as LanguageCode)}>
+
+
+
+
+ {SUPPORTED_LANGUAGES.map((item) => (
+
+ {item.nativeName}
+
+ ))}
+
+
+
+
+
+ {SUPPORTED_LANGUAGES.map((item) => (
+
+
+ {item.nativeName}
+
+ {item.code}
+
+
+
{getLanguageStatusLabel(item.status)}
+
+ ))}
+
+
+
+
{/* Network Units Settings */}
- Network Units
+ {t("settings.networkUnits.title")}
- Change how network traffic is displayed
+ {t("settings.networkUnits.description")}
{loadingUnitSettings ? (
@@ -922,7 +986,7 @@ export function Settings() {
) : (
-
Network Unit Display
+
{t("settings.networkUnits.label")}
diff --git a/AppImage/components/sidebar.tsx b/AppImage/components/sidebar.tsx
index 364d7e42..7fdfecdf 100644
--- a/AppImage/components/sidebar.tsx
+++ b/AppImage/components/sidebar.tsx
@@ -1,6 +1,7 @@
"use client"
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText, SettingsIcon, Terminal } from "lucide-react"
+import { useT } from "../lib/i18n/provider"
const menuItems = [
{ name: "Overview", href: "/", icon: LayoutDashboard },
@@ -14,6 +15,8 @@ const menuItems = [
]
const Sidebar = ({ currentPath, setOpen }) => {
+ const t = useT()
+
const handleNavigation = (tabName: string) => {
// Dispatch custom event to change tab in dashboard
const event = new CustomEvent("changeTab", { detail: { tab: tabName } })
@@ -32,7 +35,7 @@ const Sidebar = ({ currentPath, setOpen }) => {
}`}
>
- Overview
+ {t("navigation.overview")}
{
}`}
>
- Storage
+ {t("navigation.storage")}
{
}`}
>
- Network
+ {t("navigation.network")}
{
}`}
>
- VMs & LXCs
+ {t("navigation.virtualMachines")}
{
}`}
>
- Hardware
+ {t("navigation.hardware")}
{
}`}
>
- System Logs
+ {t("navigation.systemLogs")}
{
}`}
>
- Terminal
+ {t("navigation.terminal")}
{
}`}
>
- Settings
+ {t("navigation.settings")}
)
diff --git a/AppImage/components/theme-toggle.tsx b/AppImage/components/theme-toggle.tsx
index 881c4887..74a8f651 100644
--- a/AppImage/components/theme-toggle.tsx
+++ b/AppImage/components/theme-toggle.tsx
@@ -4,8 +4,10 @@ import { useTheme } from "next-themes"
import { useEffect, useState } from "react"
import { Button } from "./ui/button"
+import { useT } from "../lib/i18n/provider"
export function ThemeToggle() {
+ const t = useT()
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
@@ -22,7 +24,7 @@ export function ThemeToggle() {
return (
- Toggle theme
+ {t("actions.toggleTheme")}
)
}
@@ -31,7 +33,7 @@ export function ThemeToggle() {
- Toggle theme
+ {t("actions.toggleTheme")}
)
}
diff --git a/AppImage/lib/i18n/languages.ts b/AppImage/lib/i18n/languages.ts
new file mode 100644
index 00000000..cada315e
--- /dev/null
+++ b/AppImage/lib/i18n/languages.ts
@@ -0,0 +1,39 @@
+export const LANGUAGE_STORAGE_KEY = "proxmenux-ui-language"
+export const DEFAULT_LANGUAGE = "en"
+
+export type LanguageCode = "en" | "es" | "fr" | "de" | "it" | "pt" | "sk"
+
+export type LanguageStatus = "complete" | "partial" | "needs-translation"
+
+export interface SupportedLanguage {
+ code: LanguageCode
+ englishName: string
+ nativeName: string
+ status: LanguageStatus
+}
+
+export const SUPPORTED_LANGUAGES: SupportedLanguage[] = [
+ { code: "en", englishName: "English", nativeName: "English", status: "complete" },
+ { code: "sk", englishName: "Slovak", nativeName: "Slovenčina", status: "partial" },
+ { code: "es", englishName: "Spanish", nativeName: "Español", status: "needs-translation" },
+ { code: "fr", englishName: "French", nativeName: "Français", status: "needs-translation" },
+ { code: "de", englishName: "German", nativeName: "Deutsch", status: "needs-translation" },
+ { code: "it", englishName: "Italian", nativeName: "Italiano", status: "needs-translation" },
+ { code: "pt", englishName: "Portuguese", nativeName: "Português", status: "needs-translation" },
+]
+
+export function isSupportedLanguage(value: string | null | undefined): value is LanguageCode {
+ return SUPPORTED_LANGUAGES.some((language) => language.code === value)
+}
+
+export function detectBrowserLanguage(): LanguageCode {
+ if (typeof navigator === "undefined") return DEFAULT_LANGUAGE
+
+ const candidates = [navigator.language, ...(navigator.languages || [])]
+ for (const candidate of candidates) {
+ const code = candidate?.split("-")[0]?.toLowerCase()
+ if (isSupportedLanguage(code)) return code
+ }
+
+ return DEFAULT_LANGUAGE
+}
diff --git a/AppImage/lib/i18n/provider.tsx b/AppImage/lib/i18n/provider.tsx
new file mode 100644
index 00000000..07d9ca1d
--- /dev/null
+++ b/AppImage/lib/i18n/provider.tsx
@@ -0,0 +1,133 @@
+"use client"
+
+import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
+import enMessages from "../../messages/en/common.json"
+import skMessages from "../../messages/sk/common.json"
+import esMessages from "../../messages/es/common.json"
+import frMessages from "../../messages/fr/common.json"
+import deMessages from "../../messages/de/common.json"
+import itMessages from "../../messages/it/common.json"
+import ptMessages from "../../messages/pt/common.json"
+import {
+ DEFAULT_LANGUAGE,
+ LANGUAGE_STORAGE_KEY,
+ type LanguageCode,
+ SUPPORTED_LANGUAGES,
+ detectBrowserLanguage,
+ isSupportedLanguage,
+} from "./languages"
+
+type MessageTree = Record
+type TranslationParams = Record
+
+const MESSAGE_CATALOG: Record = {
+ en: enMessages as MessageTree,
+ sk: skMessages as MessageTree,
+ es: esMessages as MessageTree,
+ fr: frMessages as MessageTree,
+ de: deMessages as MessageTree,
+ it: itMessages as MessageTree,
+ pt: ptMessages as MessageTree,
+}
+
+interface I18nContextValue {
+ language: LanguageCode
+ setLanguage: (language: LanguageCode) => void
+ t: (key: string, params?: TranslationParams) => string
+}
+
+const I18nContext = createContext(null)
+
+function getInitialLanguage(): LanguageCode {
+ if (typeof window === "undefined") return DEFAULT_LANGUAGE
+
+ try {
+ const stored = window.localStorage.getItem(LANGUAGE_STORAGE_KEY)
+ if (isSupportedLanguage(stored)) return stored
+ } catch {
+ // localStorage may be unavailable in private browsing.
+ }
+
+ return detectBrowserLanguage()
+}
+
+function getMessage(messages: MessageTree, key: string): string | undefined {
+ const value = key.split(".").reduce((cursor, segment) => {
+ if (!cursor || typeof cursor !== "object") return undefined
+ return (cursor as Record)[segment]
+ }, messages)
+
+ return typeof value === "string" ? value : undefined
+}
+
+function interpolate(template: string, params?: TranslationParams): string {
+ if (!params) return template
+
+ return template.replace(/\{(\w+)\}/g, (match, name) => {
+ const value = params[name]
+ return value === undefined ? match : String(value)
+ })
+}
+
+export function I18nProvider({ children }: { children: React.ReactNode }) {
+ const [language, setLanguageState] = useState(DEFAULT_LANGUAGE)
+ const [isHydrated, setIsHydrated] = useState(false)
+
+ useEffect(() => {
+ setLanguageState(getInitialLanguage())
+ setIsHydrated(true)
+ }, [])
+
+ useEffect(() => {
+ if (!isHydrated) return
+
+ document.documentElement.lang = language
+ try {
+ window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language)
+ } catch {
+ // Best-effort; the in-memory language still works for this session.
+ }
+ }, [isHydrated, language])
+
+ useEffect(() => {
+ const onStorage = (event: StorageEvent) => {
+ if (event.key === LANGUAGE_STORAGE_KEY && isSupportedLanguage(event.newValue)) {
+ setLanguageState(event.newValue)
+ }
+ }
+
+ window.addEventListener("storage", onStorage)
+ return () => window.removeEventListener("storage", onStorage)
+ }, [])
+
+ const setLanguage = useCallback((nextLanguage: LanguageCode) => {
+ setLanguageState(nextLanguage)
+ }, [])
+
+ const t = useCallback(
+ (key: string, params?: TranslationParams) => {
+ const localized = getMessage(MESSAGE_CATALOG[language], key)
+ const fallback = getMessage(MESSAGE_CATALOG.en, key)
+ return interpolate(localized ?? fallback ?? key, params)
+ },
+ [language],
+ )
+
+ const value = useMemo(() => ({ language, setLanguage, t }), [language, setLanguage, t])
+
+ return {children}
+}
+
+export function useI18n() {
+ const context = useContext(I18nContext)
+ if (!context) {
+ throw new Error("useI18n must be used within I18nProvider")
+ }
+ return context
+}
+
+export function useT() {
+ return useI18n().t
+}
+
+export { SUPPORTED_LANGUAGES }
diff --git a/AppImage/messages/README.md b/AppImage/messages/README.md
new file mode 100644
index 00000000..307a1480
--- /dev/null
+++ b/AppImage/messages/README.md
@@ -0,0 +1,12 @@
+# Monitor dashboard translations
+
+The ProxMenux Monitor dashboard uses a small client-side i18n layer.
+
+- English (`en`) is the source language and the fallback.
+- Slovak (`sk`) is partially translated.
+- Spanish, French, German, Italian and Portuguese are registered as
+ community translation targets and currently fall back to English.
+
+To add or improve a translation, copy the matching keys from
+`messages/en/common.json` into your locale's `common.json` file and
+translate only the values. Keep placeholders such as `{uptime}` unchanged.
diff --git a/AppImage/messages/de/common.json b/AppImage/messages/de/common.json
new file mode 100644
index 00000000..08918611
--- /dev/null
+++ b/AppImage/messages/de/common.json
@@ -0,0 +1,3 @@
+{
+ "_meta": "Community translation target. Copy keys from ../en/common.json and translate the values."
+}
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json
new file mode 100644
index 00000000..5fa9355d
--- /dev/null
+++ b/AppImage/messages/en/common.json
@@ -0,0 +1,132 @@
+{
+ "app": {
+ "title": "ProxMenux Monitor",
+ "description": "Proxmox System Dashboard",
+ "loading": "Loading...",
+ "connecting": "Connecting to ProxMenux Monitor",
+ "unknown": "Unknown",
+ "notAvailable": "N/A",
+ "serverOffline": "Server Offline",
+ "supportProject": "Support and contribute to the project"
+ },
+ "actions": {
+ "refresh": "Refresh",
+ "toggleTheme": "Toggle theme",
+ "openUserMenu": "Open user menu",
+ "cancel": "Cancel",
+ "save": "Save",
+ "edit": "Edit",
+ "close": "Close"
+ },
+ "navigation": {
+ "overview": "Overview",
+ "storage": "Storage",
+ "network": "Network",
+ "virtualMachines": "VMs & LXCs",
+ "hardware": "Hardware",
+ "backup": "Backup",
+ "terminal": "Terminal",
+ "systemLogs": "System Logs",
+ "security": "Security",
+ "settings": "Settings",
+ "about": "About",
+ "profile": "Profile",
+ "node": "Node",
+ "admin": "Admin",
+ "menu": "Navigation Menu"
+ },
+ "status": {
+ "healthy": "Healthy",
+ "warning": "Warning",
+ "critical": "Critical",
+ "uptime": "Uptime: {uptime}",
+ "node": "Node: {node}",
+ "connectionFailed": "ProxMenux Server Connection Failed",
+ "checkService": "Check that the monitor.service is running correctly.",
+ "serverPort": "The ProxMenux server should start automatically on port 8008",
+ "tryAccessing": "Try accessing:"
+ },
+ "settings": {
+ "title": "Settings",
+ "description": "Manage your dashboard preferences",
+ "interfaceLanguage": {
+ "title": "Interface language",
+ "description": "Choose the language used by the Monitor dashboard. Missing translations fall back to English.",
+ "label": "Dashboard language",
+ "fallbackNote": "Untranslated text is shown in English until the community fills it in.",
+ "statusComplete": "complete",
+ "statusPartial": "partial",
+ "statusNeedsTranslation": "community translation needed"
+ },
+ "networkUnits": {
+ "title": "Network Units",
+ "description": "Change how network traffic is displayed",
+ "label": "Network Unit Display"
+ }
+ },
+ "login": {
+ "subtitle": "Sign in to access your dashboard",
+ "username": "Username",
+ "password": "Password",
+ "usernamePlaceholder": "Enter your username",
+ "passwordPlaceholder": "Enter your password",
+ "rememberMe": "Remember me",
+ "missingCredentials": "Please enter username and password",
+ "missingTotp": "Please enter your 2FA code",
+ "loginFailed": "Login failed",
+ "signingIn": "Signing in...",
+ "signIn": "Sign In",
+ "twoFactorTitle": "Two-Factor Authentication",
+ "twoFactorDescription": "Enter the 6-digit code from your authentication app",
+ "authenticationCode": "Authentication Code",
+ "backupCodeHint": "You can also use a backup code (format: XXXX-XXXX)",
+ "backToLogin": "Back to login",
+ "verifyCode": "Verify Code",
+ "version": "ProxMenux Monitor v1.2.4.1-beta"
+ },
+ "account": {
+ "signedIn": "Signed in",
+ "viewProfile": "View profile",
+ "security": "Security",
+ "signOut": "Sign out"
+ },
+ "authSetup": {
+ "choiceTitle": "Setup Dashboard Protection",
+ "passwordTitle": "Create Password",
+ "protectTitle": "Protect Your Dashboard?",
+ "protectDescription": "Add an extra layer of security to protect your Proxmox data when accessing from non-private networks.",
+ "setupPassword": "Yes, Setup Password",
+ "skipProtection": "No, Continue Without Protection",
+ "enableLater": "You can always enable this later in Settings",
+ "setupTitle": "Setup Authentication",
+ "setupDescription": "Create a username and password to protect your dashboard",
+ "fillFields": "Please fill in all fields",
+ "passwordMismatch": "Passwords do not match",
+ "passwordTooShort": "Password must be at least 6 characters",
+ "skipFailed": "Failed to skip authentication",
+ "savePreferenceFailed": "Failed to save preference",
+ "setupFailed": "Failed to setup authentication",
+ "username": "Username",
+ "usernamePlaceholder": "Enter username",
+ "password": "Password",
+ "passwordPlaceholder": "Enter password",
+ "confirmPassword": "Confirm Password",
+ "confirmPasswordPlaceholder": "Confirm password",
+ "profileOptional": "Profile · optional",
+ "displayName": "Display name",
+ "displayNamePlaceholder": "Shown above the username in the menu",
+ "displayNameHint": "Leave empty to render the username itself. Up to 64 characters.",
+ "avatar": "Avatar",
+ "change": "Change",
+ "chooseImage": "Choose image",
+ "clear": "Clear",
+ "avatarHint": "PNG, JPEG, WebP or GIF · up to 2 MB · pre-crop square for best results.",
+ "settingUp": "Setting up...",
+ "setupAuthentication": "Setup Authentication",
+ "back": "Back"
+ },
+ "about": {
+ "releaseNotes": "Release notes",
+ "changelog": "Changelog"
+ }
+}
diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json
new file mode 100644
index 00000000..08918611
--- /dev/null
+++ b/AppImage/messages/es/common.json
@@ -0,0 +1,3 @@
+{
+ "_meta": "Community translation target. Copy keys from ../en/common.json and translate the values."
+}
diff --git a/AppImage/messages/fr/common.json b/AppImage/messages/fr/common.json
new file mode 100644
index 00000000..08918611
--- /dev/null
+++ b/AppImage/messages/fr/common.json
@@ -0,0 +1,3 @@
+{
+ "_meta": "Community translation target. Copy keys from ../en/common.json and translate the values."
+}
diff --git a/AppImage/messages/it/common.json b/AppImage/messages/it/common.json
new file mode 100644
index 00000000..08918611
--- /dev/null
+++ b/AppImage/messages/it/common.json
@@ -0,0 +1,3 @@
+{
+ "_meta": "Community translation target. Copy keys from ../en/common.json and translate the values."
+}
diff --git a/AppImage/messages/pt/common.json b/AppImage/messages/pt/common.json
new file mode 100644
index 00000000..08918611
--- /dev/null
+++ b/AppImage/messages/pt/common.json
@@ -0,0 +1,3 @@
+{
+ "_meta": "Community translation target. Copy keys from ../en/common.json and translate the values."
+}
diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json
new file mode 100644
index 00000000..c18a1348
--- /dev/null
+++ b/AppImage/messages/sk/common.json
@@ -0,0 +1,132 @@
+{
+ "app": {
+ "title": "ProxMenux Monitor",
+ "description": "Systémový prehľad Proxmoxu",
+ "loading": "Načítava sa...",
+ "connecting": "Pripájam sa k ProxMenux Monitoru",
+ "unknown": "Neznáme",
+ "notAvailable": "Nedostupné",
+ "serverOffline": "Server je offline",
+ "supportProject": "Podporte projekt alebo prispejte vlastnými úpravami"
+ },
+ "actions": {
+ "refresh": "Obnoviť",
+ "toggleTheme": "Prepnúť vzhľad",
+ "openUserMenu": "Otvoriť používateľské menu",
+ "cancel": "Zrušiť",
+ "save": "Uložiť",
+ "edit": "Upraviť",
+ "close": "Zavrieť"
+ },
+ "navigation": {
+ "overview": "Prehľad",
+ "storage": "Úložiská",
+ "network": "Sieť",
+ "virtualMachines": "VM a LXC",
+ "hardware": "Hardvér",
+ "backup": "Záloha",
+ "terminal": "Terminál",
+ "systemLogs": "Systémové logy",
+ "security": "Bezpečnosť",
+ "settings": "Nastavenia",
+ "about": "O projekte",
+ "profile": "Profil",
+ "node": "Server",
+ "admin": "Správa",
+ "menu": "Navigačné menu"
+ },
+ "status": {
+ "healthy": "V poriadku",
+ "warning": "Upozornenie",
+ "critical": "Problém",
+ "uptime": "Beží: {uptime}",
+ "node": "Server: {node}",
+ "connectionFailed": "Nepodarilo sa pripojiť k ProxMenux serveru",
+ "checkService": "Skontrolujte, či služba monitor.service beží správne.",
+ "serverPort": "ProxMenux server by sa mal spustiť automaticky na porte 8008",
+ "tryAccessing": "Skúste otvoriť:"
+ },
+ "settings": {
+ "title": "Nastavenia",
+ "description": "Spravujte správanie dashboardu",
+ "interfaceLanguage": {
+ "title": "Jazyk rozhrania",
+ "description": "Vyberte jazyk, ktorý bude používať Monitor dashboard. Texty bez prekladu sa zobrazia po anglicky.",
+ "label": "Jazyk dashboardu",
+ "fallbackNote": "Nepreložené texty zostanú po anglicky, kým ich komunita nedoplní.",
+ "statusComplete": "hotové",
+ "statusPartial": "čiastočne preložené",
+ "statusNeedsTranslation": "čaká na komunitný preklad"
+ },
+ "networkUnits": {
+ "title": "Jednotky siete",
+ "description": "Zmeňte, ako sa zobrazuje sieťová prevádzka",
+ "label": "Zobrazovanie sieťových jednotiek"
+ }
+ },
+ "login": {
+ "subtitle": "Prihláste sa do dashboardu",
+ "username": "Používateľské meno",
+ "password": "Heslo",
+ "usernamePlaceholder": "Zadajte používateľské meno",
+ "passwordPlaceholder": "Zadajte heslo",
+ "rememberMe": "Zapamätať si ma",
+ "missingCredentials": "Zadajte používateľské meno aj heslo",
+ "missingTotp": "Zadajte 2FA kód",
+ "loginFailed": "Prihlásenie zlyhalo",
+ "signingIn": "Prihlasujem...",
+ "signIn": "Prihlásiť sa",
+ "twoFactorTitle": "Dvojfaktorové overenie",
+ "twoFactorDescription": "Zadajte 6-miestny kód z overovacej aplikácie",
+ "authenticationCode": "Overovací kód",
+ "backupCodeHint": "Môžete použiť aj záložný kód vo formáte XXXX-XXXX",
+ "backToLogin": "Späť na prihlásenie",
+ "verifyCode": "Overiť kód",
+ "version": "ProxMenux Monitor v1.2.4.1-beta"
+ },
+ "account": {
+ "signedIn": "Prihlásený",
+ "viewProfile": "Zobraziť profil",
+ "security": "Bezpečnosť",
+ "signOut": "Odhlásiť sa"
+ },
+ "authSetup": {
+ "choiceTitle": "Nastavenie ochrany dashboardu",
+ "passwordTitle": "Vytvoriť heslo",
+ "protectTitle": "Chcete chrániť dashboard?",
+ "protectDescription": "Pridajte ďalšiu vrstvu ochrany pre svoje Proxmox dáta, hlavne pri prístupe mimo súkromnej siete.",
+ "setupPassword": "Áno, nastaviť heslo",
+ "skipProtection": "Nie, pokračovať bez ochrany",
+ "enableLater": "Ochranu môžete zapnúť aj neskôr v Nastaveniach",
+ "setupTitle": "Nastavenie prihlásenia",
+ "setupDescription": "Vytvorte používateľské meno a heslo na ochranu dashboardu",
+ "fillFields": "Vyplňte všetky polia",
+ "passwordMismatch": "Heslá sa nezhodujú",
+ "passwordTooShort": "Heslo musí mať aspoň 6 znakov",
+ "skipFailed": "Nepodarilo sa preskočiť prihlásenie",
+ "savePreferenceFailed": "Nepodarilo sa uložiť nastavenie",
+ "setupFailed": "Nepodarilo sa nastaviť prihlásenie",
+ "username": "Používateľské meno",
+ "usernamePlaceholder": "Zadajte používateľské meno",
+ "password": "Heslo",
+ "passwordPlaceholder": "Zadajte heslo",
+ "confirmPassword": "Potvrdiť heslo",
+ "confirmPasswordPlaceholder": "Zadajte heslo znova",
+ "profileOptional": "Profil · voliteľné",
+ "displayName": "Zobrazované meno",
+ "displayNamePlaceholder": "Zobrazí sa nad používateľským menom v menu",
+ "displayNameHint": "Ak pole necháte prázdne, zobrazí sa samotné používateľské meno. Najviac 64 znakov.",
+ "avatar": "Avatar",
+ "change": "Zmeniť",
+ "chooseImage": "Vybrať obrázok",
+ "clear": "Vymazať",
+ "avatarHint": "PNG, JPEG, WebP alebo GIF · najviac 2 MB · najlepšie funguje štvorcový obrázok.",
+ "settingUp": "Nastavujem...",
+ "setupAuthentication": "Nastaviť prihlásenie",
+ "back": "Späť"
+ },
+ "about": {
+ "releaseNotes": "Poznámky k vydaniu",
+ "changelog": "Zoznam zmien"
+ }
+}
diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py
index f071a81e..c5ce1b49 100644
--- a/AppImage/scripts/notification_manager.py
+++ b/AppImage/scripts/notification_manager.py
@@ -2789,7 +2789,7 @@ class NotificationManager:
# injection lands in the system prompt verbatim. Audit Tier 3.2 #4.
_ALLOWED_DETAIL_LEVELS = ('brief', 'standard', 'detailed')
_ALLOWED_AI_LANGUAGES = (
- 'en', 'es', 'fr', 'de', 'it', 'pt', 'ru',
+ 'en', 'sk', 'es', 'fr', 'de', 'it', 'pt', 'ru',
'ja', 'zh', 'ko', 'pl', 'nl', 'tr', 'ar',
)
if short_key.endswith('.ai_detail_level') or short_key == 'ai_detail_level':
diff --git a/AppImage/scripts/notification_templates.py b/AppImage/scripts/notification_templates.py
index 2dfc6bb9..ce6d57f5 100644
--- a/AppImage/scripts/notification_templates.py
+++ b/AppImage/scripts/notification_templates.py
@@ -1976,6 +1976,7 @@ def enrich_with_emojis(event_type: str, title: str, body: str,
# Supported languages for AI translation
AI_LANGUAGES = {
'en': 'English',
+ 'sk': 'Slovak',
'es': 'Spanish',
'fr': 'French',
'de': 'German',