From 8302f5f62aaf36e915d6b9ec78f68ea4c6c4af08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 19:04:30 +0200 Subject: [PATCH] Write mood insight figures with the reader's decimal separator Averages, deltas and correlation figures on the mood insights went through toFixed and read '3.4' in German. They use the locale formatter now, and the whole-number crosstab cells pick up thousands grouping. --- .../insights/mood/mood-better-days.tsx | 7 ++++--- .../insights/mood/mood-context-comparison.tsx | 7 ++++--- .../insights/mood/mood-correlation-cards.tsx | 7 ++++--- .../mood/mood-discovered-relations.tsx | 7 ++++--- .../mood/mood-factor-metric-crosstab.tsx | 18 ++++++++++++------ .../mood/mood-structured-tag-breakdown.tsx | 5 +++-- .../insights/mood/mood-tag-breakdown.tsx | 5 +++-- .../insights/mood/mood-tag-influence.tsx | 9 +++++---- .../insights/mood/mood-tag-metric-crosstab.tsx | 18 ++++++++++++------ .../insights/mood/mood-time-of-day-chart.tsx | 5 +++-- .../insights/mood/mood-weekday-chart.tsx | 5 +++-- 11 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src/components/insights/mood/mood-better-days.tsx b/src/components/insights/mood/mood-better-days.tsx index 07b507214..55738c513 100644 --- a/src/components/insights/mood/mood-better-days.tsx +++ b/src/components/insights/mood/mood-better-days.tsx @@ -2,7 +2,7 @@ import { ArrowDownRight, ArrowUpRight } from "lucide-react"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { moodTagIcon } from "@/components/mood/mood-tag-icons"; import { cn } from "@/lib/utils"; import type { MoodInfluenceConfidence } from "./mood-tag-influence"; @@ -70,6 +70,7 @@ export function MoodBetterDays({ factors: MoodBetterDayFactor[]; }) { const { t } = useTranslations(); + const nf = useFormatters(); if (factors.length === 0) return null; return ( @@ -108,9 +109,9 @@ export function MoodBetterDays({ // metric factors show the correlation coefficient. const effectText = factor.source === "tag" && factor.delta != null - ? `${factor.delta >= 0 ? "+" : ""}${factor.delta.toFixed(1)}` + ? `${factor.delta >= 0 ? "+" : ""}${nf.number(factor.delta, 1)}` : factor.r != null - ? `r ${factor.r.toFixed(2)}` + ? `r ${nf.number(factor.r, 2)}` : ""; return ( diff --git a/src/components/insights/mood/mood-context-comparison.tsx b/src/components/insights/mood/mood-context-comparison.tsx index 3df6ff9ba..4bcf8a63e 100644 --- a/src/components/insights/mood/mood-context-comparison.tsx +++ b/src/components/insights/mood/mood-context-comparison.tsx @@ -16,7 +16,7 @@ * them; a second threshold in the presentation layer would be a second place * to get the statistics wrong. */ -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { contextValueLabelKey } from "@/lib/mood/context-vocabulary"; import type { ContextComparisonRow } from "@/lib/insights/mood-context-crosstab"; @@ -26,6 +26,7 @@ export function MoodContextComparison({ rows: ContextComparisonRow[]; }) { const { t } = useTranslations(); + const nf = useFormatters(); if (rows.length === 0) return null; return ( @@ -50,8 +51,8 @@ export function MoodContextComparison({ {t("insights.mood.contextComparison.statement", { label, direction, - withAvg: row.withAvg.toFixed(1), - withoutAvg: row.withoutAvg.toFixed(1), + withAvg: nf.number(row.withAvg, 1), + withoutAvg: nf.number(row.withoutAvg, 1), })}

