Explicitly set locale to avoid hydration mismatch

Some functions in luxon default to the system's locale while other
functions default to "en-US".  Explicitly set the locale everywhere
the luxon objects are created to avoid possible mismatches and
unexpected behaviour should the system's locale be different.
This commit is contained in:
Hornwitser 2025-05-25 23:32:50 +02:00
parent e722876aae
commit ed67982ec0
5 changed files with 31 additions and 31 deletions

View file

@ -53,7 +53,7 @@ const { data: accounts } = await useAccounts();
const idToAccount = computed(() => new Map(accounts.value?.map(a => [a.id, a]))); const idToAccount = computed(() => new Map(accounts.value?.map(a => [a.id, a])));
function formatTime(time: string) { function formatTime(time: string) {
return DateTime.fromISO(time, { zone: accountStore.activeTimezone }).toFormat("yyyy-LL-dd HH:mm"); return DateTime.fromISO(time, { zone: accountStore.activeTimezone, locale: "en-US" }).toFormat("yyyy-LL-dd HH:mm");
} }
async function toggle(id: string, slotIds?: string[]) { async function toggle(id: string, slotIds?: string[]) {

View file

@ -462,12 +462,12 @@ const newEventStart = ref("");
const newEventDuration = ref("01:00"); const newEventDuration = ref("01:00");
const newEventEnd = computed({ const newEventEnd = computed({
get: () => ( get: () => (
DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone }) DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone, locale: "en-US" })
.plus(Duration.fromISOTime(newEventDuration.value)) .plus(Duration.fromISOTime(newEventDuration.value, { locale: "en-US" }))
.toFormat("HH:mm") .toFormat("HH:mm")
), ),
set: (value: string) => { set: (value: string) => {
const start = DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone }); const start = DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone, locale: "en-US" });
const end = endFromTime(start, value); const end = endFromTime(start, value);
newEventDuration.value = dropDay(end.diff(start)).toFormat("hh:mm"); newEventDuration.value = dropDay(end.diff(start)).toFormat("hh:mm");
}, },
@ -478,16 +478,16 @@ watch(() => props.location, () => {
}); });
function endFromTime(start: DateTime, time: string) { function endFromTime(start: DateTime, time: string) {
let end = start.startOf("day").plus(Duration.fromISOTime(time)); let end = start.startOf("day").plus(Duration.fromISOTime(time, { locale: "en-US" }));
if (end.toMillis() <= start.toMillis()) { if (end.toMillis() <= start.toMillis()) {
end = end.plus({ days: 1 }); end = end.plus({ days: 1 });
} }
return end; return end;
} }
function durationFromTime(time: string) { function durationFromTime(time: string) {
let duration = Duration.fromISOTime(time); let duration = Duration.fromISOTime(time, { locale: "en-US" });
if (duration.toMillis() === 0) { if (duration.toMillis() === 0) {
duration = Duration.fromMillis(oneDayMs); duration = Duration.fromMillis(oneDayMs, { locale: "en-US" });
} }
return duration; return duration;
} }
@ -504,7 +504,7 @@ function editEventSlot(
} }
) { ) {
if (edits.start) { if (edits.start) {
const start = DateTime.fromISO(edits.start, { zone: accountStore.activeTimezone }); const start = DateTime.fromISO(edits.start, { zone: accountStore.activeTimezone, locale: "en-US" });
eventSlot = { eventSlot = {
...eventSlot, ...eventSlot,
start, start,
@ -573,7 +573,7 @@ function newEventSlot(options: { start?: DateTime, end?: DateTime } = {}) {
end = options.end; end = options.end;
start = options.end.minus(duration); start = options.end.minus(duration);
} else { } else {
start = DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone }); start = DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone, locale: "en-US" });
end = endFromTime(start, newEventEnd.value); end = endFromTime(start, newEventEnd.value);
} }
if (!start.isValid || !end.isValid) { if (!start.isValid || !end.isValid) {
@ -637,8 +637,8 @@ const eventSlots = computed(() => {
location, location,
assigned: slot.assigned ?? [], assigned: slot.assigned ?? [],
origLocation: location, origLocation: location,
start: DateTime.fromISO(slot.start, { zone: accountStore.activeTimezone }), start: DateTime.fromISO(slot.start, { zone: accountStore.activeTimezone, locale: "en-US" }),
end: DateTime.fromISO(slot.end, { zone: accountStore.activeTimezone }), end: DateTime.fromISO(slot.end, { zone: accountStore.activeTimezone, locale: "en-US" }),
}); });
} }
} }
@ -655,7 +655,7 @@ const eventSlots = computed(() => {
gaps.push([index, { gaps.push([index, {
type: "gap", type: "gap",
location: props.location, location: props.location,
start: DateTime.fromMillis(maxEnd), start: DateTime.fromMillis(maxEnd, { locale: "en-US" }),
end: second.start, end: second.start,
}]); }]);
} }

