Skip to content

Commit

Permalink
feat: language URL search parameter (#698)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Oct 19, 2024
1 parent 7d13dcb commit 6bcfad2
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 9 deletions.
10 changes: 10 additions & 0 deletions docs/urlParams.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
13 changes: 11 additions & 2 deletions src/context/Global.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,22 @@ const GlobalProvider = (props: { children: any }) => {
...stringSerializer,
},
);

const [i18nConfigured, setI18nConfigured] = makePersisted(
createSignal(null),
{
name: "i18n",
...stringSerializer,
},
);
const [i18nUrl, setI18nUrl] = makePersisted(
createSignal<string | null>(null),
{
name: "i18nUrl",
...stringSerializer,
},
);

const [denomination, setDenomination] = makePersisted(
createSignal<Denomination>(Denomination.Sat),
{
Expand Down Expand Up @@ -282,7 +291,7 @@ const GlobalProvider = (props: { children: any }) => {
const getRdnsForAddress = (address: string) =>
rdnsForage.getItem<string>(address.toLowerCase());

setI18n(detectLanguage(i18nConfigured()));
setI18n(detectLanguage(i18nConfigured(), i18nUrl(), setI18nUrl));
detectWebLNProvider().then((state: boolean) => setWebln(state));
setWasmSupported(checkWasmSupported());

Expand Down Expand Up @@ -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]),
);
Expand Down
34 changes: 29 additions & 5 deletions src/i18n/detect.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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}`,
);
Expand All @@ -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>,
): 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);
};
62 changes: 60 additions & 2 deletions tests/i18n/detect.spec.ts
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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);
});
});
});

0 comments on commit 6bcfad2

Please sign in to comment.