/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. * v1.0.0-beta.97 */ import { BigDecimal, isValidNumber, sanitizeExponentialNumberString } from "./number"; import { createObserver } from "./observers"; import { closestElementCrossShadowBoundary, containsCrossShadowBoundary } from "./dom"; export const defaultLocale = "en"; export const locales = [ "ar", "bg", "bs", "ca", "cs", "da", "de", "de-CH", "el", defaultLocale, "en-AU", "en-CA", "en-GB", "es", "es-MX", "et", "fi", "fr", "fr-CH", "he", "hi", "hr", "hu", "id", "it", "it-CH", "ja", "ko", "lt", "lv", "mk", "nb", "nl", "pl", "pt", "pt-PT", "ro", "ru", "sk", "sl", "sr", "sv", "th", "tr", "uk", "vi", "zh-CN", "zh-HK", "zh-TW" ]; export const numberingSystems = [ "arab", "arabext", "bali", "beng", "deva", "fullwide", "gujr", "guru", "hanidec", "khmr", "knda", "laoo", "latn", "limb", "mlym", "mong", "mymr", "orya", "tamldec", "telu", "thai", "tibt" ]; const isNumberingSystemSupported = (numberingSystem) => numberingSystems.includes(numberingSystem); const browserNumberingSystem = new Intl.NumberFormat().resolvedOptions().numberingSystem; export const defaultNumberingSystem = browserNumberingSystem === "arab" || !isNumberingSystemSupported(browserNumberingSystem) ? "latn" : browserNumberingSystem; export const getSupportedNumberingSystem = (numberingSystem) => isNumberingSystemSupported(numberingSystem) ? numberingSystem : defaultNumberingSystem; export function getSupportedLocale(locale) { if (locales.indexOf(locale) > -1) { return locale; } if (!locale) { return defaultLocale; } locale = locale.toLowerCase(); // we support both 'nb' and 'no' (BCP 47) for Norwegian if (locale === "nb") { return "no"; } if (locale.includes("-")) { locale = locale.replace(/(\w+)-(\w+)/, (_match, language, region) => `${language}-${region.toUpperCase()}`); if (!locales.includes(locale)) { locale = locale.split("-")[0]; } } return locales.includes(locale) ? locale : defaultLocale; } const connectedComponents = new Set(); /** * This utility sets up internals for messages support. * * It needs to be called in `connectedCallback` before any logic that depends on locale * * @param component */ export function connectLocalized(component) { updateEffectiveLocale(component); if (connectedComponents.size === 0) { mutationObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"], subtree: true }); } connectedComponents.add(component); } /** * This is only exported for components that implemented the now deprecated `locale` prop. * * Do not use this utils for new components. * * @param component */ export function updateEffectiveLocale(component) { component.effectiveLocale = getLocale(component); } /** * This utility tears down internals for messages support. * * It needs to be called in `disconnectedCallback` * * @param component */ export function disconnectLocalized(component) { connectedComponents.delete(component); if (connectedComponents.size === 0) { mutationObserver.disconnect(); } } const mutationObserver = createObserver("mutation", (records) => { records.forEach((record) => { const el = record.target; connectedComponents.forEach((component) => { const hasOverridingLocale = !!(component.locale && !component.el.lang); const inUnrelatedSubtree = !containsCrossShadowBoundary(el, component.el); if (hasOverridingLocale || inUnrelatedSubtree) { return; } const closestLangEl = closestElementCrossShadowBoundary(component.el, "[lang]"); if (!closestLangEl) { component.effectiveLocale = defaultLocale; return; } const closestLang = closestLangEl.lang; component.effectiveLocale = // user set lang="" means unknown language, so we use default closestLangEl.hasAttribute("lang") && closestLang === "" ? defaultLocale : closestLang; }); }); }); /** * This util helps resolve a component's locale. * It will also fall back on the deprecated `locale` if a component implemented this previously. * * @param component */ function getLocale(component) { var _a; return (component.el.lang || component.locale || ((_a = closestElementCrossShadowBoundary(component.el, "[lang]")) === null || _a === void 0 ? void 0 : _a.lang) || document.documentElement.lang || defaultLocale); } /** * This util formats and parses numbers for localization */ class NumberStringFormat { constructor() { this.delocalize = (numberString) => // For performance, (de)localization is skipped if the formatter isn't initialized. // In order to localize/delocalize, e.g. when lang/numberingSystem props are not default values, // `numberFormatOptions` must be set in a component to create and cache the formatter. this._numberFormatOptions ? sanitizeExponentialNumberString(numberString, (nonExpoNumString) => nonExpoNumString .trim() .replace(new RegExp(`[${this._minusSign}]`, "g"), "-") .replace(new RegExp(`[${this._group}]`, "g"), "") .replace(new RegExp(`[${this._decimal}]`, "g"), ".") .replace(new RegExp(`[${this._digits.join("")}]`, "g"), this._getDigitIndex)) : numberString; this.localize = (numberString) => this._numberFormatOptions ? sanitizeExponentialNumberString(numberString, (nonExpoNumString) => isValidNumber(nonExpoNumString.trim()) ? new BigDecimal(nonExpoNumString.trim()) .format(this._numberFormatter) .replace(new RegExp(`[${this._actualGroup}]`, "g"), this._group) : nonExpoNumString) : numberString; } get group() { return this._group; } get decimal() { return this._decimal; } get minusSign() { return this._minusSign; } get digits() { return this._digits; } get numberFormatter() { return this._numberFormatter; } get numberFormatOptions() { return this._numberFormatOptions; } /** * numberFormatOptions needs to be set before localize/delocalize is called to ensure the options are up to date */ set numberFormatOptions(options) { options.locale = getSupportedLocale(options === null || options === void 0 ? void 0 : options.locale); options.numberingSystem = getSupportedNumberingSystem(options === null || options === void 0 ? void 0 : options.numberingSystem); if ( // No need to create the formatter if `locale` and `numberingSystem` // are the default values and `numberFormatOptions` has not been set (!this._numberFormatOptions && options.locale === defaultLocale && options.numberingSystem === defaultNumberingSystem && // don't skip initialization if any options besides locale/numberingSystem are set Object.keys(options).length === 2) || // cache formatter by only recreating when options change JSON.stringify(this._numberFormatOptions) === JSON.stringify(options)) { return; } this._numberFormatOptions = options; this._numberFormatter = new Intl.NumberFormat(this._numberFormatOptions.locale, this._numberFormatOptions); this._digits = [ ...new Intl.NumberFormat(this._numberFormatOptions.locale, { useGrouping: false, numberingSystem: this._numberFormatOptions.numberingSystem }).format(9876543210) ].reverse(); const index = new Map(this._digits.map((d, i) => [d, i])); const parts = new Intl.NumberFormat(this._numberFormatOptions.locale).formatToParts(-12345678.9); this._actualGroup = parts.find((d) => d.type === "group").value; // change whitespace group characters that don't render correctly this._group = this._actualGroup.trim().length === 0 ? " " : this._actualGroup; this._decimal = parts.find((d) => d.type === "decimal").value; this._minusSign = parts.find((d) => d.type === "minusSign").value; this._getDigitIndex = (d) => index.get(d); } } export const numberStringFormatter = new NumberStringFormat();