View file

@ -432,12 +432,12 @@ const newShiftStart = ref("");
const newShiftDuration = ref("01:00"); const newShiftDuration = ref("01:00");
const newShiftEnd = computed({ const newShiftEnd = computed({
get: () => ( get: () => (
DateTime.fromISO(newShiftStart.value, { zone: accountStore.activeTimezone }) DateTime.fromISO(newShiftStart.value, { zone: accountStore.activeTimezone, locale: "en-US" })
.plus(Duration.fromISOTime(newShiftDuration.value)) .plus(Duration.fromISOTime(newShiftDuration.value, { locale: "en-US" }))
.toFormat("HH:mm") .toFormat("HH:mm")
), ),
set: (value: string) => { set: (value: string) => {
const start = DateTime.fromISO(newShiftStart.value, { zone: accountStore.activeTimezone }); const start = DateTime.fromISO(newShiftStart.value, { zone: accountStore.activeTimezone, locale: "en-US" });
const end = endFromTime(start, value); const end = endFromTime(start, value);
newShiftDuration.value = dropDay(end.diff(start)).toFormat("hh:mm"); newShiftDuration.value = dropDay(end.diff(start)).toFormat("hh:mm");
}, },
@ -448,16 +448,16 @@ watch(() => props.role, () => {
}); });
function endFromTime(start: DateTime, time: string) { function endFromTime(start: DateTime, time: string) {
let end = start.startOf("day").plus(Duration.fromISOTime(time)); let end = start.startOf("day").plus(Duration.fromISOTime(time, { locale: "en-US" }));
if (end.toMillis() <= start.toMillis()) { if (end.toMillis() <= start.toMillis()) {
end = end.plus({ days: 1 }); end = end.plus({ days: 1 });
} }
return end; return end;
} }
function durationFromTime(time: string) { function durationFromTime(time: string) {
let duration = Duration.fromISOTime(time); let duration = Duration.fromISOTime(time, { locale: "en-US" });
if (duration.toMillis() === 0) { if (duration.toMillis() === 0) {
duration = Duration.fromMillis(oneDayMs); duration = Duration.fromMillis(oneDayMs, { locale: "en-US" });
} }
return duration; return duration;
} }
@ -474,7 +474,7 @@ function editShiftSlot(
} }
) { ) {
if (edits.start) { if (edits.start) {
const start = DateTime.fromISO(edits.start, { zone: accountStore.activeTimezone }); const start = DateTime.fromISO(edits.start, { zone: accountStore.activeTimezone, locale: "en-US" });
shiftSlot = { shiftSlot = {
...shiftSlot, ...shiftSlot,
start, start,
@ -554,7 +554,7 @@ function newShiftSlot(options: { start?: DateTime, end?: DateTime } = {}) {
end = options.end; end = options.end;
start = options.end.minus(duration); start = options.end.minus(duration);
} else { } else {
start = DateTime.fromISO(newShiftStart.value, { zone: accountStore.activeTimezone }); start = DateTime.fromISO(newShiftStart.value, { zone: accountStore.activeTimezone, locale: "en-US" });
end = endFromTime(start, newShiftEnd.value); end = endFromTime(start, newShiftEnd.value);
} }
if (!start.isValid || !end.isValid) { if (!start.isValid || !end.isValid) {
@ -617,8 +617,8 @@ const shiftSlots = computed(() => {
role: shift.role, role: shift.role,
assigned: slot.assigned ?? [], assigned: slot.assigned ?? [],
origRole: shift.role, origRole: shift.role,
start: DateTime.fromISO(slot.start, { zone: accountStore.activeTimezone }), start: DateTime.fromISO(slot.start, { zone: accountStore.activeTimezone, locale: "en-US" }),
end: DateTime.fromISO(slot.end, { zone: accountStore.activeTimezone }), end: DateTime.fromISO(slot.end, { zone: accountStore.activeTimezone, locale: "en-US" }),
}); });
} }
} }
@ -634,7 +634,7 @@ const shiftSlots = computed(() => {
gaps.push([index, { gaps.push([index, {
type: "gap", type: "gap",
role: props.role, role: props.role,
start: DateTime.fromMillis(maxEnd), start: DateTime.fromMillis(maxEnd, { locale: "en-US" }),
end: second.start, end: second.start,
}]); }]);
} }

