fix: add Icelandic, Lithuanian, and Norwegian locale support for Booker (#25519)
- Create unified date/time formatter with Intl-first approach and dayjs fallback - Add support for is/lt/nb locales in Booker components - Unify date formatting across Booker (Header, dates, weekdays) - Update translation pipeline to include is, lt, nb - Add tests for new locales and formatter - Add timezone support in dayjs fallback - Add formatter caching with circuit breaker - Use unified formatDateTime in Header.tsx
This commit is contained in:
@@ -7,6 +7,7 @@ import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { useInitializeWeekStart } from "@calcom/features/bookings/Booker/components/hooks/useInitializeWeekStart";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { formatDateTime } from "@calcom/lib/dateTimeFormatter";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
@@ -97,13 +98,14 @@ export function Header({
|
||||
const isSameYear = () => {
|
||||
return selectedDate.format("YYYY") === endDate.format("YYYY");
|
||||
};
|
||||
const formattedMonth = new Intl.DateTimeFormat(i18n.language ?? "en", { month: "short" });
|
||||
const formatMonthLabel = (date: Date) =>
|
||||
formatDateTime(date, { locale: i18n.language ?? "en", month: "short" });
|
||||
const FormattedSelectedDateRange = () => {
|
||||
return (
|
||||
<h3 className="min-w-[150px] text-base font-semibold leading-4">
|
||||
{formattedMonth.format(selectedDate.toDate())} {selectedDate.format("D")}
|
||||
{formatMonthLabel(selectedDate.toDate())} {selectedDate.format("D")}
|
||||
{!isSameYear() && <span className="text-subtle">, {selectedDate.format("YYYY")} </span>}-{" "}
|
||||
{!isSameMonth() && formattedMonth.format(endDate.toDate())} {endDate.format("D")},{" "}
|
||||
{!isSameMonth() && formatMonthLabel(endDate.toDate())} {endDate.format("D")},{" "}
|
||||
<span className="text-subtle">
|
||||
{isSameYear() ? selectedDate.format("YYYY") : endDate.format("YYYY")}
|
||||
</span>
|
||||
|
||||
@@ -8,9 +8,12 @@ import "dayjs/locale/fi";
|
||||
import "dayjs/locale/fr";
|
||||
import "dayjs/locale/he";
|
||||
import "dayjs/locale/hu";
|
||||
import "dayjs/locale/is";
|
||||
import "dayjs/locale/it";
|
||||
import "dayjs/locale/ja";
|
||||
import "dayjs/locale/ko";
|
||||
import "dayjs/locale/lt";
|
||||
import "dayjs/locale/nb";
|
||||
import "dayjs/locale/nl";
|
||||
import "dayjs/locale/pl";
|
||||
import "dayjs/locale/pt";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatDateTime, formatDateTimeRange } from "@calcom/lib/dateTimeFormatter";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
|
||||
interface EventFromToTime {
|
||||
@@ -17,16 +18,19 @@ interface EventFromTime {
|
||||
|
||||
export const formatEventFromTime = ({ date, timeFormat, timeZone, language }: EventFromTime) => {
|
||||
const startDate = new Date(date);
|
||||
const formattedDate = new Intl.DateTimeFormat(language, {
|
||||
|
||||
const formattedDate = formatDateTime(startDate, {
|
||||
locale: language,
|
||||
timeZone,
|
||||
dateStyle: "full",
|
||||
}).format(startDate);
|
||||
});
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat(language, {
|
||||
const formattedTime = formatDateTime(startDate, {
|
||||
locale: language,
|
||||
timeZone,
|
||||
timeStyle: "short",
|
||||
hour12: timeFormat === TimeFormat.TWELVE_HOUR ? true : false,
|
||||
}).format(startDate);
|
||||
hour12: timeFormat === TimeFormat.TWELVE_HOUR,
|
||||
});
|
||||
|
||||
return {
|
||||
date: formattedDate,
|
||||
@@ -49,16 +53,18 @@ export const formatEventFromToTime = ({
|
||||
? new Date(new Date(date).setMinutes(startDate.getMinutes() + duration))
|
||||
: startDate;
|
||||
|
||||
const formattedDate = new Intl.DateTimeFormat(language, {
|
||||
const formattedDate = formatDateTimeRange(startDate, endDate, {
|
||||
locale: language,
|
||||
timeZone,
|
||||
dateStyle: "full",
|
||||
}).formatRange(startDate, endDate);
|
||||
});
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat(language, {
|
||||
const formattedTime = formatDateTimeRange(startDate, endDate, {
|
||||
locale: language,
|
||||
timeZone,
|
||||
timeStyle: "short",
|
||||
hour12: timeFormat === TimeFormat.TWELVE_HOUR ? true : false,
|
||||
}).formatRange(startDate, endDate);
|
||||
hour12: timeFormat === TimeFormat.TWELVE_HOUR,
|
||||
});
|
||||
|
||||
return {
|
||||
date: formattedDate,
|
||||
|
||||
@@ -30,6 +30,9 @@ const SUPPORTED_LOCALES = [
|
||||
"ja", // Japanese
|
||||
"sv", // Swedish
|
||||
"da", // Danish
|
||||
"is", // Icelandic
|
||||
"lt", // Lithuanian
|
||||
"nb", // Norwegian Bokmål
|
||||
] as const;
|
||||
|
||||
async function processTranslations({
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import "@calcom/dayjs/locales";
|
||||
|
||||
import { formatDateTime, formatDateTimeRange, formatWeekday, getWeekdayNames } from "./dateTimeFormatter";
|
||||
|
||||
describe("dateTimeFormatter", () => {
|
||||
describe("formatDateTime", () => {
|
||||
describe("supported locales (Intl path)", () => {
|
||||
it("formats full date style in English", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "en-US",
|
||||
timeZone: "UTC",
|
||||
dateStyle: "full",
|
||||
});
|
||||
|
||||
// Validate actual content
|
||||
expect(result).toContain("Monday");
|
||||
expect(result).toContain("December");
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("2025");
|
||||
});
|
||||
|
||||
it("formats time with 12h format", () => {
|
||||
const date = new Date("2025-12-01T14:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "en-US",
|
||||
timeZone: "UTC",
|
||||
timeStyle: "short",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
expect(result).toMatch(/2:30\s*PM/);
|
||||
});
|
||||
|
||||
it("formats month only", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "en-US",
|
||||
month: "long",
|
||||
});
|
||||
|
||||
expect(result).toBe("December");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsupported locales (dayjs fallback)", () => {
|
||||
it("formats Icelandic date correctly", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "is",
|
||||
dateStyle: "long",
|
||||
});
|
||||
|
||||
// Validate Icelandic content
|
||||
expect(result).toMatch(/desember/i); // December in Icelandic
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("2025");
|
||||
});
|
||||
|
||||
it("formats Lithuanian date correctly", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "lt",
|
||||
dateStyle: "long",
|
||||
});
|
||||
|
||||
// Validate Lithuanian content
|
||||
expect(result).toMatch(/gruod/i); // December in Lithuanian
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("2025");
|
||||
});
|
||||
|
||||
it("formats Norwegian Bokmål date correctly", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "nb",
|
||||
dateStyle: "long",
|
||||
});
|
||||
|
||||
// Validate Norwegian content
|
||||
expect(result).toMatch(/desember/i); // December in Norwegian
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("2025");
|
||||
});
|
||||
|
||||
it("formats month-only in Icelandic", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "is",
|
||||
month: "long",
|
||||
});
|
||||
|
||||
expect(result).toMatch(/desember/i);
|
||||
});
|
||||
|
||||
it("formats narrow month (first character)", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "is",
|
||||
month: "narrow",
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("formats time with 12h format in Icelandic", () => {
|
||||
const date = new Date("2025-12-01T14:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "is",
|
||||
timeStyle: "short",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
expect(result).toMatch(/2:30/);
|
||||
expect(result).toMatch(/(PM|e\.h\.)/i); // English PM or Icelandic "eftir hádegi"
|
||||
});
|
||||
|
||||
it("formats combined date and time", () => {
|
||||
const date = new Date("2025-12-01T14:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "is",
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
expect(result).toContain("1");
|
||||
expect(result).toMatch(/14:30/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDateTimeRange", () => {
|
||||
describe("supported locales", () => {
|
||||
it("formats time range on same day", () => {
|
||||
const startDate = new Date("2025-12-01T10:30:00Z");
|
||||
const endDate = new Date("2025-12-01T11:30:00Z");
|
||||
|
||||
const result = formatDateTimeRange(startDate, endDate, {
|
||||
locale: "en-US",
|
||||
timeZone: "UTC",
|
||||
timeStyle: "short",
|
||||
});
|
||||
|
||||
expect(result).toMatch(/10:30/);
|
||||
expect(result).toMatch(/11:30/);
|
||||
});
|
||||
|
||||
it("formats date range across different days", () => {
|
||||
const startDate = new Date("2025-12-01T10:30:00Z");
|
||||
const endDate = new Date("2025-12-02T11:30:00Z");
|
||||
|
||||
const result = formatDateTimeRange(startDate, endDate, {
|
||||
locale: "en-US",
|
||||
timeZone: "UTC",
|
||||
dateStyle: "short",
|
||||
});
|
||||
|
||||
expect(result).toContain("12/1");
|
||||
expect(result).toContain("12/2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsupported locales (dayjs fallback)", () => {
|
||||
it("formats time range in Icelandic", () => {
|
||||
const startDate = new Date("2025-12-01T10:30:00Z");
|
||||
const endDate = new Date("2025-12-01T11:30:00Z");
|
||||
|
||||
const result = formatDateTimeRange(startDate, endDate, {
|
||||
locale: "is",
|
||||
timeZone: "UTC",
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
expect(result).toContain("10:30");
|
||||
expect(result).toContain("11:30");
|
||||
expect(result).toContain("–"); // En dash separator
|
||||
});
|
||||
|
||||
it("formats date range in Lithuanian", () => {
|
||||
const startDate = new Date("2025-12-01T10:30:00Z");
|
||||
const endDate = new Date("2025-12-02T10:30:00Z");
|
||||
|
||||
const result = formatDateTimeRange(startDate, endDate, {
|
||||
locale: "lt",
|
||||
dateStyle: "medium",
|
||||
});
|
||||
|
||||
expect(result).toContain("–");
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("2");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatWeekday", () => {
|
||||
describe("supported locales", () => {
|
||||
it("formats long weekday names in English", () => {
|
||||
expect(formatWeekday("en-US", 0, "long")).toBe("Sunday");
|
||||
expect(formatWeekday("en-US", 1, "long")).toBe("Monday");
|
||||
expect(formatWeekday("en-US", 6, "long")).toBe("Saturday");
|
||||
});
|
||||
|
||||
it("formats short weekday names in English", () => {
|
||||
expect(formatWeekday("en-US", 0, "short")).toBe("Sun");
|
||||
expect(formatWeekday("en-US", 1, "short")).toBe("Mon");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsupported locales (dayjs fallback)", () => {
|
||||
it("formats long weekday names in Icelandic", () => {
|
||||
const monday = formatWeekday("is", 1, "long");
|
||||
expect(monday).toMatch(/mánudagur/i);
|
||||
});
|
||||
|
||||
it("formats short weekday names in Icelandic", () => {
|
||||
const monday = formatWeekday("is", 1, "short");
|
||||
expect(monday).toMatch(/mán/i);
|
||||
});
|
||||
|
||||
it("formats weekdays in Lithuanian", () => {
|
||||
const monday = formatWeekday("lt", 1, "long");
|
||||
expect(monday).toMatch(/pirmadienis/i); // Monday in Lithuanian
|
||||
});
|
||||
|
||||
it("formats weekdays in Norwegian Bokmål", () => {
|
||||
const monday = formatWeekday("nb", 1, "long");
|
||||
expect(monday).toMatch(/mandag/i); // Monday in Norwegian
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWeekdayNames", () => {
|
||||
it("returns 7 weekday names starting Sunday by default", () => {
|
||||
const result = getWeekdayNames("en-US");
|
||||
|
||||
expect(result).toHaveLength(7);
|
||||
expect(result[0]).toBe("Sunday");
|
||||
expect(result[1]).toBe("Monday");
|
||||
expect(result[6]).toBe("Saturday");
|
||||
});
|
||||
|
||||
it("supports custom week start (Monday)", () => {
|
||||
const result = getWeekdayNames("en-US", 1);
|
||||
|
||||
expect(result).toHaveLength(7);
|
||||
expect(result[0]).toBe("Monday");
|
||||
expect(result[6]).toBe("Sunday");
|
||||
});
|
||||
|
||||
it("supports short format", () => {
|
||||
const result = getWeekdayNames("en-US", 0, "short");
|
||||
|
||||
expect(result).toHaveLength(7);
|
||||
expect(result[0]).toBe("Sun");
|
||||
expect(result[1]).toBe("Mon");
|
||||
});
|
||||
|
||||
it("returns Icelandic weekday names", () => {
|
||||
const result = getWeekdayNames("is", 0, "long");
|
||||
|
||||
expect(result).toHaveLength(7);
|
||||
// Validate at least one Icelandic weekday
|
||||
expect(result.some((day) => day.match(/dagur$/i))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timezone handling", () => {
|
||||
it("formats in UTC correctly for both Intl and dayjs paths", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
// Intl path (supported locale)
|
||||
const intlResult = formatDateTime(date, {
|
||||
locale: "en",
|
||||
timeZone: "UTC",
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
expect(intlResult).toContain("10:30");
|
||||
|
||||
// Dayjs path (unsupported locale)
|
||||
const dayjsResult = formatDateTime(date, {
|
||||
locale: "is",
|
||||
timeZone: "UTC",
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
expect(dayjsResult).toContain("10:30");
|
||||
});
|
||||
|
||||
it("applies Tokyo timezone offset correctly", () => {
|
||||
const date = new Date("2025-12-01T10:30:00Z");
|
||||
|
||||
// Intl path
|
||||
const intlResult = formatDateTime(date, {
|
||||
locale: "en",
|
||||
timeZone: "Asia/Tokyo",
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
expect(intlResult).toContain("19:30"); // UTC+9
|
||||
|
||||
// Test timezone respect in dayjs fallback
|
||||
const dayjsResult = formatDateTime(date, {
|
||||
locale: "is",
|
||||
timeZone: "Asia/Tokyo",
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
expect(dayjsResult).toContain("19:30"); // UTC+9
|
||||
});
|
||||
|
||||
it("handles day boundary crossing with timezone offset", () => {
|
||||
const date = new Date("2025-12-01T20:30:00Z");
|
||||
|
||||
const result = formatDateTime(date, {
|
||||
locale: "is",
|
||||
timeZone: "Asia/Tokyo",
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
// 20:30 UTC + 9h = 05:30 next day
|
||||
expect(result).toContain("05:30");
|
||||
expect(result).toContain("2"); // December 2nd
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
type DateTimeStyle = "full" | "long" | "medium" | "short";
|
||||
|
||||
interface DateTimeFormatOptions {
|
||||
locale: string;
|
||||
timeZone?: string;
|
||||
dateStyle?: DateTimeStyle;
|
||||
timeStyle?: DateTimeStyle;
|
||||
month?: "numeric" | "2-digit" | "long" | "short" | "narrow";
|
||||
hour12?: boolean;
|
||||
}
|
||||
|
||||
const MAX_FORMATTER_CACHE = 1000;
|
||||
const MAX_WEEKDAY_CACHE = 500;
|
||||
|
||||
const WEEKDAY_FORMATS = { short: "ddd", long: "dddd" } as const;
|
||||
const MONTH_FORMATS = { long: "MMMM", short: "MMM", numeric: "M", "2-digit": "MM", narrow: "MMM" } as const;
|
||||
const DATE_FORMATS = {
|
||||
full: "dddd, MMMM D, YYYY",
|
||||
long: "MMMM D, YYYY",
|
||||
medium: "MMM D, YYYY",
|
||||
short: "M/D/YY",
|
||||
} as const;
|
||||
|
||||
const localeSupport = new Map<string, boolean>();
|
||||
const formatterCache = new Map<string, Intl.DateTimeFormat>();
|
||||
const weekdayCache = new Map<string, string[]>();
|
||||
|
||||
function isSupported(locale: string): boolean {
|
||||
let supported = localeSupport.get(locale);
|
||||
if (supported === undefined) {
|
||||
supported = Intl.DateTimeFormat.supportedLocalesOf([locale]).length > 0;
|
||||
localeSupport.set(locale, supported);
|
||||
}
|
||||
return supported;
|
||||
}
|
||||
|
||||
function getFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
|
||||
const key = `${locale}|${JSON.stringify(options)}`;
|
||||
let formatter = formatterCache.get(key);
|
||||
|
||||
if (!formatter) {
|
||||
if (formatterCache.size >= MAX_FORMATTER_CACHE) {
|
||||
formatterCache.clear();
|
||||
}
|
||||
formatter = new Intl.DateTimeFormat(locale, options);
|
||||
formatterCache.set(key, formatter);
|
||||
}
|
||||
|
||||
return formatter;
|
||||
}
|
||||
|
||||
function buildIntlOptions(options: DateTimeFormatOptions, includeMonth = true): Intl.DateTimeFormatOptions {
|
||||
const { timeZone, dateStyle, timeStyle, month, hour12 } = options;
|
||||
return {
|
||||
...(timeZone && { timeZone }),
|
||||
...(dateStyle && { dateStyle }),
|
||||
...(timeStyle && { timeStyle }),
|
||||
...(includeMonth && month && { month }),
|
||||
...(hour12 !== undefined && { hour12 }),
|
||||
};
|
||||
}
|
||||
|
||||
function formatWithDayjs(date: Date, options: DateTimeFormatOptions): string {
|
||||
const { locale, dateStyle, timeStyle, month, hour12, timeZone } = options;
|
||||
const dayjsDate = timeZone ? dayjs(date).tz(timeZone).locale(locale) : dayjs(date).locale(locale);
|
||||
|
||||
if (month) {
|
||||
const formatted = dayjsDate.format(MONTH_FORMATS[month] || "MMM");
|
||||
return month === "narrow" ? formatted.charAt(0) : formatted;
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (dateStyle) parts.push(DATE_FORMATS[dateStyle]);
|
||||
if (timeStyle) parts.push(hour12 ? "h:mm A" : "HH:mm");
|
||||
|
||||
return dayjsDate.format(parts.join(" ") || "YYYY-MM-DD HH:mm");
|
||||
}
|
||||
|
||||
function getDateForWeekday(day: number): Date {
|
||||
const today = new Date();
|
||||
const diff = day - today.getDay();
|
||||
const target = new Date(today);
|
||||
target.setDate(today.getDate() + diff);
|
||||
return target;
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date, options: DateTimeFormatOptions): string {
|
||||
if (isSupported(options.locale)) {
|
||||
return getFormatter(options.locale, buildIntlOptions(options)).format(date);
|
||||
}
|
||||
return formatWithDayjs(date, options);
|
||||
}
|
||||
|
||||
export function formatDateTimeRange(startDate: Date, endDate: Date, options: DateTimeFormatOptions): string {
|
||||
if (isSupported(options.locale)) {
|
||||
return getFormatter(options.locale, buildIntlOptions(options, false)).formatRange(startDate, endDate);
|
||||
}
|
||||
return `${formatWithDayjs(startDate, options)} — ${formatWithDayjs(endDate, options)}`;
|
||||
}
|
||||
|
||||
export function formatWeekday(locale: string, day: number, format: "short" | "long"): string {
|
||||
if (isSupported(locale)) {
|
||||
return getFormatter(locale, { weekday: format }).format(getDateForWeekday(day));
|
||||
}
|
||||
return dayjs().day(day).locale(locale).format(WEEKDAY_FORMATS[format]);
|
||||
}
|
||||
|
||||
export function getWeekdayNames(locale: string, weekStart = 0, format: "short" | "long" = "long"): string[] {
|
||||
const cacheKey = `${locale}|${weekStart}|${format}`;
|
||||
let names = weekdayCache.get(cacheKey);
|
||||
|
||||
if (!names) {
|
||||
if (weekdayCache.size >= MAX_WEEKDAY_CACHE) {
|
||||
weekdayCache.clear();
|
||||
}
|
||||
|
||||
if (isSupported(locale)) {
|
||||
const formatter = getFormatter(locale, { weekday: format });
|
||||
names = Array.from({ length: 7 }, (_, i) => formatter.format(getDateForWeekday((i + weekStart) % 7)));
|
||||
} else {
|
||||
names = Array.from({ length: 7 }, (_, i) =>
|
||||
dayjs()
|
||||
.day((i + weekStart) % 7)
|
||||
.locale(locale)
|
||||
.format(WEEKDAY_FORMATS[format])
|
||||
);
|
||||
}
|
||||
weekdayCache.set(cacheKey, names);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import "@calcom/dayjs/locales";
|
||||
|
||||
import { nameOfDay, weekdayNames } from "./weekday";
|
||||
|
||||
describe("Weekday tests", () => {
|
||||
@@ -62,5 +64,11 @@ describe("Weekday tests", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with Icelandic, Lithuanian, and Norwegian locales", () => {
|
||||
expect(nameOfDay("is", 1, "long")).toMatch(/mánudagur/i); // Monday in Icelandic
|
||||
expect(nameOfDay("lt", 1, "long")).toMatch(/pirmadienis/i); // Monday in Lithuanian
|
||||
expect(nameOfDay("nb", 1, "long")).toMatch(/mandag/i); // Monday in Norwegian
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getWeekdayNames, formatWeekday } from "./dateTimeFormatter";
|
||||
|
||||
type WeekdayFormat = "short" | "long";
|
||||
|
||||
// By default starts on Sunday (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
|
||||
export function weekdayNames(locale: string | string[], weekStart = 0, format: WeekdayFormat = "long") {
|
||||
return Array(7)
|
||||
.fill(null)
|
||||
.map((_, day) => nameOfDay(locale, day + weekStart, format));
|
||||
const normalizedLocale = Array.isArray(locale) ? locale[0] : locale || "en";
|
||||
return getWeekdayNames(normalizedLocale, weekStart, format);
|
||||
}
|
||||
|
||||
export function nameOfDay(
|
||||
@@ -12,5 +12,6 @@ export function nameOfDay(
|
||||
day: number,
|
||||
format: WeekdayFormat = "long"
|
||||
) {
|
||||
return new Intl.DateTimeFormat(locale, { weekday: format }).format(new Date(1970, 0, day + 4));
|
||||
const normalizedLocale = Array.isArray(locale) ? locale[0] : locale || "en";
|
||||
return formatWeekday(normalizedLocale, day, format);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user