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}}