Skip to content

Commit bff4e18

Browse files
committed
feat(calendar-web): add localization
1 parent 6a92bca commit bff4e18

File tree

8 files changed

+287
-75
lines changed

8 files changed

+287
-75
lines changed

packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P
4444
"customViewShowSunday"
4545
]);
4646
} else {
47-
hidePropertyIn(defaultProperties, values, "defaultViewStandard");
47+
hidePropertiesIn(defaultProperties, values, ["defaultViewStandard", "topBarDateFormat"]);
4848
}
4949

5050
values.toolbarItems?.forEach((item, index) => {

packages/pluggableWidgets/calendar-web/src/Calendar.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DnDCalendar } from "./utils/calendar-utils";
66
import { constructWrapperStyle } from "./utils/style-utils";
77
import "./ui/Calendar.scss";
88
import { useCalendarEvents } from "./helpers/useCalendarEvents";
9+
import { useLocalizer } from "./helpers/useLocalizer";
910

1011
export default function MxCalendar(props: CalendarContainerProps): ReactElement {
1112
// useMemo with empty dependency array is used
@@ -15,10 +16,14 @@ export default function MxCalendar(props: CalendarContainerProps): ReactElement
1516
const wrapperStyle = useMemo(() => constructWrapperStyle(props), []);
1617
// eslint-disable-next-line react-hooks/exhaustive-deps
1718
const calendarController = useMemo(() => new CalendarPropsBuilder(props), []);
19+
20+
// Get locale-aware localizer
21+
const { localizer, culture } = useLocalizer();
22+
1823
const calendarProps = useMemo(() => {
1924
calendarController.updateProps(props);
20-
return calendarController.build();
21-
}, [props, calendarController]);
25+
return calendarController.build(localizer, culture);
26+
}, [props, calendarController, localizer, culture]);
2227

