Fixed problems with user preferred languages

This commit is contained in:
DrMint 2023-06-08 12:25:03 +02:00
parent e0ee70814d
commit 62e64b9319
3 changed files with 42 additions and 41 deletions

View File

@ -67,7 +67,7 @@ A detailled look at the technologies used in this repository:
- The website is built before running in production - The website is built before running in production
- Performances are great, and it's possible to deploy the app on a CDN - Performances are great, and it's possible to deploy the app on a CDN
- On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted - On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
- UI localizations are downloaded separetely into the `public/local-data` to avoid fetching the same static props for every pages. - Some widely used data (e.g: UI localizations) are downloaded separetely into `public/local-data` as some form of request deduping + it make this data hot-swappable without the need to rebuild the entire website.
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/) - Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
@ -102,17 +102,15 @@ A detailled look at the technologies used in this repository:
- Multilingual - Multilingual
- By default, use the browser's language as the main language - Users are given a list of supported languages. The first language in this list is the primary language (the language of the UI), the others are fallback languages. The others are fallback languages.
- Fallback languages are used for content which are not available in the main language - By default, the list is ordered following the browser's languages (and most spoken languages woldwide for the remaining languages). The list can also be reordered manually.
- Main and fallback languages can be ordered manually by the user - Contents can be available in any number of languages. By default, the best matching language will be presented to the user. However, the user can also decide to temporary select another language for a specific content, without affecting their list of preferred languages.
- At the content level, the user can know which language is available
- Furthermore, the user can temporary select another language then the one that was automatically selected
- UI Localizations - UI Localizations
- The translated wordings use [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) to include variables, plural, dates... - The translated wordings use [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) to include variables, plural, dates...
- Use a custom ICU Typescript transformation script to provide type safety when formatting ICU wordings - Use a custom ICU Typescript transformation script to provide type safety when formatting ICU wordings
- Fallback to English if a specific working isn't available in the user's language - Fallback to English if the translation is missing.
- SEO - SEO

View File

@ -2,11 +2,11 @@ import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
import { atom } from "jotai"; import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils"; import { atomWithStorage } from "jotai/utils";
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms"; import { atomPairing, useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
import { getDefaultPreferredLanguages } from "helpers/locales"; import { isDefined } from "helpers/asserts";
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { usePrefersDarkMode } from "hooks/useMediaQuery"; import { usePrefersDarkMode } from "hooks/useMediaQuery";
import { userAgent } from "contexts/userAgent"; import { userAgent } from "contexts/userAgent";
import { getLogger } from "helpers/logger";
export enum ThemeMode { export enum ThemeMode {
Dark = "dark", Dark = "dark",
@ -20,6 +20,8 @@ export enum PerfMode {
Off = "off", Off = "off",
} }
const logger = getLogger("⚙️ [Settings Context]");
const preferredLanguagesAtom = atomPairing(atomWithStorage<string[]>("preferredLanguages", [])); const preferredLanguagesAtom = atomPairing(atomWithStorage<string[]>("preferredLanguages", []));
const themeModeAtom = atomPairing(atomWithStorage("themeMode", ThemeMode.Auto)); const themeModeAtom = atomPairing(atomWithStorage("themeMode", ThemeMode.Auto));
const darkModeAtom = atomPairing(atom(false)); const darkModeAtom = atomPairing(atom(false));
@ -67,7 +69,7 @@ export const settings = {
export const useSettings = (): void => { export const useSettings = (): void => {
const router = useRouter(); const router = useRouter();
const [preferredLanguages, setPreferredLanguages] = useAtomPair(preferredLanguagesAtom); const setPreferredLanguages = useAtomSetter(preferredLanguagesAtom);
const fontSize = useAtomGetter(fontSizeAtom); const fontSize = useAtomGetter(fontSizeAtom);
const isDyslexic = useAtomGetter(dyslexicAtom); const isDyslexic = useAtomGetter(dyslexicAtom);
const [isDarkMode, setDarkMode] = useAtomPair(darkModeAtom); const [isDarkMode, setDarkMode] = useAtomPair(darkModeAtom);
@ -114,24 +116,33 @@ export const useSettings = (): void => {
}, [isDarkMode]); }, [isDarkMode]);
/* PREFERRED LANGUAGES */ /* PREFERRED LANGUAGES */
useEffect(() => { useEffect(() => {
if (preferredLanguages.length === 0) { if (!router.locale || !router.locales) return;
if (isDefinedAndNotEmpty(router.locale) && router.locales) { const localStorageValue: string[] = JSON.parse(
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales)); localStorage.getItem("preferredLanguages") ?? "[]"
}
} else if (router.locale !== preferredLanguages[0]) {
/*
* Using a timeout to the code getting stuck into a loop when reaching the website with a
* different preferredLanguages[0] from router.locale
*/
setTimeout(
async () =>
router.replace(router.asPath, router.asPath, {
locale: preferredLanguages[0],
}),
250
); );
if (localStorageValue.length === 0) {
const defaultLanguages = router.locales;
defaultLanguages.sort((a, b) => {
const evaluate = (value: string) =>
navigator.languages.includes(value)
? navigator.languages.findIndex((v) => value === v)
: navigator.languages.length;
return evaluate(a) - evaluate(b);
});
logger.log("First time visitor, initializing preferred languages to", defaultLanguages);
setPreferredLanguages(defaultLanguages);
} else if (router.locale !== localStorageValue[0]) {
logger.log(
"Router locale",
router.locale,
"doesn't correspond to preferred locale. Switching to",
localStorageValue[0]
);
router.replace(router.asPath, router.asPath, {
locale: localStorageValue[0],
});
} }
}, [preferredLanguages, router, setPreferredLanguages]); }, [router, setPreferredLanguages]);
}; };

View File

@ -1,19 +1,11 @@
import { isDefined } from "./asserts"; import { isDefined } from "./asserts";
export const getDefaultPreferredLanguages = (routerLocal: string, locales: string[]): string[] => { export const getDefaultPreferredLanguages = (routerLocal: string, locales: string[]): string[] => {
let defaultPreferredLanguages: string[] = []; const defaultPreferredLanguages: Set<string> = new Set();
if (routerLocal === "en") { defaultPreferredLanguages.add(routerLocal);
defaultPreferredLanguages = [routerLocal]; defaultPreferredLanguages.add("en");
locales.map((locale) => { locales.forEach((locale) => defaultPreferredLanguages.add(locale));
if (locale !== routerLocal) defaultPreferredLanguages.push(locale); return [...defaultPreferredLanguages.values()];
});
} else {
defaultPreferredLanguages = [routerLocal, "en"];
locales.map((locale) => {
if (locale !== routerLocal && locale !== "en") defaultPreferredLanguages.push(locale);
});
}
return defaultPreferredLanguages;
}; };
export const getPreferredLanguage = ( export const getPreferredLanguage = (