locale-678ce361.js 14 KB

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