mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
fix(insights): localise correlation narratives across all six locales
The generated-insight narrative layer built its user-facing sentences by
string interpolation and never routed through the i18n bundle, so a
non-English UI rendered English text spliced into localised copy — e.g.
"Higher activity steps tends to go with lower next-day weight" surfacing
mid-German on the Weight insight. Because those strings bypassed t(), the
locale-integrity guard could not catch them.
Route every correlation narrative through the server translator:
- Correlation discovery (daily lagged pairs + labs-vs-outcome) now takes
the reader's locale, resolved from the correlations route the same way
other insight generators resolve it. Sentence templates, the direction
word, and every channel label live under a new insights.correlation.*
namespace, translated in de/en/es/fr/it/pl. The three fixed hypotheses
(BP-compliance, mood-pulse, weight-weekday) and their weekday labels are
localised the same way; the analytics cache key now includes the locale
so a language switch cannot serve another locale's cached text.
- Metric labels resolve to stable localised channel labels; the never-
causal framing ("not a cause" / "an association, never a cause") and the
faint/moderate/high hedging tiers are preserved in every language.
English output is byte-for-byte unchanged. Adds a regression test pinning
German narration and asserting it is not the English string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018V8V9qapgHi7fhgtMJ8SkM
This commit is contained in:
1 parent
36f73233b0
commit
04320c415d
12 files changed
+566
-67
No files matched your search
@@ -22,6 +22,8 @@ import { resolveRestMode } from "@/lib/illness/rest-mode";
|
||||
import { buildHealthScoreBpInputs } from "@/lib/analytics/health-score-inputs";
|
||||
import { deriveBpWindow90 } from "@/lib/analytics/window-confidence";
|
||||
import { computeCorrelationHypothesesFastPath } from "@/lib/analytics/correlations-fast-path";
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
import { type Locale } from "@/lib/i18n/config";
|
||||
import {
|
||||
computeGlucoseClinicalMetrics,
|
||||
GLUCOSE_PANEL_WINDOW_DAYS,
|
||||
@@ -96,12 +98,18 @@ export const GET = apiHandler(async (request?: Request) => {
|
||||
// thick body is the 30-query chain feeding the dashboard + insights
|
||||
// hero score; after a measurement sync the next mount serves the
|
||||
// prior body instantly while one background recompute refreshes it.
|
||||
// v1.30.10 — the `correlations` block carries user-facing narration that is
|
||||
// localised per reader, so the cache key MUST include the locale; otherwise a
|
||||
// language switch would serve another locale's cached interpretation for up to
|
||||
// the TTL. Every other block is locale-independent, so per-locale entries only
|
||||
// cost a little duplication (most users have one locale).
|
||||
const locale = await resolveServerLocale({ userLocale: user.locale ?? null });
|
||||
const cachedBody = await cachedSwr(
|
||||
caches.analytics as ServerCache<
|
||||
Awaited<ReturnType<typeof buildAnalyticsResponse>>
|
||||
>,
|
||||
`${user.id}|default`,
|
||||
() => buildAnalyticsResponse(user),
|
||||
`${user.id}|default|${locale}`,
|
||||
() => buildAnalyticsResponse(user, locale),
|
||||
annotate,
|
||||
);
|
||||
|
||||
@@ -173,7 +181,7 @@ function enrichLastSeenDaysAgo<
|
||||
* matches the v1.4.33 snapshot LRU's `buildCoachSnapshot` →
|
||||
* `buildCoachSnapshotImpl` shape.
|
||||
*/
|
||||
async function buildAnalyticsResponse(user: AuthedUser) {
|
||||
async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
// v1.4.37.1 hotfix — fire-and-forget the rollup-fresh check.
|
||||
// Awaiting it on the read path can stall the Node event loop for
|
||||
// 30–60 s on a power-user account whose iOS step samples keep the
|
||||
@@ -405,6 +413,7 @@ async function buildAnalyticsResponse(user: AuthedUser) {
|
||||
userTz,
|
||||
now: new Date(),
|
||||
coverage,
|
||||
locale,
|
||||
});
|
||||
|
||||
// v1.4.20 phase B5 — Personal Health Score. Server-deterministic
|
||||
|
||||
@@ -21,6 +21,7 @@ import { annotate } from "@/lib/logging/context";
|
||||
import { checkAnalyticsReadRateLimit } from "@/lib/rate-limit";
|
||||
import { requireAssistantSurface } from "@/lib/feature-flags";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { wallClockInTz } from "@/lib/tz/wall-clock";
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
@@ -82,6 +83,11 @@ export const GET = apiHandler(async () => {
|
||||
select: { timezone: true },
|
||||
});
|
||||
const tz = profile?.timezone ?? "Europe/Berlin";
|
||||
// Reader's locale for the narrated `interpretation` — the correlation cards
|
||||
// render this string verbatim, so it MUST be localised (cookie / User.locale /
|
||||
// Accept-Language). Without it the never-causal sentence leaked English into a
|
||||
// non-English UI.
|
||||
const locale = await resolveServerLocale({ userLocale: user.locale ?? null });
|
||||
const now = new Date();
|
||||
const since = new Date(now.getTime() - WINDOW_DAYS * MS_PER_DAY);
|
||||
|
||||
@@ -165,7 +171,7 @@ export const GET = apiHandler(async () => {
|
||||
series.push(envSeries);
|
||||
}
|
||||
|
||||
const result = discoverCorrelations(series);
|
||||
const result = discoverCorrelations(series, { locale });
|
||||
|
||||
// v1.22 — rolling early-detection pass over the trailing window, re-using
|
||||
// the already-built series (no extra DB read). Emerging pairs exclude anything
|
||||
@@ -176,11 +182,14 @@ export const GET = apiHandler(async () => {
|
||||
);
|
||||
const emerging = discoverEmergingCorrelations(series, result, {
|
||||
recentFromDayKey,
|
||||
locale,
|
||||
});
|
||||
|
||||
// v1.22 — labs ↔ outcome pass (point-vs-window over sparse draws). Degrades
|
||||
// to absent when the user has too few draws to clear the per-pair floor.
|
||||
const labCorrelations = discoverLabOutcomeCorrelations(labDraws, series);
|
||||
const labCorrelations = discoverLabOutcomeCorrelations(labDraws, series, {
|
||||
locale,
|
||||
});
|
||||
|
||||
annotate({
|
||||
action: { name: "insights.correlations.discover" },
|
||||
|
||||
Reference in new issue
Block a user