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 { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ApiError,
|
||||
fetchTariff,
|
||||
@@ -78,6 +79,7 @@ function toStructure(f: FormState): TariffStructure {
|
||||
}
|
||||
|
||||
export function TariffComposer() {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<TariffState | null>(null);
|
||||
const [form, setForm] = useState<FormState>(emptyForm);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -112,7 +114,7 @@ export function TariffComposer() {
|
||||
await publishTariffVersion({ currency: form.currency.trim().toUpperCase(), structure: toStructure(form) });
|
||||
const fresh = await fetchTariff();
|
||||
setState(fresh);
|
||||
setMsg({ kind: "ok", text: "New tariff version published — it's now the active rate card." });
|
||||
setMsg({ kind: "ok", text: t("tariff.publishedOk") });
|
||||
} catch (e) {
|
||||
const text =
|
||||
e instanceof ApiError && (e as ApiError & { problems?: string[] }).problems
|
||||
@@ -126,44 +128,40 @@ export function TariffComposer() {
|
||||
|
||||
return (
|
||||
<section style={{ marginTop: "2rem" }}>
|
||||
<h2>Tariff</h2>
|
||||
<h2>{t("tariff.title")}</h2>
|
||||
{!state?.active ? (
|
||||
<p style={{ color: "#b45309" }}>
|
||||
No rate card published yet — the pay station can't charge until you publish one.
|
||||
</p>
|
||||
<p style={{ color: "#b45309" }}>{t("tariff.noRateCard")}</p>
|
||||
) : (
|
||||
<p style={{ color: "#555" }}>
|
||||
Active since {new Date(state.active.effectiveFrom).toLocaleString()} ·{" "}
|
||||
{state.versions.length} version(s) in history. Publishing creates a new version; past
|
||||
sessions keep their original pricing.
|
||||
{t("tariff.activeSince", {
|
||||
date: new Date(state.active.effectiveFrom).toLocaleString(),
|
||||
count: state.versions.length,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "max-content 1fr", gap: "0.4rem 0.75rem", alignItems: "center", maxWidth: 460 }}>
|
||||
<label>Currency</label>
|
||||
<label>{t("tariff.currency")}</label>
|
||||
<input value={form.currency} onChange={(e) => set("currency", e.target.value)} maxLength={3} style={{ width: 80 }} />
|
||||
<label>Free entry grace (min)</label>
|
||||
<label>{t("tariff.freeEntryGrace")}</label>
|
||||
<input value={form.gracePeriodEntryMin} onChange={(e) => set("gracePeriodEntryMin", e.target.value)} />
|
||||
<label>Billing increment (min)</label>
|
||||
<label>{t("tariff.billingIncrement")}</label>
|
||||
<input value={form.incrementMin} onChange={(e) => set("incrementMin", e.target.value)} />
|
||||
<label>Daily cap (blank = none)</label>
|
||||
<input value={form.dailyCap} onChange={(e) => set("dailyCap", e.target.value)} placeholder="e.g. 12.00" />
|
||||
<label>Lost-ticket fee</label>
|
||||
<label>{t("tariff.dailyCap")}</label>
|
||||
<input value={form.dailyCap} onChange={(e) => set("dailyCap", e.target.value)} placeholder={t("tariff.dailyCapPh")} />
|
||||
<label>{t("tariff.lostTicketFee")}</label>
|
||||
<input value={form.lostTicket} onChange={(e) => set("lostTicket", e.target.value)} />
|
||||
<label>Exit walk-back grace (min)</label>
|
||||
<label>{t("tariff.exitGrace")}</label>
|
||||
<input value={form.gracePeriodExitMin} onChange={(e) => set("gracePeriodExitMin", e.target.value)} />
|
||||
</div>
|
||||
|
||||
<h3 style={{ marginBottom: "0.25rem" }}>Rate blocks</h3>
|
||||
<p style={{ color: "#777", margin: "0 0 0.5rem", fontSize: "0.9em" }}>
|
||||
Consumed in order as time accrues. "Up to (min)" is the block's upper bound; leave the last
|
||||
block's bound blank for "thereafter". Price is per billing increment.
|
||||
</p>
|
||||
<h3 style={{ marginBottom: "0.25rem" }}>{t("tariff.rateBlocks")}</h3>
|
||||
<p style={{ color: "#777", margin: "0 0 0.5rem", fontSize: "0.9em" }}>{t("tariff.rateBlocksHint")}</p>
|
||||
<table style={{ borderCollapse: "collapse" }}>
|
||||
<thead>
|
||||
<tr style={{ textAlign: "left", color: "#555" }}>
|
||||
<th style={{ padding: "0 0.5rem" }}>Up to (min)</th>
|
||||
<th style={{ padding: "0 0.5rem" }}>Price / increment</th>
|
||||
<th style={{ padding: "0 0.5rem" }}>{t("tariff.upToMin")}</th>
|
||||
<th style={{ padding: "0 0.5rem" }}>{t("tariff.pricePerIncrement")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -174,7 +172,7 @@ export function TariffComposer() {
|
||||
<input
|
||||
value={b.uptoMin}
|
||||
onChange={(e) => setBlock(i, { uptoMin: e.target.value })}
|
||||
placeholder={i === form.blocks.length - 1 ? "thereafter" : "e.g. 60"}
|
||||
placeholder={i === form.blocks.length - 1 ? t("tariff.thereafter") : t("tariff.egExample")}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
</td>
|
||||
@@ -183,7 +181,7 @@ export function TariffComposer() {
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" onClick={() => removeBlock(i)} disabled={form.blocks.length <= 1}>
|
||||
Remove
|
||||
{t("tariff.remove")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -191,12 +189,12 @@ export function TariffComposer() {
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" onClick={addBlock} style={{ marginTop: "0.4rem" }}>
|
||||
+ Add block
|
||||
{t("tariff.addBlock")}
|
||||
</button>
|
||||
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<button type="button" onClick={publish} disabled={saving}>
|
||||
{saving ? "Publishing…" : "Publish new version"}
|
||||
{saving ? t("tariff.publishing") : t("tariff.publishNewVersion")}
|
||||
</button>
|
||||
</div>
|
||||
{msg && (
|
||||
|
||||
Reference in New Issue
Block a user