{/* The counts ride with the statement, in the same block a diff --git a/src/components/insights/mood/mood-correlation-cards.tsx b/src/components/insights/mood/mood-correlation-cards.tsx index 804bf52ff..36c07db54 100644 --- a/src/components/insights/mood/mood-correlation-cards.tsx +++ b/src/components/insights/mood/mood-correlation-cards.tsx @@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { TileHeader } from "@/components/insights/tile-header"; import { EmptyState } from "@/components/ui/empty-state"; import { Skeleton } from "@/components/ui/skeleton"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; import type { CorrelationResult } from "@/lib/analytics/correlations"; import { MoodExplainerIcon } from "./mood-explainer-icon"; @@ -106,6 +106,7 @@ function MoodCorrelationCard({ className?: string; }) { const { t } = useTranslations(); + const nf = useFormatters(); const hasResult = data.result != null; // Three different silences, and the reader deserves to know which one they @@ -164,7 +165,7 @@ function MoodCorrelationCard({ label={t("insights.mood.correlation.sourceLabel")} detail={t("insights.mood.correlation.source", { n: data.n, - r: data.result.r.toFixed(2), + r: nf.number(data.result.r, 2), })} /> @@ -179,7 +180,7 @@ function MoodCorrelationCard({ label={t("insights.mood.correlation.sourceLabel")} detail={t("insights.mood.correlation.source", { n: data.n, - r: data.result.r.toFixed(2), + r: nf.number(data.result.r, 2), })} /> ) : null} diff --git a/src/components/insights/mood/mood-discovered-relations.tsx b/src/components/insights/mood/mood-discovered-relations.tsx index 53bca5e81..4a9adf235 100644 --- a/src/components/insights/mood/mood-discovered-relations.tsx +++ b/src/components/insights/mood/mood-discovered-relations.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { MoodExplainerIcon } from "./mood-explainer-icon"; import { PatternDismissButton, @@ -94,6 +94,7 @@ export function MoodDiscoveredRelations({ pairsTested: number; }) { const { t } = useTranslations(); + const nf = useFormatters(); const dismissal = usePatternDismissalOverrides(); if (pairs.length === 0) return null; @@ -169,8 +170,8 @@ export function MoodDiscoveredRelations({ label={t("insights.mood.discovery.statLabel")} detail={t("insights.mood.discovery.stat", { n: pair.n, - r: pair.r.toFixed(2), - q: pair.qValue.toFixed(3), + r: nf.number(pair.r, 2), + q: nf.number(pair.qValue, 3), })} /> diff --git a/src/components/insights/mood/mood-factor-metric-crosstab.tsx b/src/components/insights/mood/mood-factor-metric-crosstab.tsx index 65d1b5a4e..6cd598af6 100644 --- a/src/components/insights/mood/mood-factor-metric-crosstab.tsx +++ b/src/components/insights/mood/mood-factor-metric-crosstab.tsx @@ -1,6 +1,7 @@ "use client"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; +import type { Formatters } from "@/lib/format-locale"; import { useUnitDisplay } from "@/hooks/use-unit-display"; import { moodTagIcon } from "@/components/mood/mood-tag-icons"; import { cn } from "@/lib/utils"; @@ -84,8 +85,12 @@ const CONFIDENCE_CLASS: Record = { }; /** Whole numbers for steps; one decimal otherwise. */ -function fmt(value: number, display: MoodFactorCrosstabDisplay): string { - return display === "steps" ? Math.round(value).toString() : value.toFixed(1); +function formatValue( + value: number, + display: MoodFactorCrosstabDisplay, + nf: Formatters, +): string { + return display === "steps" ? nf.integer(value) : nf.number(value, 1); } export function MoodFactorMetricCrosstab({ @@ -94,6 +99,7 @@ export function MoodFactorMetricCrosstab({ rows: MoodFactorMetricCrosstabRow[]; }) { const { t } = useTranslations(); + const nf = useFormatters(); const unitDisplay = useUnitDisplay(); const dismissal = usePatternDismissalOverrides(); if (rows.length === 0) return null; @@ -143,7 +149,7 @@ export function MoodFactorMetricCrosstab({ // green/red would imply a health verdict the data doesn't support. // The sign prefix + `data-direction` carry the direction. const up = row.delta >= 0; - const deltaText = `${up ? "+" : ""}${fmt(conv(row.delta), row.display)} ${unit}`; + const deltaText = `${up ? "+" : ""}${formatValue(conv(row.delta), row.display, nf)} ${unit}`; return (
  • Math.max(m, row.count), 0) || 1; return ( @@ -74,7 +75,7 @@ export function MoodStructuredTagBreakdown({ style={{ color: colorForScore(row.avgScore) }} title={t("insights.mood.tagAvgMood")} > - {row.avgScore.toFixed(1)} + {nf.number(row.avgScore, 1)}
  • ); diff --git a/src/components/insights/mood/mood-tag-breakdown.tsx b/src/components/insights/mood/mood-tag-breakdown.tsx index 2e4c3d5bc..53e63793e 100644 --- a/src/components/insights/mood/mood-tag-breakdown.tsx +++ b/src/components/insights/mood/mood-tag-breakdown.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; /** * v1.8.5 — tag / trigger breakdown. @@ -30,6 +30,7 @@ function colorForScore(score: number): string { export function MoodTagBreakdown({ tags }: { tags: MoodTagRow[] }) { const { t } = useTranslations(); + const nf = useFormatters(); const maxCount = tags.reduce((m, row) => Math.max(m, row.count), 0) || 1; return ( @@ -61,7 +62,7 @@ export function MoodTagBreakdown({ tags }: { tags: MoodTagRow[] }) { style={{ color: colorForScore(row.avgScore) }} title={t("insights.mood.tagAvgMood")} > - {row.avgScore.toFixed(1)} + {nf.number(row.avgScore, 1)} ))} diff --git a/src/components/insights/mood/mood-tag-influence.tsx b/src/components/insights/mood/mood-tag-influence.tsx index 619c2d699..4db56866a 100644 --- a/src/components/insights/mood/mood-tag-influence.tsx +++ b/src/components/insights/mood/mood-tag-influence.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { moodTagIcon } from "@/components/mood/mood-tag-icons"; import { cn } from "@/lib/utils"; @@ -62,6 +62,7 @@ function deltaColor(delta: number): string { export function MoodTagInfluence({ rows }: { rows: MoodTagInfluenceRow[] }) { const { t } = useTranslations(); + const nf = useFormatters(); if (rows.length === 0) return null; return ( @@ -71,7 +72,7 @@ export function MoodTagInfluence({ rows }: { rows: MoodTagInfluenceRow[] }) { const Icon = row.labelKey ? moodTagIcon(row.icon) : null; const label = row.label ?? (row.labelKey ? t(row.labelKey) : row.tag); const up = row.delta >= 0; - const deltaText = `${up ? "+" : ""}${row.delta.toFixed(1)}`; + const deltaText = `${up ? "+" : ""}${nf.number(row.delta, 1)}`; return (
  • {t("insights.mood.influence.detail", { - withAvg: row.withAvg.toFixed(1), - withoutAvg: row.withoutAvg.toFixed(1), + withAvg: nf.number(row.withAvg, 1), + withoutAvg: nf.number(row.withoutAvg, 1), withDays: row.withDays, })}

    diff --git a/src/components/insights/mood/mood-tag-metric-crosstab.tsx b/src/components/insights/mood/mood-tag-metric-crosstab.tsx index 9263fbaee..60d926a38 100644 --- a/src/components/insights/mood/mood-tag-metric-crosstab.tsx +++ b/src/components/insights/mood/mood-tag-metric-crosstab.tsx @@ -1,6 +1,7 @@ "use client"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; +import type { Formatters } from "@/lib/format-locale"; import { moodTagIcon } from "@/components/mood/mood-tag-icons"; import { cn } from "@/lib/utils"; import type { MoodInfluenceConfidence } from "./mood-tag-influence"; @@ -92,8 +93,12 @@ const CONFIDENCE_CLASS: Record = { }; /** One decimal for hours/score, whole numbers for kcal. */ -function fmt(value: number, display: MoodCrosstabDisplay): string { - return display === "kcal" ? Math.round(value).toString() : value.toFixed(1); +function formatValue( + value: number, + display: MoodCrosstabDisplay, + nf: Formatters, +): string { + return display === "kcal" ? nf.integer(value) : nf.number(value, 1); } export function MoodTagMetricCrosstab({ @@ -102,6 +107,7 @@ export function MoodTagMetricCrosstab({ rows: MoodTagMetricCrosstabRow[]; }) { const { t } = useTranslations(); + const nf = useFormatters(); const dismissal = usePatternDismissalOverrides(); if (rows.length === 0) return null; @@ -135,7 +141,7 @@ export function MoodTagMetricCrosstab({ ); const unit = t(UNIT_KEY[row.display]); const up = row.delta >= 0; - const deltaText = `${up ? "+" : ""}${fmt(row.delta, row.display)} ${unit}`; + const deltaText = `${up ? "+" : ""}${formatValue(row.delta, row.display, nf)} ${unit}`; return (
  • ({ bucket: row.bucket, @@ -125,7 +126,7 @@ export function MoodTimeOfDayChart({ return [t("insights.mood.weekdayNoData"), ""]; } return [ - `${payload.avgScore.toFixed(1)} (${payload.count})`, + `${nf.number(payload.avgScore, 1)} (${payload.count})`, t("insights.mood.timeOfDay.title"), ]; }} diff --git a/src/components/insights/mood/mood-weekday-chart.tsx b/src/components/insights/mood/mood-weekday-chart.tsx index a0d69225f..ef53ee877 100644 --- a/src/components/insights/mood/mood-weekday-chart.tsx +++ b/src/components/insights/mood/mood-weekday-chart.tsx @@ -12,7 +12,7 @@ import { YAxis, } from "recharts"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; /** * v1.8.5 — average mood per weekday (Monday-aligned). @@ -50,6 +50,7 @@ function colorForScore(score: number): string { export function MoodWeekdayChart({ weekday }: { weekday: MoodWeekdayRow[] }) { const { t } = useTranslations(); + const nf = useFormatters(); const data = weekday.map((row) => ({ weekday: row.weekday, @@ -134,7 +135,7 @@ export function MoodWeekdayChart({ weekday }: { weekday: MoodWeekdayRow[] }) { return [t("insights.mood.weekdayNoData"), ""]; } return [ - `${payload.avgScore.toFixed(1)} (${payload.count})`, + `${nf.number(payload.avgScore, 1)} (${payload.count})`, t("insights.mood.weekdayTitle"), ]; }}