UNPKG

@schukai/monster

Version:

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

196 lines (171 loc) 6.74 kB
/** * Copyright © Volker Schukai 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 Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { languages } from "./map/languages.mjs"; /** * Normalizes a number represented as a string by converting it into a valid floating-point number format, * based on the provided or detected locale. It accounts for different decimal and a thousand separator conventions. * * @param {string} input - The string representation of the number to normalize. May include locale-specific formatting. * @param {string} [locale=navigator.language] - The locale used to determine the decimal separator convention. Defaults to the user's browser locale. * @return {number} The normalized number as a floating-point value. Returns NaN if the input is not a parsable number. */ export function normalizeNumber(input, locale = navigator.language) { if (input === null || input === undefined) return NaN; if (typeof input === "number") return input; // If input is already a number, return it directly if (typeof input !== "string") return NaN; const cleaned = input.trim().replace(/\s/g, ""); const decimalSeparator = getDecimalSeparator(locale); let normalized = cleaned; if (decimalSeparator === "," && cleaned.includes(".")) { normalized = cleaned.replace(/\./g, "").replace(",", "."); } else if (decimalSeparator === "." && cleaned.includes(",")) { normalized = cleaned.replace(/,/g, "").replace(".", "."); } else if (decimalSeparator === "," && cleaned.includes(",")) { normalized = cleaned.replace(",", "."); } const result = parseFloat(normalized); return Number.isNaN(result) ? NaN : result; } /** * Retrieves the decimal separator for a given locale. * * @param {string} [locale=navigator.language] - The locale identifier to determine the decimal separator. Defaults to the user's current locale if not provided. * @return {string} The decimal separator used in the specified locale. */ function getDecimalSeparator(locale = navigator.language) { const numberWithDecimal = 1.1; const formatted = new Intl.NumberFormat(locale).format(numberWithDecimal); return formatted.replace(/\d/g, "")[0]; // z.B. "," oder "." } /** * 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.getAttribute("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.getAttribute("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), ); const allOfferableLanguages = availableLanguages.filter( (lang) => lang.baseLang !== currentLang && lang.fullLang !== currentLang, ); if (offerableLanguages.length === 0) { return { current: currentLang || null, message: "No available languages match the user's preferences.", available: availableLanguages.map((lang) => ({ ...lang, weight: 1, })), allOfferable: allOfferableLanguages, }; } // 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 > 0) 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), })), allOfferable: allOfferableLanguages, }; } // 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), })), allOfferable: allOfferableLanguages, }; }