Skip to content

Commit a6a8232

Browse files
committed
Unify date formatting, base it on locale
This patch aims for a consistent date display across the entire ui. That it is not always consistent can be seen for example in the event details metadata, where the start date is formatted differently from the created date. Furthermore, this patch aims to base date formatting more on the chosen locale to create a more intuitive experience for users. No more A.M./P.M. in the german locale for example. To achieve this, date-fns is chosen as the gold standard and momentjs is removed from the project completely. Not only because moment is legacy, but also because our datepickers were using date-fns already and will not use anything else. Additionally, this removes our custom date formatting from the translation files and replaces it with date-fns tokens. In my opinion, we do not need to try and be smarter than our datetime library when it comes to formatting dates. Translators retain the ability to enforce their favourite formatting should they so chose, but will have to be aware that it will not be respect everywhere, e.g. not in the datepickers. This patch contains changes to all translation files. I am aware that everything except en_US will get overwritten, the changes to other languages are merely for ease of reviewing. If this patch is merged, the translations should be adapted asap as per this example.
1 parent 028b289 commit a6a8232

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+552
-537
lines changed

package-lock.json

Lines changed: 80 additions & 130 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
"i18next-browser-languagedetector": "^8.2.1",
2424
"i18next-http-backend": "^3.0.2",
2525
"lodash": "^4.17.23",
26-
"moment": "^2.30.1",
27-
"moment-timezone": "^0.6.0",
2826
"react": "^19.2.4",
2927
"react-chartjs-2": "^5.3.1",
3028
"react-datepicker": "^8.8.0",

src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import SchedulingInputs from "../wizards/scheduling/SchedulingInputs";
5252
import SchedulingConflicts from "../wizards/scheduling/SchedulingConflicts";
5353
import { ParseKeys } from "i18next";
5454
import ModalContentTable from "../../../shared/modals/ModalContentTable";
55+
import i18n from "../../../../i18n/i18n";
5556

