fix: localize widget chart formatting
This commit is contained in:
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`}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user