diff --git a/src/lib/currency.test.ts b/src/lib/currency.test.ts index 8b609a1..ed61e5c 100644 --- a/src/lib/currency.test.ts +++ b/src/lib/currency.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { CURRENCIES, isValidExchangeRates, type Currency, type CurrencyInfo } from "./currency"; +import { CURRENCIES, currencyForLanguages, isValidExchangeRates, type Currency, type CurrencyInfo } from "./currency"; describe("CURRENCIES", () => { it("is a non-empty array", () => { @@ -94,3 +94,30 @@ describe("isValidExchangeRates", () => { expect(isValidExchangeRates("nope")).toBe(false); }); }); + +describe("currencyForLanguages", () => { + it("keeps EUR for a Spanish list that ends in en-US (Android WebView)", () => { + expect(currencyForLanguages(["es-ES", "en-US"])).toBe("EUR"); + expect(currencyForLanguages(["es-ES", "es", "en-US", "en"])).toBe("EUR"); + }); + + it("uses the first region's currency", () => { + expect(currencyForLanguages(["en-US"])).toBe("USD"); + expect(currencyForLanguages(["en-GB", "en-US"])).toBe("GBP"); + expect(currencyForLanguages(["de-CH", "de-DE"])).toBe("CHF"); + }); + + it("skips languages without a region", () => { + expect(currencyForLanguages(["pl", "pl-PL"])).toBe("PLN"); + }); + + it("reads the region after a script subtag", () => { + expect(currencyForLanguages(["zh-Hant-TW"])).toBe("TWD"); + }); + + it("falls back to EUR", () => { + expect(currencyForLanguages([])).toBe("EUR"); + expect(currencyForLanguages(["es"])).toBe("EUR"); + expect(currencyForLanguages(["not a locale"])).toBe("EUR"); + }); +}); diff --git a/src/lib/currency.tsx b/src/lib/currency.tsx index 88dc29a..58e66b4 100644 --- a/src/lib/currency.tsx +++ b/src/lib/currency.tsx @@ -91,16 +91,33 @@ const REGION_TO_CURRENCY: Record = { AU: "AUD", NZ: "NZD", }; -function detectBrowserCurrency(): Currency { - if (typeof navigator === "undefined") return "EUR"; - const langs = navigator.languages ?? [navigator.language]; +function regionOf(lang: string): string | undefined { + try { + return new Intl.Locale(lang).region?.toUpperCase(); + } catch { + return undefined; + } +} + +/** + * The first language that names a region decides: that region's currency, or + * EUR when the region is not in the map (Spain, France, ...). Later languages + * never override it. Android's WebView appends en-US to every list, so + * ["es-ES", "en-US"] must stay EUR. + */ +export function currencyForLanguages(langs: readonly string[]): Currency { for (const lang of langs) { - const region = lang.split("-")[1]?.toUpperCase(); - if (region && region in REGION_TO_CURRENCY) return REGION_TO_CURRENCY[region]; + const region = regionOf(lang); + if (region) return REGION_TO_CURRENCY[region] ?? "EUR"; } return "EUR"; } +function detectBrowserCurrency(): Currency { + if (typeof navigator === "undefined") return "EUR"; + return currencyForLanguages(navigator.languages ?? [navigator.language]); +} + // --------------------------------------------------------------------------- // Exchange rates (ECB publishes ~30 currencies; others won't convert) // ---------------------------------------------------------------------------