diff --git a/API Server/internal/api/me.go b/API Server/internal/api/me.go
index a79bbdf..cff9dbb 100644
--- a/API Server/internal/api/me.go
+++ b/API Server/internal/api/me.go
@@ -30,6 +30,7 @@ type userRecord struct {
Locale string `json:"locale"`
DateFormat string `json:"date_format"`
TimeFormat string `json:"time_format"`
+ WeekStart string `json:"week_start"`
Currency string `json:"currency"`
FontSize string `json:"font_size"`
DragLocked bool `json:"drag_locked"`
@@ -99,6 +100,7 @@ func (rec userRecord) toModel() models.User {
Locale: orDefault(rec.Locale, "en-US"),
DateFormat: orDefault(rec.DateFormat, "YMD"),
TimeFormat: orDefault(rec.TimeFormat, "auto"),
+ WeekStart: orDefault(rec.WeekStart, "auto"),
Currency: orDefault(rec.Currency, "USD"),
FontSize: orDefault(rec.FontSize, "medium"),
DragLocked: rec.DragLocked,
@@ -164,6 +166,7 @@ type updateMeRequest struct {
Locale *string `json:"locale"`
DateFormat *string `json:"dateFormat"`
TimeFormat *string `json:"timeFormat"`
+ WeekStart *string `json:"weekStart"`
Currency *string `json:"currency"`
FontSize *string `json:"fontSize"`
DragLocked *bool `json:"dragLocked"`
@@ -264,6 +267,12 @@ var validDateFormats = map[string]bool{"YMD": true, "DMY_NUM": true, "DMY": true
// the app did before there was a setting; the other two say it outright, for
// the people whose region and habit disagree.
var validTimeFormats = map[string]bool{"auto": true, "24": true, "12": true}
+
+// Which day a week is drawn as starting on. "auto" is the chosen region's own
+// convention — Monday across most of Europe, Sunday in the US — and is what
+// every weekday row read before there was a setting; the other two say it
+// outright, for the people whose region and habit disagree.
+var validWeekStarts = map[string]bool{"auto": true, "monday": true, "sunday": 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:
@@ -333,6 +342,13 @@ func (s *Server) handleUpdateMe(w http.ResponseWriter, r *http.Request) {
}
payload["time_format"] = *in.TimeFormat
}
+ if in.WeekStart != nil {
+ if !validWeekStarts[*in.WeekStart] {
+ writeError(w, http.StatusBadRequest, "weekStart must be auto, monday, or sunday")
+ return
+ }
+ payload["week_start"] = *in.WeekStart
+ }
if in.Currency != nil {
if !validCurrencies[*in.Currency] {
writeError(w, http.StatusBadRequest, "currency must be a supported ISO 4217 code")
diff --git a/API Server/internal/bootstrap/schema.go b/API Server/internal/bootstrap/schema.go
index c07c5f4..1fce83f 100644
--- a/API Server/internal/bootstrap/schema.go
+++ b/API Server/internal/bootstrap/schema.go
@@ -262,6 +262,10 @@ var collectionsSchema = map[string][]fieldDef{
// "auto" is the region's own convention, which is what every clock in the
// app read before this field existed.
fSelect("time_format", []string{"auto", "24", "12"}, false),
+ // The day a week is drawn as starting on, wherever weekdays are laid out
+ // in a row. "auto" is the region's own convention, which is what the
+ // scheduler's day picker read before this field existed.
+ fSelect("week_start", []string{"auto", "monday", "sunday"}, false),
fSelect("currency", []string{
"EUR", "GBP", "CHF", "PLN", "CZK", "HUF", "RON", "BGN", "DKK", "SEK", "NOK",
"ISK", "ALL", "AMD", "AZN", "BAM", "BYN", "GEL", "MDL", "MKD", "RSD", "RUB",
diff --git a/API Server/internal/models/models.go b/API Server/internal/models/models.go
index f8116c4..b527cd9 100644
--- a/API Server/internal/models/models.go
+++ b/API Server/internal/models/models.go
@@ -519,9 +519,12 @@ type User struct {
Locale string `json:"locale"` // e.g. "en-US"
DateFormat string `json:"dateFormat"` // YMD | DMY | MDY
TimeFormat string `json:"timeFormat"` // auto (the region's own) | 24 | 12
- Currency string `json:"currency"` // ISO 4217 code, e.g. "EUR"
- FontSize string `json:"fontSize"` // small | medium | large
- Role string `json:"role"` // user | admin
+ // The day a week is drawn as starting on, wherever a client lays weekdays
+ // out in a row — the scheduler's day picker today.
+ WeekStart string `json:"weekStart"` // auto (the region's own) | monday | sunday
+ Currency string `json:"currency"` // ISO 4217 code, e.g. "EUR"
+ FontSize string `json:"fontSize"` // small | medium | large
+ Role string `json:"role"` // user | admin
// DragLocked holds every arrangement on this account's pages still: the
// garage, a car's tabs, its Information rows, the provider's readings. A
diff --git a/API Server/scripts/setup-pocketbase.mjs b/API Server/scripts/setup-pocketbase.mjs
index 61e2fbb..0e3dc19 100644
--- a/API Server/scripts/setup-pocketbase.mjs
+++ b/API Server/scripts/setup-pocketbase.mjs
@@ -517,6 +517,10 @@ const DESIRED = {
// "auto" is the region's own convention. Kept in step with
// validTimeFormats in internal/api/me.go.
F.select("time_format", ["auto", "24", "12"]),
+ // The day a week is drawn as starting on, wherever a client lays weekdays
+ // out in a row. "auto" is the region's own convention. Kept in step with
+ // validWeekStarts in internal/api/me.go.
+ F.select("week_start", ["auto", "monday", "sunday"]),
// European currencies plus the non-European ones the panel already offered.
// Kept in step with validCurrencies in internal/api/me.go and CURRENCY_CODES
// in the web app's Settings.vue.
diff --git a/Web App/web/src/components/ChargingTaskModal.vue b/Web App/web/src/components/ChargingTaskModal.vue
index 389bba7..dc958b9 100644
--- a/Web App/web/src/components/ChargingTaskModal.vue
+++ b/Web App/web/src/components/ChargingTaskModal.vue
@@ -16,6 +16,7 @@
import { ref, computed, watch } from "vue";
import { api } from "../api";
import { t } from "../i18n";
+import { weekdaysInOrder, weekdayShortName } from "../lib/format.js";
import Modal from "./Modal.vue";
import TimeField from "./TimeField.vue";
@@ -51,19 +52,13 @@ const error = ref("");
// it fires — same as the buttons on the page behind this.
const ACTIONS = ["start", "stop", "limit", "boost"];
-// Sunday first, as Intl numbers the weekdays — the names come from the user's
-// own locale, so the row reads Pn Wt Śr… in Polish without a table here.
-const WEEKDAYS = [0, 1, 2, 3, 4, 5, 6];
-
-function weekdayLabel(day) {
- // 2024-01-07 was a Sunday, so this offset lands each index on its own day.
- const date = new Date(Date.UTC(2024, 0, 7 + day));
- try {
- return new Intl.DateTimeFormat(undefined, { weekday: "short", timeZone: "UTC" }).format(date);
- } catch {
- return String(day);
- }
-}
+// The row starts on whichever day this account reads a week as starting on —
+// Settings › Appearance › First day of the week, following the region unless it
+// was answered outright. A computed rather than a constant, so changing the
+// setting in another tab re-lays the row out instead of leaving it on the old
+// week. Both of these come from lib/format.js, which owns the rule for every
+// weekday row in the app.
+const weekdays = computed(() => weekdaysInOrder());
function toggleDay(day) {
everyDay.value = false;
@@ -209,7 +204,7 @@ function browserZone() {
diff --git a/Web App/web/src/i18n/da.json b/Web App/web/src/i18n/da.json
index 3961a0c..ff36d78 100644
--- a/Web App/web/src/i18n/da.json
+++ b/Web App/web/src/i18n/da.json
@@ -555,6 +555,11 @@
"time24": "24-timers",
"time12": "12-timers",
"timeExample": "Eksempel: {example}",
+ "weekStart": "Første dag i ugen",
+ "weekAuto": "Følg regionen",
+ "weekMonday": "Mandag",
+ "weekSunday": "Søndag",
+ "weekExample": "Eksempel: {example}",
"currency": "Valuta",
"currencyExample": "Eksempel: {example} — kun visning, ingen beløb omregnes.",
"fontSize": "Skriftstørrelse",
diff --git a/Web App/web/src/i18n/en.json b/Web App/web/src/i18n/en.json
index 4ad506b..13e17de 100644
--- a/Web App/web/src/i18n/en.json
+++ b/Web App/web/src/i18n/en.json
@@ -554,6 +554,11 @@
"time24": "24-hour",
"time12": "12-hour",
"timeExample": "Example: {example}",
+ "weekStart": "First day of the week",
+ "weekAuto": "Follow the region",
+ "weekMonday": "Monday",
+ "weekSunday": "Sunday",
+ "weekExample": "Example: {example}",
"currency": "Currency",
"currencyExample": "Example: {example} — display only, no amounts are converted.",
"fontSize": "Font size",
diff --git a/Web App/web/src/i18n/pl.json b/Web App/web/src/i18n/pl.json
index 1c4980f..b855717 100644
--- a/Web App/web/src/i18n/pl.json
+++ b/Web App/web/src/i18n/pl.json
@@ -561,6 +561,11 @@
"time24": "24-godzinny",
"time12": "12-godzinny",
"timeExample": "Przykład: {example}",
+ "weekStart": "Pierwszy dzień tygodnia",
+ "weekAuto": "Zgodnie z regionem",
+ "weekMonday": "Poniedziałek",
+ "weekSunday": "Niedziela",
+ "weekExample": "Przykład: {example}",
"currency": "Waluta",
"currencyExample": "Przykład: {example} — tylko wyświetlanie, kwoty nie są przeliczane.",
"fontSize": "Rozmiar czcionki",
diff --git a/Web App/web/src/lib/format.js b/Web App/web/src/lib/format.js
index 3a38a78..d410b74 100644
--- a/Web App/web/src/lib/format.js
+++ b/Web App/web/src/lib/format.js
@@ -151,6 +151,78 @@ function regionReadsTwelveHour() {
return twelveHourRegions.get(locale);
}
+// --- Weekdays --------------------------------------------------------------
+//
+// A week does not start on the same day everywhere: Monday across most of
+// Europe, Sunday in the US and a good deal of Asia. A row of weekday buttons
+// that always begins on Sunday reads wrong to half the people looking at it,
+// and reads wrong in a way that is easy to misclick — Settings › Appearance ›
+// First day of the week is the answer, with "auto" following the chosen region
+// the way the clock setting does.
+//
+// Everything that lays weekdays out in a row goes through these two, so there
+// is one answer to "which day comes first" rather than one per screen. Days are
+// numbered the way Date.getDay() and the scheduler's stored tasks number them:
+// 0 = Sunday … 6 = Saturday.
+
+// Whether weeks are drawn as starting on Monday right now: what the setting
+// says outright, or what the region says when it is left on auto.
+export function weekStartsOnMonday() {
+ const mode = prefs.weekStart;
+ if (mode === "monday") return true;
+ if (mode === "sunday") return false;
+ return regionStartsOnMonday();
+}
+
+// The one question "auto" asks the region. Cached per locale like the clock's,
+// and for the same reason — it is asked once per weekday button.
+const mondayRegions = new Map();
+
+function regionStartsOnMonday() {
+ const locale = prefs.locale || "";
+ if (!mondayRegions.has(locale)) {
+ // ISO 8601 numbers the days 1 = Monday … 7 = Sunday, which is what weekInfo
+ // reports. Browsers expose it as a method on some engines and a property on
+ // others, hence both.
+ let monday = true;
+ try {
+ const info = new Intl.Locale(locale || "en-US");
+ const first = (info.getWeekInfo?.() || info.weekInfo)?.firstDay;
+ if (first) monday = first === 1;
+ } catch {
+ // An engine without week information, or an unusable locale. Monday is
+ // the safer default: it is ISO 8601's, and the convention in every region
+ // this app's own currency list covers bar one.
+ }
+ mondayRegions.set(locale, monday);
+ }
+ return mondayRegions.get(locale);
+}
+
+// The seven days in the order they should be drawn, as day numbers.
+export function weekdaysInOrder() {
+ return weekStartsOnMonday() ? [1, 2, 3, 4, 5, 6, 0] : [0, 1, 2, 3, 4, 5, 6];
+}
+
+// One day's short name in the user's own language, so a row reads Pn Wt Śr in
+// Polish without a table here. 2024-01-07 was a Sunday, which is where day 0
+// sits, so the offset lands each number on its own day.
+export function weekdayShortName(day) {
+ try {
+ return new Intl.DateTimeFormat(prefs.locale || undefined, { weekday: "short", timeZone: "UTC" })
+ .format(new Date(Date.UTC(2024, 0, 7 + day)));
+ } catch {
+ return String(day);
+ }
+}
+
+// A set of days, listed in the order this account reads a week in — so the same
+// three days always come out in the same order wherever they are shown.
+export function sortWeekdays(days) {
+ const order = weekdaysInOrder();
+ return [...(days || [])].sort((a, b) => order.indexOf(a) - order.indexOf(b));
+}
+
// 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.
diff --git a/Web App/web/src/prefs.js b/Web App/web/src/prefs.js
index 5725461..daef697 100644
--- a/Web App/web/src/prefs.js
+++ b/Web App/web/src/prefs.js
@@ -7,6 +7,10 @@ export const prefs = reactive({
locale: "en-US",
dateFormat: "YMD", // YMD | DMY | MDY
timeFormat: "auto", // auto (the region's own convention) | 24 | 12
+ // The day a week is drawn as starting on, wherever weekdays are laid out in a
+ // row — the charging scheduler's day picker today. See lib/format.js, which
+ // owns the rule so every such row reads the same.
+ weekStart: "auto", // auto (the region's own convention) | monday | sunday
currency: "USD", // ISO 4217 code
fontSize: "medium", // small | medium | large
// Holds every arrangement still: the garage, a car's tabs, its Information
@@ -58,6 +62,7 @@ export function applyProfilePrefs(profile) {
prefs.locale = profile.locale || "en-US";
prefs.dateFormat = profile.dateFormat || "YMD";
prefs.timeFormat = profile.timeFormat || "auto";
+ prefs.weekStart = profile.weekStart || "auto";
prefs.currency = profile.currency || "USD";
prefs.fontSize = profile.fontSize || "medium";
prefs.dragLocked = !!profile.dragLocked;
diff --git a/Web App/web/src/views/Charging.vue b/Web App/web/src/views/Charging.vue
index 80651de..3263771 100644
--- a/Web App/web/src/views/Charging.vue
+++ b/Web App/web/src/views/Charging.vue
@@ -4,7 +4,8 @@ import { t } from "../i18n";
import { prefs } from "../prefs";
import { askConfirm } from "../lib/confirm.js";
import { api } from "../api";
-import { formatDateTime, clockIsTwelveHour } from "../lib/format.js";
+import { formatDateTime, clockIsTwelveHour, weekdayShortName, sortWeekdays }
+ from "../lib/format.js";
import TimeField from "../components/TimeField.vue";
import { CHARGING_TABS, defaultTabFor } from "../lib/tabs.js";
import ChargerImportModal from "../components/ChargerImportModal.vue";
@@ -2053,18 +2054,9 @@ function taskChargersLabel(task) {
function taskDaysLabel(task) {
const days = task.days || [];
if (days.length === 0) return t("charging.scheduler.everyDay");
- return [...days].sort((a, b) => a - b).map(weekdayShort).join(" ");
-}
-
-// The weekday in the user's own language. 2024-01-07 was a Sunday, which is
-// where Intl starts counting, so the offset lands each number on its own day.
-function weekdayShort(day) {
- try {
- return new Intl.DateTimeFormat(undefined, { weekday: "short", timeZone: "UTC" })
- .format(new Date(Date.UTC(2024, 0, 7 + day)));
- } catch {
- return String(day);
- }
+ // Listed in the order this account reads a week in, so "Mon Fri" and the
+ // picker that wrote it agree about which end of the week comes first.
+ return sortWeekdays(days).map(weekdayShortName).join(" ");
}
// The task's time, on the clock the user chose. It is stored as 24-hour "HH:MM"
diff --git a/Web App/web/src/views/Settings.vue b/Web App/web/src/views/Settings.vue
index b8546ac..79fc574 100644
--- a/Web App/web/src/views/Settings.vue
+++ b/Web App/web/src/views/Settings.vue
@@ -4,7 +4,8 @@ import { useRoute, useRouter } from "vue-router";
import { api } from "../api";
import { state, isAdmin, logout, refreshProfile } from "../auth";
import { prefs, applyProfilePrefs } from "../prefs";
-import { formatDate, formatMoney, formatTime } from "../lib/format.js";
+import { formatDate, formatMoney, formatTime, weekdaysInOrder, weekdayShortName }
+ from "../lib/format.js";
import { t, tSplit, TRANSLATED_LANGUAGES } from "../i18n";
import { TAB_SURFACES, SETTINGS_TABS, defaultTabFor } from "../lib/tabs.js";
import { askConfirm } from "../lib/confirm.js";
@@ -197,6 +198,9 @@ const timeFormatExample = computed(() => {
return formatTime(d);
});
const currencyExample = computed(() => formatMoney(1234.5));
+// The week as this account will now see it drawn — the clearest possible
+// example, because the setting has no other visible effect on this page.
+const weekStartExample = computed(() => weekdaysInOrder().map(weekdayShortName).join(" "));
// Language and region are two controls over the one stored BCP-47 locale, so
// the pair can be mixed freely (English in Poland, say) rather than being
@@ -1161,6 +1165,18 @@ onBeforeUnmount(() => {