5657
export type InitialValues = {
5758
scheduleStartDate: string;
@@ -104,7 +105,7 @@ const EventDetailsSchedulingTab = ({
104105
}, []);
105106

106107
// Get info about the current language and its date locale
107-
const currentLanguage = getCurrentLanguageInformation();
108+
const currentLanguage = getCurrentLanguageInformation(i18n.language);
108109

109110
// Get timezone offset; Checks should be performed on UTC times
110111
const offset = getTimezoneOffset();

src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import SchedulingInputs from "../wizards/scheduling/SchedulingInputs";
4949
import SchedulingConflicts from "../wizards/scheduling/SchedulingConflicts";
5050
import { ParseKeys } from "i18next";
5151
import { LuCircleX } from "react-icons/lu";
52+
import i18n from "../../../../i18n/i18n";
5253

5354
/**
5455
* This component renders the source page for new events in the new event wizard.
@@ -390,7 +391,7 @@ const Schedule = <T extends {
390391
inputDevices: Recording[]
391392
}) => {
392393
const { t } = useTranslation();
393-
const currentLanguage = getCurrentLanguageInformation();
394+
const currentLanguage = getCurrentLanguageInformation(i18n.language);
394395
const dispatch = useAppDispatch();
395396

396397
// Parse start-Date strings

src/components/events/partials/wizards/scheduling/SchedulingTime.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { hours, minutes } from "../../../../../configs/modalConfig";
44
import { formatTimeForDropdown } from "../../../../../utils/dropDownUtils";
55
import { getCurrentLanguageInformation } from "../../../../../utils/utils";
66
import { ParseKeys } from "i18next";
7+
import i18n from "../../../../../i18n/i18n";
78

89
const SchedulingTime = ({
910
hour,
@@ -28,7 +29,7 @@ const SchedulingTime = ({
2829
}) => {
2930
const { t } = useTranslation();
3031
// Get info about the current language and its date locale
31-
const currentLanguage = getCurrentLanguageInformation();
32+
const currentLanguage = getCurrentLanguageInformation(i18n.language);
3233

3334
return (
3435
<tr>

src/components/shared/TableFilters.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
import TableFilterProfiles from "./TableFilterProfiles";
2020
import { availableHotkeys } from "../../configs/hotkeysConfig";
2121
import { useHotkeys } from "react-hotkeys-hook";
22-
import moment from "moment";
2322
import { AppThunk, useAppDispatch, useAppSelector } from "../../store";
2423
import { renderValidDate } from "../../utils/dateUtils";
2524
import { getCurrentLanguageInformation } from "../../utils/utils";
@@ -32,6 +31,8 @@ import { Resource } from "../../slices/tableSlice";
3231
import { HiFunnel } from "react-icons/hi2";
3332
import { LuSettings, LuX } from "react-icons/lu";
3433
import { useLocation } from "react-router";
34+
import { isValid } from "date-fns";
35+
import i18n from "../../i18n/i18n";
3536

3637
/**
3738
* This component renders the table filters in the upper right corner of the table
@@ -205,7 +206,7 @@ const TableFilters = ({
205206
};
206207

207208
const submitDateFilter = async (start: Date | undefined | null, end: Date | undefined | null) => {
208-
if (start && end && moment(start).isValid() && moment(end).isValid()) {
209+
if (start && end && isValid(start) && isValid(end)) {
209210
const filter = filterMap.find(({ name }) => name === selectedFilter);
210211
if (filter) {
211212
dispatch(editFilterValue({
@@ -491,7 +492,7 @@ const FilterSwitch = ({
491492
popperPlacement="bottom"
492493
popperClassName="datepicker-custom"
493494
className="datepicker-custom-input"
494-
locale={getCurrentLanguageInformation()?.dateLocale}
495+
locale={getCurrentLanguageInformation(i18n.language)?.dateLocale}
495496
strictParsing
496497
/>
497498
</div>

src/components/shared/TimeSeriesStatistics.tsx

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useState } from "react";
2-
import moment from "moment";
32
import { getCurrentLanguageInformation } from "../../utils/utils";
43
import DatePicker from "react-datepicker";
54
import { Formik, FormikErrors } from "formik";
@@ -11,14 +10,16 @@ import {
1110
statisticDateFormatStrings,
1211
statisticTimeModes,
1312
} from "../../configs/statisticsConfig";
14-
import { localizedMoment } from "../../utils/dateUtils";
13+
import { formatRangeEnd, formatRangeStart } from "../../utils/dateUtils";
1514
import { useTranslation } from "react-i18next";
1615
import type { ChartOptions } from "chart.js";
1716
import { AsyncThunk } from "@reduxjs/toolkit";
1817
import { useAppDispatch } from "../../store";
1918
import { DataResolution, Statistics, TimeMode } from "../../slices/statisticsSlice";
2019
import { ParseKeys } from "i18next";
2120
import { LuChevronLeft, LuChevronRight, LuDownload } from "react-icons/lu";
21+
import { add, format, parseISO, sub } from "date-fns";
22+
import i18n from "../../i18n/i18n";
2223

2324

2425
/**
@@ -73,12 +74,17 @@ const TimeSeriesStatistics = ({
7374
const formatStrings = statisticDateFormatStrings;
7475

7576
// Get info about the current language and its date locale
76-
const currentLanguage = getCurrentLanguageInformation();
77+
const currentLanguage = getCurrentLanguageInformation(i18n.language);
7778

7879
// Set the date for the react-datepicker
7980
const [startDatepicker, setStartDatepicker] = useState(fromDate ? new Date(fromDate) : null);
8081
const [endDatepicker, setEndDatepicker] = useState(toDate ? new Date(toDate) : null);
8182

83+
const unitMap = {
84+
year: "years",
85+
month: "months",
86+
} as const;
87+
8288
// change formik values and get new statistic values from API
8389
const change = (
8490
setFormikValue: (field: string, value: any) => Promise<void | FormikErrors<any>>,
@@ -88,8 +94,8 @@ const TimeSeriesStatistics = ({
8894
dataResolution: DataResolution,
8995
) => {
9096
if (timeMode === "year" || timeMode === "month") {
91-
from = moment(from).clone().startOf(timeMode).format("YYYY-MM-DD");
92-
to = moment(from).clone().endOf(timeMode).format("YYYY-MM-DD");
97+
from = formatRangeStart(from, timeMode);
98+
to = formatRangeEnd(from, timeMode);
9399
setStartDatepicker(new Date(from));
94100
setEndDatepicker(new Date(to));
95101
setFormikValue("fromDate", from);
@@ -129,9 +135,8 @@ const TimeSeriesStatistics = ({
129135
from: string,
130136
timeMode: keyof typeof formatStrings,
131137
) => {
132-
return localizedMoment(from, currentLanguage ? currentLanguage.dateLocale.code : "en").format(
133-
formatStrings[timeMode],
134-
);
138+
const locale = currentLanguage?.dateLocale;
139+
return format(parseISO(from), formatStrings[timeMode], { locale });
135140
};
136141

137142
// change to and from dates in formik to previous timeframe and get new values from API
@@ -141,10 +146,11 @@ const TimeSeriesStatistics = ({
141146
timeMode: TimeMode,
142147
dataResolution: DataResolution,
143148
) => {
144-
const newFrom = moment(from)
145-
// According to the moment.js docs, string is supported as a second argument here
146-
.subtract(1, timeMode + "s" as moment.unitOfTime.DurationConstructor)
147-
.format("YYYY-MM-DD");
149+
if (timeMode === "custom") { return; }
150+
151+
const date = parseISO(from);
152+
const newFromDate = sub(date, { [unitMap[timeMode]]: 1 });
153+
const newFrom = format(newFromDate, "yyyy-MM-dd");
148154
const to = newFrom;
149155
change(setFormikValue, timeMode, newFrom, to, dataResolution);
150156
};
@@ -156,10 +162,11 @@ const TimeSeriesStatistics = ({
156162
timeMode: TimeMode,
157163
dataResolution: DataResolution,
158164
) => {
159-
const newFrom = moment(from)
160-
// According to the moment.js docs, string is supported as a second argument here
161-
.add(1, timeMode + "s" as moment.unitOfTime.DurationConstructor)
162-
.format("YYYY-MM-DD");
165+
if (timeMode === "custom") { return; }
166+
167+
const date = parseISO(from);
168+
const newFromDate = add(date, { [unitMap[timeMode]]: 1 });
169+
const newFrom = format(newFromDate, "yyyy-MM-dd");
163170
const to = newFrom;
164171
change(setFormikValue, timeMode, newFrom, to, dataResolution);
165172
};
@@ -171,12 +178,8 @@ const TimeSeriesStatistics = ({
171178
initialValues={{
172179
timeMode: timeMode,
173180
dataResolution: dataResolution,
174-
// Typescript complains that the method "startOf" cannot take "custom" as a parameter, but in practice
175-
// this does not seem to be a problem
176-
// @ts-expect-error: timeMode should be assignable here
177-
fromDate: moment(fromDate).startOf(timeMode).format("YYYY-MM-DD"),
178-
// @ts-expect-error: timeMode should be assignable here
179-
toDate: moment(toDate).endOf(timeMode).format("YYYY-MM-DD"),
181+
fromDate: formatRangeStart(fromDate, timeMode),
182+
toDate: formatRangeEnd(toDate, timeMode),
180183
}}
181184
onSubmit={() => {}}
182185
>
@@ -282,8 +285,8 @@ const TimeSeriesStatistics = ({
282285
const [startDate, endDate] = dates;
283286
setStartDatepicker(startDate);
284287
setEndDatepicker(endDate);
285-
const newStartDate = startDate ? moment(startDate).format("YYYY-MM-DD") : formik.values.fromDate;
286-
const newEndDate = endDate ? moment(endDate).format("YYYY-MM-DD") : formik.values.toDate;
288+
const newStartDate = startDate ? format(startDate, "yyyy-MM-dd") : formik.values.fromDate;
289+
const newEndDate = endDate ? format(endDate, "yyyy-MM-dd") : formik.values.toDate;
287290
change(
288291
formik.setFieldValue,
289292
formik.values.timeMode,
@@ -304,7 +307,7 @@ const TimeSeriesStatistics = ({
304307
popperPlacement="bottom"
305308
popperClassName="datepicker-custom"
306309
className="datepicker-custom-input"
307-
locale={getCurrentLanguageInformation()?.dateLocale}
310+
locale={getCurrentLanguageInformation(i18n.language)?.dateLocale}
308311
strictParsing
309312
/>
310313
</div>

src/components/shared/wizard/RenderField.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { GroupBase, SelectInstance } from "react-select";
1212
import TextareaAutosize from "react-textarea-autosize";
1313
import { LuCheck, LuSquarePen } from "react-icons/lu";
1414
import axios from "axios";
15+
import i18n from "../../../i18n/i18n";
1516

1617
/**
1718
* This component renders an editable field for single values depending on the type of the corresponding metadata
@@ -188,7 +189,7 @@ const EditableDateValue = ({
188189
popperClassName="datepicker-custom"
189190
className="datepicker-custom-input"
190191
wrapperClassName="datepicker-custom-wrapper"
191-
locale={getCurrentLanguageInformation()?.dateLocale}
192+
locale={getCurrentLanguageInformation(i18n.language)?.dateLocale}
192193
strictParsing
193194
autoFocus={isFirstField}
194195
/>
@@ -319,7 +320,7 @@ const EditableSingleValueTime = ({
319320
popperClassName="datepicker-custom"
320321
className="datepicker-custom-input"
321322
wrapperClassName="datepicker-custom-wrapper"
322-
locale={getCurrentLanguageInformation()?.dateLocale}
323+
locale={getCurrentLanguageInformation(i18n.language)?.dateLocale}
323324
strictParsing
324325
autoFocus={isFirstField}
325326
/>

src/components/systems/partials/MeanQueueTimeCell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Service } from "../../../slices/serviceSlice";
2-
import moment from "moment";
2+
import { formatHMS } from "../../../utils/dateUtils";
33

44
/**
55
* This component renders the mean queue time cells of systems in the table view
@@ -12,7 +12,7 @@ const MeanQueueTimeCell = ({
1212

1313
return (
1414
<span>
15-
{ moment.utc(moment.duration(row.meanQueueTime * 1000).asMilliseconds()).format("HH:mm:ss") }
15+
{formatHMS(row.meanQueueTime)}
1616
</span>
1717
);
1818
};

src/components/systems/partials/MeanRunTimeCell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Service } from "../../../slices/serviceSlice";
2-
import moment from "moment";
2+
import { formatHMS } from "../../../utils/dateUtils";
33

44
/**
55
* This component renders the mean run time cells of systems in the table view
@@ -12,7 +12,7 @@ const MeanRunTimeCell = ({
1212

1313
return (
1414
<span>
15-
{ moment.utc(moment.duration(row.meanRunTime * 1000).asMilliseconds()).format("HH:mm:ss") }
15+
{formatHMS(row.meanQueueTime)}
1616
</span>
1717
);
1818
};

0 commit comments

Comments
 (0)