A week that starts where the person reading it starts theirs
The scheduler's day picker began on Sunday because that is where Intl numbers the days from, which is a fact about the API and not about anybody's week. Monday leads it across most of Europe. A row of seven buttons in the wrong order is not just odd to read — it is easy to misclick, and a misclicked day in a schedule is a car charging on the wrong night. So Settings › Appearance asks, beneath the date and the clock, as the third question a region gets: first day of the week, following the region unless it is answered outright. The same shape the time format already had, and the same "auto" default, so nothing changes for an account that never opens it. The rule lives in lib/format.js beside the clock's, with the ordering, the day names and the sort all coming from there. The two places that lay weekdays out — the picker and the line each task is summarised on — read it rather than each keeping an opinion, so a day set is written and read back in the same order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5a4515978f
commit
0f48093d1a
@@ -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() {
|
||||
</label>
|
||||
<div class="mt-1 flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="d in WEEKDAYS"
|
||||
v-for="d in weekdays"
|
||||
:key="d"
|
||||
type="button"
|
||||
class="rounded-pill border px-3 py-1.5 text-xs font-semibold transition-colors"
|
||||
@@ -218,7 +213,7 @@ function browserZone() {
|
||||
: 'border-subtle text-muted hover:bg-sunken'"
|
||||
@click="toggleDay(d)"
|
||||
>
|
||||
{{ weekdayLabel(d) }}
|
||||
{{ weekdayShortName(d) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(() => {
|
||||
<p class="mt-1 text-xs text-muted">{{ t("settings.appearance.timeExample", { example: timeFormatExample }) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Under the clock, as the last of the three questions a region is
|
||||
asked and the one it is least often asked out loud. -->
|
||||
<div>
|
||||
<label class="dh-label">{{ t("settings.appearance.weekStart") }}</label>
|
||||
<select :value="prefs.weekStart" class="dh-input" @change="saveAppearance({ weekStart: $event.target.value })">
|
||||
<option value="auto">{{ t("settings.appearance.weekAuto") }}</option>
|
||||
<option value="monday">{{ t("settings.appearance.weekMonday") }}</option>
|
||||
<option value="sunday">{{ t("settings.appearance.weekSunday") }}</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-muted">{{ t("settings.appearance.weekExample", { example: weekStartExample }) }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="dh-label">{{ t("settings.appearance.currency") }}</label>
|
||||
<select :value="prefs.currency" class="dh-input" @change="saveAppearance({ currency: $event.target.value })">
|
||||
|
||||
Reference in New Issue
Block a user