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:
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import { SnapshotStrip } from "./ui/SnapshotStrip.js";
|
||||
type Phase = "review" | "paying" | "finishing" | "done" | "error";
|
||||
|
||||
export function BoothPayModal({ identity, onClose }: { identity: string; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const session = useQuery({ queryKey: ["session", identity], queryFn: () => lookupSession(identity) });
|
||||
const config = useQuery({ queryKey: qk.siteConfig, queryFn: fetchSiteConfig });
|
||||
@@ -52,13 +54,13 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
setPhase("finishing");
|
||||
if (voucher) {
|
||||
const r = await printVoucher(identity);
|
||||
setResult(`Exit voucher printed on ${r.printedBy}. Customer self-exits at the exit.`);
|
||||
setResult(t("pay.voucherPrinted", { printer: r.printedBy }));
|
||||
} else {
|
||||
const r = await boothExit(identity);
|
||||
setResult(
|
||||
r.opened
|
||||
? "Paid — barrier opened. Car may exit."
|
||||
: `Paid and exit recorded, but the barrier did not open: ${r.reason ?? "open manually"}.`,
|
||||
? t("pay.paidBarrierOpened")
|
||||
: t("pay.paidExitRecorded", { reason: r.reason ?? t("booth.openManually") }),
|
||||
);
|
||||
}
|
||||
// Refresh the live views.
|
||||
@@ -81,25 +83,25 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-term-border bg-term-panel-2 px-4 py-2">
|
||||
<Dialog.Title className="m-0 text-[12px] font-semibold uppercase tracking-wider text-term-amber">
|
||||
Ticket {identity}
|
||||
{t("pay.ticket")} {identity}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="text-term-muted hover:text-term-text" aria-label="Close">
|
||||
<Dialog.Close className="text-term-muted hover:text-term-text" aria-label={t("common.close")}>
|
||||
✕
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
{session.isLoading && <div className="text-term-muted">looking up…</div>}
|
||||
{session.isLoading && <div className="text-term-muted">{t("pay.lookingUp")}</div>}
|
||||
|
||||
{s && !s.found && (
|
||||
<div className="rounded-term border border-term-red px-3 py-2 text-term-red">
|
||||
No session found for this ticket.
|
||||
{t("pay.noSessionFound")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{s && s.found && !s.open && (
|
||||
<div className="rounded-term border border-term-amber px-3 py-2 text-term-amber">
|
||||
This session is already closed (exited {formatTime(s.exitedAt)}).
|
||||
{t("pay.alreadyClosed", { time: formatTime(s.exitedAt) })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -107,25 +109,28 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
<>
|
||||
{/* Session figures */}
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-1 tabular-nums">
|
||||
<Row label="Entry" value={formatTime(s.enteredAt)} />
|
||||
<Row label="Now" value={formatTime(new Date().toISOString())} />
|
||||
<Row label="Duration" value={s.enteredAt ? formatDuration(s.enteredAt, new Date().toISOString()) : "—"} />
|
||||
<Row label={t("pay.entry")} value={formatTime(s.enteredAt)} />
|
||||
<Row label={t("pay.now")} value={formatTime(new Date().toISOString())} />
|
||||
<Row
|
||||
label="Status"
|
||||
value={alreadyPaid ? "PAID" : "UNPAID"}
|
||||
label={t("pay.duration")}
|
||||
value={s.enteredAt ? formatDuration(s.enteredAt, new Date().toISOString()) : "—"}
|
||||
/>
|
||||
<Row
|
||||
label={t("pay.statusLabel")}
|
||||
value={alreadyPaid ? t("pay.paid") : t("pay.unpaid")}
|
||||
valueClass={alreadyPaid ? "text-term-green" : "text-term-amber"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="flex items-end justify-between rounded-term bg-term-panel-2 px-3 py-2">
|
||||
<span className="text-[11px] uppercase tracking-wider text-term-muted">Total</span>
|
||||
<span className="text-[11px] uppercase tracking-wider text-term-muted">{t("pay.total")}</span>
|
||||
<span className="text-3xl font-bold text-term-cyan">
|
||||
{s.amountMinor != null && s.currency
|
||||
? formatMoney(s.amountMinor, s.currency)
|
||||
: alreadyPaid
|
||||
? "paid"
|
||||
: "no tariff"}
|
||||
? t("booth.badgePaid")
|
||||
: t("pay.noTariff")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -137,19 +142,19 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
{/* Tender */}
|
||||
{canPay && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] uppercase tracking-wider text-term-muted">Tender</span>
|
||||
{(["cash", "card"] as const).map((t) => (
|
||||
<span className="text-[11px] uppercase tracking-wider text-term-muted">{t("pay.tender")}</span>
|
||||
{(["cash", "card"] as const).map((tn) => (
|
||||
<button
|
||||
key={t}
|
||||
key={tn}
|
||||
type="button"
|
||||
onClick={() => setTender(t)}
|
||||
onClick={() => setTender(tn)}
|
||||
className={`rounded-term border px-3 py-1 text-[12px] uppercase tracking-wider ${
|
||||
tender === t
|
||||
tender === tn
|
||||
? "border-term-amber text-term-amber"
|
||||
: "border-term-border text-term-muted hover:text-term-text"
|
||||
}`}
|
||||
>
|
||||
{t}
|
||||
{t(`pay.${tn}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -162,8 +167,8 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
checked={voucher}
|
||||
onChange={(e) => setPrintVoucherChecked(e.target.checked)}
|
||||
/>
|
||||
Printo biletë dalje
|
||||
<span className="text-term-muted">(customer self-exits at the exit)</span>
|
||||
{t("pay.printExitVoucher")}
|
||||
<span className="text-term-muted">{t("pay.selfExitHint")}</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
@@ -181,7 +186,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
onClick={onClose}
|
||||
className="rounded-term border border-term-amber px-4 py-1.5 text-[12px] uppercase tracking-wider text-term-amber"
|
||||
>
|
||||
Close
|
||||
{t("common.close")}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
@@ -190,7 +195,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
onClick={onClose}
|
||||
className="rounded-term border border-term-border px-3 py-1.5 text-[12px] uppercase tracking-wider text-term-muted hover:text-term-text"
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -199,18 +204,18 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
className="rounded-term border border-term-green bg-term-green/10 px-4 py-1.5 text-[12px] font-semibold uppercase tracking-wider text-term-green disabled:opacity-50"
|
||||
>
|
||||
{phase === "paying"
|
||||
? "taking payment…"
|
||||
? t("pay.takingPayment")
|
||||
: phase === "finishing"
|
||||
? voucher
|
||||
? "printing voucher…"
|
||||
: "opening…"
|
||||
? t("pay.printingVoucher")
|
||||
: t("pay.opening")
|
||||
: alreadyPaid
|
||||
? voucher
|
||||
? "Print voucher"
|
||||
: "Open barrier"
|
||||
? t("pay.printVoucher")
|
||||
: t("pay.openBarrier")
|
||||
: voucher
|
||||
? "Pay + print voucher"
|
||||
: "Pay + open barrier"}
|
||||
? t("pay.payAndVoucher")
|
||||
: t("pay.payAndOpen")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user