diff --git a/docs/urlParams.md b/docs/urlParams.md index 208ab488..a4119b7f 100644 --- a/docs/urlParams.md +++ b/docs/urlParams.md @@ -31,3 +31,13 @@ are: `sendAmount` or `receiveAmount` set the respective amounts. Value is denominated in satoshis and `sendAmount` takes precedence. + +## Language + +When no language has been set before, the default can be set with `lang`. +Available values are: + +- English: `en` +- German: `de` +- Spanish: `es` +- Chinese: `zh` diff --git a/src/context/Global.tsx b/src/context/Global.tsx index 33592289..e3abc2b7 100644 --- a/src/context/Global.tsx +++ b/src/context/Global.tsx @@ -134,6 +134,7 @@ const GlobalProvider = (props: { children: any }) => { ...stringSerializer, }, ); + const [i18nConfigured, setI18nConfigured] = makePersisted( createSignal(null), { @@ -141,6 +142,14 @@ const GlobalProvider = (props: { children: any }) => { ...stringSerializer, }, ); + const [i18nUrl, setI18nUrl] = makePersisted( + createSignal(null), + { + name: "i18nUrl", + ...stringSerializer, + }, + ); + const [denomination, setDenomination] = makePersisted( createSignal(Denomination.Sat), { @@ -282,7 +291,7 @@ const GlobalProvider = (props: { children: any }) => { const getRdnsForAddress = (address: string) => rdnsForage.getItem(address.toLowerCase()); - setI18n(detectLanguage(i18nConfigured())); + setI18n(detectLanguage(i18nConfigured(), i18nUrl(), setI18nUrl)); detectWebLNProvider().then((state: boolean) => setWebln(state)); setWasmSupported(checkWasmSupported()); @@ -312,7 +321,7 @@ const GlobalProvider = (props: { children: any }) => { ); // i18n - createMemo(() => setI18n(i18nConfigured())); + createMemo(() => setI18n(i18nConfigured() || i18nUrl())); const dictLocale = createMemo(() => flatten(dict[i18n() || config.defaultLanguage]), ); diff --git a/src/i18n/detect.ts b/src/i18n/detect.ts index 5ad7011b..cc152a44 100644 --- a/src/i18n/detect.ts +++ b/src/i18n/detect.ts @@ -1,8 +1,12 @@ import log from "loglevel"; +import { Setter } from "solid-js"; import { config } from "../config"; +import { getUrlParam } from "../utils/urlParams"; import locales from "./i18n"; +const isValidLang = (lang: string) => Object.keys(locales).includes(lang); + export const getNavigatorLanguage = (language: string): string => { const defaultLanguage = config.defaultLanguage; if (language === undefined) { @@ -13,7 +17,7 @@ export const getNavigatorLanguage = (language: string): string => { } const lang = language.split("-")[0]; - if (!Object.keys(locales).includes(lang)) { + if (!isValidLang(lang)) { log.info( `browser language "${lang}" not found; using default: ${defaultLanguage}`, ); @@ -24,9 +28,29 @@ export const getNavigatorLanguage = (language: string): string => { return lang; }; -export const detectLanguage = (i18nConfigured: string | null): string => { - if (i18nConfigured === null) { - return getNavigatorLanguage(navigator.language); +export const detectLanguage = ( + i18nConfigured: string | null, + i18nUrl: string | null, + setI18nUrl: Setter, +): string => { + if (i18nConfigured !== null) { + return i18nConfigured; + } + + const urlParam = getUrlParam("lang"); + if (urlParam) { + if (isValidLang(urlParam)) { + log.info("Using language URL parameter:", urlParam); + setI18nUrl(urlParam); + return urlParam; + } else { + log.warn("Invalid language URL parameter:", urlParam); + } } - return i18nConfigured; + + if (i18nUrl !== null) { + return i18nUrl; + } + + return getNavigatorLanguage(navigator.language); }; diff --git a/tests/i18n/detect.spec.ts b/tests/i18n/detect.spec.ts index 4c5f1d46..ad9817b6 100644 --- a/tests/i18n/detect.spec.ts +++ b/tests/i18n/detect.spec.ts @@ -1,5 +1,5 @@ import { config } from "../../src/config"; -import { getNavigatorLanguage } from "../../src/i18n/detect"; +import { detectLanguage, getNavigatorLanguage } from "../../src/i18n/detect"; describe("detect", () => { test.each` @@ -16,9 +16,67 @@ describe("detect", () => { ${"ro-RO"} | ${config.defaultLanguage} ${undefined} | ${config.defaultLanguage} `( - "getNavigatorLanguage $navigatorLanguage <=> $expected", + "getNavigatorLanguage $navigatorLanguage => $expected", ({ navigatorLanguage, expected }) => { expect(getNavigatorLanguage(navigatorLanguage)).toEqual(expected); }, ); + + describe("detectLanguage", () => { + test.each(["en", "de", "something"])( + "should prefer configured language", + (lang) => { + const setter = jest.fn(); + expect(detectLanguage(lang, null, setter)).toEqual(lang); + expect(setter).toHaveBeenCalledTimes(0); + }, + ); + + test.each(["de", "en", "zh"])( + "should use valid language URL params", + (lang) => { + Object.defineProperty(window, "location", { + value: { + search: `?lang=${lang}`, + }, + writable: true, + }); + + const setter = jest.fn(); + expect(detectLanguage(null, "not used", setter)).toEqual(lang); + + expect(setter).toHaveBeenCalledTimes(1); + expect(setter).toHaveBeenCalledWith(lang); + }, + ); + + test("should use last used URL params over browser default", () => { + Object.defineProperty(window, "location", { + value: { + search: "", + }, + writable: true, + }); + + const lastParam = "asdf"; + const setter = jest.fn(); + + expect(detectLanguage(null, lastParam, setter)).toEqual(lastParam); + expect(setter).toHaveBeenCalledTimes(0); + }); + + test("should default to browser language for invalid language URL params", () => { + Object.defineProperty(window, "location", { + value: { + search: `?lang=invalid`, + }, + writable: true, + }); + + const setter = jest.fn(); + expect(detectLanguage(null, null, setter)).toEqual("en"); + + expect(setter).toHaveBeenCalledTimes(0); + }); + }); });