From 09cc6f07e428dc29d0b55167a7a71b27e735beee Mon Sep 17 00:00:00 2001 From: Aaron Presley <155617+AaronPresley@users.noreply.github.com> Date: Thu, 16 Feb 2023 09:16:22 -0800 Subject: [PATCH] Booking page now displays localized dates, times, and timezones (#7130) --- apps/web/pages/booking/[uid].tsx | 39 ++++++++++++++-------- packages/lib/date-fns/index.ts | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index 4c0f2cfa45..f88475b53c 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -28,7 +28,11 @@ import { import { parseRecurringEvent } from "@calcom/lib"; import CustomBranding from "@calcom/lib/CustomBranding"; import { APP_NAME } from "@calcom/lib/constants"; -import { formatTime } from "@calcom/lib/date-fns"; +import { + formatToLocalizedDate, + formatToLocalizedTime, + formatToLocalizedTimezone, +} from "@calcom/lib/date-fns"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; @@ -820,7 +824,10 @@ export function RecurringBookings({ isCancelled, }: RecurringBookingsProps) { const [moreEventsVisible, setMoreEventsVisible] = useState(false); - const { t } = useLocale(); + const { + t, + i18n: { language }, + } = useLocale(); const recurringBookingsSorted = recurringBookings ? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1)) @@ -843,11 +850,13 @@ export function RecurringBookings({ {eventType.recurringEvent?.count && recurringBookingsSorted.slice(0, 4).map((dateStr: string, idx: number) => (
- {dayjs.tz(dateStr, timeZone()).format("dddd, DD MMMM YYYY")} + {formatToLocalizedDate(dayjs.tz(dateStr, timeZone()), language, "full")}
- {formatTime(dateStr, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "} - {formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "} - ({timeZone()}) + {formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h)} -{" "} + {formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h)}{" "} + + ({formatToLocalizedTimezone(dayjs(dateStr), language)}) +
))} {recurringBookingsSorted.length > 4 && ( @@ -861,11 +870,13 @@ export function RecurringBookings({ {eventType.recurringEvent?.count && recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
- {dayjs.tz(dateStr, timeZone()).format("dddd, DD MMMM YYYY")} + {formatToLocalizedDate(dayjs.tz(date, timeZone()), language, "full")}
- {formatTime(dateStr, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "} - {formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "} - ({timeZone()}) + {formatToLocalizedTime(date, language, undefined, !is24h)} -{" "} + {formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "} + + ({formatToLocalizedTimezone(dayjs(dateStr), language)}) +
))} @@ -877,11 +888,11 @@ export function RecurringBookings({ return (
- {dayjs.tz(date, timeZone()).format("dddd, DD MMMM YYYY")} + {formatToLocalizedDate(dayjs.tz(date, timeZone()), language, "full")}
- {formatTime(date, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "} - {formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "} - ({timeZone()}) + {formatToLocalizedTime(date, language, undefined, !is24h)} -{" "} + {formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "} + ({formatToLocalizedTimezone(date, language)})
); } diff --git a/packages/lib/date-fns/index.ts b/packages/lib/date-fns/index.ts index f816377583..d1bab2bf7a 100644 --- a/packages/lib/date-fns/index.ts +++ b/packages/lib/date-fns/index.ts @@ -29,6 +29,61 @@ export const formatTime = ( : dayjs(date).format(timeFormat === 12 ? "h:mma" : "HH:mm"); }; +/** + * Returns a localized and translated date or time, based on the native + * Intl.DateTimeFormat available to JS. Undefined values mean the browser's + * locale will be used. + */ +export const formatLocalizedDateTime = ( + date: Date | Dayjs, + options: Intl.DateTimeFormatOptions = {}, + locale: string | undefined = undefined +) => { + const theDate = date instanceof dayjs ? (date as Dayjs).toDate() : (date as Date); + return Intl.DateTimeFormat(locale, options).format(theDate); +}; + +/** + * Returns a localized and translated calendar day based on the + * given Date object and locale. Undefined values mean the defaults + * associated with the browser's current locale will be used. + */ +export const formatToLocalizedDate = ( + date: Date | Dayjs, + locale: string | undefined = undefined, + dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "long" +) => formatLocalizedDateTime(date, { dateStyle }, locale); + +/** + * Returns a localized and translated time of day based on the + * given Date object and locale. Undefined values mean the defaults + * associated with the browser's current locale will be used. + */ +export const formatToLocalizedTime = ( + date: Date | Dayjs, + locale: string | undefined = undefined, + timeStyle: Intl.DateTimeFormatOptions["timeStyle"] = "short", + hour12: Intl.DateTimeFormatOptions["hour12"] = undefined +) => formatLocalizedDateTime(date, { timeStyle, hour12 }, locale); + +/** + * Returns a translated timezone based on the given Date object and + * locale. Undefined values mean the browser's current locale + * will be used. + */ +export const formatToLocalizedTimezone = ( + date: Date | Dayjs, + locale: string | undefined = undefined, + timeZoneName: Intl.DateTimeFormatOptions["timeZoneName"] = "long" +) => { + // Intl.DateTimeFormat doesn't format into a timezone only, so we must + // formatToParts() and return the piece we want + const theDate = date instanceof dayjs ? (date as Dayjs).toDate() : (date as Date); + return Intl.DateTimeFormat(locale, { timeZoneName }) + .formatToParts(theDate) + .find((d) => d.type == "timeZoneName")?.value; +}; + /** * Sorts two timezones by their offset from GMT. */