Files
HealthLog/src/app/layout.tsx
T
Marc-André Bombeck 3c4bbdef4f feat(i18n): MaintainershipBanner notice + foundation for 4 new locales
Add the four AI-initial locale bundles (FR / ES / IT / PL) and a
small dismissible MaintainershipBanner strip that paints at the top
of the auth shell whenever the active locale falls outside the
maintainer-curated set (currently DE + EN). The banner names the
AI-initial provenance and links to a GitHub translation-feedback
template so users can contribute fixes back. Dismissal persists
per-locale in localStorage so dismissing the FR banner does not
hide the IT banner once the user switches language.

The bundles are generated by scripts/i18n/build-locale.py — a layered
walk of messages/en.json that applies a vocabulary lookup keyed on
the lowercase EN value, preserves every {interpolation} placeholder
and <tag> marker verbatim, and falls through to the English value
for long-tail prose the script can't translate. The banner advertises
this status so users know what they are looking at. All four locale
files pass the i18n-locale-integrity test verbatim — full key
parity to en.json, no empty values, no TODO/FIXME placeholders,
no value-equals-last-segment matches.

Foundation work bundled with the banner so the build never lands in
a half-wired state:
- locales array extended to ["de","en","fr","es","it","pl"] with
  matching localeLabels + MAINTAINED_LOCALES set + isMaintainedLocale
  helper in src/lib/i18n/config.ts.
- INTL_LOCALE_MAP and parseLocaleFromAcceptLanguage in
  format-locale.ts gained the four new tags so SSR locale resolution
  and Intl number/date formatting work end-to-end.
- i18n/context.tsx and i18n/server-translator.ts import the four new
  JSON bundles alongside DE + EN.
- Coach (src/lib/ai/coach/system-prompt.ts) and the strict insights
  generator (src/lib/ai/prompts/insight-generator.ts) ship the EN
  system prompt with a one-line REPLY LANGUAGE footer for the four
  AI-initial locales. The DE branch keeps its hand-curated body
  verbatim. Routing through the EN system prompt preserves every
  safety contract (zero hallucinations, no dose-prescription,
  evidence-block sentinel format) without re-validating four new
  language bodies.
- Five locale-parameter signatures (no-key-fallbacks, glp1-plateau,
  time-window-format, target-prompts, medication-form) widened
  from "de" | "en" to the canonical Locale type. Helpers that
  ship DE+EN copy route non-DE locales to the EN body, mirroring
  the same fallback chain the JSON bundles use.
- OpenGraph alternateLocale on the root layout lists the four new
  BCP-47 tags so social-link previews advertise the broader
  language coverage.

Tests:
- 4 new SSR tests pin the locale gate (EN/DE hidden, FR/ES/IT/PL
  visible by default, dismissed flag honoured) and the re-exported
  isMaintainedLocale helper.
- parseLocaleFromAcceptLanguage gains a FR/ES/IT/PL coverage block
  alongside the existing DE / EN / unknown branches.
- The existing i18n-locale-integrity suite auto-discovers each new
  messages/<locale>.json and runs the 25-sub-test parity + value-
  quality battery against it — all six locales pass.

LocalePicker is data-driven (settings and onboarding both iterate
the locales array) so promoting the array to 6 entries grows both
dropdowns without per-call-site changes. The PDF doctor-report
renderer is t()-driven for every label, so the four new JSON
bundles give that renderer FR/ES/IT/PL output without touching
doctor-report-pdf-core.ts.
2026-05-14 15:40:34 +02:00

105 lines
3.3 KiB
TypeScript

import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import { headers, cookies } from "next/headers";
import "./globals.css";
import { Providers } from "@/components/providers";
import { AuthShell } from "@/components/layout/auth-shell";
import { MonitoringBootstrap } from "@/components/monitoring/bootstrap";
import { parseLocaleFromAcceptLanguage } from "@/lib/format-locale";
import { locales, type Locale } from "@/lib/i18n/config";
async function resolveInitialLocale(): Promise<Locale> {
// Both cookies() and headers() can throw (DynamicServerError, etc.) —
// fall back to the default so a locale hiccup never crashes the root
// layout into global-error.tsx.
try {
const cookieStore = await cookies();
const cookieLocale = cookieStore.get("healthlog-locale")?.value;
if (cookieLocale && (locales as readonly string[]).includes(cookieLocale)) {
return cookieLocale as Locale;
}
const headerList = await headers();
return parseLocaleFromAcceptLanguage(headerList.get("accept-language"));
} catch {
return "en";
}
}
const inter = Inter({
variable: "--font-sans",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "HealthLog",
description:
"Personal health tracking — weight, blood pressure, pulse, mood, medication compliance",
manifest: "/manifest.json",
icons: {
icon: "/favicon.svg",
apple: "/logo-192.png",
},
appleWebApp: {
capable: true,
statusBarStyle: "black-translucent",
title: "HealthLog",
},
openGraph: {
title: "HealthLog",
description:
"Personal health tracking — weight, blood pressure, pulse, mood, medication compliance",
type: "website",
locale: "en_US",
alternateLocale: ["de_DE", "fr_FR", "es_ES", "it_IT", "pl_PL"],
siteName: "HealthLog",
},
twitter: {
card: "summary",
title: "HealthLog",
description:
"Personal health tracking — weight, blood pressure, pulse, mood, medication compliance",
},
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
themeColor: "#282a36",
};
// Inline script to apply theme before first paint (prevents FOUC)
const themeScript = `(function(){try{var t=localStorage.getItem("healthlog-theme");var c=(t==="light"||t==="dark")?t:(window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light");document.documentElement.classList.add(c)}catch(e){document.documentElement.classList.add("dark")}})()`;
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const nonce =
process.env.NODE_ENV === "production"
? ((await headers()).get("x-nonce") ?? undefined)
: undefined;
const initialLocale = await resolveInitialLocale();
return (
<html lang={initialLocale} suppressHydrationWarning>
<head>
<script
suppressHydrationWarning
nonce={nonce}
dangerouslySetInnerHTML={{ __html: themeScript }}
/>
</head>
<body className={`${inter.variable} font-sans antialiased`}>
<Providers initialLocale={initialLocale}>
<MonitoringBootstrap />
<AuthShell>{children}</AuthShell>
</Providers>
</body>
</html>
);
}