2328
const calendarEvents = useCalendarEvents(props);
2429
return (

packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,30 @@ jest.mock("react-big-calendar", () => {
99
const originalModule = jest.requireActual("react-big-calendar");
1010
return {
1111
...originalModule,
12-
Calendar: ({ children, ...props }: any) => (
13-
<div data-testid="mock-calendar" {...props}>
12+
Calendar: ({
13+
children,
14+
defaultView,
15+
culture,
16+
resizable,
17+
selectable,
18+
showAllEvents,
19+
min,
20+
max,
21+
events,
22+
...domProps
23+
}: any) => (
24+
<div
25+
data-testid="mock-calendar"
26+
data-default-view={defaultView}
27+
data-culture={culture}
28+
data-resizable={resizable}
29+
data-selectable={selectable}
30+
data-show-all-events={showAllEvents}
31+
data-min={min?.toISOString()}
32+
data-max={max?.toISOString()}
33+
data-events-count={events?.length ?? 0}
34+
{...domProps}
35+
>
1436
{children}
1537
</div>
1638
),

packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ exports[`Calendar renders correctly with basic props 1`] = `
77
>
88
<div
99
components="[object Object]"
10+
data-culture="en-US"
11+
data-default-view="day"
12+
data-events-count="0"
13+
data-max="2025-04-28T23:59:59.000Z"
14+
data-min="2025-04-28T00:00:00.000Z"
15+
data-resizable="true"
16+
data-selectable="true"
17+
data-show-all-events="true"
1018
data-testid="mock-calendar"
11-
defaultview="work_week"
12-
events=""
1319
formats="[object Object]"
1420
localizer="[object Object]"
15-
max="Mon Apr 28 2025 23:59:59 GMT+0000 (Coordinated Universal Time)"
1621
messages="[object Object]"
17-
min="Mon Apr 28 2025 00:00:00 GMT+0000 (Coordinated Universal Time)"
1822
views="[object Object]"
1923
/>
2024
</div>

packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts

Lines changed: 55 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ObjectItem } from "mendix";
22
import { DateLocalizer, Formats, ViewsProps } from "react-big-calendar";
33
import { CalendarContainerProps } from "../../typings/CalendarProps";
44
import { createConfigurableToolbar, CustomToolbar, ResolvedToolbarItem } from "../components/Toolbar";
5-
import { eventPropGetter, localizer } from "../utils/calendar-utils";
5+
import { eventPropGetter } from "../utils/calendar-utils";
66
import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings";
77
import { CustomWeekController } from "./CustomWeekController";
88

@@ -32,8 +32,8 @@ export class CalendarPropsBuilder {
3232
this.toolbarItems = this.buildToolbarItems();
3333
}
3434

35-
build(): DragAndDropCalendarProps<CalendarEvent> {
36-
const formats = this.buildFormats();
35+
build(localizer: DateLocalizer, culture: string): DragAndDropCalendarProps<CalendarEvent> {
36+
const formats = this.buildFormats(localizer);
3737
const views = this.buildVisibleViews();
3838
const toolbar =
3939
this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0
@@ -50,14 +50,15 @@ export class CalendarPropsBuilder {
5050
const safeDefaultView = enabledViews.includes(this.defaultView) ? this.defaultView : enabledViews[0];
5151

5252
return {
53+
localizer,
54+
culture,
5355
components: {
5456
toolbar
5557
},
5658
defaultView: safeDefaultView,
5759
messages: this.buildMessages(workWeekCaption),
5860
events: this.events,
5961
formats,
60-
localizer,
6162
resizable: this.props.editable.value ?? true,
6263
selectable: this.props.editable.value ?? true,
6364
views,
@@ -121,114 +122,101 @@ export class CalendarPropsBuilder {
121122
}
122123
}
123124

124-
private buildFormats(): Formats {
125+
private buildFormats(_localizer: DateLocalizer): Formats {
125126
const formats: Formats = {};
126127

127128
const timePattern = this.getSafeTimePattern();
128129
if (timePattern) {
129-
const formatWith = (date: Date, localizer: DateLocalizer, fallback = "p"): string => {
130+
const formatWith = (date: Date, culture: string, loc: DateLocalizer, fallback = "p"): string => {
130131
try {
131-
return localizer.format(date, timePattern);
132+
return loc.format(date, timePattern, culture);
132133
} catch (e) {
133134
console.warn(
134135
`[Calendar] Failed to format time using pattern "${timePattern}" – falling back to default pattern "${fallback}".`,
135136
e
136137
);
137-
return localizer.format(date, fallback);
138+
return loc.format(date, fallback, culture);
138139
}
139140
};
140141

141-
formats.timeGutterFormat = (date: Date, _culture: string, loc: DateLocalizer) => formatWith(date, loc);
142+
formats.timeGutterFormat = (date: Date, culture: string, loc: DateLocalizer) =>
143+
formatWith(date, culture, loc);
142144
formats.eventTimeRangeFormat = (
143145
{ start, end }: { start: Date; end: Date },
144-
_culture: string,
146+
culture: string,
145147
loc: DateLocalizer
146-
) => `${formatWith(start, loc)}${formatWith(end, loc)}`;
148+
) => `${formatWith(start, culture, loc)}${formatWith(end, culture, loc)}`;
147149
formats.agendaTimeRangeFormat = (
148150
{ start, end }: { start: Date; end: Date },
149-
_culture: string,
151+
culture: string,
150152
loc: DateLocalizer
151-
) => `${formatWith(start, loc)}${formatWith(end, loc)}`;
153+
) => `${formatWith(start, culture, loc)}${formatWith(end, culture, loc)}`;
152154
}
153155

154156
const titlePattern = this.props.topBarDateFormat?.value?.trim();
155157
if (titlePattern) {
156-
formats.dayHeaderFormat = (date: Date, _culture: string, loc: DateLocalizer) =>
157-
loc.format(date, titlePattern);
158-
formats.monthHeaderFormat = (date: Date, _culture: string, loc: DateLocalizer) =>
159-
loc.format(date, titlePattern);
158+
formats.dayHeaderFormat = (date: Date, culture: string, loc: DateLocalizer) =>
159+
loc.format(date, titlePattern, culture);
160+
formats.monthHeaderFormat = (date: Date, culture: string, loc: DateLocalizer) =>
161+
loc.format(date, titlePattern, culture);
160162
formats.dayRangeHeaderFormat = (
161163
{ start, end }: { start: Date; end: Date },
162-
_culture: string,
164+
culture: string,
163165
loc: DateLocalizer
164-
) => `${loc.format(start, titlePattern)}${loc.format(end, titlePattern)}`;
166+
) => `${loc.format(start, titlePattern, culture)}${loc.format(end, titlePattern, culture)}`;
165167
formats.agendaHeaderFormat = (
166168
{ start, end }: { start: Date; end: Date },
167-
_culture: string,
169+
culture: string,
168170
loc: DateLocalizer
169-
) => `${loc.format(start, titlePattern)}${loc.format(end, titlePattern)}`;
171+
) => `${loc.format(start, titlePattern, culture)}${loc.format(end, titlePattern, culture)}`;
170172
}
171173

172174
// Apply per-view custom formats only in custom view mode
173175
if (this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0) {
174176
const byType = new Map(this.toolbarItems.map(i => [i.itemType, i]));
175177

176-
type HeaderFormat = Formats["dayHeaderFormat"];
177-
const applyHeader = (pattern?: string, existing?: HeaderFormat): HeaderFormat | undefined => {
178-
if (!pattern || pattern.trim() === "") return existing;
179-
return (date: Date, _culture: string, loc: DateLocalizer) => loc.format(date, pattern);
180-
};
181-
182-
// Helper to get non-empty pattern or fallback
183-
const getPattern = (pattern?: string, fallback?: string): string | undefined => {
178+
// Helper to get non-empty pattern
179+
const getPattern = (pattern?: string): string | undefined => {
184180
const trimmed = pattern?.trim();
185-
return trimmed && trimmed.length > 0 ? trimmed : fallback;
181+
return trimmed && trimmed.length > 0 ? trimmed : undefined;
186182
};
187183

188-
const dayHeaderPattern = getPattern(
189-
byType.get("day")?.customViewHeaderDayFormat,
190-
this.props.topBarDateFormat?.value
191-
);
192-
const weekHeaderPattern = getPattern(
193-
byType.get("week")?.customViewHeaderDayFormat || byType.get("work_week")?.customViewHeaderDayFormat,
194-
this.props.topBarDateFormat?.value
195-
);
196-
const monthHeaderPattern = getPattern(
197-
byType.get("month")?.customViewHeaderDayFormat,
198-
this.props.topBarDateFormat?.value
199-
);
200-
const agendaHeaderPattern = getPattern(
201-
byType.get("agenda")?.customViewHeaderDayFormat,
202-
this.props.topBarDateFormat?.value
203-
);
204-
205-
// Only apply if we have a valid pattern
184+
// Header formats
185+
const dayHeaderPattern = getPattern(byType.get("day")?.customViewHeaderDayFormat);
206186
if (dayHeaderPattern) {
207-
formats.dayHeaderFormat = applyHeader(dayHeaderPattern, formats.dayHeaderFormat);
187+
formats.dayHeaderFormat = (date: Date, culture: string, loc: DateLocalizer) =>
188+
loc.format(date, dayHeaderPattern, culture);
208189
}
190+
191+
const weekHeaderPattern = getPattern(
192+
byType.get("week")?.customViewHeaderDayFormat || byType.get("work_week")?.customViewHeaderDayFormat
193+
);
209194
if (weekHeaderPattern) {
210195
formats.dayRangeHeaderFormat = (
211196
range: { start: Date; end: Date },
212-
_culture: string,
197+
culture: string,
213198
loc: DateLocalizer
214-
) => `${loc.format(range.start, weekHeaderPattern)}${loc.format(range.end, weekHeaderPattern)}`;
199+
) =>
200+
`${loc.format(range.start, weekHeaderPattern, culture)}${loc.format(range.end, weekHeaderPattern, culture)}`;
215201
}
202+
203+
const monthHeaderPattern = getPattern(byType.get("month")?.customViewHeaderDayFormat);
216204
if (monthHeaderPattern) {
217-
formats.monthHeaderFormat = applyHeader(monthHeaderPattern, formats.monthHeaderFormat);
205+
formats.monthHeaderFormat = (date: Date, culture: string, loc: DateLocalizer) =>
206+
loc.format(date, monthHeaderPattern, culture);
218207
}
208+
209+
const agendaHeaderPattern = getPattern(byType.get("agenda")?.customViewHeaderDayFormat);
219210
if (agendaHeaderPattern) {
220-
formats.agendaHeaderFormat = (
221-
range: { start: Date; end: Date },
222-
_culture: string,
223-
loc: DateLocalizer
224-
) => `${loc.format(range.start, agendaHeaderPattern)}${loc.format(range.end, agendaHeaderPattern)}`;
211+
formats.agendaHeaderFormat = (range: { start: Date; end: Date }, culture: string, loc: DateLocalizer) =>
212+
`${loc.format(range.start, agendaHeaderPattern, culture)}${loc.format(range.end, agendaHeaderPattern, culture)}`;
225213
}
226214

227215
// Month day numbers
228216
const monthCellDate = getPattern(byType.get("month")?.customViewCellDateFormat);
229217
if (monthCellDate) {
230-
formats.dateFormat = (date: Date, _culture: string, loc: DateLocalizer) =>
231-
loc.format(date, monthCellDate);
218+
formats.dateFormat = (date: Date, culture: string, loc: DateLocalizer) =>
219+
loc.format(date, monthCellDate, culture);
232220
}
233221

234222
// Time gutters
@@ -237,18 +225,20 @@ export class CalendarPropsBuilder {
237225
const workWeekTimeGutter = getPattern(byType.get("work_week")?.customViewGutterTimeFormat);
238226
const chosenTimeGutter = weekTimeGutter || dayTimeGutter || workWeekTimeGutter;
239227
if (chosenTimeGutter) {
240-
formats.timeGutterFormat = (date: Date, _culture: string, loc: DateLocalizer) =>
241-
loc.format(date, chosenTimeGutter);
228+
formats.timeGutterFormat = (date: Date, culture: string, loc: DateLocalizer) =>
229+
loc.format(date, chosenTimeGutter, culture);
242230
}
231+
243232
const agendaTime = getPattern(byType.get("agenda")?.customViewGutterTimeFormat);
244233
if (agendaTime) {
245-
formats.agendaTimeFormat = (date: Date, _culture: string, loc: DateLocalizer) =>
246-
loc.format(date, agendaTime);
234+
formats.agendaTimeFormat = (date: Date, culture: string, loc: DateLocalizer) =>
235+
loc.format(date, agendaTime, culture);
247236
}
237+
248238
const agendaDate = getPattern(byType.get("agenda")?.customViewGutterDateFormat);
249239
if (agendaDate) {
250-
formats.agendaDateFormat = (date: Date, _culture: string, loc: DateLocalizer) =>
251-
loc.format(date, agendaDate);
240+
formats.agendaDateFormat = (date: Date, culture: string, loc: DateLocalizer) =>
241+
loc.format(date, agendaDate, culture);
252242
}
253243
}
254244

0 commit comments

Comments
 (0)