diff --git a/app/server/lib/__tests__/accept-language.test.ts b/app/server/lib/__tests__/accept-language.test.ts index f1f29013..272f0d7f 100644 --- a/app/server/lib/__tests__/accept-language.test.ts +++ b/app/server/lib/__tests__/accept-language.test.ts @@ -2,10 +2,18 @@ import { describe, expect, test } from "vitest"; import { getLocaleFromAcceptLanguage } from "~/server/lib/accept-language"; describe("getLocaleFromAcceptLanguage", () => { - test("strips quality values from the preferred language", () => { + test("strips quality values from a single preferred language", () => { expect(getLocaleFromAcceptLanguage("en;q=0.5")).toBe("en"); }); + test("prefers the highest quality language", () => { + expect(getLocaleFromAcceptLanguage("en;q=0.5, fr;q=0.9")).toBe("fr"); + }); + + test("treats language tags without q as the highest priority", () => { + expect(getLocaleFromAcceptLanguage("en;q=0.9, fr-CH")).toBe("fr-CH"); + }); + test("skips invalid language tags and uses the next valid one", () => { expect(getLocaleFromAcceptLanguage("*, en-GB;q=0.8")).toBe("en-GB"); }); diff --git a/app/server/lib/accept-language.ts b/app/server/lib/accept-language.ts index 5f0ca86a..0aa7d323 100644 --- a/app/server/lib/accept-language.ts +++ b/app/server/lib/accept-language.ts @@ -5,20 +5,48 @@ export function getLocaleFromAcceptLanguage(acceptLanguage?: string | null) { return DEFAULT_LOCALE; } + let locale = DEFAULT_LOCALE; + let highestQuality = -1; + for (const value of acceptLanguage.split(",")) { - const languageTag = value.split(";")[0]?.trim(); + const [rawLanguageTag, ...parameters] = value.split(";"); + const languageTag = rawLanguageTag?.trim(); if (!languageTag) { continue; } + let quality = 1; + + for (const parameter of parameters) { + const [key, value] = parameter.split("="); + + if (key?.trim().toLowerCase() !== "q") { + continue; + } + + const parsedQuality = Number(value?.trim()); + + if (!Number.isFinite(parsedQuality) || parsedQuality < 0 || parsedQuality > 1) { + quality = -1; + break; + } + + quality = parsedQuality; + break; + } + + if (quality <= highestQuality) { + continue; + } + try { - Intl.getCanonicalLocales(languageTag); - return languageTag; + locale = Intl.getCanonicalLocales(languageTag)[0] || DEFAULT_LOCALE; + highestQuality = quality; } catch { continue; } } - return DEFAULT_LOCALE; + return locale; }