fix: sanitize Accept-Language locale parsing

This commit is contained in:
Nicolas Meienberger 2026-04-04 13:05:31 +02:00
parent afc77f55b8
commit 0a4768df6b
3 changed files with 43 additions and 1 deletions

View file

@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import { getLocaleFromAcceptLanguage } from "~/server/lib/accept-language";
describe("getLocaleFromAcceptLanguage", () => {
test("strips quality values from the preferred language", () => {
expect(getLocaleFromAcceptLanguage("en;q=0.5")).toBe("en");
});
test("skips invalid language tags and uses the next valid one", () => {
expect(getLocaleFromAcceptLanguage("*, en-GB;q=0.8")).toBe("en-GB");
});
test("falls back to en-US when no valid language is present", () => {
expect(getLocaleFromAcceptLanguage(";;;, ???")).toBe("en-US");
expect(getLocaleFromAcceptLanguage(null)).toBe("en-US");
});
});

View file

@ -0,0 +1,24 @@
const DEFAULT_LOCALE = "en-US";
export function getLocaleFromAcceptLanguage(acceptLanguage?: string | null) {
if (!acceptLanguage) {
return DEFAULT_LOCALE;
}
for (const value of acceptLanguage.split(",")) {
const languageTag = value.split(";")[0]?.trim();
if (!languageTag) {
continue;
}
try {
Intl.getCanonicalLocales(languageTag);
return languageTag;
} catch {
continue;
}
}
return DEFAULT_LOCALE;
}

View file

@ -2,6 +2,7 @@ import { createServerFn } from "@tanstack/react-start";
import { getCookie, getRequestHeaders } from "@tanstack/react-start/server";
import { THEME_COOKIE_NAME } from "~/client/components/theme-provider";
import type { DateFormatPreference, TimeFormatPreference } from "~/client/lib/datetime";
import { getLocaleFromAcceptLanguage } from "~/server/lib/accept-language";
import { auth } from "~/server/lib/auth";
export const getRootLoaderData = createServerFn({ method: "GET" }).handler(async () => {
@ -12,7 +13,7 @@ export const getRootLoaderData = createServerFn({ method: "GET" }).handler(async
return {
theme: (themeCookie === "light" ? "light" : "dark") as "light" | "dark",
locale: (acceptLanguage?.split(",")[0] || "en-US") as string,
locale: getLocaleFromAcceptLanguage(acceptLanguage),
timeZone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
dateFormat: (session?.user.dateFormat ?? "MM/DD/YYYY") as DateFormatPreference,
timeFormat: (session?.user.timeFormat ?? "12h") as TimeFormatPreference,