From b976975db2c149179ccb483e9f01dc6844f5207e Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Sat, 4 Jan 2025 19:26:52 +0100 Subject: [PATCH] feat: new language control #276 --- source/i18n/util.mjs | 139 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 source/i18n/util.mjs diff --git a/source/i18n/util.mjs b/source/i18n/util.mjs new file mode 100644 index 000000000..14f752b03 --- /dev/null +++ b/source/i18n/util.mjs @@ -0,0 +1,139 @@ +/** + * 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 = 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), + })), + }; +} -- GitLab