Add currency setting and split locale into language + region
Costs rendered as bare numbers because the project had no currency to render them with. Add one to the profile, beside the existing appearance preferences: - users.currency in PocketBase, exposed via /api/me, validated against the same 28 codes in the schema, the API and the web app. - Settings offers the European currencies plus the non-European ones the panel already had. Labels come from Intl.DisplayNames rather than a hand-kept table, so the lists read in the user's own language and sort by what is actually on screen. The single "Language & region" picker becomes two controls over the one stored BCP-47 tag, covering European languages and countries, so the two can be mixed (English in Poland). The API now enforces the language-REGION shape: the web app feeds the tag straight to Intl, which throws on a malformed one rather than falling back. formatKm and the km-count labels passed no locale, so kilometres followed the browser while the dates and costs beside them followed the saved preference. They now share one helper that uses the preference. Rename the "Maintenance log" tab to "Maintenance", the name the reminder-type list already used. Amounts are display-only: nothing is converted, so changing currency reinterprets existing records rather than recalculating them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
07192f1238
commit
03738f08dc
8 files changed
+152
-24
No files matched your search
@@ -28,9 +28,16 @@ export function formatDate(value) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every number we render goes through here so the grouping separator follows
|
||||
// the user's chosen region rather than the browser's own locale — otherwise the
|
||||
// odometer disagrees with the dates and costs beside it.
|
||||
function num(value) {
|
||||
return Number(value).toLocaleString(prefs.locale || undefined);
|
||||
}
|
||||
|
||||
export function formatKm(value) {
|
||||
if (value == null || value === "" || value === 0) return "—";
|
||||
return Number(value).toLocaleString() + " km";
|
||||
return num(value) + " km";
|
||||
}
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
@@ -72,9 +79,9 @@ function dateSignal(nextServiceDate) {
|
||||
function kmSignal(currentKm, nextServiceKm) {
|
||||
if (!currentKm || !nextServiceKm) return { key: "unknown", label: "No km" };
|
||||
const remaining = nextServiceKm - currentKm;
|
||||
if (remaining < 0) return { key: "overdue", label: `Service Overdue ${Math.abs(remaining).toLocaleString()} km` };
|
||||
if (remaining <= KM_SOON) return { key: "soon", label: `In ${remaining.toLocaleString()} km` };
|
||||
return { key: "ok", label: `${remaining.toLocaleString()} km left` };
|
||||
if (remaining < 0) return { key: "overdue", label: `Service Overdue ${num(Math.abs(remaining))} km` };
|
||||
if (remaining <= KM_SOON) return { key: "soon", label: `In ${num(remaining)} km` };
|
||||
return { key: "ok", label: `${num(remaining)} km left` };
|
||||
}
|
||||
|
||||
// serviceStatus combines the date- and km-based signals, returning the worse of
|
||||
@@ -88,13 +95,17 @@ export function formatLiters(value) {
|
||||
return Number(value).toFixed(2) + " L";
|
||||
}
|
||||
|
||||
// Amounts are unit-less on purpose: the project stores plain numbers and has no
|
||||
// currency setting, so imposing a symbol here would be a guess.
|
||||
// Amounts are stored as plain numbers; the user's currency setting only decides
|
||||
// how they are displayed. Nothing is converted — a figure entered as 40 reads as
|
||||
// 40 in whichever currency is selected.
|
||||
export function formatMoney(value) {
|
||||
if (value == null || value === "") return "—";
|
||||
// No explicit fraction digits: currency style already pins them to the
|
||||
// currency's minor unit, which keeps the 2 decimals the fuel figures were
|
||||
// written for while still rendering yen without phantom sen.
|
||||
return Number(value).toLocaleString(prefs.locale || undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
style: "currency",
|
||||
currency: prefs.currency || "USD",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,13 +173,13 @@ export function reminderStatus(rem) {
|
||||
else if (state === "overdue") {
|
||||
const parts = [];
|
||||
if (days != null && days < 0) parts.push(`${Math.abs(days)}d`);
|
||||
if (km != null && km < 0) parts.push(`${Math.abs(km).toLocaleString()} km`);
|
||||
if (km != null && km < 0) parts.push(`${num(Math.abs(km))} km`);
|
||||
label = parts.length ? `Overdue ${parts.join(" · ")}` : "Overdue";
|
||||
} else {
|
||||
// Lead with the trigger that is closest to firing.
|
||||
const parts = [];
|
||||
if (days != null && days >= 0) parts.push(days === 0 ? "today" : `${days}d`);
|
||||
if (km != null && km >= 0) parts.push(`${km.toLocaleString()} km`);
|
||||
if (km != null && km >= 0) parts.push(`${num(km)} km`);
|
||||
label = parts.length ? `Due in ${parts.join(" · ")}` : "Upcoming";
|
||||
}
|
||||
return { key: state, label, classes: REMINDER_STYLE[state] || REMINDER_STYLE.no_trigger };
|
||||
|
||||
Reference in new issue
Block a user