UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

148 lines (129 loc) 4.44 kB
/** * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact schukai GmbH. * * SPDX-License-Identifier: AGPL-3.0 */ import { languages } from "./map/languages.mjs"; /** * Determines the user's preferred language based on browser settings and available language options. * * It evaluates the current HTML document language, the browser's defined languages, and * the language options from `<link>` elements with `hreflang` attributes in the document. * * @return {Object} An object containing information about the detected language, preferred language, and available languages. */ export function detectUserLanguagePreference() { const currentLang = document.documentElement.lang; let preferredLanguages = []; if (typeof navigator.language === "string" && navigator.language.length > 0) { preferredLanguages = [navigator.language]; } if (Array.isArray(navigator.languages) && navigator.languages.length > 0) { preferredLanguages = navigator.languages; } // add to preferredLanguages all the base languages of the preferred languages preferredLanguages = preferredLanguages.concat( preferredLanguages.map((lang) => lang.split("-")[0]), ); if (!currentLang && preferredLanguages.length === 0) { return { message: "No language information available.", }; } const linkTags = document.querySelectorAll("link[hreflang]"); if (linkTags.length === 0) { return { current: currentLang || null, message: "No <link> tags with hreflang available.", }; } const availableLanguages = [...linkTags].map((link) => { const fullLang = link.hreflang; const baseLang = fullLang.split("-")[0]; let label = link.getAttribute("data-monster-label"); if (!label) { label = link.getAttribute("title"); if (!label) { label = languages?.[fullLang]; if (!label) { label = languages?.[baseLang]; } } } return { fullLang, baseLang, label, href: link.href, }; }); // filter availableLanguages to only include languages that are in the preferredLanguages array const offerableLanguages = availableLanguages.filter( (lang) => preferredLanguages.includes(lang.fullLang) || preferredLanguages.includes(lang.baseLang), ); if (offerableLanguages.length === 0) { return { current: currentLang || null, message: "No available languages match the user's preferences.", available: availableLanguages.map((lang) => ({ ...lang, weight: 1, })), }; } // Helper function to determine the "weight" of a language match function getWeight(langEntry) { // Full match has priority 3 if (preferredLanguages.includes(langEntry.fullLang)) return 3; // Base language match has priority 2 if (preferredLanguages.includes(langEntry.baseLang)) return 2; // No match is priority 1 return 1; } // Sort the available languages by descending weight offerableLanguages.sort((a, b) => getWeight(b) - getWeight(a)); // The best match is the first in the sorted list const bestMatch = offerableLanguages[0]; const bestMatchWeight = getWeight(bestMatch); const currentLabel = languages?.[currentLang] || currentLang; // If we found a language that matches user preferences (weight > 1) if (bestMatchWeight > 0) { return { current: currentLang || null, currentLabel: currentLabel, preferred: { full: bestMatch.fullLang, base: bestMatch.baseLang, label: bestMatch.label, href: bestMatch.href, }, available: availableLanguages.map((lang) => ({ ...lang, weight: getWeight(lang), })), offerable: offerableLanguages.map((lang) => ({ ...lang, weight: getWeight(lang), })), }; } // If no language matched the user's preferences return { current: currentLang || null, message: "None of the preferred languages are available.", available: availableLanguages.map((lang) => ({ ...lang, weight: getWeight(lang), })), }; }