Skip to content

Commit c8f171d

Browse files
committed
Fix date format to respect user profile settings using date-fns (#22900)
1 parent ceb9967 commit c8f171d

File tree

4 files changed

+238
-231
lines changed

4 files changed

+238
-231
lines changed

src/common/datetime/format_date.ts

Lines changed: 137 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,96 @@
11
import type { HassConfig } from "home-assistant-js-websocket";
2-
import memoizeOne from "memoize-one";
2+
import { format } from "date-fns";
3+
import { TZDate } from "@date-fns/tz";
34
import type { FrontendLocaleData } from "../../data/translation";
45
import { DateFormat } from "../../data/translation";
56
import { resolveTimeZone } from "./resolve-time-zone";
67

8+
// Helper to get date in target timezone
9+
const toTimeZone = (date: Date, timeZone: string): Date => {
10+
try {
11+
return new TZDate(date, timeZone);
12+
} catch {
13+
return date;
14+
}
15+
};
16+
17+
// Helper to get format string based on date preference
18+
const formatForDatePreference = (
19+
template: { DMY: string; MDY: string; YMD: string },
20+
locale: FrontendLocaleData
21+
): string => {
22+
if (
23+
locale.date_format === DateFormat.language ||
24+
locale.date_format === DateFormat.system
25+
) {
26+
return template.MDY; // Default to MDY for browser locale
27+
}
28+
29+
const pattern =
30+
template[locale.date_format as unknown as keyof typeof template];
31+
return pattern || template.MDY; // Fallback to MDY if not found
32+
};
33+
734
// Tuesday, August 10
835
export const formatDateWeekdayDay = (
936
dateObj: Date,
1037
locale: FrontendLocaleData,
1138
config: HassConfig
12-
) => formatDateWeekdayDayMem(locale, config.time_zone).format(dateObj);
13-
14-
const formatDateWeekdayDayMem = memoizeOne(
15-
(locale: FrontendLocaleData, serverTimeZone: string) =>
16-
new Intl.DateTimeFormat(locale.language, {
17-
weekday: "long",
18-
month: "long",
19-
day: "numeric",
20-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
21-
})
22-
);
39+
) => {
40+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
41+
const zonedDate = toTimeZone(dateObj, timeZone);
42+
const pattern = formatForDatePreference(
43+
{ DMY: "EEEE, d MMMM", MDY: "EEEE, MMMM d", YMD: "EEEE, MMMM d" },
44+
locale
45+
);
46+
return format(zonedDate, pattern);
47+
};
2348

2449
// August 10, 2021
2550
export const formatDate = (
2651
dateObj: Date,
2752
locale: FrontendLocaleData,
2853
config: HassConfig
29-
) => formatDateMem(locale, config.time_zone).format(dateObj);
30-
31-
const formatDateMem = memoizeOne(
32-
(locale: FrontendLocaleData, serverTimeZone: string) =>
33-
new Intl.DateTimeFormat(locale.language, {
34-
year: "numeric",
35-
month: "long",
36-
day: "numeric",
37-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
38-
})
39-
);
54+
) => {
55+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
56+
const zonedDate = toTimeZone(dateObj, timeZone);
57+
const pattern = formatForDatePreference(
58+
{
59+
DMY: "d MMMM, yyyy",
60+
MDY: "MMMM d, yyyy",
61+
YMD: "yyyy, MMMM d",
62+
},
63+
locale
64+
);
65+
return format(zonedDate, pattern);
66+
};
4067

4168
// Aug 10, 2021
4269
export const formatDateShort = (
4370
dateObj: Date,
4471
locale: FrontendLocaleData,
4572
config: HassConfig
46-
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
47-
48-
const formatDateShortMem = memoizeOne(
49-
(locale: FrontendLocaleData, serverTimeZone: string) =>
50-
new Intl.DateTimeFormat(locale.language, {
51-
year: "numeric",
52-
month: "short",
53-
day: "numeric",
54-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
55-
})
56-
);
73+
) => {
74+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
75+
const zonedDate = toTimeZone(dateObj, timeZone);
76+
const pattern = formatForDatePreference(
77+
{
78+
DMY: "d MMM, yyyy",
79+
MDY: "MMM d, yyyy",
80+
YMD: "yyyy, MMM d",
81+
},
82+
locale
83+
);
84+
return format(zonedDate, pattern);
85+
};
5786

