fix: localize widget chart formatting

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:07:08 +02:00
1 parent 2214acd17e
commit 3485ef0877
5 files changed
+52 -40

No files matched your search

@@ -8,7 +8,7 @@ import { ReferenceLine, XAxis } from "recharts";
import { useRequiredBoard } from "@homarr/boards/context";
import type { SpeedtestTrackerResult } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
import { SectionLabel } from "./section-label";
@@ -40,17 +40,17 @@ function buildXAxisTicks(data: { ts: number }[]): XAxisTicks {
return {
midnightTs: foundMidnight,
xTicks: Array.from(tickSet).sort((tsA, tsB) => tsA - tsB),
xTicks: Array.from(tickSet).toSorted((tsA, tsB) => tsA - tsB),
topDateTicks: topTicks,
};
}
function makeXTickRenderer(midnightTs: number | null) {
function makeXTickRenderer(midnightTs: number | null, locale: string) {
return ({ x, y, payload }: { x: number | string; y: number | string; payload: { value: number } }) => {
const date = new Date(payload.value);
const timeStr = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
const timeStr = date.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
const isMidnight = midnightTs !== null && payload.value === midnightTs;
const dateStr = isMidnight ? date.toLocaleDateString([], { month: "short", day: "numeric" }) : null;
const dateStr = isMidnight ? date.toLocaleDateString(locale, { month: "short", day: "numeric" }) : null;
return (
<g transform={`translate(${x},${y})`}>
@@ -67,23 +67,29 @@ function makeXTickRenderer(midnightTs: number | null) {
};
}
function renderTopDateTick(props: {
x: number | string;
y: number | string;
payload: { value: number };
index: number;
}) {
const { x, y, payload, index } = props;
const date = new Date(payload.value);
const dateStr = date.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
const anchor = index === 0 ? "start" : "middle";
return (
<g transform={`translate(${x},${y})`}>
<text x={0} y={-4} textAnchor={anchor} fontSize={10} fill="var(--mantine-color-dimmed)">
{dateStr}
</text>
</g>
);
function makeTopDateTickRenderer(locale: string) {
return ({
x,
y,
payload,
index,
}: {
x: number | string;
y: number | string;
payload: { value: number };
index: number;
}) => {
const date = new Date(payload.value);
const dateStr = date.toLocaleDateString(locale, { weekday: "short", month: "short", day: "numeric" });
const anchor = index === 0 ? "start" : "middle";
return (
<g transform={`translate(${x},${y})`}>
<text x={0} y={-4} textAnchor={anchor} fontSize={10} fill="var(--mantine-color-dimmed)">
{dateStr}
</text>
</g>
);
};
}
function buildYAxisConfig(maxVal: number, stepOptions: { threshold: number; step: number }[]) {
@@ -94,10 +100,10 @@ function buildYAxisConfig(maxVal: number, stepOptions: { threshold: number; step
return { ticks, domain: [0, roundedMax] as [number, number] };
}
function formatTooltipDate(label: number | string | undefined): string {
function formatTooltipDate(label: number | string | undefined, locale: string): string {
const timestamp = typeof label === "number" ? label : Number(label);
if (Number.isNaN(timestamp)) return "";
return new Date(timestamp).toLocaleString([], {
return new Date(timestamp).toLocaleString(locale, {
month: "short",
day: "numeric",
hour: "2-digit",
@@ -157,11 +163,12 @@ function SpeedHistoryChart({ results, height }: { results: SpeedtestTrackerResul
const board = useRequiredBoard();
const theme = useMantineTheme();
const t = useScopedI18n("widget.speedtestTracker");
const locale = useCurrentIntlLocale();
const data = useMemo(
() =>
[...results]
.sort((resultA, resultB) => resultA.created_at.getTime() - resultB.created_at.getTime())
results
.toSorted((resultA, resultB) => resultA.created_at.getTime() - resultB.created_at.getTime())
.filter((result) => (result.download_bits ?? 0) > 0)
.map((result) => ({
ts: result.created_at.getTime(),
@@ -184,7 +191,8 @@ function SpeedHistoryChart({ results, height }: { results: SpeedtestTrackerResul
);
const { midnightTs, xTicks, topDateTicks } = useMemo(() => buildXAxisTicks(data), [data]);
const renderXTick = useMemo(() => makeXTickRenderer(midnightTs), [midnightTs]);
const renderXTick = useMemo(() => makeXTickRenderer(midnightTs, locale), [locale, midnightTs]);
const renderTopDateTick = useMemo(() => makeTopDateTickRenderer(locale), [locale]);
if (data.length === 0) return null;
@@ -230,7 +238,7 @@ function SpeedHistoryChart({ results, height }: { results: SpeedtestTrackerResul
if (!active || !payload?.length) return null;
return (
<ChartTooltip
label={formatTooltipDate(label)}
label={formatTooltipDate(label, locale)}
payload={payload}
valueFormatter={(val: number) => `${val} Mbps`}
/>
@@ -269,11 +277,12 @@ function PingHistoryChart({ results, height }: { results: SpeedtestTrackerResult
const board = useRequiredBoard();
const theme = useMantineTheme();
const t = useScopedI18n("widget.speedtestTracker");
const locale = useCurrentIntlLocale();
const data = useMemo(
() =>
[...results]
.sort((resultA, resultB) => resultA.created_at.getTime() - resultB.created_at.getTime())
results
.toSorted((resultA, resultB) => resultA.created_at.getTime() - resultB.created_at.getTime())
.map((result) =>
result.ping === null
? null
@@ -298,7 +307,7 @@ function PingHistoryChart({ results, height }: { results: SpeedtestTrackerResult
);
const { midnightTs, xTicks } = useMemo(() => buildXAxisTicks(data), [data]);
const renderXTick = useMemo(() => makeXTickRenderer(midnightTs), [midnightTs]);
const renderXTick = useMemo(() => makeXTickRenderer(midnightTs, locale), [locale, midnightTs]);
if (data.length === 0) return null;
@@ -341,7 +350,7 @@ function PingHistoryChart({ results, height }: { results: SpeedtestTrackerResult
if (!active || !payload?.length) return null;
return (
<ChartTooltip
label={formatTooltipDate(label)}
label={formatTooltipDate(label, locale)}
payload={payload}
valueFormatter={(val: number) => `${val} ms`}
/>
+1 -1
View File
@@ -91,7 +91,7 @@ export function UmamiContent({
const { visitorStats } = firstResult;
const chartData = visitorStats.dataPoints.map((point: UmamiVisitorStats["dataPoints"][number]) => ({
label: formatXLabel(point.timestamp, timeFrame),
label: formatXLabel(point.timestamp, timeFrame, locale),
visitors: point.visitors,
...(point.events !== undefined ? { events: point.events } : {}),
}));
@@ -5,7 +5,7 @@ import { Box, Group, Stack, Text, useMantineColorScheme } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import type { UmamiEventSeries } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
import { EVENT_COLORS, formatXLabel, umamiQueryOptions } from "./umami-utils";
@@ -27,6 +27,7 @@ export function UmamiEventsContent({
showXAxis,
}: UmamiEventsContentProps) {
const t = useScopedI18n("widget.umami");
const locale = useCurrentIntlLocale();
const { colorScheme } = useMantineColorScheme();
const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057";
@@ -59,7 +60,7 @@ export function UmamiEventsContent({
);
const chartData = allTimestamps.map((timestamp) => {
const row: Record<string, string | number> = { label: formatXLabel(timestamp, timeFrame) };
const row: Record<string, string | number> = { label: formatXLabel(timestamp, timeFrame, locale) };
for (const serie of series) {
row[serie.eventName] = byEvent.get(serie.eventName)?.get(timestamp) ?? 0;
}
@@ -4,7 +4,7 @@ import { Group, ScrollArea, Stack, Text } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import type { UmamiMetricItem } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
import { umamiQueryOptions } from "./umami-utils";
@@ -48,6 +48,8 @@ function UmamiTopList({
heading: string;
emptyLabel: string;
}) {
const locale = useCurrentIntlLocale();
return (
<Stack gap={2} h="100%">
<Text size="xs" c="dimmed" fw={500}>
@@ -64,7 +66,7 @@ function UmamiTopList({
{item.x || emptyLabel}
</Text>
<Text size="xs" fw={600} flex="0 0 auto">
{item.y.toLocaleString()}
{item.y.toLocaleString(locale)}
</Text>
</Group>
))}
+3 -3
View File
@@ -6,17 +6,17 @@ export const umamiQueryOptions = { refetchInterval: 60_000 } as const;
export const EVENT_COLORS = ["blue.5", "orange.5", "green.5", "red.5", "violet.5", "teal.5", "yellow.5", "pink.5"];
export function formatXLabel(timestamp: string, timeFrame: string): string {
export function formatXLabel(timestamp: string, timeFrame: string, locale: string): string {
const date = new Date(timestamp);
switch (timeFrame) {
case "today":
case "24h":
return `${date.getHours().toString().padStart(2, "0")}:00`;
return date.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit", hourCycle: "h23" });
case "7d":
case "30d":
case "month":
case "lastMonth":
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
return date.toLocaleDateString(locale, { month: "short", day: "numeric" });
default:
return timestamp;
}