fix(insights): ask the empty-state Coach question in the reader's language

A metric page without data offers to ask the Coach about the metric.
44 of the 49 pages passed that question as an English literal, so a
German, French or Korean reader got an English first message; only 5
read it from the catalog. 31 of the 44 were one sentence with the
metric's name swapped in.

`HealthKitMetricPage` now asks that sentence once, translated, around
the page's own title, and the 31 pages drop the prop. The 13 pages
with a question of their own (steps, HRV, sleep, mood, workouts, blood
pressure and others) read it from the catalog like the 5 did, in all
ten languages.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:09:23 +01:00
1 parent b970ea7468
commit eec52534d5
57 files changed
+395 -189

No files matched your search

@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nProvider } from "@/lib/i18n/context";
import { primeMessages } from "@/lib/i18n/load-locale";
import deMessages from "../../../../messages/de.json";
primeMessages("de", deMessages);
/**
* A metric page with no data offers the Coach a question in the reader's
* language. The pages used to pass their own English sentence, and most of
* them were one sentence with the metric's name swapped in; the page now asks
* that one, translated, with its own title, and a metric with a question of
* its own reads it from the catalog too.
*/
const prefills: Array<string | null | undefined> = [];
vi.mock("@/components/insights/metric-empty-state", () => ({
MetricEmptyState: ({ coachPrefill }: { coachPrefill?: string | null }) => {
prefills.push(coachPrefill);
return null;
},
}));
vi.mock("@/hooks/use-ai-capability", () => ({
useAiCapability: () => ({ available: true, reason: null }),
}));
vi.mock("next/dynamic", () => ({ default: () => () => null }));
vi.mock("next/navigation", () => ({
usePathname: () => "/insights/visceral-fat",
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({
user: { timezone: "UTC", dateOfBirth: null, gender: null },
isAuthenticated: true,
}),
}));
vi.mock("@/hooks/use-insights-layout-prefs", () => ({
useInsightsLayoutPrefs: () => ({ layout: null, compareBaseline: false }),
}));
vi.mock("@/hooks/use-insights-analytics", () => ({
useInsightsAnalytics: () => ({ data: { summaries: {} }, isEmpty: true }),
}));
import InsightsViszeralfettPage from "@/app/insights/visceral-fat/page";
import InsightsStepsPage from "@/app/insights/steps/page";
function emptyStatePrefill(node: React.ReactNode, locale: "en" | "de") {
prefills.length = 0;
renderToStaticMarkup(
<QueryClientProvider client={new QueryClient()}>
<I18nProvider initialLocale={locale}>{node}</I18nProvider>
</QueryClientProvider>,
);
return prefills.at(-1);
}
describe("the empty metric page's Coach question", () => {
it("is the shared sentence with the page's title, in the reader's language", () => {
expect(emptyStatePrefill(<InsightsViszeralfettPage />, "de")).toBe(
"Viszeralfett: Ich habe hier noch nichts erfasst. Was sagt dieser Wert über meine Gesundheit aus, und wie verbessere ich ihn?",
);
expect(emptyStatePrefill(<InsightsViszeralfettPage />, "en")).toBe(
"Visceral Fat: I haven't logged anything here yet. What does this metric tell me about my health, and how do I improve it?",
);
});
it("keeps a metric's own question, translated", () => {
expect(emptyStatePrefill(<InsightsStepsPage />, "de")).toBe(
"Ich habe noch keine Schritte erfasst. Was ist ein sinnvolles tägliches Schrittziel, und wie hilft mehr Gehen meiner Gesundheit?",
);
});
});
@@ -148,9 +148,8 @@ export interface HealthKitMetricPageProps {
*/
statIcon?: ComponentType<{ className?: string }>;
/**
* Optional Coach prefill for the empty-state launch. Falls back to a
* generic onboarding prompt threaded through `<CoachLaunchButton>`'s
* default.
* The empty state's Coach question, for a metric that asks its own. Without
* it the page asks the shared, translated one with its own title.
*/
coachPrefill?: string;
/**
@@ -223,6 +222,17 @@ export interface HealthKitMetricPageProps {
afterAssessment?: ReactNode;
}
/**
* The empty state's Coach question for a metric that asks none of its own:
* one translated sentence around the page's title.
*/
function sharedCoachPrefill(
t: ReturnType<typeof useTranslations>["t"],
metric: string,
): string {
return t("insights.subPage.emptyCoachPrefill", { metric });
}
export function HealthKitMetricPage({
measurementType,
fallbackMeasurementType,
@@ -424,7 +434,7 @@ export function HealthKitMetricPage({
title={t(`${i18nPrefix}.emptyState.title`)}
description={t(`${i18nPrefix}.emptyState.description`)}
cta={ctaNode}
coachPrefill={coachPrefill ?? null}
coachPrefill={coachPrefill ?? sharedCoachPrefill(t, title)}
/>
</SubPageShell>
);