sh-ui-cli 0.52.3 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,8 +12,8 @@ import "./styles.css";
12
12
 
13
13
  /* ───────── Helpers ───────── */
14
14
 
15
-
16
- const DEFAULT_WEEKDAYS_KO = ["", "월", "화", "수", "목", "금", "토"] as const;
15
+ /** 미지정 시의 기본 로케일. 기존 동작(한국어) 보존. */
16
+ export const DEFAULT_LOCALE = "ko-KR";
17
17
 
18
18
  const isSameDay = (a: Date, b: Date) =>
19
19
  a.getFullYear() === b.getFullYear() &&
@@ -32,8 +32,68 @@ const addMonths = (d: Date, n: number) =>
32
32
  const formatIsoDate = (d: Date) =>
33
33
  `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
34
34
 
35
- const defaultMonthLabel = (year: number, month: number) =>
36
- `${year}년 ${month + 1}월`;
35
+ /** Intl 기반 short weekday 라벨, Sunday-first 7개. */
36
+ function deriveWeekdayLabels(locale: string): string[] {
37
+ const fmt = new Intl.DateTimeFormat(locale, { weekday: "short" });
38
+ // 2017-01-01 은 일요일 — 안전한 기준점.
39
+ return Array.from({ length: 7 }, (_, i) =>
40
+ fmt.format(new Date(2017, 0, 1 + i)),
41
+ );
42
+ }
43
+
44
+ function deriveMonthLabel(locale: string) {
45
+ const fmt = new Intl.DateTimeFormat(locale, { year: "numeric", month: "long" });
46
+ return (year: number, month: number) => fmt.format(new Date(year, month, 1));
47
+ }
48
+
49
+ function deriveYearLabel(locale: string) {
50
+ const fmt = new Intl.DateTimeFormat(locale, { year: "numeric" });
51
+ return (y: number) => fmt.format(new Date(y, 0, 1));
52
+ }
53
+
54
+ function deriveMonthSelectLabel(locale: string) {
55
+ const fmt = new Intl.DateTimeFormat(locale, { month: "long" });
56
+ return (m: number) => fmt.format(new Date(2017, m, 1));
57
+ }
58
+
59
+ /** Calendar 의 비-날짜 텍스트(aria-label 등). 일부만 override 가능. */
60
+ export interface CalendarMessages {
61
+ /** 1년 이전 버튼 aria-label. */
62
+ prevYear?: string;
63
+ /** 1년 다음 버튼 aria-label. */
64
+ nextYear?: string;
65
+ /** 1개월 이전 버튼 aria-label. */
66
+ prevMonth?: string;
67
+ /** 1개월 다음 버튼 aria-label. */
68
+ nextMonth?: string;
69
+ /** 연도 select aria-label. */
70
+ yearSelectLabel?: string;
71
+ /** 월 select aria-label. */
72
+ monthSelectLabel?: string;
73
+ }
74
+
75
+ const MESSAGES_KO: Required<CalendarMessages> = {
76
+ prevYear: "이전 해",
77
+ nextYear: "다음 해",
78
+ prevMonth: "이전 달",
79
+ nextMonth: "다음 달",
80
+ yearSelectLabel: "연도",
81
+ monthSelectLabel: "월",
82
+ };
83
+
84
+ const MESSAGES_EN: Required<CalendarMessages> = {
85
+ prevYear: "Previous year",
86
+ nextYear: "Next year",
87
+ prevMonth: "Previous month",
88
+ nextMonth: "Next month",
89
+ yearSelectLabel: "Year",
90
+ monthSelectLabel: "Month",
91
+ };
92
+
93
+ function defaultMessagesFor(locale: string): Required<CalendarMessages> {
94
+ const lang = locale.toLowerCase().split(/[-_]/)[0];
95
+ return lang === "ko" ? MESSAGES_KO : MESSAGES_EN;
96
+ }
37
97
 
38
98
  function getDaysGrid(year: number, month: number, weekStartsOn: 0 | 1) {
39
99
  const first = new Date(year, month, 1);
@@ -94,10 +154,22 @@ interface CalendarCommonProps {
94
154
  showOutsideDays?: boolean;
95
155
  /** 주의 시작 요일. 0=일, 1=월. @default 0 */
96
156
  weekStartsOn?: 0 | 1;
97
- /** 요일 라벨 (Sunday-first 7개). */
157
+ /** 요일 라벨 (Sunday-first 7개). 미지정 시 `locale` 기반 자동 생성. */
98
158
  weekdayLabels?: readonly string[];
99
- /** 월 헤더 그룹의 aria-label 포맷. @default "{year}년 {month+1}월" */
159
+ /** 월 헤더 그룹의 aria-label 포맷. 미지정 `locale` 기반 자동 생성. */
100
160
  formatMonthLabel?: (year: number, month: number) => string;
161
+ /**
162
+ * BCP47 로케일 (예: `"en-US"`, `"ja-JP"`, `"ko-KR"`). 요일·월·연도 라벨이 `Intl.DateTimeFormat` 으로
163
+ * 자동 생성된다. 개별 포맷터(`weekdayLabels`/`formatMonthLabel`/`CalendarYearSelect.formatYear` 등) 가
164
+ * 우선되고, 미지정 항목만 이 로케일에서 파생.
165
+ * @default "ko-KR"
166
+ */
167
+ locale?: string;
168
+ /**
169
+ * Nav 버튼/select 의 aria-label 등 비-날짜 텍스트 override. 미지정 시 `locale` 의 언어 코드(`ko`/`en`)
170
+ * 기반 기본값. 다른 언어를 쓸 땐 해당 언어 문자열을 직접 넘긴다.
171
+ */
172
+ messages?: CalendarMessages;
101
173
  /** 연도 dropdown 시작 연도. */
102
174
  fromYear?: number;
103
175
  /** 연도 dropdown 끝 연도. */
@@ -195,6 +267,11 @@ interface CalendarContextValue {
195
267
  weekdayLabels: string[];
196
268
  showOutsideDays: boolean;
197
269
  formatMonthLabel: (year: number, month: number) => string;
270
+ /** YearSelect 가 자체 `formatYear` 를 받지 않을 때 사용할 로케일 기본값. */
271
+ defaultFormatYear: (y: number) => string;
272
+ /** MonthSelect 가 자체 `formatMonth` 를 받지 않을 때 사용할 로케일 기본값. */
273
+ defaultFormatMonth: (m: number) => string;
274
+ messages: Required<CalendarMessages>;
198
275
  ariaLabel?: string;
199
276
 
200
277
  isSelected: (date: Date) => boolean;
@@ -240,7 +317,9 @@ export function Calendar(props: CalendarProps) {
240
317
  showOutsideDays = true,
241
318
  weekStartsOn = 0,
242
319
  weekdayLabels: weekdayLabelsProp,
243
- formatMonthLabel = defaultMonthLabel,
320
+ formatMonthLabel: formatMonthLabelProp,
321
+ locale = DEFAULT_LOCALE,
322
+ messages: messagesProp,
244
323
  fromYear,
245
324
  toYear,
246
325
  className,
@@ -304,11 +383,26 @@ export function Calendar(props: CalendarProps) {
304
383
  return new Date();
305
384
  });
306
385
 
386
+ /* locale-derived defaults */
387
+ const localeWeekdays = React.useMemo(() => deriveWeekdayLabels(locale), [locale]);
388
+ const localeMonthLabel = React.useMemo(() => deriveMonthLabel(locale), [locale]);
389
+ const localeYearLabel = React.useMemo(() => deriveYearLabel(locale), [locale]);
390
+ const localeMonthSelectLabel = React.useMemo(
391
+ () => deriveMonthSelectLabel(locale),
392
+ [locale],
393
+ );
394
+ const resolvedMessages = React.useMemo<Required<CalendarMessages>>(
395
+ () => ({ ...defaultMessagesFor(locale), ...messagesProp }),
396
+ [locale, messagesProp],
397
+ );
398
+
307
399
  /* weekday labels */
308
400
  const weekdayLabels = React.useMemo(() => {
309
- const base = weekdayLabelsProp ?? DEFAULT_WEEKDAYS_KO;
401
+ const base = weekdayLabelsProp ?? localeWeekdays;
310
402
  return rotateWeekdays(base, weekStartsOn);
311
- }, [weekdayLabelsProp, weekStartsOn]);
403
+ }, [weekdayLabelsProp, localeWeekdays, weekStartsOn]);
404
+
405
+ const formatMonthLabel = formatMonthLabelProp ?? localeMonthLabel;
312
406
 
313
407
  /* year options */
314
408
  const nowYear = new Date().getFullYear();
@@ -490,6 +584,9 @@ export function Calendar(props: CalendarProps) {
490
584
  weekdayLabels,
491
585
  showOutsideDays,
492
586
  formatMonthLabel,
587
+ defaultFormatYear: localeYearLabel,
588
+ defaultFormatMonth: localeMonthSelectLabel,
589
+ messages: resolvedMessages,
493
590
  ariaLabel,
494
591
  isSelected: isDateSelected,
495
592
  isInRange: getRangeState,
@@ -562,7 +659,7 @@ export interface CalendarNavButtonProps
562
659
  function makeNavButton(
563
660
  displayName: string,
564
661
  defaultIcon: React.ReactNode,
565
- defaultLabel: string,
662
+ resolveDefaultLabel: (ctx: CalendarContextValue) => string,
566
663
  resolveHandler: (ctx: CalendarContextValue) => () => void,
567
664
  ) {
568
665
  const Component = React.forwardRef<HTMLButtonElement, CalendarNavButtonProps>(
@@ -573,7 +670,7 @@ function makeNavButton(
573
670
  ref={ref}
574
671
  type="button"
575
672
  className={cn("sh-ui-calendar__nav", className)}
576
- aria-label={ariaLabel ?? defaultLabel}
673
+ aria-label={ariaLabel ?? resolveDefaultLabel(ctx)}
577
674
  onClick={(e) => {
578
675
  resolveHandler(ctx)();
579
676
  onClick?.(e);
@@ -593,7 +690,7 @@ function makeNavButton(
593
690
  export const CalendarPrevYearButton = makeNavButton(
594
691
  "CalendarPrevYearButton",
595
692
  <ChevronDoubleLeftIcon />,
596
- "이전 해",
693
+ (ctx) => ctx.messages.prevYear,
597
694
  (ctx) => ctx.prevYear,
598
695
  );
599
696
 
@@ -601,7 +698,7 @@ export const CalendarPrevYearButton = makeNavButton(
601
698
  export const CalendarNextYearButton = makeNavButton(
602
699
  "CalendarNextYearButton",
603
700
  <ChevronDoubleRightIcon />,
604
- "다음 해",
701
+ (ctx) => ctx.messages.nextYear,
605
702
  (ctx) => ctx.nextYear,
606
703
  );
607
704
 
@@ -609,7 +706,7 @@ export const CalendarNextYearButton = makeNavButton(
609
706
  export const CalendarPrevMonthButton = makeNavButton(
610
707
  "CalendarPrevMonthButton",
611
708
  <ChevronLeftIcon />,
612
- "이전 달",
709
+ (ctx) => ctx.messages.prevMonth,
613
710
  (ctx) => ctx.prevMonth,
614
711
  );
615
712
 
@@ -617,7 +714,7 @@ export const CalendarPrevMonthButton = makeNavButton(
617
714
  export const CalendarNextMonthButton = makeNavButton(
618
715
  "CalendarNextMonthButton",
619
716
  <ChevronRightIcon />,
620
- "다음 달",
717
+ (ctx) => ctx.messages.nextMonth,
621
718
  (ctx) => ctx.nextMonth,
622
719
  );
623
720
 
@@ -626,16 +723,17 @@ export const CalendarNextMonthButton = makeNavButton(
626
723
  export interface CalendarYearSelectProps {
627
724
  /** select trigger 의 className. */
628
725
  className?: string;
629
- /** label 표시 포맷. @default "{year}년" */
726
+ /** label 표시 포맷. 미지정 Calendar `locale` 기반 자동 생성. */
630
727
  formatYear?: (year: number) => string;
631
728
  }
632
729
 
633
730
  /** 연도 dropdown. sh-ui Select 로 구현. */
634
731
  export function CalendarYearSelect({
635
732
  className,
636
- formatYear = (y) => `${y}년`,
733
+ formatYear,
637
734
  }: CalendarYearSelectProps) {
638
735
  const ctx = useCalendarContext("CalendarYearSelect");
736
+ const resolvedFormat = formatYear ?? ctx.defaultFormatYear;
639
737
  const year = ctx.visibleMonth.getFullYear();
640
738
  const items = ctx.yearOptions.includes(year)
641
739
  ? ctx.yearOptions
@@ -648,14 +746,14 @@ export function CalendarYearSelect({
648
746
  >
649
747
  <SelectTrigger
650
748
  className={cn("sh-ui-calendar__select-trigger", className)}
651
- aria-label="연도"
749
+ aria-label={ctx.messages.yearSelectLabel}
652
750
  >
653
- <span className="sh-ui-calendar__select-value">{formatYear(year)}</span>
751
+ <span className="sh-ui-calendar__select-value">{resolvedFormat(year)}</span>
654
752
  </SelectTrigger>
655
753
  <SelectContent className="sh-ui-calendar__select-popup">
656
754
  {items.map((y) => (
657
755
  <SelectItem key={y} value={String(y)}>
658
- {formatYear(y)}
756
+ {resolvedFormat(y)}
659
757
  </SelectItem>
660
758
  ))}
661
759
  </SelectContent>
@@ -665,16 +763,17 @@ export function CalendarYearSelect({
665
763
 
666
764
  export interface CalendarMonthSelectProps {
667
765
  className?: string;
668
- /** label 표시 포맷. @default "{month+1}월" */
766
+ /** label 표시 포맷. 미지정 Calendar `locale` 기반 자동 생성. */
669
767
  formatMonth?: (month: number) => string;
670
768
  }
671
769
 
672
770
  /** 월 dropdown. */
673
771
  export function CalendarMonthSelect({
674
772
  className,
675
- formatMonth = (m) => `${m + 1}월`,
773
+ formatMonth,
676
774
  }: CalendarMonthSelectProps) {
677
775
  const ctx = useCalendarContext("CalendarMonthSelect");
776
+ const resolvedFormat = formatMonth ?? ctx.defaultFormatMonth;
678
777
  const month = ctx.visibleMonth.getMonth();
679
778
  return (
680
779
  <Select
@@ -683,14 +782,14 @@ export function CalendarMonthSelect({
683
782
  >
684
783
  <SelectTrigger
685
784
  className={cn("sh-ui-calendar__select-trigger", className)}
686
- aria-label="월"
785
+ aria-label={ctx.messages.monthSelectLabel}
687
786
  >
688
- <span className="sh-ui-calendar__select-value">{formatMonth(month)}</span>
787
+ <span className="sh-ui-calendar__select-value">{resolvedFormat(month)}</span>
689
788
  </SelectTrigger>
690
789
  <SelectContent className="sh-ui-calendar__select-popup">
691
790
  {Array.from({ length: 12 }, (_, m) => (
692
791
  <SelectItem key={m} value={String(m)}>
693
- {formatMonth(m)}
792
+ {resolvedFormat(m)}
694
793
  </SelectItem>
695
794
  ))}
696
795
  </SelectContent>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { Popover as BasePopover } from "@base-ui/react/popover";
5
- import { Calendar, type DateRange } from "../calendar";
5
+ import { Calendar, DEFAULT_LOCALE, type CalendarMessages, type DateRange } from "../calendar";
6
6
  import styles from "./styles.module.css";
7
7
 
8
8
  import { cn } from "@SH_UI_UTILS@";
@@ -17,6 +17,15 @@ const formatDefault = (d: Date) =>
17
17
  const startOfMonth = (d: Date) =>
18
18
  new Date(d.getFullYear(), d.getMonth(), 1);
19
19
 
20
+ function defaultDatePlaceholder(locale: string): string {
21
+ const lang = locale.toLowerCase().split(/[-_]/)[0];
22
+ return lang === "ko" ? "날짜 선택" : "Select date";
23
+ }
24
+ function defaultRangePlaceholder(locale: string): string {
25
+ const lang = locale.toLowerCase().split(/[-_]/)[0];
26
+ return lang === "ko" ? "시작일 ~ 종료일" : "Start date – end date";
27
+ }
28
+
20
29
  /* ───────── Icons ───────── */
21
30
 
22
31
  function CalendarIcon() {
@@ -39,6 +48,8 @@ interface DatePickerContextValue {
39
48
  setFocusedDate: (date: Date) => void;
40
49
  formatDate: (date: Date) => string;
41
50
  placeholder: string;
51
+ locale: string;
52
+ messages?: CalendarMessages;
42
53
  min?: Date;
43
54
  max?: Date;
44
55
  disabled?: boolean;
@@ -76,10 +87,13 @@ export interface DatePickerProps {
76
87
  /** 선택 가능 최대 날짜 (포함). 이후 날짜는 비활성. */
77
88
  max?: Date;
78
89
  /**
79
- * 미선택 상태의 트리거 텍스트.
80
- * @default "날짜 선택"
90
+ * 미선택 상태의 트리거 텍스트. 미지정 시 `locale` 기반 기본값.
81
91
  */
82
92
  placeholder?: string;
93
+ /** BCP47 로케일. 내부 Calendar 와 placeholder 에 모두 적용. @default "ko-KR" */
94
+ locale?: string;
95
+ /** 내부 Calendar 의 nav/select aria-label override. */
96
+ messages?: CalendarMessages;
83
97
  /** 비활성. 트리거 클릭·키보드 모두 차단. */
84
98
  disabled?: boolean;
85
99
  /** 읽기 전용. 트리거 표시는 유지하되 popover가 열리지 않는다. */
@@ -119,7 +133,9 @@ export function DatePicker({
119
133
  formatDate = formatDefault,
120
134
  min,
121
135
  max,
122
- placeholder = "날짜 선택",
136
+ placeholder,
137
+ locale = DEFAULT_LOCALE,
138
+ messages,
123
139
  disabled,
124
140
  readOnly,
125
141
  "aria-invalid": ariaInvalid,
@@ -128,6 +144,7 @@ export function DatePicker({
128
144
  container,
129
145
  children,
130
146
  }: DatePickerProps) {
147
+ const resolvedPlaceholder = placeholder ?? defaultDatePlaceholder(locale);
131
148
  const isControlled = value !== undefined;
132
149
  const [internal, setInternal] = React.useState<Date | undefined>(defaultValue);
133
150
  const selected = isControlled ? value : internal;
@@ -160,7 +177,9 @@ export function DatePicker({
160
177
  focusedDate,
161
178
  setFocusedDate,
162
179
  formatDate,
163
- placeholder,
180
+ placeholder: resolvedPlaceholder,
181
+ locale,
182
+ messages,
164
183
  min,
165
184
  max,
166
185
  disabled,
@@ -174,7 +193,9 @@ export function DatePicker({
174
193
  open,
175
194
  focusedDate,
176
195
  formatDate,
177
- placeholder,
196
+ resolvedPlaceholder,
197
+ locale,
198
+ messages,
178
199
  min,
179
200
  max,
180
201
  disabled,
@@ -355,6 +376,8 @@ export function DatePickerCalendar() {
355
376
  onMonthChange={ctx.setFocusedDate}
356
377
  min={ctx.min}
357
378
  max={ctx.max}
379
+ locale={ctx.locale}
380
+ messages={ctx.messages}
358
381
  />
359
382
  );
360
383
  }
@@ -407,10 +430,13 @@ export interface DateRangePickerProps {
407
430
  /** 선택 가능 최대 날짜. */
408
431
  max?: Date;
409
432
  /**
410
- * 미선택 상태의 트리거 텍스트.
411
- * @default "시작일 ~ 종료일"
433
+ * 미선택 상태의 트리거 텍스트. 미지정 시 `locale` 기반 기본값.
412
434
  */
413
435
  placeholder?: string;
436
+ /** BCP47 로케일. @default "ko-KR" */
437
+ locale?: string;
438
+ /** 내부 Calendar 의 nav/select aria-label override. */
439
+ messages?: CalendarMessages;
414
440
  /** 비활성. */
415
441
  disabled?: boolean;
416
442
  /** 읽기 전용. popover가 열리지 않는다. */
@@ -438,7 +464,9 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
438
464
  formatDate = formatDefault,
439
465
  min,
440
466
  max,
441
- placeholder = "시작일 ~ 종료일",
467
+ placeholder,
468
+ locale = DEFAULT_LOCALE,
469
+ messages,
442
470
  disabled,
443
471
  readOnly,
444
472
  "aria-invalid": ariaInvalid,
@@ -447,6 +475,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
447
475
  },
448
476
  ref,
449
477
  ) {
478
+ const resolvedPlaceholder = placeholder ?? defaultRangePlaceholder(locale);
450
479
  const isControlled = value !== undefined;
451
480
  const [internal, setInternal] = React.useState<DateRange | undefined>(defaultValue);
452
481
  const selected = isControlled ? value : internal;
@@ -485,7 +514,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
485
514
  }}
486
515
  >
487
516
  <span className={cn(styles["date-picker__value"], !displayText && styles["date-picker__placeholder"])}>
488
- {displayText ?? placeholder}
517
+ {displayText ?? resolvedPlaceholder}
489
518
  </span>
490
519
  <span className={styles["date-picker__icon"]} aria-hidden>
491
520
  <CalendarIcon />
@@ -509,6 +538,8 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
509
538
  onMonthChange={setCalendarMonth}
510
539
  min={min}
511
540
  max={max}
541
+ locale={locale}
542
+ messages={messages}
512
543
  />
513
544
  </BasePopover.Popup>
514
545
  </BasePopover.Positioner>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { Popover as BasePopover } from "@base-ui/react/popover";
5
- import { Calendar, type DateRange } from "../calendar";
5
+ import { Calendar, DEFAULT_LOCALE, type CalendarMessages, type DateRange } from "../calendar";
6
6
 
7
7
  import { cn } from "@SH_UI_UTILS@";
8
8
  export type { DateRange };
@@ -12,6 +12,15 @@ const formatDefault = (d: Date) =>
12
12
  `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
