feat(web): one date standard across the UI — "25 Qer 14:30"
Dates were a mix: catalog-formatted "25 Qershor 20:01" where screens used
formatRelativeDateTime, and browser-locale "7/6/2026, 9:34 AM" in ~20
places that called raw toLocaleString/-Date-/-Time-String. Unified:
- common.monthsShort in both catalogs (Jan/Shk/…/Qer/Korr/…/Dhj);
formatDate ("25 Qer", year only when not current), formatDateTime
("25 Qer 14:30", optional seconds), formatClock ("HH:mm", 24h) in
lib/format.ts. formatRelativeDateTime keeps Sot/Dje and switches its
older-dates branch to the same short months.
- Every raw toLocale* DATE call swept: shifts X-report line, plan
effective dates, sub version labels, drawer today feed, snapshot
tooltips, device footer checkedAt, event-detail timestamp (keeps
seconds — chain evidence), tariff composer active-since + version
sidebar. Number toLocaleString (thousand separators) untouched.
The catalogs in this commit also carry the keys for the two follow-up
commits (fee breakdown, composer increment labels).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -85,6 +85,43 @@ function monthName(d: Date, t: TFn): string {
|
||||
return String(d.getMonth() + 1);
|
||||
}
|
||||
|
||||
/** Short month ("Qer", "Korr") from the catalog — the UI-wide date standard
|
||||
* (2026-07-06): every visible date reads "25 Qer" / "7 Korr 2025", never the
|
||||
* browser-locale "7/6/2026". Falls back to the full name, then the number. */
|
||||
function monthShort(d: Date, t: TFn): string {
|
||||
const months = t("common.monthsShort", { returnObjects: true });
|
||||
if (Array.isArray(months) && typeof months[d.getMonth()] === "string") {
|
||||
return months[d.getMonth()] as string;
|
||||
}
|
||||
return monthName(d, t);
|
||||
}
|
||||
|
||||
/** "HH:mm" (local, 24h) — the unified time-of-day everywhere ("—" for bad input). */
|
||||
export function formatClock(iso: string | null): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? "—" : hhmm(d);
|
||||
}
|
||||
|
||||
/** "25 Qer" (current year) / "25 Qer 2025" (other years) — the unified DATE. */
|
||||
export function formatDate(iso: string | null, t: TFn): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return "—";
|
||||
const base = `${d.getDate()} ${monthShort(d, t)}`;
|
||||
return d.getFullYear() === new Date().getFullYear() ? base : `${base} ${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
/** "25 Qer 14:30" (+ ":ss" when `seconds`) — the unified absolute DATE+TIME. Use
|
||||
* formatRelativeDateTime instead where "Sot/Dje" reads better (feeds, history). */
|
||||
export function formatDateTime(iso: string | null, t: TFn, opts?: { seconds?: boolean }): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return "—";
|
||||
const sec = opts?.seconds ? `:${String(d.getSeconds()).padStart(2, "0")}` : "";
|
||||
return `${formatDate(iso, t)} ${hhmm(d)}${sec}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human, day-relative date+time for sessions/logs/history. An event from earlier
|
||||
* today reads "Sot 10:48", yesterday "Dje 17:33", and anything older a localized
|
||||
@@ -101,9 +138,6 @@ export function formatRelativeDateTime(iso: string | null, t: TFn): string {
|
||||
const diff = dayDiff(d, new Date());
|
||||
if (diff === 0) return `${t("common.today")} ${hhmm(d)}`;
|
||||
if (diff === 1) return `${t("common.yesterday")} ${hhmm(d)}`;
|
||||
// Older (or future): "17 Qershor 10:48", with the year only if it differs.
|
||||
const sameYear = d.getFullYear() === new Date().getFullYear();
|
||||
const month = monthName(d, t);
|
||||
const date = sameYear ? `${d.getDate()} ${month}` : `${d.getDate()} ${month} ${d.getFullYear()}`;
|
||||
return `${date} ${hhmm(d)}`;
|
||||
// Older (or future): "17 Qer 10:48" — the short-month standard, year only if it differs.
|
||||
return `${formatDate(iso, t)} ${hhmm(d)}`;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export const en: Catalog = {
|
||||
"November",
|
||||
"December",
|
||||
],
|
||||
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
},
|
||||
auth: {
|
||||
title: "Parking System",
|
||||
@@ -330,6 +331,12 @@ export const en: Catalog = {
|
||||
hoursUnit: "hours",
|
||||
egHours: "e.g. 2",
|
||||
pricePerIncrement: "Price / increment (per hour)",
|
||||
pricePerHour: "Price / hour",
|
||||
pricePerN: "Price / {{min}} min",
|
||||
modeFlatN: "Flat price / {{min}} min",
|
||||
perHourEquiv: "= {{amount}} / hour",
|
||||
incrementWarning:
|
||||
"Careful: the billing increment is {{min}} min — every price below is charged per started {{min}} minutes, NOT per hour.",
|
||||
thereafter: "thereafter (open-ended)",
|
||||
remove: "Remove",
|
||||
addBlock: "+ Add block",
|
||||
@@ -562,6 +569,16 @@ export const en: Catalog = {
|
||||
graceExpires: "Grace expires",
|
||||
curve: "Duration curve",
|
||||
curveHint: "Fee from entry at several durations — see where the daily cap flattens or windows shift.",
|
||||
bd: {
|
||||
title: "How the amount is produced",
|
||||
rounding: "{{raw}} min parked → {{billed}} min billed ({{inc}}-min increments)",
|
||||
grace: "Free — within the entry grace ({{min}} min)",
|
||||
package: "window package",
|
||||
step: "Day {{day}}: stay up to {{hours}}h — total",
|
||||
stepRepeated: "Day {{day}}: beyond the top tier — full-day total",
|
||||
cap: "Daily cap {{cap}} applied (day {{day}})",
|
||||
total: "Total",
|
||||
},
|
||||
},
|
||||
subs: {
|
||||
title: "Subscriptions",
|
||||
|
||||
@@ -35,6 +35,8 @@ export const sq = {
|
||||
"Nëntor",
|
||||
"Dhjetor",
|
||||
],
|
||||
// Short month names — the UI-wide date standard ("25 Qer", "7 Korr").
|
||||
monthsShort: ["Jan", "Shk", "Mar", "Pri", "Maj", "Qer", "Korr", "Gush", "Sht", "Tet", "Nën", "Dhj"],
|
||||
},
|
||||
auth: {
|
||||
title: "Sistemi i Parkimit",
|
||||
@@ -332,7 +334,13 @@ export const sq = {
|
||||
bandDuration: "Kohëzgjatja e brezit",
|
||||
hoursUnit: "orë",
|
||||
egHours: "p.sh. 2",
|
||||
pricePerIncrement: "Çmimi / interval (orë)",
|
||||
pricePerIncrement: "Çmimi / interval (min)",
|
||||
pricePerHour: "Çmimi / orë",
|
||||
pricePerN: "Çmimi / {{min}} min",
|
||||
modeFlatN: "Çmim fiks / {{min}} min",
|
||||
perHourEquiv: "= {{amount}} / orë",
|
||||
incrementWarning:
|
||||
"Kujdes: intervali i faturimit është {{min}} min — çdo çmim më poshtë faturohet për çdo {{min}} minuta të filluara, JO për orë.",
|
||||
thereafter: "më pas (i hapur)",
|
||||
remove: "Hiq",
|
||||
addBlock: "+ Shto bllok",
|
||||
@@ -574,6 +582,16 @@ export const sq = {
|
||||
graceExpires: "Afati skadon",
|
||||
curve: "Kurba sipas kohëzgjatjes",
|
||||
curveHint: "Tarifa nga hyrja për disa kohëzgjatje — shih ku rrafshohet kufiri ditor ose ndryshojnë dritaret.",
|
||||
bd: {
|
||||
title: "Si prodhohet shuma",
|
||||
rounding: "{{raw}} min qëndrim → {{billed}} min të faturuara (njësi {{inc}} min)",
|
||||
grace: "Falas — brenda minutave të hirit ({{min}} min)",
|
||||
package: "paketë dritareje",
|
||||
step: "Dita {{day}}: qëndrim deri në {{hours}}h — total",
|
||||
stepRepeated: "Dita {{day}}: mbi shkallën më të lartë — totali ditor",
|
||||
cap: "U zbatua kufiri ditor {{cap}} (dita {{day}})",
|
||||
total: "Totali",
|
||||
},
|
||||
},
|
||||
subs: {
|
||||
title: "Abonimet",
|
||||
|
||||
Reference in New Issue
Block a user