mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(report): pass the in-app locale through to the pdf export
The health-record export panel sent no locale, so the server resolved the report language from the browser's Accept-Language header. A German user on an English-default browser got an English doctor report. Send the active in-app locale from the panel, and on the server resolve most-specific-first: explicit selection, then the healthlog-locale cookie (what the user actually chose in the UI), then Accept-Language as the weakest fallback.
This commit is contained in:
1 parent
24ffca7220
commit
c6e782cdb8
3 files changed
+74
-4
No files matched your search
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
import { PDFParse } from "pdf-parse";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
@@ -38,14 +39,27 @@ const SESSION_OK = {
|
||||
user: { id: "user-1", email: "test@example.com", role: "USER" },
|
||||
} as const;
|
||||
|
||||
function mkReq(body: unknown): NextRequest {
|
||||
function mkReq(
|
||||
body: unknown,
|
||||
headers: Record<string, string> = {},
|
||||
): NextRequest {
|
||||
return new NextRequest("http://localhost/api/export/health-record", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: { "content-type": "application/json", ...headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
async function pdfText(res: Response): Promise<string> {
|
||||
const bytes = new Uint8Array(await res.arrayBuffer());
|
||||
const parser = new PDFParse({ data: bytes });
|
||||
try {
|
||||
return (await parser.getText()).text;
|
||||
} finally {
|
||||
await parser.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
@@ -137,6 +151,46 @@ describe("POST /api/export/health-record — outputs", () => {
|
||||
expect(buf.subarray(0, 2).toString("latin1")).toBe("PK");
|
||||
});
|
||||
|
||||
it("renders the PDF in the explicit selection locale", async () => {
|
||||
const { POST } = await import("../route");
|
||||
const res = await POST(
|
||||
mkReq({ format: "pdf", locale: "de", practiceName: "Sample Practice" }),
|
||||
);
|
||||
const text = await pdfText(res);
|
||||
// German cover label.
|
||||
expect(text).toContain("Praxis:");
|
||||
});
|
||||
|
||||
it("falls back to the healthlog-locale cookie when no locale is sent", async () => {
|
||||
const { POST } = await import("../route");
|
||||
const res = await POST(
|
||||
mkReq(
|
||||
{ format: "pdf", practiceName: "Sample Practice" },
|
||||
{
|
||||
// Browser default English, but the in-app cookie says German — the
|
||||
// cookie must win over Accept-Language.
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
cookie: "healthlog-locale=de",
|
||||
},
|
||||
),
|
||||
);
|
||||
const text = await pdfText(res);
|
||||
expect(text).toContain("Praxis:");
|
||||
expect(text).not.toContain("Practice:");
|
||||
});
|
||||
|
||||
it("uses Accept-Language only when neither selection nor cookie is present", async () => {
|
||||
const { POST } = await import("../route");
|
||||
const res = await POST(
|
||||
mkReq(
|
||||
{ format: "pdf", practiceName: "Sample Practice" },
|
||||
{ "accept-language": "en-US,en;q=0.9" },
|
||||
),
|
||||
);
|
||||
const text = await pdfText(res);
|
||||
expect(text).toContain("Practice:");
|
||||
});
|
||||
|
||||
it("scopes the aggregator measurement read to the session user", async () => {
|
||||
const { POST } = await import("../route");
|
||||
await POST(mkReq({ format: "fhir", userId: "user-2" } as never));
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
} from "@/lib/validations/health-record-export";
|
||||
import { getServerTranslator } from "@/lib/i18n/server-translator";
|
||||
import { parseLocaleFromAcceptLanguage } from "@/lib/format-locale";
|
||||
import { type Locale } from "@/lib/i18n/config";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
@@ -72,8 +72,20 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const sections = toDoctorReportPrefs(selection.sections);
|
||||
const includeCharts = selection.includeCharts ?? true;
|
||||
|
||||
// Locale resolution, most-specific first: the explicit selection wins, then
|
||||
// the in-app `healthlog-locale` cookie (what the user actually chose in the
|
||||
// UI), and only then the browser's Accept-Language header. The header is the
|
||||
// weakest signal — a German user on an English-default browser would
|
||||
// otherwise get an English report.
|
||||
const cookieValue = request.cookies.get("healthlog-locale")?.value;
|
||||
const cookieLocale = (locales as readonly string[]).includes(
|
||||
cookieValue ?? "",
|
||||
)
|
||||
? (cookieValue as Locale)
|
||||
: null;
|
||||
const locale: Locale =
|
||||
selection.locale ??
|
||||
cookieLocale ??
|
||||
parseLocaleFromAcceptLanguage(
|
||||
request.headers.get("accept-language") ?? "",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user