View file

@ -251,7 +251,7 @@ function* stretchesFromSpans(spans: Iterable<Span>, minSeparation: number): Gene
/** Cuts up a span by whole hours that crosses it */ /** Cuts up a span by whole hours that crosses it */
function* cutSpansByHours(span: Span, timezone: string): Generator<Span> { function* cutSpansByHours(span: Span, timezone: string): Generator<Span> {
const startHour = DateTime.fromMillis(span.start.ts, { zone: timezone }) const startHour = DateTime.fromMillis(span.start.ts, { zone: timezone, locale: "en-US" })
.startOf("hour") .startOf("hour")
; ;
const end = span.end.ts; const end = span.end.ts;
@ -294,11 +294,11 @@ function* cutSpansByHours(span: Span, timezone: string): Generator<Span> {
function padStretch(stretch: Stretch, timezone: string): Stretch { function padStretch(stretch: Stretch, timezone: string): Stretch {
// Pad by one hour and extend it to the nearest whole hour. // Pad by one hour and extend it to the nearest whole hour.
let start = DateTime.fromMillis(stretch.start, { zone: timezone }) let start = DateTime.fromMillis(stretch.start, { zone: timezone, locale: "en-US" })
.minus(oneHourMs) .minus(oneHourMs)
.startOf("hour") .startOf("hour")
; ;
let end = DateTime.fromMillis(stretch.end, { zone: timezone }) let end = DateTime.fromMillis(stretch.end, { zone: timezone, locale: "en-US" })
.plus(2 * oneHourMs - 1) .plus(2 * oneHourMs - 1)
.startOf("hour") .startOf("hour")
; ;
@ -389,7 +389,7 @@ function tableElementsFromStretches(
let first = true; let first = true;
for (let stretch of stretches) { for (let stretch of stretches) {
stretch = padStretch(stretch, timezone); stretch = padStretch(stretch, timezone);
const startDate = DateTime.fromMillis(stretch.start, { zone: timezone }); const startDate = DateTime.fromMillis(stretch.start, { zone: timezone, locale: "en-US" });
if (first) { if (first) {
first = false; first = false;
startColumnGroup(); startColumnGroup();
@ -452,16 +452,16 @@ function tableElementsFromStretches(
} }
pushColumn(durationMs / oneMinMs); pushColumn(durationMs / oneMinMs);
const endDate = DateTime.fromMillis(end, { zone: timezone }); const endDate = DateTime.fromMillis(end, { zone: timezone, locale: "en-US" });
if (end === endDate.startOf("day").toMillis()) { if (end === endDate.startOf("day").toMillis()) {
startDay( startDay(
DateTime.fromMillis(cutSpan.end.ts, { zone: timezone }) DateTime.fromMillis(cutSpan.end.ts, { zone: timezone, locale: "en-US" })
.toFormat("yyyy-LL-dd") .toFormat("yyyy-LL-dd")
); );
} }
if (end === endDate.startOf("hour").toMillis()) { if (end === endDate.startOf("hour").toMillis()) {
startHour( startHour(
DateTime.fromMillis(cutSpan.end.ts, { zone: timezone }) DateTime.fromMillis(cutSpan.end.ts, { zone: timezone, locale: "en-US" })
.toFormat("HH:mm") .toFormat("HH:mm")
); );
} }

View file

@ -26,7 +26,7 @@ export default defineEventHandler(async (event) => {
}); });
} }
if (body.timezone.length) { if (body.timezone.length) {
const zonedTime = DateTime.local().setZone(body.timezone); const zonedTime = DateTime.local({ locale: "en-US" }).setZone(body.timezone);
if (!zonedTime.isValid) { if (!zonedTime.isValid) {
throw createError({ throw createError({
status: 400, status: 400,