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:
co-authored by
Claude Opus 4.8
parent
07192f1238
commit
03738f08dc
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -28,6 +29,7 @@ type userRecord struct {
|
||||
Theme string `json:"theme"`
|
||||
Locale string `json:"locale"`
|
||||
DateFormat string `json:"date_format"`
|
||||
Currency string `json:"currency"`
|
||||
FontSize string `json:"font_size"`
|
||||
DeletionRequestedAt string `json:"deletion_requested_at"`
|
||||
Role string `json:"role"`
|
||||
@@ -45,6 +47,7 @@ func (rec userRecord) toModel() models.User {
|
||||
Theme: orDefault(rec.Theme, "system"),
|
||||
Locale: orDefault(rec.Locale, "en-US"),
|
||||
DateFormat: orDefault(rec.DateFormat, "YMD"),
|
||||
Currency: orDefault(rec.Currency, "USD"),
|
||||
FontSize: orDefault(rec.FontSize, "medium"),
|
||||
Role: orDefault(rec.Role, "user"),
|
||||
Created: rec.Created,
|
||||
@@ -90,6 +93,7 @@ type updateMeRequest struct {
|
||||
Theme *string `json:"theme"`
|
||||
Locale *string `json:"locale"`
|
||||
DateFormat *string `json:"dateFormat"`
|
||||
Currency *string `json:"currency"`
|
||||
FontSize *string `json:"fontSize"`
|
||||
}
|
||||
|
||||
@@ -97,6 +101,23 @@ var validThemes = map[string]bool{"light": true, "dark": true, "system": true}
|
||||
var validDateFormats = map[string]bool{"YMD": true, "DMY_NUM": true, "DMY": true, "MDY": true}
|
||||
var validFontSizes = map[string]bool{"small": true, "medium": true, "large": true}
|
||||
|
||||
// Kept in step with the users.currency select options in setup-pocketbase.mjs:
|
||||
// PocketBase rejects anything outside its own list, so accepting a wider set
|
||||
// here would only turn a clear 400 into a confusing upstream error.
|
||||
var validCurrencies = map[string]bool{
|
||||
"EUR": true, "GBP": true, "CHF": true, "PLN": true, "CZK": true, "HUF": true,
|
||||
"RON": true, "BGN": true, "DKK": true, "SEK": true, "NOK": true, "ISK": true,
|
||||
"ALL": true, "AMD": true, "AZN": true, "BAM": true, "BYN": true, "GEL": true,
|
||||
"MDL": true, "MKD": true, "RSD": true, "RUB": true, "TRY": true, "UAH": true,
|
||||
"USD": true, "CAD": true, "AUD": true, "JPY": true,
|
||||
}
|
||||
|
||||
// The clients pick language and region separately and join them into this tag,
|
||||
// so the stored value is only ever language-REGION. Enforcing that shape here
|
||||
// keeps a bad tag out of the record: the web app feeds the locale straight to
|
||||
// Intl, which throws on a malformed one rather than falling back.
|
||||
var localePattern = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
|
||||
|
||||
// handleUpdateMe applies a partial update — only fields present in the request
|
||||
// body are touched, so the Account/Profile/Appearance sections of the settings
|
||||
// panel can each save independently without clobbering the others.
|
||||
@@ -127,6 +148,10 @@ func (s *Server) handleUpdateMe(w http.ResponseWriter, r *http.Request) {
|
||||
payload["theme"] = *in.Theme
|
||||
}
|
||||
if in.Locale != nil {
|
||||
if !localePattern.MatchString(*in.Locale) {
|
||||
writeError(w, http.StatusBadRequest, "locale must look like en-US")
|
||||
return
|
||||
}
|
||||
payload["locale"] = *in.Locale
|
||||
}
|
||||
if in.DateFormat != nil {
|
||||
@@ -136,6 +161,13 @@ func (s *Server) handleUpdateMe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
payload["date_format"] = *in.DateFormat
|
||||
}
|
||||
if in.Currency != nil {
|
||||
if !validCurrencies[*in.Currency] {
|
||||
writeError(w, http.StatusBadRequest, "currency must be a supported ISO 4217 code")
|
||||
return
|
||||
}
|
||||
payload["currency"] = *in.Currency
|
||||
}
|
||||
if in.FontSize != nil {
|
||||
if !validFontSizes[*in.FontSize] {
|
||||
writeError(w, http.StatusBadRequest, "fontSize must be small, medium, or large")
|
||||
|
||||
Reference in New Issue
Block a user