feat(web): i18n with react-i18next — Albanian default, English second

Add react-i18next with two key-parity-checked catalogs (sq default/fallback, en).
Active language driven by the logged-in user's stored preference (applied after
/me resolves); SQ/EN toggle in the header persists via PUT /api/auth/language.
Translate the booth (screen, pay/exit modal, active sessions, snapshots, status),
Login, ShiftControl, SiteSettings, PermitManager, TariffComposer.

SetupWizard deferred (its content is server-provided; needs backend catalog i18n).
This commit is contained in:
2026-06-18 11:47:39 +02:00
parent 445bca0bf6
commit 14c83e182a
19 changed files with 840 additions and 201 deletions
+6 -4
View File
@@ -1,7 +1,9 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { login, type SessionUser } from "./api.js";
export function Login({ onLoggedIn }: { onLoggedIn: (u: SessionUser) => void }) {
const { t } = useTranslation();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
@@ -22,11 +24,11 @@ export function Login({ onLoggedIn }: { onLoggedIn: (u: SessionUser) => void })
return (
<main style={{ fontFamily: "system-ui", maxWidth: 320, margin: "4rem auto" }}>
<h1>Parking System</h1>
<h1>{t("auth.title")}</h1>
<form onSubmit={submit}>
<div style={{ margin: "0.5rem 0" }}>
<label>
Username
{t("auth.username")}
<br />
<input
value={username}
@@ -39,7 +41,7 @@ export function Login({ onLoggedIn }: { onLoggedIn: (u: SessionUser) => void })
</div>
<div style={{ margin: "0.5rem 0" }}>
<label>
Password
{t("auth.password")}
<br />
<input
type="password"
@@ -52,7 +54,7 @@ export function Login({ onLoggedIn }: { onLoggedIn: (u: SessionUser) => void })
</div>
{error && <p style={{ color: "crimson" }}>{error}</p>}
<button type="submit" disabled={busy || !username || !password}>
{busy ? "Signing in…" : "Sign in"}
{busy ? t("auth.signingIn") : t("auth.signIn")}
</button>
</form>
</main>