13
13
  const startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);
14
14
 
15
+ function defaultDatePlaceholder(locale: string): string {
16
+ const lang = locale.toLowerCase().split(/[-_]/)[0];
17
+ return lang === "ko" ? "날짜 선택" : "Select date";
18
+ }
19
+ function defaultRangePlaceholder(locale: string): string {
20
+ const lang = locale.toLowerCase().split(/[-_]/)[0];
21
+ return lang === "ko" ? "시작일 ~ 종료일" : "Start date – end date";
22
+ }
23
+
15
24
  const triggerClasses =
16
25
  "inline-flex items-center justify-between w-full h-[var(--control-md)] px-[var(--space-3)] bg-background text-foreground border border-border rounded-[var(--radius)] font-[inherit] text-[length:var(--text-sm)] leading-none cursor-pointer transition-[border-color,box-shadow] duration-[var(--duration-fast)] hover:not-disabled:border-border-strong focus-visible:outline-none focus-visible:border-foreground focus-visible:shadow-[0_0_0_1px_var(--foreground)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed disabled:bg-background-subtle aria-[invalid=true]:border-danger aria-[invalid=true]:focus-visible:shadow-[0_0_0_1px_var(--danger)] [@media(hover:none)_and_(pointer:coarse)]:h-11 [@media(hover:none)_and_(pointer:coarse)]:text-[length:var(--text-base)]";