5887
// 10/08/2021
5988
export const formatDateNumeric = (
6089
dateObj: Date,
6190
locale: FrontendLocaleData,
6291
config: HassConfig
6392
) => {
64-
const formatter = formatDateNumericMem(locale, config.time_zone);
93+
const formatter = createDateNumericFormatter(locale, config.time_zone);
6594

6695
if (
6796
locale.date_format === DateFormat.language ||
@@ -93,193 +122,142 @@ export const formatDateNumeric = (
93122
return formats[locale.date_format];
94123
};
95124

96-
const formatDateNumericMem = memoizeOne(
97-
(locale: FrontendLocaleData, serverTimeZone: string) => {
98-
const localeString =
99-
locale.date_format === DateFormat.system ? undefined : locale.language;
100-
101-
if (
102-
locale.date_format === DateFormat.language ||
103-
locale.date_format === DateFormat.system
104-
) {
105-
return new Intl.DateTimeFormat(localeString, {
106-
year: "numeric",
107-
month: "numeric",
108-
day: "numeric",
109-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
110-
});
111-
}
112-
113-
return new Intl.DateTimeFormat(localeString, {
114-
year: "numeric",
115-
month: "numeric",
116-
day: "numeric",
117-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
118-
});
119-
}
120-
);
125+
const createDateNumericFormatter = (
126+
locale: FrontendLocaleData,
127+
serverTimeZone: string
128+
) => {
129+
const localeString =
130+
locale.date_format === DateFormat.system ? undefined : locale.language;
131+
return new Intl.DateTimeFormat(localeString, {
132+
year: "numeric",
133+
month: "numeric",
134+
day: "numeric",
135+
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
136+
});
137+
};
121138

122139
// Aug 10
123140
export const formatDateVeryShort = (
124141
dateObj: Date,
125142
locale: FrontendLocaleData,
126143
config: HassConfig
127-
) => formatDateVeryShortMem(locale, config.time_zone).format(dateObj);
128-
129-
const formatDateVeryShortMem = memoizeOne(
130-
(locale: FrontendLocaleData, serverTimeZone: string) =>
131-
new Intl.DateTimeFormat(locale.language, {
132-
day: "numeric",
133-
month: "short",
134-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
135-
})
136-
);
144+
) => {
145+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
146+
const zonedDate = toTimeZone(dateObj, timeZone);
147+
const pattern = formatForDatePreference(
148+
{ DMY: "d MMM", MDY: "MMM d", YMD: "MMM d" },
149+
locale
150+
);
151+
return format(zonedDate, pattern);
152+
};
137153

138154
// August 2021
139155
export const formatDateMonthYear = (
140156
dateObj: Date,
141157
locale: FrontendLocaleData,
142158
config: HassConfig
143-
) => formatDateMonthYearMem(locale, config.time_zone).format(dateObj);
144-
145-
const formatDateMonthYearMem = memoizeOne(
146-
(locale: FrontendLocaleData, serverTimeZone: string) =>
147-
new Intl.DateTimeFormat(locale.language, {
148-
month: "long",
149-
year: "numeric",
150-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
151-
})
152-
);
159+
) => {
160+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
161+
const zonedDate = toTimeZone(dateObj, timeZone);
162+
const pattern = formatForDatePreference(
163+
{
164+
DMY: "MMMM yyyy",
165+
MDY: "MMMM yyyy",
166+
YMD: "yyyy MMMM",
167+
},
168+
locale
169+
);
170+
return format(zonedDate, pattern);
171+
};
153172

154173
// August
155174
export const formatDateMonth = (
156175
dateObj: Date,
157176
locale: FrontendLocaleData,
158177
config: HassConfig
159-
) => formatDateMonthMem(locale, config.time_zone).format(dateObj);
160-
161-
const formatDateMonthMem = memoizeOne(
162-
(locale: FrontendLocaleData, serverTimeZone: string) =>
163-
new Intl.DateTimeFormat(locale.language, {
164-
month: "long",
165-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
166-
})
167-
);
178+
) => {
179+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
180+
const zonedDate = toTimeZone(dateObj, timeZone);
181+
return format(zonedDate, "MMMM");
182+
};
168183

169184
// Aug
170185
export const formatDateMonthShort = (
171186
dateObj: Date,
172187
locale: FrontendLocaleData,
173188
config: HassConfig
174-
) => formatDateMonthShortMem(locale, config.time_zone).format(dateObj);
175-
176-
const formatDateMonthShortMem = memoizeOne(
177-
(locale: FrontendLocaleData, serverTimeZone: string) =>
178-
new Intl.DateTimeFormat(locale.language, {
179-
month: "short",
180-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
181-
})
182-
);
189+
) => {
190+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
191+
const zonedDate = toTimeZone(dateObj, timeZone);
192+
return format(zonedDate, "MMM");
193+
};
183194

