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")}

-

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) {
setUsername(e.target.value)} className="pl-10 text-base" @@ -285,14 +287,14 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
setPassword(e.target.value)} className="pl-10 text-base" @@ -312,14 +314,14 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
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")}

setDisplayName(e.target.value)} maxLength={64} @@ -366,12 +368,12 @@ export function AuthSetup({ onComplete }: AuthSetupProps) { />

- Leave empty to render the username itself. Up to 64 characters. + {t("authSetup.displayNameHint")}

- +
{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 && ( )}

- 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) {
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
)} {!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) { <>
setUsername(e.target.value)} className="pl-10 text-base" @@ -176,14 +178,14 @@ export function Login({ onLogin }: LoginProps) {
setPassword(e.target.value)} className="pl-10 pr-10 text-base" @@ -214,7 +216,7 @@ export function Login({ onLogin }: LoginProps) { disabled={loading} />
@@ -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")}

- You can also use a backup code (format: XXXX-XXXX) + {t("login.backupCodeHint")}

@@ -260,18 +262,18 @@ export function Login({ onLogin }: LoginProps) { }} className="w-full" > - Back to login + {t("login.backToLogin")}
)}
-

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") })}
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() {
) @@ -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")}

+
+ +
+ +
+ {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")}