17
26
 
@@ -36,6 +45,8 @@ interface DatePickerContextValue {
36
45
  setFocusedDate: (date: Date) => void;
37
46
  formatDate: (date: Date) => string;
38
47
  placeholder: string;
48
+ locale: string;
49
+ messages?: CalendarMessages;
39
50
  min?: Date; max?: Date; disabled?: boolean; readOnly?: boolean;
40
51
  ariaInvalid?: boolean | "true";
41
52
  closeOnSelect: boolean;
@@ -54,7 +65,8 @@ export interface DatePickerProps {
54
65
  onValueChange?: (date: Date | undefined) => void;
55
66
  formatDate?: (date: Date) => string;
56
67
  min?: Date; max?: Date;
57
- placeholder?: string; disabled?: boolean; readOnly?: boolean;
68
+ placeholder?: string; locale?: string; messages?: CalendarMessages;
69
+ disabled?: boolean; readOnly?: boolean;
58
70
  "aria-invalid"?: boolean | "true";
59
71
  className?: string; closeOnSelect?: boolean;
60
72
  container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
@@ -63,9 +75,10 @@ export interface DatePickerProps {
63
75
 
64
76
  export function DatePicker({
65
77
  value, defaultValue, onValueChange, formatDate = formatDefault,
66
- min, max, placeholder = "날짜 선택", disabled, readOnly,
78
+ min, max, placeholder, locale = DEFAULT_LOCALE, messages, disabled, readOnly,
67
79
  "aria-invalid": ariaInvalid, className, closeOnSelect = true, container, children,
68
80
  }: DatePickerProps) {
81
+ const resolvedPlaceholder = placeholder ?? defaultDatePlaceholder(locale);
69
82
  const isControlled = value !== undefined;
70
83
  const [internal, setInternal] = React.useState<Date | undefined>(defaultValue);
71
84
  const selected = isControlled ? value : internal;
@@ -84,9 +97,10 @@ export function DatePicker({
84
97
  const ctx = React.useMemo<DatePickerContextValue>(
85
98
  () => ({
86
99
  selected, setSelected, open, setOpen, focusedDate, setFocusedDate,
87
- formatDate, placeholder, min, max, disabled, readOnly, ariaInvalid, closeOnSelect,
100
+ formatDate, placeholder: resolvedPlaceholder, locale, messages,
101
+ min, max, disabled, readOnly, ariaInvalid, closeOnSelect,
88
102
  }),
89
- [selected, setSelected, open, focusedDate, formatDate, placeholder, min, max, disabled, readOnly, ariaInvalid, closeOnSelect],
103
+ [selected, setSelected, open, focusedDate, formatDate, resolvedPlaceholder, locale, messages, min, max, disabled, readOnly, ariaInvalid, closeOnSelect],
90
104
  );
91
105
 
92
106
  return (
@@ -187,6 +201,8 @@ export function DatePickerCalendar() {
187
201
  onMonthChange={ctx.setFocusedDate}
188
202
  min={ctx.min}
189
203
  max={ctx.max}
204
+ locale={ctx.locale}
205
+ messages={ctx.messages}
190
206
  />
191
207
  );
192
208
  }
@@ -221,7 +237,8 @@ export interface DateRangePickerProps {
221
237
  onValueChange?: (range: DateRange | undefined) => void;
222
238
  formatDate?: (date: Date) => string;
223
239
  min?: Date; max?: Date;
224
- placeholder?: string; disabled?: boolean; readOnly?: boolean;
240
+ placeholder?: string; locale?: string; messages?: CalendarMessages;
241
+ disabled?: boolean; readOnly?: boolean;
225
242
  "aria-invalid"?: boolean | "true";
226
243
  className?: string;
227
244
  container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
@@ -230,9 +247,10 @@ export interface DateRangePickerProps {
230
247
  export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(
231
248
  function DateRangePicker(
232
249
  { value, defaultValue, onValueChange, formatDate = formatDefault, min, max,
233
- placeholder = "시작일 ~ 종료일", disabled, readOnly, "aria-invalid": ariaInvalid, className, container },
250
+ placeholder, locale = DEFAULT_LOCALE, messages, disabled, readOnly, "aria-invalid": ariaInvalid, className, container },
234
251
  ref,
235
252
  ) {
253
+ const resolvedPlaceholder = placeholder ?? defaultRangePlaceholder(locale);
236
254
  const isControlled = value !== undefined;
237
255
  const [internal, setInternal] = React.useState<DateRange | undefined>(defaultValue);
238
256
  const selected = isControlled ? value : internal;
@@ -262,7 +280,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
262
280
  onClick={(e) => { if (readOnly) e.preventDefault(); }}
263
281
  >
264
282
  <span className={cn("overflow-hidden text-ellipsis whitespace-nowrap", !displayText && "text-foreground-subtle")}>
265
- {displayText ?? placeholder}
283
+ {displayText ?? resolvedPlaceholder}
266
284
  </span>
267
285
  <span className="shrink-0 inline-flex text-foreground-muted ml-[var(--space-2)]" aria-hidden>
268
286
  <CalendarIcon />
@@ -281,6 +299,8 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
281
299
  onMonthChange={setCalendarMonth}
282
300
  min={min}
283
301
  max={max}
302
+ locale={locale}
303
+ messages={messages}
284
304
  />
285
305
  </BasePopover.Popup>
286
306
  </BasePopover.Positioner>