browsertrix/frontend/src/utils/cron.ts
Emma Segal-Grossman c5b808ba40
Ensure dates are formatted with the current app locale (and not browser default) (#1697)
### Motivation

While using the browser default locale is often good enough, we probably
want more full control over locales, especially when we allow users to
choose their own locale — it's a poor experience to not have dates and
numbers formatted consistently with your chosen locale.

### Changes

Ensures (almost) all instances of `<sl-format-date>` as well as `Intl.*`
have the correct locale passed into them from our Lit localization
system.

The one exception here is in `frontend/src/utils/number.ts` where
ordinal suffixes aren't localized, so the locale is hardcoded to `en` —
I'll revisit this in the future.
2024-04-20 16:30:33 -04:00

185 lines
4.3 KiB
TypeScript

import { parseCron } from "@cheap-glitch/mi-cron";
import { msg, str } from "@lit/localize";
import { getLocale } from "./localization";
import * as numberUtils from "./number";
export const getNextDate = parseCron.nextDate;
export type ScheduleInterval = "daily" | "weekly" | "monthly";
/**
* Parse interval from cron expression
**/
export function getScheduleInterval(schedule: string): ScheduleInterval {
const [_minute, _hour, dayOfMonth, _month, dayOfWeek] = schedule.split(" ");
if (dayOfMonth === "*") {
if (dayOfWeek === "*") {
return "daily";
}
return "weekly";
}
return "monthly";
}
/**
* Get human-friendly date from cron expression
* Example: "Monday, December 12, 2022 at 12:00 AM PST"
**/
export function humanizeNextDate(
schedule: string,
options: { length?: "short" } = {},
): string {
const nextDate = parseCron.nextDate(schedule);
if (!nextDate) return "";
if (options.length === "short") {
return nextDate.toLocaleString(undefined, {
month: "numeric",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
});
}
return nextDate.toLocaleString(undefined, {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
timeZoneName: "short",
});
}
/**
* Get human-friendly schedule from cron expression
* Example: "Every day at 9:30 AM CDT"
**/
export function humanizeSchedule(
schedule: string,
options: { length?: "short" } = {},
numberFormatter = numberUtils.numberFormatter,
): string {
const interval = getScheduleInterval(schedule);
const parsed = parseCron(schedule);
if (!parsed) {
// Invalid date
return "";
}
const { days } = parsed;
const nextDate = parseCron.nextDate(schedule)!;
const formattedWeekDay = nextDate.toLocaleString(undefined, {
weekday: "long",
});
let intervalMsg = "";
if (options.length === "short") {
switch (interval) {
case "daily": {
const formattedTime = nextDate.toLocaleString(undefined, {
minute: "numeric",
hour: "numeric",
});
intervalMsg = msg(str`${formattedTime} daily`);
break;
}
case "weekly":
intervalMsg = msg(str`Every ${formattedWeekDay}`);
break;
case "monthly": {
const { format } = numberFormatter(getLocale());
intervalMsg = msg(
str`Monthly on the ${format(days[0], { ordinal: true })}`,
);
break;
}
default:
break;
}
} else {
const formattedTime = nextDate.toLocaleString(undefined, {
minute: "numeric",
hour: "numeric",
timeZoneName: "short",
});
switch (interval) {
case "daily":
intervalMsg = msg(str`Every day at ${formattedTime}`);
break;
case "weekly":
intervalMsg = msg(
str`Every ${formattedWeekDay}
at ${formattedTime}`,
);
break;
case "monthly":
intervalMsg = msg(
str`On day ${nextDate.getDate()} of the month at ${formattedTime}`,
);
break;
default:
break;
}
}
return intervalMsg;
}
/**
* Get schedule as UTC cron job expression
* https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-schedule-syntax
**/
export function getUTCSchedule({
interval,
minute,
hour,
period,
dayOfWeek,
dayOfMonth,
}: {
interval: ScheduleInterval;
minute: number | string;
hour: number | string;
period: "AM" | "PM";
dayOfWeek?: number;
dayOfMonth?: number;
}): string {
const localDate = new Date();
// Convert 12-hr to 24-hr time
let periodOffset = 0;
if (hour === 12) {
if (period === "AM") {
periodOffset = -12;
}
} else if (period === "PM") {
periodOffset = 12;
}
localDate.setHours(+hour + periodOffset);
localDate.setMinutes(+minute);
if (interval === "monthly" && dayOfMonth) {
localDate.setDate(dayOfMonth);
} else if (interval == "weekly" && dayOfWeek) {
localDate.setDate(localDate.getDate() + dayOfWeek - localDate.getDay());
}
const date = interval === "monthly" ? localDate.getUTCDate() : "*";
const day = interval === "weekly" ? localDate.getUTCDay() : "*";
const month = "*";
const schedule = `${localDate.getUTCMinutes()} ${localDate.getUTCHours()} ${date} ${month} ${day}`;
return schedule;
}