184195
// 2021
185196
export const formatDateYear = (
186197
dateObj: Date,
187198
locale: FrontendLocaleData,
188199
config: HassConfig
189-
) => formatDateYearMem(locale, config.time_zone).format(dateObj);
190-
191-
const formatDateYearMem = memoizeOne(
192-
(locale: FrontendLocaleData, serverTimeZone: string) =>
193-
new Intl.DateTimeFormat(locale.language, {
194-
year: "numeric",
195-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
196-
})
197-
);
200+
) => {
201+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
202+
const zonedDate = toTimeZone(dateObj, timeZone);
203+
return format(zonedDate, "yyyy");
204+
};
198205

199206
// Monday
200207
export const formatDateWeekday = (
201208
dateObj: Date,
202209
locale: FrontendLocaleData,
203210
config: HassConfig
204-
) => formatDateWeekdayMem(locale, config.time_zone).format(dateObj);
205-
206-
const formatDateWeekdayMem = memoizeOne(
207-
(locale: FrontendLocaleData, serverTimeZone: string) =>
208-
new Intl.DateTimeFormat(locale.language, {
209-
weekday: "long",
210-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
211-
})
212-
);
211+
) => {
212+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
213+
const zonedDate = toTimeZone(dateObj, timeZone);
214+
return format(zonedDate, "EEEE");
215+
};
213216

214217
// Mon
215218
export const formatDateWeekdayShort = (
216219
dateObj: Date,
217220
locale: FrontendLocaleData,
218221
config: HassConfig
219-
) => formatDateWeekdayShortMem(locale, config.time_zone).format(dateObj);
220-
221-
const formatDateWeekdayShortMem = memoizeOne(
222-
(locale: FrontendLocaleData, serverTimeZone: string) =>
223-
new Intl.DateTimeFormat(locale.language, {
224-
weekday: "short",
225-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
226-
})
227-
);
222+
) => {
223+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
224+
const zonedDate = toTimeZone(dateObj, timeZone);
225+
return format(zonedDate, "EEE");
226+
};
228227

229228
// Mon, Aug 10
230229
export const formatDateWeekdayVeryShortDate = (
231230
dateObj: Date,
232231
locale: FrontendLocaleData,
233232
config: HassConfig
234-
) =>
235-
formatDateWeekdayVeryShortDateMem(locale, config.time_zone).format(dateObj);
236-
237-
const formatDateWeekdayVeryShortDateMem = memoizeOne(
238-
(locale: FrontendLocaleData, serverTimeZone: string) =>
239-
new Intl.DateTimeFormat(locale.language, {
240-
weekday: "short",
241-
month: "short",
242-
day: "numeric",
243-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
244-
})
245-
);
233+
) => {
234+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
235+
const zonedDate = toTimeZone(dateObj, timeZone);
236+
return format(zonedDate, "EEE, MMM d");
237+
};
246238

247239
// Mon, Aug 10, 2021
248240
export const formatDateWeekdayShortDate = (
249241
dateObj: Date,
250242
locale: FrontendLocaleData,
251243
config: HassConfig
252-
) => formatDateWeekdayShortDateMem(locale, config.time_zone).format(dateObj);
253-
254-
const formatDateWeekdayShortDateMem = memoizeOne(
255-
(locale: FrontendLocaleData, serverTimeZone: string) =>
256-
new Intl.DateTimeFormat(locale.language, {
257-
weekday: "short",
258-
month: "short",
259-
day: "numeric",
260-
year: "numeric",
261-
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
262-
})
263-
);
244+
) => {
245+
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
246+
const zonedDate = toTimeZone(dateObj, timeZone);
247+
return format(zonedDate, "EEE, MMM d, yyyy");
248+
};
264249

265250
/**
266-
* Format a date as YYYY-MM-DD. Uses "en-CA" because it's the only
267-
* Intl locale that natively outputs ISO 8601 date format.
268-
* Locale/config are only used to resolve the time zone.
251+
* Format a date as YYYY-MM-DD
269252
*/
270253
export const formatISODateOnly = (
271254
dateObj: Date,
272255
locale: FrontendLocaleData,
273256
config: HassConfig
274257
) => {
275258
const timeZone = resolveTimeZone(locale.time_zone, config.time_zone);
276-
const formatter = new Intl.DateTimeFormat("en-CA", {
277-
year: "numeric",
278-
month: "2-digit",
279-
day: "2-digit",
280-
timeZone,
281-
});
282-
return formatter.format(dateObj);
259+
const zonedDate = toTimeZone(dateObj, timeZone);
260+
return format(zonedDate, "yyyy-MM-dd");
283261
};
284262

285263
// 2026-08-10/2026-08-15

0 commit comments

Comments
 (0)