Implementing multi-language Admin UI
build / build-linux-amd64 (push) Successful in 2m2s

This commit is contained in:
2026-05-09 16:58:20 +02:00
parent a7b3e83f02
commit 1e961cdd92
14 changed files with 2989 additions and 487 deletions
+26 -13
View File
@@ -1,8 +1,8 @@
<!doctype html>
<html lang="en" class="h-full">
<html lang="{{LANG_CODE}}" class="h-full">
<head>
<meta charset="utf-8" />
<title>RustDesk Admin</title>
<title>{{T_APP_TITLE}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/admin/assets/tailwindcss.js"></script>
<script src="/admin/assets/htmx.min.js"></script>
@@ -23,38 +23,51 @@
<div class="min-h-full flex">
<aside class="w-56 shrink-0 bg-slate-900 border-r border-slate-800 flex flex-col">
<div class="px-4 py-5 border-b border-slate-800">
<h1 class="text-base font-semibold">RustDesk Admin</h1>
<h1 class="text-base font-semibold">{{T_APP_TITLE}}</h1>
<p id="me-display" class="text-xs text-slate-500 mt-1" hx-get="/admin/me" hx-trigger="load" hx-swap="innerHTML"></p>
</div>
<nav class="flex-1 px-2 py-3 space-y-1">
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/users" hx-target="#main" hx-push-url="#users">Users</a>
hx-get="/admin/pages/users" hx-target="#main" hx-push-url="#users">{{T_NAV_USERS}}</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/devices" hx-target="#main" hx-push-url="#devices">Devices</a>
hx-get="/admin/pages/devices" hx-target="#main" hx-push-url="#devices">{{T_NAV_DEVICES}}</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/groups" hx-target="#main" hx-push-url="#groups">Device groups</a>
hx-get="/admin/pages/groups" hx-target="#main" hx-push-url="#groups">{{T_NAV_GROUPS}}</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/strategies" hx-target="#main" hx-push-url="#strategies">Strategies</a>
hx-get="/admin/pages/strategies" hx-target="#main" hx-push-url="#strategies">{{T_NAV_STRATEGIES}}</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/address-books" hx-target="#main" hx-push-url="#address-books">Address books</a>
hx-get="/admin/pages/address-books" hx-target="#main" hx-push-url="#address-books">{{T_NAV_AB}}</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/audit" hx-target="#main" hx-push-url="#audit">Audit log</a>
hx-get="/admin/pages/audit" hx-target="#main" hx-push-url="#audit">{{T_NAV_AUDIT}}</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/deploy" hx-target="#main" hx-push-url="#deploy">Deploy</a>
hx-get="/admin/pages/deploy" hx-target="#main" hx-push-url="#deploy">{{T_NAV_DEPLOY}}</a>
</nav>
<div class="px-2 py-3 border-t border-slate-800 space-y-1">
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-400 hover:bg-slate-800"
hx-get="/admin/pages/profile" hx-target="#main" hx-push-url="#profile">My profile</a>
hx-get="/admin/pages/profile" hx-target="#main" hx-push-url="#profile">{{T_NAV_PROFILE}}</a>
<button
class="w-full text-left px-3 py-1.5 text-sm rounded text-slate-400 hover:bg-slate-800"
hx-post="/admin/logout"
hx-on::after-request="window.location.href = '/admin/login.html'"
>Sign out</button>
>{{T_NAV_SIGNOUT}}</button>
<div class="pt-2">
<label class="block text-[10px] uppercase tracking-wide text-slate-600 px-3 mb-1">{{T_LANGUAGE}}</label>
<select
class="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
onchange="document.cookie='admin_lang='+this.value+'; path=/; max-age=31536000; samesite=strict'; window.location.reload();"
>
<option value="en"{{LANG_SEL_EN}}>English</option>
<option value="de"{{LANG_SEL_DE}}>Deutsch</option>
<option value="es"{{LANG_SEL_ES}}>Español</option>
<option value="fr"{{LANG_SEL_FR}}>Français</option>
<option value="ro"{{LANG_SEL_RO}}>Română</option>
</select>
</div>
</div>
</aside>
<main id="main" class="flex-1 p-6 overflow-x-hidden">
<div class="text-slate-500 text-sm">Loading…</div>
<div class="text-slate-500 text-sm">{{T_LOADING}}</div>
</main>
</div>
+25 -10
View File
@@ -1,8 +1,8 @@
<!doctype html>
<html lang="en" class="h-full">
<html lang="{{LANG_CODE}}" class="h-full">
<head>
<meta charset="utf-8" />
<title>Sign in — RustDesk Admin</title>
<title>{{T_SIGNIN}} — {{T_TITLE}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/admin/assets/tailwindcss.js"></script>
<script src="/admin/assets/htmx.min.js"></script>
@@ -13,8 +13,8 @@
<body class="h-full bg-slate-950 text-slate-100 flex items-center justify-center">
<main class="w-full max-w-sm px-6">
<div class="text-center mb-8">
<h1 class="text-2xl font-semibold">RustDesk Admin</h1>
<p class="text-slate-400 text-sm mt-1">Sign in to manage the server.</p>
<h1 class="text-2xl font-semibold">{{T_TITLE}}</h1>
<p class="text-slate-400 text-sm mt-1">{{T_SUBTITLE}}</p>
</div>
<form
@@ -42,7 +42,7 @@
"
>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1" for="username">Username</label>
<label class="block text-xs font-medium text-slate-400 mb-1" for="username">{{T_USERNAME}}</label>
<input
id="username" name="username" type="text" required autocomplete="username"
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500"
@@ -50,7 +50,7 @@
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1" for="password">Password</label>
<label class="block text-xs font-medium text-slate-400 mb-1" for="password">{{T_PASSWORD}}</label>
<input
id="password" name="password" type="password" required autocomplete="current-password"
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500"
@@ -58,7 +58,7 @@
</div>
<div id="tfa-section" class="hidden">
<label class="block text-xs font-medium text-slate-400 mb-1" for="tfaCode">6-digit TOTP code</label>
<label class="block text-xs font-medium text-slate-400 mb-1" for="tfaCode">{{T_TOTP_LABEL}}</label>
<input
id="tfaCode" name="tfaCode" type="text" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code"
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm tracking-widest text-center focus:outline-none focus:border-sky-500"
@@ -70,7 +70,7 @@
type="submit"
class="w-full bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium rounded px-4 py-2 transition"
>
Sign in
{{T_SIGNIN}}
</button>
<div id="err" class="text-sm text-rose-400 min-h-[1.25em]"></div>
@@ -80,11 +80,25 @@
<div id="oidc-block" class="mt-6 hidden">
<div class="flex items-center gap-3 mb-3">
<div class="flex-1 h-px bg-slate-800"></div>
<span class="text-xs text-slate-500">or</span>
<span class="text-xs text-slate-500">{{T_OR}}</span>
<div class="flex-1 h-px bg-slate-800"></div>
</div>
<div id="oidc-buttons" class="space-y-2"></div>
</div>
<div class="mt-6 text-center">
<label class="text-[10px] uppercase tracking-wide text-slate-600 mr-2">{{T_LANGUAGE}}</label>
<select
class="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
onchange="document.cookie='admin_lang='+this.value+'; path=/; max-age=31536000; samesite=strict'; window.location.reload();"
>
<option value="en"{{LANG_SEL_EN}}>English</option>
<option value="de"{{LANG_SEL_DE}}>Deutsch</option>
<option value="es"{{LANG_SEL_ES}}>Español</option>
<option value="fr"{{LANG_SEL_FR}}>Français</option>
<option value="ro"{{LANG_SEL_RO}}>Română</option>
</select>
</div>
</main>
<script>
@@ -92,6 +106,7 @@
// navigates to /admin/login/oidc/<name>, which 302s the browser to the
// IdP. After the IdP redirects to /oidc/callback, the server sets our
// session cookie and redirects to /admin/.
var SIGNIN_WITH = {{T_SIGNIN_WITH_JSON}};
fetch('/admin/oidc/providers').then(r => r.json()).then(list => {
if (!Array.isArray(list) || list.length === 0) return;
const block = document.getElementById('oidc-block');
@@ -100,7 +115,7 @@
const a = document.createElement('a');
a.href = '/admin/login/oidc/' + encodeURIComponent(p.name);
a.className = 'block w-full text-center bg-slate-800 hover:bg-slate-700 border border-slate-700 text-sm rounded px-4 py-2 transition';
a.textContent = 'Sign in with ' + (p.display_name || p.name);
a.textContent = SIGNIN_WITH + ' ' + (p.display_name || p.name);
root.appendChild(a);
});
block.classList.remove('hidden');