Proper i18n date/time handling (#17858)

* install date-fns-tz

* add date locale hook

* refactor formatUnixTimestampToDateTime

Use date-fns style instead of using strftime. This requires changing the i18n keys to the way date-fns represents dates (eg: "MMM d, h:mm:ss aaa"  instead of "%b %-d, %H:%M"

* refactor calendar to use new hook

* fix useFormattedTimestamp to use new formatUnixTimestampToDateTime date_format

* change i18n keys to new format

* fix timeline

* fix review

* fix explore

* fix metrics

* fix notifications

* fix face library

* clean up
This commit is contained in:
Josh Hawkins
2025-04-22 15:50:21 -06:00
committed by GitHub
parent 645c84bc1a
commit b6e0e5698a
23 changed files with 356 additions and 176 deletions
+64
View File
@@ -0,0 +1,64 @@
import { useState, useEffect } from "react";
import { enUS, Locale } from "date-fns/locale";
import { useTranslation } from "react-i18next";
// Map of locale codes to dynamic import functions
const localeMap: Record<string, () => Promise<Locale>> = {
"zh-CN": () => import("date-fns/locale/zh-CN").then((module) => module.zhCN),
es: () => import("date-fns/locale/es").then((module) => module.es),
hi: () => import("date-fns/locale/hi").then((module) => module.hi),
fr: () => import("date-fns/locale/fr").then((module) => module.fr),
ar: () => import("date-fns/locale/ar").then((module) => module.ar),
pt: () => import("date-fns/locale/pt").then((module) => module.pt),
ru: () => import("date-fns/locale/ru").then((module) => module.ru),
de: () => import("date-fns/locale/de").then((module) => module.de),
ja: () => import("date-fns/locale/ja").then((module) => module.ja),
tr: () => import("date-fns/locale/tr").then((module) => module.tr),
it: () => import("date-fns/locale/it").then((module) => module.it),
nl: () => import("date-fns/locale/nl").then((module) => module.nl),
sv: () => import("date-fns/locale/sv").then((module) => module.sv),
cs: () => import("date-fns/locale/cs").then((module) => module.cs),
nb: () => import("date-fns/locale/nb").then((module) => module.nb),
ko: () => import("date-fns/locale/ko").then((module) => module.ko),
vi: () => import("date-fns/locale/vi").then((module) => module.vi),
fa: () => import("date-fns/locale/fa-IR").then((module) => module.faIR),
pl: () => import("date-fns/locale/pl").then((module) => module.pl),
uk: () => import("date-fns/locale/uk").then((module) => module.uk),
he: () => import("date-fns/locale/he").then((module) => module.he),
el: () => import("date-fns/locale/el").then((module) => module.el),
ro: () => import("date-fns/locale/ro").then((module) => module.ro),
hu: () => import("date-fns/locale/hu").then((module) => module.hu),
fi: () => import("date-fns/locale/fi").then((module) => module.fi),
da: () => import("date-fns/locale/da").then((module) => module.da),
sk: () => import("date-fns/locale/sk").then((module) => module.sk),
};
export function useDateLocale(): Locale {
const { i18n } = useTranslation();
const [locale, setLocale] = useState<Locale>(enUS);
useEffect(() => {
const loadLocale = async () => {
if (i18n.language === "en") {
setLocale(enUS);
return;
}
const localeLoader = localeMap[i18n.language];
if (localeLoader) {
try {
const loadedLocale = await localeLoader();
setLocale(loadedLocale);
} catch (error) {
setLocale(enUS);
}
} else {
setLocale(enUS);
}
};
loadLocale();
}, [i18n.language]);
return locale;
}
+18 -9
View File
@@ -1,33 +1,42 @@
import { FrigateConfig } from "@/types/frigateConfig";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useMemo } from "react";
import { useDateLocale } from "@/hooks/use-date-locale";
export function useFormattedTimestamp(
timestamp: number,
format: string,
timezone?: string,
) {
const locale = useDateLocale();
const formattedTimestamp = useMemo(() => {
return formatUnixTimestampToDateTime(timestamp, {
timezone,
strftime_fmt: format,
date_format: format,
locale,
});
}, [format, timestamp, timezone]);
}, [format, timestamp, timezone, locale]);
return formattedTimestamp;
}
export function useFormattedRange(start: number, end: number, format: string) {
const locale = useDateLocale();
const formattedStart = useMemo(() => {
return formatUnixTimestampToDateTime(start, {
strftime_fmt: format,
date_format: format,
locale,
});
}, [format, start]);
}, [format, start, locale]);
const formattedEnd = useMemo(() => {
return formatUnixTimestampToDateTime(end, {
strftime_fmt: format,
date_format: format,
locale,
});
}, [format, end]);
}, [format, end, locale]);
return `${formattedStart} - ${formattedEnd}`;
}
@@ -44,7 +53,7 @@ export function useTimezone(config: FrigateConfig | undefined) {
}, [config]);
}
function use24HourTime(config: FrigateConfig | undefined) {
export function use24HourTime(config: FrigateConfig | undefined) {
const localeUses24HourTime = useMemo(
() =>
new Intl.DateTimeFormat(undefined, {
@@ -60,8 +69,8 @@ function use24HourTime(config: FrigateConfig | undefined) {
return false;
}
if (config.ui.time_format != "browser") {
return config.ui.time_format == "24hour";
if (config.ui.time_format !== "browser") {
return config.ui.time_format === "24hour";
}
return localeUses24HourTime;
+18 -5
View File
@@ -3,6 +3,8 @@ import { useTimelineUtils } from "./use-timeline-utils";
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useDateLocale } from "./use-date-locale";
import { useTranslation } from "react-i18next";
type DraggableElementProps = {
contentRef: React.RefObject<HTMLElement>;
@@ -162,17 +164,28 @@ function useDraggableElement({
[segmentDuration, timelineStartAligned, segmentHeight],
);
const { t } = useTranslation(["common"]);
const locale = useDateLocale();
const timeFormat = config?.ui.time_format === "24hour" ? "24hour" : "12hour";
const format = useMemo(() => {
const formatKey = `time.${
segmentDuration < 60 && !dense
? "formattedTimestampHourMinuteSecond"
: "formattedTimestampHourMinute"
}.${timeFormat}`;
return t(formatKey);
}, [t, timeFormat, segmentDuration, dense]);
const getFormattedTimestamp = useCallback(
(segmentStartTime: number) => {
return formatUnixTimestampToDateTime(segmentStartTime, {
timezone: config?.ui.timezone,
strftime_fmt:
config?.ui.time_format == "24hour"
? `%H:%M${segmentDuration < 60 && !dense ? ":%S" : ""}`
: `%I:%M${segmentDuration < 60 && !dense ? ":%S" : ""} %p`,
date_format: format,
locale,
});
},
[config, dense, segmentDuration],
[config?.ui.timezone, format, locale],
);
const updateDraggableElementPosition = useCallback(