From c9b99eabb44fc406e86fa597a1bf54be776b8b6d Mon Sep 17 00:00:00 2001 From: fabiangigler Date: Mon, 31 Aug 2026 15:49:13 +0200 Subject: [PATCH 1/3] Localize monitor timing details --- .../design-elements/MonitorStatus.tsx | 10 ++++- .../Components/monitors/MonitorStatBoxes.tsx | 13 +++--- client/src/Utils/TimeUtils.ts | 43 +++++++++++++++++-- client/src/locales/de.json | 1 + client/src/locales/en.json | 1 + 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/client/src/Components/design-elements/MonitorStatus.tsx b/client/src/Components/design-elements/MonitorStatus.tsx index cc0f8df5b..76934f2b3 100644 --- a/client/src/Components/design-elements/MonitorStatus.tsx +++ b/client/src/Components/design-elements/MonitorStatus.tsx @@ -8,8 +8,11 @@ import { typographyLevels } from "@/Utils/Theme/Palette"; import useMediaQuery from "@mui/material/useMediaQuery"; import { LAYOUT } from "@/Utils/Theme/constants"; import { formatDuration } from "@/Utils/TimeUtils"; +import { useTranslation } from "react-i18next"; + export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { const theme = useTheme(); + const { t, i18n } = useTranslation(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); if (!monitor) { @@ -52,7 +55,12 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { <> - Checking every {formatDuration(monitor?.interval, true)} + {t("pages.common.monitors.statBoxes.checkingEvery", { + interval: formatDuration(monitor.interval, { + long: true, + locale: i18n.resolvedLanguage, + }), + })} )} diff --git a/client/src/Components/monitors/MonitorStatBoxes.tsx b/client/src/Components/monitors/MonitorStatBoxes.tsx index 8b201b474..020eda78a 100644 --- a/client/src/Components/monitors/MonitorStatBoxes.tsx +++ b/client/src/Components/monitors/MonitorStatBoxes.tsx @@ -21,7 +21,7 @@ export const MonitorStatBoxes = ({ domainExpiry, }: MonitorStatBoxesProps) => { const theme = useTheme(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); if (!monitorStats || !monitor) { return null; } @@ -29,13 +29,16 @@ export const MonitorStatBoxes = ({ const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0; const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure : 0; - // Determine time since last check const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0; - const timeSinceLastCheck = Date.now() - timeOfLastCheck || 0; const streakTime = formatDuration(timeSinceLastFailure); - - const lastCheckTime = formatDuration(timeSinceLastCheck); + const lastCheckTime = timeOfLastCheck + ? formatDuration(Date.now() - timeOfLastCheck, { + long: true, + relative: true, + locale: i18n.resolvedLanguage, + }) + : t("common.labels.na"); const isActive = monitor?.status === "up" || monitor?.status === "paused" || diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts index 9b809e1da..d82649c81 100644 --- a/client/src/Utils/TimeUtils.ts +++ b/client/src/Utils/TimeUtils.ts @@ -104,9 +104,46 @@ export const formatMs = (ms: number, hasSpace: boolean = true): string => { return formatted; }; -export const formatDuration = (ms: number, verbose: boolean = false, hasSpace = true) => { - if (verbose) { - return prettyMilliseconds(ms, { verbose: true }); +type DurationUnit = "day" | "hour" | "minute" | "second"; + +interface FormatDurationOptions { + long?: boolean; + relative?: boolean; + hasSpace?: boolean; + locale?: string; +} + +const DURATION_UNITS: Array<{ unit: DurationUnit; milliseconds: number }> = [ + { unit: "day", milliseconds: MS_PER_DAY }, + { unit: "hour", milliseconds: MS_PER_HOUR }, + { unit: "minute", milliseconds: MS_PER_MINUTE }, + { unit: "second", milliseconds: MS_PER_SECOND }, +]; + +const getDurationUnit = (ms: number) => + DURATION_UNITS.find(({ milliseconds }) => Math.abs(ms) >= milliseconds) ?? + DURATION_UNITS[DURATION_UNITS.length - 1]; + +export const formatDuration = ( + ms: number, + { long = false, relative = false, hasSpace = true, locale }: FormatDurationOptions = {} +): string => { + if (long || relative) { + const { unit, milliseconds } = getDurationUnit(ms); + const value = Math.sign(ms) * Math.floor(Math.abs(ms) / milliseconds); + + if (relative) { + return new Intl.RelativeTimeFormat(locale, { + numeric: "always", + style: long ? "long" : "narrow", + }).format(-value, unit); + } + + return new Intl.NumberFormat(locale, { + style: "unit", + unit, + unitDisplay: "long", + }).format(value); } const formatted = prettyMilliseconds(ms, { diff --git a/client/src/locales/de.json b/client/src/locales/de.json index 6a39ee1aa..23db1be49 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -448,6 +448,7 @@ }, "statBoxes": { "activeFor": "Aktiv seit", + "checkingEvery": "Check alle {{interval}}", "certificateExpiry": "Zertifikatablauf", "lastCheck": "Letzter Check", "lastResponseTime": "Letzte Antwortzeit", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index cbebadded..03b69a3bf 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -545,6 +545,7 @@ }, "statBoxes": { "activeFor": "Active for", + "checkingEvery": "Checking every {{interval}}", "certificateExpiry": "Certificate expiry", "domainExpiry": "Domain expiry", "lastCheck": "Last check", From 0f474d82d66e8dbd8426af968da3cd4bbb40b293 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 14 Sep 2026 09:45:09 -0700 Subject: [PATCH 2/3] use exact unit for duration --- client/src/Utils/TimeUtils.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts index d82649c81..2fc9412a4 100644 --- a/client/src/Utils/TimeUtils.ts +++ b/client/src/Utils/TimeUtils.ts @@ -120,30 +120,36 @@ const DURATION_UNITS: Array<{ unit: DurationUnit; milliseconds: number }> = [ { unit: "second", milliseconds: MS_PER_SECOND }, ]; +const FALLBACK_DURATION_UNIT = DURATION_UNITS[DURATION_UNITS.length - 1]; + const getDurationUnit = (ms: number) => DURATION_UNITS.find(({ milliseconds }) => Math.abs(ms) >= milliseconds) ?? - DURATION_UNITS[DURATION_UNITS.length - 1]; + FALLBACK_DURATION_UNIT; + +const getExactDurationUnit = (ms: number) => + DURATION_UNITS.find(({ milliseconds }) => ms !== 0 && ms % milliseconds === 0) ?? + FALLBACK_DURATION_UNIT; export const formatDuration = ( ms: number, { long = false, relative = false, hasSpace = true, locale }: FormatDurationOptions = {} ): string => { - if (long || relative) { + if (relative) { const { unit, milliseconds } = getDurationUnit(ms); const value = Math.sign(ms) * Math.floor(Math.abs(ms) / milliseconds); + return new Intl.RelativeTimeFormat(locale, { + numeric: "always", + style: long ? "long" : "narrow", + }).format(-value, unit); + } - if (relative) { - return new Intl.RelativeTimeFormat(locale, { - numeric: "always", - style: long ? "long" : "narrow", - }).format(-value, unit); - } - + if (long) { + const { unit, milliseconds } = getExactDurationUnit(ms); return new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "long", - }).format(value); + }).format(ms / milliseconds); } const formatted = prettyMilliseconds(ms, { From 308672d558c3bc560f1aba204ae8c31efcb6a94b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 14 Sep 2026 09:45:24 -0700 Subject: [PATCH 3/3] pass locale to formatting --- client/src/Pages/CreateMonitor/index.tsx | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 921f3deb3..31ba8d3aa 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -87,14 +87,18 @@ interface IntervalOption extends AutocompleteOption { const INTERVAL_UNIT_MULTIPLIERS = [1000, 60000, 3600000, 86400000] as const; -const toIntervalOption = (intervalMs: number): IntervalOption => ({ +const toIntervalOption = ( + intervalMs: number, + locale: string | undefined +): IntervalOption => ({ id: intervalMs, - name: formatDuration(intervalMs, true), + name: formatDuration(intervalMs, { long: true, locale }), }); const filterIntervalOptions = ( options: IntervalOption[], - inputValue: string + inputValue: string, + locale: string | undefined ): IntervalOption[] => { const input = inputValue.trim(); if (input === "" || options.some((option) => option.name === input)) { @@ -108,7 +112,7 @@ const filterIntervalOptions = ( return INTERVAL_UNIT_MULTIPLIERS.map((multiplier) => amount * multiplier) .filter(Number.isSafeInteger) - .map(toIntervalOption); + .map((ms) => toIntervalOption(ms, locale)); }; interface GeneralSettingsConfig { @@ -288,7 +292,7 @@ const getGeneralSettingsConfig = ( const CreateMonitorPage = () => { const theme = useTheme(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { monitorId } = useParams(); const location = useLocation(); const navigate = useNavigate(); @@ -509,15 +513,17 @@ const CreateMonitorPage = () => { ); const intervalOptions = useMemo(() => { - const options = MonitorIntervalOptions.map(({ value }) => toIntervalOption(value)); + const options = MonitorIntervalOptions.map(({ value }) => + toIntervalOption(value, i18n.resolvedLanguage) + ); if ( typeof watchedInterval === "number" && !options.some((option) => option.id === watchedInterval) ) { - options.push(toIntervalOption(watchedInterval)); + options.push(toIntervalOption(watchedInterval, i18n.resolvedLanguage)); } return options; - }, [watchedInterval]); + }, [watchedInterval, i18n.resolvedLanguage]); const matchMethodOptions = useMemo( () => @@ -808,7 +814,7 @@ const CreateMonitorPage = () => { )} options={intervalOptions} filterOptions={(options, { inputValue }) => - filterIntervalOptions(options, inputValue) + filterIntervalOptions(options, inputValue, i18n.resolvedLanguage) } getOptionDisabled={(option) => option.id < MIN_MONITOR_INTERVAL_MS} disableClearable