Skip to content

Commit bf65358

Browse files
jpmckinneyclaude
andcommitted
i18n(Calendar): localize month names via useDateFormatter
YearCalendar and MultiSelectYearCalendar hardcoded English month names in monthsObj — both the visible abbreviation ("Jan") and the full name used in the cell aria-label ("January"). These are CLDR data that Intl already knows for every locale, so route them through react-aria's useDateFormatter (month: 'short' for the visible text, month: 'long' for the aria-label) keyed off the active locale from the nearest <I18nProvider>, matching the MultiMonthPicker i18n approach. monthsObj now carries only month numbers for the 3x4 grid layout; names are formatted at render time. The day is fixed to the 1st in local time (new Date(year, month - 1, 1)) to avoid the UTC-midnight off-by-one that bit MultiMonthPicker. Adds a Storybook story rendering the months in French via <I18nProvider>. This leaves the consumer-supplied `labels` prop for only the genuinely app-authored strings (e.g. "selected"), which Intl does not provide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 79b7b46 commit bf65358

3 files changed

Lines changed: 58 additions & 49 deletions

File tree

packages/opub-ui/src/components/Calendar/Calendar.stories.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
today,
77
} from '@internationalized/date';
88
import { Meta, StoryObj } from '@storybook/react-vite';
9-
import { useDateFormatter, useLocale } from 'react-aria';
9+
import { I18nProvider, useDateFormatter, useLocale } from 'react-aria';
1010

1111
import { Calendar } from './Calendar';
1212
import { MultiSelectYearCalendar } from './MultiSelectYearCalendar';
@@ -140,3 +140,24 @@ export const YearLocalizedLabels: any = {
140140
},
141141
args: {},
142142
};
143+
144+
/**
145+
* Month names are formatted from the active locale (via `useDateFormatter`),
146+
* so wrapping the calendar in an `<I18nProvider>` localizes them with no
147+
* consumer-supplied strings. Here the months render in French.
148+
*/
149+
export const YearLocalizedMonths: any = {
150+
render: ({ ...args }) => {
151+
return (
152+
<I18nProvider locale="fr-FR">
153+
<YearCalendar
154+
{...args}
155+
onChange={(date: any) => {
156+
console.log(date);
157+
}}
158+
/>
159+
</I18nProvider>
160+
);
161+
},
162+
args: {},
163+
};

packages/opub-ui/src/components/Calendar/MultiSelectYearCalendar.tsx

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,12 @@ import { Icon } from '../Icon';
2121
import { Text } from '../Text';
2222
import styles from './Calendar.module.scss';
2323

24-
const monthsObj: {
25-
[key: number]: { month: string; value: number; label: string }[];
26-
} = {
27-
0: [
28-
{ month: 'Jan', value: 1, label: 'January' },
29-
{ month: 'Feb', value: 2, label: 'February' },
30-
{ month: 'Mar', value: 3, label: 'March' },
31-
{ month: 'Apr', value: 4, label: 'April' },
32-
],
33-
1: [
34-
{ month: 'May', value: 5, label: 'May' },
35-
{ month: 'Jun', value: 6, label: 'June' },
36-
{ month: 'Jul', value: 7, label: 'July' },
37-
{ month: 'Aug', value: 8, label: 'August' },
38-
],
39-
2: [
40-
{ month: 'Sep', value: 9, label: 'September' },
41-
{ month: 'Oct', value: 10, label: 'October' },
42-
{ month: 'Nov', value: 11, label: 'November' },
43-
{ month: 'Dec', value: 12, label: 'December' },
44-
],
24+
// Month numbers laid out as the 3x4 grid. Names are formatted per locale at
25+
// render time (see Cell), so no month strings are hardcoded here.
26+
const monthsObj: { [key: number]: { value: number }[] } = {
27+
0: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }],
28+
1: [{ value: 5 }, { value: 6 }, { value: 7 }, { value: 8 }],
29+
2: [{ value: 9 }, { value: 10 }, { value: 11 }, { value: 12 }],
4530
};
4631

