locale-35f81208.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.97
  5. */
  6. import { n as numberKeys } from './key-acc755b7.js';
  7. import { c as createObserver } from './observers-9f44e9b3.js';
  8. import { c as closestElementCrossShadowBoundary, b as containsCrossShadowBoundary } from './dom-8f0a9ff2.js';
  9. // adopted from https://stackoverflow.com/a/66939244
  10. class BigDecimal {
  11. constructor(input) {
  12. if (input instanceof BigDecimal) {
  13. return input;
  14. }
  15. const [integers, decimals] = String(input).split(".").concat("");
  16. this.value =
  17. BigInt(integers + decimals.padEnd(BigDecimal.DECIMALS, "0").slice(0, BigDecimal.DECIMALS)) +
  18. BigInt(BigDecimal.ROUNDED && decimals[BigDecimal.DECIMALS] >= "5");
  19. this.isNegative = input.charAt(0) === "-";
  20. }
  21. static _divRound(dividend, divisor) {
  22. return BigDecimal.fromBigInt(dividend / divisor + (BigDecimal.ROUNDED ? ((dividend * BigInt(2)) / divisor) % BigInt(2) : BigInt(0)));
  23. }
  24. static fromBigInt(bigint) {
  25. return Object.assign(Object.create(BigDecimal.prototype), { value: bigint });
  26. }
  27. toString() {
  28. const s = this.value
  29. .toString()
  30. .replace(new RegExp("-", "g"), "")
  31. .padStart(BigDecimal.DECIMALS + 1, "0");
  32. const i = s.slice(0, -BigDecimal.DECIMALS);
  33. const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");
  34. const value = i.concat(d.length ? "." + d : "");
  35. return `${this.isNegative ? "-" : ""}${value}`;
  36. }
  37. formatToParts(formatter) {
  38. const s = this.value
  39. .toString()
  40. .replace(new RegExp("-", "g"), "")
  41. .padStart(BigDecimal.DECIMALS + 1, "0");
  42. const i = s.slice(0, -BigDecimal.DECIMALS);
  43. const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");
  44. const parts = formatter.formatToParts(BigInt(i));
  45. this.isNegative && parts.unshift({ type: "minusSign", value: numberStringFormatter.minusSign });
  46. if (d.length) {
  47. parts.push({ type: "decimal", value: numberStringFormatter.decimal });
  48. d.split("").forEach((char) => parts.push({ type: "fraction", value: char }));
  49. }
  50. return parts;
  51. }
  52. format(formatter) {
  53. const s = this.value
  54. .toString()
  55. .replace(new RegExp("-", "g"), "")
  56. .padStart(BigDecimal.DECIMALS + 1, "0");
  57. const i = s.slice(0, -BigDecimal.DECIMALS);
  58. const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");
  59. const iFormatted = `${this.isNegative ? numberStringFormatter.minusSign : ""}${formatter.format(BigInt(i))}`;
  60. const dFormatted = d.length ? `${numberStringFormatter.decimal}${formatter.format(BigInt(d))}` : "";
  61. return `${iFormatted}${dFormatted}`;
  62. }
  63. add(num) {
  64. return BigDecimal.fromBigInt(this.value + new BigDecimal(num).value);
  65. }
  66. subtract(num) {
  67. return BigDecimal.fromBigInt(this.value - new BigDecimal(num).value);
  68. }
  69. multiply(num) {
  70. return BigDecimal._divRound(this.value * new BigDecimal(num).value, BigDecimal.SHIFT);
  71. }
  72. divide(num) {
  73. return BigDecimal._divRound(this.value * BigDecimal.SHIFT, new BigDecimal(num).value);
  74. }
  75. }
  76. // Configuration: constants
  77. BigDecimal.DECIMALS = 100; // number of decimals on all instances
  78. BigDecimal.ROUNDED = true; // numbers are truncated (false) or rounded (true)
  79. BigDecimal.SHIFT = BigInt("1" + "0".repeat(BigDecimal.DECIMALS)); // derived constant
  80. function isValidNumber(numberString) {
  81. return !(!numberString || isNaN(Number(numberString)));
  82. }
  83. function parseNumberString(numberString) {
  84. if (!numberString || !stringContainsNumbers(numberString)) {
  85. return "";
  86. }
  87. return sanitizeExponentialNumberString(numberString, (nonExpoNumString) => {
  88. let containsDecimal = false;
  89. const result = nonExpoNumString
  90. .split("")
  91. .filter((value, i) => {
  92. if (value.match(/\./g) && !containsDecimal) {
  93. containsDecimal = true;
  94. return true;
  95. }
  96. if (value.match(/\-/g) && i === 0) {
  97. return true;
  98. }
  99. return numberKeys.includes(value);
  100. })
  101. .reduce((string, part) => string + part);
  102. return isValidNumber(result) ? new BigDecimal(result).toString() : "";
  103. });
  104. }
  105. // regex for number sanitization
  106. const allLeadingZerosOptionallyNegative = /^([-0])0+(?=\d)/;
  107. const decimalOnlyAtEndOfString = /(?!^\.)\.$/;
  108. const allHyphensExceptTheStart = /(?!^-)-/g;
  109. const isNegativeDecimalOnlyZeros = /^-\b0\b\.?0*$/;
  110. const sanitizeNumberString = (numberString) => sanitizeExponentialNumberString(numberString, (nonExpoNumString) => {
  111. const sanitizedValue = nonExpoNumString
  112. .replace(allHyphensExceptTheStart, "")
  113. .replace(decimalOnlyAtEndOfString, "")
  114. .replace(allLeadingZerosOptionallyNegative, "$1");
  115. return isValidNumber(sanitizedValue)
  116. ? isNegativeDecimalOnlyZeros.test(sanitizedValue)
  117. ? sanitizedValue
  118. : new BigDecimal(sanitizedValue).toString()
  119. : nonExpoNumString;
  120. });
  121. function sanitizeExponentialNumberString(numberString, func) {
  122. if (!numberString) {
  123. return numberString;
  124. }
  125. const firstE = numberString.toLowerCase().indexOf("e") + 1;
  126. if (!firstE) {
  127. return func(numberString);
  128. }
  129. return numberString
  130. .replace(/[eE]*$/g, "")
  131. .substring(0, firstE)
  132. .concat(numberString.slice(firstE).replace(/[eE]/g, ""))
  133. .split(/[eE]/)
  134. .map((section, i) => (i === 1 ? func(section.replace(/\./g, "")) : func(section)))
  135. .join("e")
  136. .replace(/^e/, "1e");
  137. }
  138. function stringContainsNumbers(string) {
  139. return numberKeys.some((number) => string.includes(number));
  140. }
  141. const defaultLocale = "en";
  142. const locales = [
  143. "ar",
  144. "bg",
  145. "bs",
  146. "ca",
  147. "cs",
  148. "da",
  149. "de",
  150. "de-CH",
  151. "el",
  152. defaultLocale,
  153. "en-AU",
  154. "en-CA",
  155. "en-GB",
  156. "es",
  157. "es-MX",
  158. "et",
  159. "fi",
  160. "fr",
  161. "fr-CH",
  162. "he",
  163. "hi",
  164. "hr",
  165. "hu",
  166. "id",
  167. "it",
  168. "it-CH",
  169. "ja",
  170. "ko",
  171. "lt",
  172. "lv",
  173. "mk",
  174. "nb",
  175. "nl",
  176. "pl",
  177. "pt",
  178. "pt-PT",
  179. "ro",
  180. "ru",
  181. "sk",
  182. "sl",
  183. "sr",
  184. "sv",
  185. "th",
  186. "tr",
  187. "uk",
  188. "vi",
  189. "zh-CN",
  190. "zh-HK",
  191. "zh-TW"
  192. ];
  193. const numberingSystems = [
  194. "arab",
  195. "arabext",
  196. "bali",
  197. "beng",
  198. "deva",
  199. "fullwide",
  200. "gujr",
  201. "guru",
  202. "hanidec",
  203. "khmr",
  204. "knda",
  205. "laoo",
  206. "latn",
  207. "limb",
  208. "mlym",
  209. "mong",
  210. "mymr",
  211. "orya",
  212. "tamldec",
  213. "telu",
  214. "thai",
  215. "tibt"
  216. ];
  217. const isNumberingSystemSupported = (numberingSystem) => numberingSystems.includes(numberingSystem);
  218. const browserNumberingSystem = new Intl.NumberFormat().resolvedOptions().numberingSystem;
  219. const defaultNumberingSystem = browserNumberingSystem === "arab" || !isNumberingSystemSupported(browserNumberingSystem)
  220. ? "latn"
  221. : browserNumberingSystem;
  222. const getSupportedNumberingSystem = (numberingSystem) => isNumberingSystemSupported(numberingSystem) ? numberingSystem : defaultNumberingSystem;
  223. function getSupportedLocale(locale) {
  224. if (locales.indexOf(locale) > -1) {
  225. return locale;
  226. }
  227. if (!locale) {
  228. return defaultLocale;
  229. }
  230. locale = locale.toLowerCase();
  231. // we support both 'nb' and 'no' (BCP 47) for Norwegian
  232. if (locale === "nb") {
  233. return "no";
  234. }
  235. if (locale.includes("-")) {
  236. locale = locale.replace(/(\w+)-(\w+)/, (_match, language, region) => `${language}-${region.toUpperCase()}`);
  237. if (!locales.includes(locale)) {
  238. locale = locale.split("-")[0];
  239. }
  240. }
  241. return locales.includes(locale) ? locale : defaultLocale;
  242. }
  243. const connectedComponents = new Set();
  244. /**
  245. * This utility sets up internals for messages support.
  246. *
  247. * It needs to be called in `connectedCallback` before any logic that depends on locale
  248. *
  249. * @param component
  250. */
  251. function connectLocalized(component) {
  252. updateEffectiveLocale(component);
  253. if (connectedComponents.size === 0) {
  254. mutationObserver.observe(document.documentElement, {
  255. attributes: true,
  256. attributeFilter: ["lang"],
  257. subtree: true
  258. });
  259. }
  260. connectedComponents.add(component);
  261. }
  262. /**
  263. * This is only exported for components that implemented the now deprecated `locale` prop.
  264. *
  265. * Do not use this utils for new components.
  266. *
  267. * @param component
  268. */
  269. function updateEffectiveLocale(component) {
  270. component.effectiveLocale = getLocale(component);
  271. }
  272. /**
  273. * This utility tears down internals for messages support.
  274. *
  275. * It needs to be called in `disconnectedCallback`
  276. *
  277. * @param component
  278. */
  279. function disconnectLocalized(component) {
  280. connectedComponents.delete(component);
  281. if (connectedComponents.size === 0) {
  282. mutationObserver.disconnect();
  283. }
  284. }
  285. const mutationObserver = createObserver("mutation", (records) => {
  286. records.forEach((record) => {
  287. const el = record.target;
  288. connectedComponents.forEach((component) => {
  289. const hasOverridingLocale = !!(component.locale && !component.el.lang);
  290. const inUnrelatedSubtree = !containsCrossShadowBoundary(el, component.el);
  291. if (hasOverridingLocale || inUnrelatedSubtree) {
  292. return;
  293. }
  294. const closestLangEl = closestElementCrossShadowBoundary(component.el, "[lang]");
  295. if (!closestLangEl) {
  296. component.effectiveLocale = defaultLocale;
  297. return;
  298. }
  299. const closestLang = closestLangEl.lang;
  300. component.effectiveLocale =
  301. // user set lang="" means unknown language, so we use default
  302. closestLangEl.hasAttribute("lang") && closestLang === "" ? defaultLocale : closestLang;
  303. });
  304. });
  305. });
  306. /**
  307. * This util helps resolve a component's locale.
  308. * It will also fall back on the deprecated `locale` if a component implemented this previously.
  309. *
  310. * @param component
  311. */
  312. function getLocale(component) {
  313. var _a;
  314. return (component.el.lang ||
  315. component.locale ||
  316. ((_a = closestElementCrossShadowBoundary(component.el, "[lang]")) === null || _a === void 0 ? void 0 : _a.lang) ||
  317. document.documentElement.lang ||
  318. defaultLocale);
  319. }
  320. /**
  321. * This util formats and parses numbers for localization
  322. */
  323. class NumberStringFormat {
  324. constructor() {
  325. this.delocalize = (numberString) =>
  326. // For performance, (de)localization is skipped if the formatter isn't initialized.
  327. // In order to localize/delocalize, e.g. when lang/numberingSystem props are not default values,
  328. // `numberFormatOptions` must be set in a component to create and cache the formatter.
  329. this._numberFormatOptions
  330. ? sanitizeExponentialNumberString(numberString, (nonExpoNumString) => nonExpoNumString
  331. .trim()
  332. .replace(new RegExp(`[${this._minusSign}]`, "g"), "-")
  333. .replace(new RegExp(`[${this._group}]`, "g"), "")
  334. .replace(new RegExp(`[${this._decimal}]`, "g"), ".")
  335. .replace(new RegExp(`[${this._digits.join("")}]`, "g"), this._getDigitIndex))
  336. : numberString;
  337. this.localize = (numberString) => this._numberFormatOptions
  338. ? sanitizeExponentialNumberString(numberString, (nonExpoNumString) => isValidNumber(nonExpoNumString.trim())
  339. ? new BigDecimal(nonExpoNumString.trim())
  340. .format(this._numberFormatter)
  341. .replace(new RegExp(`[${this._actualGroup}]`, "g"), this._group)
  342. : nonExpoNumString)
  343. : numberString;
  344. }
  345. get group() {
  346. return this._group;
  347. }
  348. get decimal() {
  349. return this._decimal;
  350. }
  351. get minusSign() {
  352. return this._minusSign;
  353. }
  354. get digits() {
  355. return this._digits;
  356. }
  357. get numberFormatter() {
  358. return this._numberFormatter;
  359. }
  360. get numberFormatOptions() {
  361. return this._numberFormatOptions;
  362. }
  363. /**
  364. * numberFormatOptions needs to be set before localize/delocalize is called to ensure the options are up to date
  365. */
  366. set numberFormatOptions(options) {
  367. options.locale = getSupportedLocale(options === null || options === void 0 ? void 0 : options.locale);
  368. options.numberingSystem = getSupportedNumberingSystem(options === null || options === void 0 ? void 0 : options.numberingSystem);
  369. if (
  370. // No need to create the formatter if `locale` and `numberingSystem`
  371. // are the default values and `numberFormatOptions` has not been set
  372. (!this._numberFormatOptions &&
  373. options.locale === defaultLocale &&
  374. options.numberingSystem === defaultNumberingSystem &&
  375. // don't skip initialization if any options besides locale/numberingSystem are set
  376. Object.keys(options).length === 2) ||
  377. // cache formatter by only recreating when options change
  378. JSON.stringify(this._numberFormatOptions) === JSON.stringify(options)) {
  379. return;
  380. }
  381. this._numberFormatOptions = options;
  382. this._numberFormatter = new Intl.NumberFormat(this._numberFormatOptions.locale, this._numberFormatOptions);
  383. this._digits = [
  384. ...new Intl.NumberFormat(this._numberFormatOptions.locale, {
  385. useGrouping: false,
  386. numberingSystem: this._numberFormatOptions.numberingSystem
  387. }).format(9876543210)
  388. ].reverse();
  389. const index = new Map(this._digits.map((d, i) => [d, i]));
  390. const parts = new Intl.NumberFormat(this._numberFormatOptions.locale).formatToParts(-12345678.9);
  391. this._actualGroup = parts.find((d) => d.type === "group").value;
  392. // change whitespace group characters that don't render correctly
  393. this._group = this._actualGroup.trim().length === 0 ? " " : this._actualGroup;
  394. this._decimal = parts.find((d) => d.type === "decimal").value;
  395. this._minusSign = parts.find((d) => d.type === "minusSign").value;
  396. this._getDigitIndex = (d) => index.get(d);
  397. }
  398. }
  399. const numberStringFormatter = new NumberStringFormat();
  400. export { defaultNumberingSystem as a, getSupportedNumberingSystem as b, connectLocalized as c, disconnectLocalized as d, getSupportedLocale as g, isValidNumber as i, numberStringFormatter as n, parseNumberString as p, sanitizeNumberString as s, updateEffectiveLocale as u };