| 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { acceptLanguageTags, detectLocaleFromHeaders, matchLocaleTag } from "./detect"; |
| 3 | |
| 4 | describe("matchLocaleTag", () => { |
| 5 | it("matches exact full tags case-insensitively", () => { |
| 6 | expect(matchLocaleTag("pt-BR")).toBe("pt-BR"); |
| 7 | expect(matchLocaleTag("PT-br")).toBe("pt-BR"); |
| 8 | expect(matchLocaleTag("ru")).toBe("ru"); |
| 9 | expect(matchLocaleTag("uk")).toBe("uk"); |
| 10 | }); |
| 11 | |
| 12 | it("maps regional variants to the routed base tag", () => { |
| 13 | expect(matchLocaleTag("ru-RU")).toBe("ru"); |
| 14 | expect(matchLocaleTag("uk-UA")).toBe("uk"); |
| 15 | expect(matchLocaleTag("es-MX")).toBe("es"); |
| 16 | expect(matchLocaleTag("es-419")).toBe("es"); |
| 17 | expect(matchLocaleTag("zh-Hant")).toBe("zh"); |
| 18 | expect(matchLocaleTag("zh-TW")).toBe("zh"); |
| 19 | expect(matchLocaleTag("ja-JP")).toBe("ja"); |
| 20 | expect(matchLocaleTag("ko-KR")).toBe("ko"); |
| 21 | expect(matchLocaleTag("vi-VN")).toBe("vi"); |
| 22 | expect(matchLocaleTag("id-ID")).toBe("id"); |
| 23 | }); |
| 24 | |
| 25 | it("routes pt to the only shipped Portuguese variant", () => { |
| 26 | expect(matchLocaleTag("pt")).toBe("pt-BR"); |
| 27 | expect(matchLocaleTag("pt-PT")).toBe("pt-BR"); |
| 28 | }); |
| 29 | |
| 30 | it("matches the routed wave-2 locales through regional variants", () => { |
| 31 | expect(matchLocaleTag("fr-FR")).toBe("fr"); |
| 32 | expect(matchLocaleTag("fr-CA")).toBe("fr"); |
| 33 | expect(matchLocaleTag("de-DE")).toBe("de"); |
| 34 | expect(matchLocaleTag("de-AT")).toBe("de"); |
| 35 | expect(matchLocaleTag("ca-ES")).toBe("ca"); |
| 36 | expect(matchLocaleTag("hi-IN")).toBe("hi"); |
| 37 | expect(matchLocaleTag("tr-TR")).toBe("tr"); |
| 38 | expect(matchLocaleTag("it-IT")).toBe("it"); |
| 39 | expect(matchLocaleTag("pl-PL")).toBe("pl"); |
| 40 | expect(matchLocaleTag("ar-EG")).toBe("ar"); |
| 41 | expect(matchLocaleTag("ar")).toBe("ar"); |
| 42 | }); |
| 43 | |
| 44 | it("rejects unrouted and empty tags deterministically", () => { |
| 45 | expect(matchLocaleTag("fa")).toBeNull(); |
| 46 | expect(matchLocaleTag("th-TH")).toBeNull(); |
| 47 | expect(matchLocaleTag("nl")).toBeNull(); |
| 48 | expect(matchLocaleTag("")).toBeNull(); |
| 49 | expect(matchLocaleTag("*")).toBeNull(); |
| 50 | }); |
| 51 | }); |
| 52 | |
| 53 | describe("detectLocaleFromHeaders", () => { |
| 54 | it("prefers an explicit cookie choice over Accept-Language", () => { |
| 55 | expect(detectLocaleFromHeaders("ru", "ja,en;q=0.8")).toBe("ru"); |
| 56 | }); |
| 57 | |
| 58 | it("ignores stale cookies for unrouted locales", () => { |
| 59 | expect(detectLocaleFromHeaders("th", "uk,en;q=0.8")).toBe("uk"); |
| 60 | }); |
| 61 | |
| 62 | it("honors Accept-Language preference order", () => { |
| 63 | expect(detectLocaleFromHeaders(undefined, "th,vi;q=0.9,ru;q=0.8")).toBe("vi"); |
| 64 | expect(detectLocaleFromHeaders(undefined, "sv,pt;q=0.7")).toBe("pt-BR"); |
| 65 | expect(detectLocaleFromHeaders(undefined, "fr,vi;q=0.9")).toBe("fr"); |
| 66 | }); |
| 67 | |
| 68 | it("sorts by q weight rather than list position", () => { |
| 69 | // Header order is not required to be preference order. Reading it |
| 70 | // positionally handed this reader English. |
| 71 | expect(detectLocaleFromHeaders(undefined, "en;q=0.2, ja;q=0.9")).toBe("ja"); |
| 72 | expect(detectLocaleFromHeaders(undefined, "en;q=0.3,de;q=0.4,ko;q=0.9")).toBe("ko"); |
| 73 | // An unweighted tag carries the default weight of 1. |
| 74 | expect(detectLocaleFromHeaders(undefined, "en;q=0.8, uk")).toBe("uk"); |
| 75 | }); |
| 76 | |
| 77 | it("drops q=0 tags, which mean 'not acceptable'", () => { |
| 78 | // RFC 9110 §12.4.2: q=0 refuses the language outright. It used to be |
| 79 | // treated as merely least-preferred, and being first in the list won. |
| 80 | expect(detectLocaleFromHeaders(undefined, "en;q=0, ja;q=0.9")).toBe("ja"); |
| 81 | expect(detectLocaleFromHeaders(undefined, "ru;q=0.000")).toBe("en"); |
| 82 | }); |
| 83 | |
| 84 | it("keeps original order as the tie-break", () => { |
| 85 | expect(detectLocaleFromHeaders(undefined, "ru,uk")).toBe("ru"); |
| 86 | expect(detectLocaleFromHeaders(undefined, "ru;q=0.5,uk;q=0.5")).toBe("ru"); |
| 87 | expect(acceptLanguageTags("fr-CA, fr;q=0.9, en;q=0.8")).toEqual([ |
| 88 | "fr-CA", |
| 89 | "fr", |
| 90 | "en", |
| 91 | ]); |
| 92 | }); |
| 93 | |
| 94 | it("leaves a malformed weight at the default rather than guessing", () => { |
| 95 | expect(acceptLanguageTags("en;q=nonsense, ja;q=0.9")).toEqual(["en", "ja"]); |
| 96 | expect(acceptLanguageTags(" , en ")).toEqual(["en"]); |
| 97 | }); |
| 98 | |
| 99 | it("falls back to the default locale with no signal", () => { |
| 100 | expect(detectLocaleFromHeaders(undefined, null)).toBe("en"); |
| 101 | expect(detectLocaleFromHeaders(undefined, "")).toBe("en"); |
| 102 | expect(detectLocaleFromHeaders(undefined, "fa,th;q=0.8")).toBe("en"); |
| 103 | }); |
| 104 | }); |
| 105 |