fix(graph-ui): rank Accept-Language by q instead of substring match

detectLanguage tested whether "zh" appeared anywhere in the header, so any
visitor whose Accept-Language mentions Chinese got the Chinese UI regardless of
preference.

    en-US,en;q=0.9,zh;q=0.5  ->  zh, expected en
    zh;q=0, en               ->  zh, expected en

The first is a bilingual visitor who prefers English and lists Chinese as a
fallback. The second sets q=0, which RFC 7231 defines as not acceptable.

Now parses q, drops q=0, sorts descending, and matches the primary subtag rather
than a substring. Chinese still wins whenever it is actually preferred.

The two existing assertions are unchanged and still pass.

Signed-off-by: lukiod <mohakgupta0981@gmail.com>
This commit is contained in:
lukiod committed 2026-07-28 08:50:52 +05:30
1 parent 97ce23f982
commit e5c3200f43
2 files changed
+41 -2

No files matched your search

+25
View File
@@ -7,6 +7,31 @@ describe("i18n", () => {
expect(detectLanguage("de-DE,de;q=0.9")).toBe("en");
});
it("ranks Accept-Language by q rather than by presence", () => {
// A bilingual visitor who prefers English but lists Chinese as a fallback.
expect(detectLanguage("en-US,en;q=0.9,zh;q=0.5")).toBe("en");
expect(detectLanguage("en,zh;q=0.1")).toBe("en");
// Chinese still wins when it is actually preferred.
expect(detectLanguage("zh;q=0.9,en;q=0.5")).toBe("zh");
});
it("treats q=0 as unacceptable", () => {
expect(detectLanguage("zh;q=0, en")).toBe("en");
expect(detectLanguage("zh;q=0")).toBe("en");
});
it("matches the language subtag, not a substring", () => {
expect(detectLanguage("en-GB")).toBe("en");
expect(detectLanguage("zh-Hant-TW")).toBe("zh");
});
it("honours the override and handles empty input", () => {
expect(detectLanguage("zh-CN", "en")).toBe("en");
expect(detectLanguage("en-US", "zh")).toBe("zh");
expect(detectLanguage(null)).toBe("en");
expect(detectLanguage("")).toBe("en");
});
it("keeps UI chrome messages in the catalog", () => {
expect(messages.zh.tabs.projects).toBe("项目");
expect(messages.zh.index.newIndex).toBe("新建索引");
+16 -2
View File
@@ -152,8 +152,22 @@ export type UiMessages = (typeof messages)[UiLanguage];
export function detectLanguage(acceptLanguage?: string | null, override?: string | null): UiLanguage {
if (override === "zh" || override === "en") return override;
if (!acceptLanguage) return "en";
const normalized = acceptLanguage.toLowerCase();
return normalized.includes("zh-cn") || normalized.includes("zh") ? "zh" : "en";
// Ranked by q, not by whether "zh" appears anywhere. A substring test served
// Chinese for "en-US,en;q=0.9,zh;q=0.5", where English is clearly preferred,
// and for "zh;q=0, en", where q=0 means Chinese is unacceptable.
const best = acceptLanguage
.split(",")
.map((part) => {
const [tag, ...params] = part.trim().split(";");
const q = params.map((p) => /^\s*q\s*=\s*([\d.]+)\s*$/i.exec(p)).find(Boolean);
return { tag: tag.trim().toLowerCase(), q: q ? Number(q[1]) : 1 };
})
.filter(({ tag, q }) => tag && Number.isFinite(q) && q > 0)
.sort((a, b) => b.q - a.q)
.find(({ tag }) => tag.split("-")[0] === "zh" || tag.split("-")[0] === "en");
return best?.tag.startsWith("zh") ? "zh" : "en";
}
let cachedLanguage: UiLanguage = "en";