diff --git a/apps/web/modules/bookings/components/Header.tsx b/apps/web/modules/bookings/components/Header.tsx
index 2de49bbe99..166c417900 100644
--- a/apps/web/modules/bookings/components/Header.tsx
+++ b/apps/web/modules/bookings/components/Header.tsx
@@ -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 (
- {formattedMonth.format(selectedDate.toDate())} {selectedDate.format("D")}
+ {formatMonthLabel(selectedDate.toDate())} {selectedDate.format("D")}
{!isSameYear() && , {selectedDate.format("YYYY")} }-{" "}
- {!isSameMonth() && formattedMonth.format(endDate.toDate())} {endDate.format("D")},{" "}
+ {!isSameMonth() && formatMonthLabel(endDate.toDate())} {endDate.format("D")},{" "}
{isSameYear() ? selectedDate.format("YYYY") : endDate.format("YYYY")}
diff --git a/packages/dayjs/locales.ts b/packages/dayjs/locales.ts
index 7310aadd30..f7ee7fb840 100644
--- a/packages/dayjs/locales.ts
+++ b/packages/dayjs/locales.ts
@@ -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";
diff --git a/packages/features/bookings/Booker/utils/dates.tsx b/packages/features/bookings/Booker/utils/dates.tsx
index 6d123085f7..d7d7b9e666 100644
--- a/packages/features/bookings/Booker/utils/dates.tsx
+++ b/packages/features/bookings/Booker/utils/dates.tsx
@@ -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,
diff --git a/packages/features/tasker/tasks/translateEventTypeData.ts b/packages/features/tasker/tasks/translateEventTypeData.ts
index 20f131537b..bbcc058cae 100644
--- a/packages/features/tasker/tasks/translateEventTypeData.ts
+++ b/packages/features/tasker/tasks/translateEventTypeData.ts
@@ -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({
diff --git a/packages/lib/dateTimeFormatter.test.ts b/packages/lib/dateTimeFormatter.test.ts
new file mode 100644
index 0000000000..ed2f69765d
--- /dev/null
+++ b/packages/lib/dateTimeFormatter.test.ts
@@ -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
+ });
+ });
+});
diff --git a/packages/lib/dateTimeFormatter.ts b/packages/lib/dateTimeFormatter.ts
new file mode 100644
index 0000000000..be22596e02
--- /dev/null
+++ b/packages/lib/dateTimeFormatter.ts
@@ -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();
+const formatterCache = new Map();
+const weekdayCache = new Map();
+
+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;
+}
diff --git a/packages/lib/weekday.test.ts b/packages/lib/weekday.test.ts
index 076a618562..e1e3ee6730 100644
--- a/packages/lib/weekday.test.ts
+++ b/packages/lib/weekday.test.ts
@@ -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
+ });
});
});
diff --git a/packages/lib/weekday.ts b/packages/lib/weekday.ts
index 517e8253d2..e2c27ff9ec 100644
--- a/packages/lib/weekday.ts
+++ b/packages/lib/weekday.ts
@@ -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);
}