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