From 6e697c3b07bbc8e7ec3be7e79a25a28d2b316358 Mon Sep 17 00:00:00 2001 From: Ignace Maes <10243652+IgnaceMaes@users.noreply.github.com> Date: Mon, 25 May 2026 15:38:47 +0200 Subject: [PATCH 1/4] feat: locale support --- .../docs/examples/calendar-locale.gts | 27 +++++++ apps/v4/app/components/docs/examples/index.ts | 1 + apps/v4/registry/new-york-v4/ui/calendar.gts | 70 +++++++++++++++---- 3 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 apps/v4/app/components/docs/examples/calendar-locale.gts diff --git a/apps/v4/app/components/docs/examples/calendar-locale.gts b/apps/v4/app/components/docs/examples/calendar-locale.gts new file mode 100644 index 0000000..97ffb26 --- /dev/null +++ b/apps/v4/app/components/docs/examples/calendar-locale.gts @@ -0,0 +1,27 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { nl } from 'date-fns/locale'; + +import { Calendar } from '@/components/ui/calendar'; + +import type { DateRange } from '@/components/ui/calendar'; + +export default class CalendarLocale extends Component { + locale = nl; + + @tracked date: Date | undefined = new Date(); + + handleSelect = (date: Date | DateRange | undefined) => { + this.date = date as Date | undefined; + }; + + +} diff --git a/apps/v4/app/components/docs/examples/index.ts b/apps/v4/app/components/docs/examples/index.ts index d4f609d..095b5b6 100644 --- a/apps/v4/app/components/docs/examples/index.ts +++ b/apps/v4/app/components/docs/examples/index.ts @@ -39,6 +39,7 @@ export { default as CalendarBookedDates } from './calendar-booked-dates'; export { default as CalendarCaption } from './calendar-caption'; export { default as CalendarCustomDays } from './calendar-custom-days'; export { default as CalendarDemo } from './calendar-demo'; +export { default as CalendarLocale } from './calendar-locale'; export { default as CalendarPresets } from './calendar-presets'; export { default as CalendarRange } from './calendar-range'; export { default as CalendarTime } from './calendar-time'; diff --git a/apps/v4/registry/new-york-v4/ui/calendar.gts b/apps/v4/registry/new-york-v4/ui/calendar.gts index 3bc5d6a..1be13a4 100644 --- a/apps/v4/registry/new-york-v4/ui/calendar.gts +++ b/apps/v4/registry/new-york-v4/ui/calendar.gts @@ -22,6 +22,7 @@ import { buttonVariants } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import type Owner from '@ember/owner'; +import type { Locale } from 'date-fns/locale'; import ChevronDown from '~icons/lucide/chevron-down'; import ChevronLeft from '~icons/lucide/chevron-left'; @@ -51,6 +52,8 @@ interface CalendarSignature { fixedWeeks?: boolean; class?: string; buttonVariant?: 'default' | 'ghost' | 'outline'; + locale?: Locale; + weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; timeZone?: string; startMonth?: Date; endMonth?: Date; @@ -88,8 +91,8 @@ interface MonthData { weeks: WeekInfo[]; } -const WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; -const MONTH_NAMES_SHORT = [ +const DEFAULT_WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; +const DEFAULT_MONTH_NAMES_SHORT = [ 'Jan', 'Feb', 'Mar', @@ -122,8 +125,11 @@ function selectedSingle(day: DayInfo): boolean { ); } -function monthNameShort(index: number): string { - return MONTH_NAMES_SHORT[index] ?? ''; +function monthNameShort( + index: number, + monthNames: string[], +): string { + return monthNames[index] ?? ''; } class Calendar extends Component { @@ -153,6 +159,44 @@ class Calendar extends Component { return this.args.buttonVariant ?? 'ghost'; } + get weekStartsOn(): 0 | 1 | 2 | 3 | 4 | 5 | 6 { + return ( + this.args.weekStartsOn ?? + this.args.locale?.options?.weekStartsOn ?? + 0 + ); + } + + get weekOptions(): { weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6 } { + return { weekStartsOn: this.weekStartsOn }; + } + + get weekdayLabels(): string[] { + if (this.args.locale) { + const refDate = startOfWeek(new Date(), this.weekOptions); + return Array.from({ length: 7 }, (_, i) => + format(addDays(refDate, i), 'EEEEEE', { + locale: this.args.locale, + }), + ); + } + const wso = this.weekStartsOn; + if (wso === 0) return DEFAULT_WEEKDAY_LABELS; + return [ + ...DEFAULT_WEEKDAY_LABELS.slice(wso), + ...DEFAULT_WEEKDAY_LABELS.slice(0, wso), + ]; + } + + get monthNamesShort(): string[] { + if (this.args.locale) { + return Array.from({ length: 12 }, (_, i) => + format(new Date(2024, i, 1), 'MMM', { locale: this.args.locale }), + ); + } + return DEFAULT_MONTH_NAMES_SHORT; + } + get displayMonth(): Date { if (this.args.month) { return startOfMonth(this.args.month); @@ -190,7 +234,7 @@ class Calendar extends Component { } get monthOptions(): { value: number; label: string }[] { - return MONTH_NAMES_SHORT.map((label, i) => ({ value: i, label })); + return this.monthNamesShort.map((label, i) => ({ value: i, label })); } get months(): MonthData[] { @@ -217,8 +261,8 @@ class Calendar extends Component { buildMonthData(monthDate: Date): MonthData { const monthStart = startOfMonth(monthDate); const monthEnd = endOfMonth(monthDate); - const calendarStart = startOfWeek(monthStart); - const calendarEnd = endOfWeek(monthEnd); + const calendarStart = startOfWeek(monthStart, this.weekOptions); + const calendarEnd = endOfWeek(monthEnd, this.weekOptions); const allDays = eachDayOfInterval({ start: calendarStart, @@ -262,7 +306,7 @@ class Calendar extends Component { }); weeks.push({ - weekNumber: getWeek(weekDays[0]!), + weekNumber: getWeek(weekDays[0]!, this.weekOptions), days, }); } @@ -270,8 +314,10 @@ class Calendar extends Component { return { month: monthDate.getMonth(), year: monthDate.getFullYear(), - label: format(monthDate, 'MMMM yyyy'), - labelShort: format(monthDate, 'MMM'), + label: format(monthDate, 'MMMM yyyy', { + locale: this.args.locale, + }), + labelShort: format(monthDate, 'MMM', { locale: this.args.locale }), weeks, }; } @@ -471,7 +517,7 @@ class Calendar extends Component { - {{monthNameShort monthData.month}} + {{monthNameShort monthData.month this.monthNamesShort}}