mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
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.
105 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|