4732
interface MultiSelectYearCalendarProps {
@@ -258,14 +243,23 @@ const Cell = ({
258243
defaultValue,
259244
labels,
260245
}: {
261-
mon: { month: string; value: number; label: string };
246+
mon: { value: number };
262247
state: CalendarState;
263248
selectedDates: DateValue[];
264249
defaultValue?: DateValue[];
265250
labels?: YearCalendarLabels;
266251
}) => {
267252
let date = state.focusedDate.set({ month: mon.value });
268253

254+
// Localized month names from the active locale (via the nearest
255+
// <I18nProvider>, falling back to the runtime locale). The day is fixed to
256+
// the 1st in local time to avoid the UTC-midnight off-by-one.
257+
const monthShortFormatter = useDateFormatter({ month: 'short' });
258+
const monthLongFormatter = useDateFormatter({ month: 'long' });
259+
const nameDate = new Date(state.focusedDate.year, mon.value - 1, 1);
260+
const monthShort = monthShortFormatter.format(nameDate);
261+
const monthLong = monthLongFormatter.format(nameDate);
262+
269263
const { minValue, maxValue } = state;
270264
const isDisabled =
271265
(minValue && date.compare(minValue) < 0) ||
@@ -293,7 +287,7 @@ const Cell = ({
293287
state.setFocusedDate(date);
294288
};
295289

296-
const ariaLabel = `${mon.label}, ${state.focusedDate.year}${
290+
const ariaLabel = `${monthLong}, ${state.focusedDate.year}${
297291
isSelected ? `, ${labels?.selected ?? 'selected'}` : ''
298292
}`;
299293

@@ -307,7 +301,7 @@ const Cell = ({
307301
aria-label={ariaLabel}
308302
data-label={`${mon.value}, ${state.focusedDate.year}`}
309303
>
310-
<Text color="subdued">{mon.month}</Text>
304+
<Text color="subdued">{monthShort}</Text>
311305
</button>
312306
</td>
313307
);

packages/opub-ui/src/components/Calendar/YearCalendar.tsx

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,12 @@ import { Icon } from '../Icon';
2121
import { Text } from '../Text';
2222
import styles from './Calendar.module.scss';
2323

24-
const monthsObj: {
25-
[key: number]: { month: string; value: number; label: string }[];
26-
} = {
27-
0: [
28-
{ month: 'Jan', value: 1, label: 'January' },
29-
{ month: 'Feb', value: 2, label: 'February' },
30-
{ month: 'Mar', value: 3, label: 'March' },
31-
{ month: 'Apr', value: 4, label: 'April' },
32-
],
33-
1: [
34-
{ month: 'May', value: 5, label: 'May' },
35-
{ month: 'Jun', value: 6, label: 'June' },
36-
{ month: 'Jul', value: 7, label: 'July' },
37-
{ month: 'Aug', value: 8, label: 'August' },
38-
],
39-
2: [
40-
{ month: 'Sep', value: 9, label: 'September' },
41-
{ month: 'Oct', value: 10, label: 'October' },
42-
{ month: 'Nov', value: 11, label: 'November' },
43-
{ month: 'Dec', value: 12, label: 'December' },
44-
],
24+
// Month numbers laid out as the 3x4 grid. Names are formatted per locale at
25+
// render time (see Cell), so no month strings are hardcoded here.
26+
const monthsObj: { [key: number]: { value: number }[] } = {
27+
0: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }],
28+
1: [{ value: 5 }, { value: 6 }, { value: 7 }, { value: 8 }],
29+
2: [{ value: 9 }, { value: 10 }, { value: 11 }, { value: 12 }],
4530
};
4631

4732
export const YearCalendar = (
@@ -185,12 +170,21 @@ const Cell = ({
185170
state,
186171
labels,
187172
}: {
188-
mon: { month: string; value: number; label: string };
173+
mon: { value: number };
189174
state: CalendarState;
190175
labels?: YearCalendarLabels;
191176
}) => {
192177
let date = state.focusedDate.set({ month: mon.value });
193178

179+
// Localized month names from the active locale (via the nearest
180+
// <I18nProvider>, falling back to the runtime locale). The day is fixed to
181+
// the 1st in local time to avoid the UTC-midnight off-by-one.
182+
const monthShortFormatter = useDateFormatter({ month: 'short' });
183+
const monthLongFormatter = useDateFormatter({ month: 'long' });
184+
const nameDate = new Date(state.focusedDate.year, mon.value - 1, 1);
185+
const monthShort = monthShortFormatter.format(nameDate);
186+
const monthLong = monthLongFormatter.format(nameDate);
187+
194188
const { minValue, maxValue } = state;
195189
const isDisabled =
196190
(minValue && date.compare(minValue) < 0) ||
@@ -217,7 +211,7 @@ const Cell = ({
217211
state.setValue(date);
218212
};
219213

220-
const ariaLabel = `${mon.label}, ${state.focusedDate.year}${
214+
const ariaLabel = `${monthLong}, ${state.focusedDate.year}${
221215
isSelected ? `, ${labels?.selected ?? 'selected'}` : ''
222216
}`;
223217

@@ -231,7 +225,7 @@ const Cell = ({
231225
aria-label={ariaLabel}
232226
data-label={`${mon.value}, ${state.focusedDate.year}`}
233227
>
234-
<Text color="subdued">{mon.month}</Text>
228+
<Text color="subdued">{monthShort}</Text>
235229
</button>
236230
</td>
237231
);

0 commit comments

Comments
 (0)