Socket
Socket
Sign inDemoInstall

@react-aria/i18n

Package Overview
Dependencies
Maintainers
2
Versions
768
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@react-aria/i18n - npm Package Compare versions

Comparing version 3.0.0-nightly.858 to 3.0.0-nightly.861

277

dist/main.js

@@ -18,3 +18,3 @@ var _babelRuntimeHelpersExtends = $parcel$interopDefault(require("@babel/runtime/helpers/extends"));

useCallback,
useRef
useMemo
} = _react2;

@@ -287,39 +287,2 @@

exports.useDateFormatter = useDateFormatter;
/**
* Provides localized number parsing for the current locale.
* Idea from https://observablehq.com/@mbostock/localized-number-parsing.
*/
function useNumberParser() {
let {
locale
} = useLocale();
const numberData = useRef({
group: null,
decimal: null,
numeral: null,
index: null
});
useEffect(() => {
const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);
const numerals = [...new Intl.NumberFormat(locale, {
useGrouping: false
}).format(9876543210)].reverse();
const index = new Map(numerals.map((d, i) => [d, i]));
numberData.current.group = new RegExp("[" + parts.find(d => d.type === 'group').value + "]", 'g');
numberData.current.decimal = new RegExp("[" + parts.find(d => d.type === 'decimal').value + "]");
numberData.current.numeral = new RegExp("[" + numerals.join('') + "]", 'g');
numberData.current.index = d => index.get(d);
}, [locale]);
const parse = useCallback(value => {
value = value.trim().replace(numberData.current.group, '').replace(numberData.current.decimal, '.').replace(numberData.current.numeral, numberData.current.index);
return value ? +value : NaN;
}, []);
return {
parse
};
}
exports.useNumberParser = useNumberParser;
let $fa77db1482937b6cdb6683d9d7eb896$var$formatterCache = new Map();

@@ -334,2 +297,3 @@ let $fa77db1482937b6cdb6683d9d7eb896$var$supportsSignDisplay = false;

} catch (e) {}
/**

@@ -340,8 +304,18 @@ * Provides localized number formatting for the current locale. Automatically updates when the locale changes,

*/
function useNumberFormatter(options) {
if (options === void 0) {
options = {};
}
function useNumberFormatter(options) {
let {
numeralSystem
} = options;
let {
locale
} = useLocale();
if (numeralSystem && locale.indexOf('-u-nu-') === -1) {
locale = locale + "-u-nu-" + numeralSystem;
}
let cacheKey = locale + (options ? Object.entries(options).sort((a, b) => a[0] < b[0] ? -1 : 1).join() : '');

@@ -372,2 +346,225 @@

exports.useNumberFormatter = useNumberFormatter;
// known supported numbering systems
let $d240dbdea722b9870443b6be31575$var$numberingSystems = {
arab: [...'٠١٢٣٤٥٦٧٨٩'],
hanidec: [...'〇一二三四五六七八九'],
latn: [...'0123456789']
};
function determineNumeralSystem(value) {
for (let i in [...value]) {
let char = value[i];
let system = Object.keys($d240dbdea722b9870443b6be31575$var$numberingSystems).find(key => $d240dbdea722b9870443b6be31575$var$numberingSystems[key].some(numeral => numeral === char));
if (system) {
return system;
}
}
return undefined;
}
exports.determineNumeralSystem = determineNumeralSystem;
let $d240dbdea722b9870443b6be31575$var$CURRENCY_SIGN_REGEX = new RegExp('^.*\\(.*\\).*$');
let $d240dbdea722b9870443b6be31575$var$replaceAllButFirstOccurrence = (val, char) => {
let first = val.indexOf(char);
let prefix = val.substring(0, first + 1);
let suffix = val.substring(first + 1).replace(char, '');
return prefix + suffix;
};
/**
* Provides localized number parsing for the current locale.
* Idea from https://observablehq.com/@mbostock/localized-number-parsing.
*/
function useNumberParser(options) {
if (options === void 0) {
options = {};
}
let {
numeralSystem
} = options;
let {
locale
} = useLocale();
if (numeralSystem && locale.indexOf('-u-nu-') === -1) {
locale = locale + "-u-nu-" + numeralSystem;
}
let formatter = useNumberFormatter(options);
let intlOptions = useMemo(() => formatter.resolvedOptions(), [formatter]);
let symbols = useMemo(() => {
var _allParts$find, _allParts$find2, _posAllParts$find, _allParts$find3, _allParts$find4;
// Get the minus sign of the current locale to filter the input value
// Automatically updates the minus sign when numberFormatter changes
// won't work for currency accounting, but we have validCharacters for that in the pattern
// Note: some locale's don't add a group symbol until there is a ten thousands place
let allParts = formatter.formatToParts(-10000.1);
let minusSign = (_allParts$find = allParts.find(p => p.type === 'minusSign')) == null ? void 0 : _allParts$find.value;
let currency = (_allParts$find2 = allParts.find(p => p.type === 'currency')) == null ? void 0 : _allParts$find2.value;
let posAllParts = formatter.formatToParts(10000.1);
let plusSign = (_posAllParts$find = posAllParts.find(p => p.type === 'plusSign')) == null ? void 0 : _posAllParts$find.value;
let decimal = (_allParts$find3 = allParts.find(p => p.type === 'decimal')) == null ? void 0 : _allParts$find3.value;
let group = (_allParts$find4 = allParts.find(p => p.type === 'group')) == null ? void 0 : _allParts$find4.value; // this is a string ready for a regex so we can identify allowed to type characters, signs are excluded because
// the user needs to decide if it's allowed based on min/max values which parsing doesn't care about
let validCharacters = allParts.filter(p => {
if (p.type === 'decimal' && intlOptions.maximumFractionDigits === 0) {
return false;
}
if (p.type === 'fraction' || p.type === 'integer' || p.type === 'minusSign' || p.type === 'plusSign') {
return false;
}
return true;
}).map(p => p.value).join(''); // this set is also for a regex, it's all literals that might be in the string we want to eventually parse that
// don't contribute to the numerical value
let literals = allParts.filter(p => {
if (p.type === 'decimal' || p.type === 'fraction' || p.type === 'integer' || p.type === 'minusSign' || p.type === 'plusSign') {
return false;
}
return true;
}).map(p => p.value).join(''); // These are for replacing non-latn characters with the latn equivalent
let numerals = [...new Intl.NumberFormat(locale, {
useGrouping: false
}).format(9876543210)].reverse();
let indexes = new Map(numerals.map((d, i) => [d, i]));
let numeral = new RegExp("[" + numerals.join('') + "]", 'g');
let index = d => String(indexes.get(d));
return {
minusSign,
plusSign,
decimal,
group,
currency,
validCharacters,
literals,
numeral,
index
};
}, [formatter, intlOptions, locale]);
const parse = useCallback(value => {
// assuming a clean string
// to parse the number, we need to remove anything that isn't actually part of the number, for example we want '-10.40' not '-10.40 USD'
let fullySanitizedValue = value.replace(new RegExp("[" + symbols.literals + "\\p{White_Space}]", 'gu'), ''); // first match the prefix/suffix wrapper of everything that isn't a number
// then preserve that as we'll need to put it back together as the current input value
// with the actual numerals, parse them into a number
fullySanitizedValue = fullySanitizedValue.trim().replace(symbols.group, '').replace(symbols.decimal, '.').replace(symbols.numeral, symbols.index);
let newValue = fullySanitizedValue ? +fullySanitizedValue : NaN;
if (isNaN(newValue)) {
return NaN;
} // accounting will always be stripped to a positive number, so if it's accounting and has a () around everything, then we need to make it negative again
if ((intlOptions == null ? void 0 : intlOptions.currencySign) === 'accounting' && $d240dbdea722b9870443b6be31575$var$CURRENCY_SIGN_REGEX.test(value)) {
newValue = -1 * newValue;
} // when reading the number, if it's a percent, then it should be interpreted as being divided by 100
if ((intlOptions == null ? void 0 : intlOptions.style) === 'percent') {
newValue = newValue / 100; // after dividing to get the percent value, javascript may get .0210999999 instead of .0211, so fix the number of fraction digits
newValue = +newValue.toFixed(intlOptions.maximumFractionDigits + 2);
}
return newValue;
}, [symbols, intlOptions]);
/**
* We could round by using toFixed and toPrecision with intlOptions fraction digits and significant digits
* respectively. However, it's hard to enforce the minimum's intlOptions also supports and it would take
* multiple trips from number to string and back again to accomplish.
* Instead, we format the number using the same format options that are passed in, strip it of everything
* that doesn't contribute to the numerical value, and parse it back to a number. This ensures the same
* rounding that the formatter will perform.
* @param value
*/
let round = value => parse(formatter.format(value));
let clean = value => {
let {
minusSign = '',
plusSign = '',
validCharacters,
group,
decimal,
currency
} = symbols;
/**
* Some currency symbols contain characters used in other parts of the number, like decimal characters.
* For example, the Bulgarian USD symbol is `щ.д.`. We don't want to remove those while we're cleaning
* the rest of the string. Store the start of the currency symbol so we can restore it later.
*/
let indexOfCurrency;
if (currency) {
indexOfCurrency = value.indexOf(currency);
} // In arab numeral system, their decimal character is 1643, but most keyboards don't type that
// instead they use the , (44) character or apparently the (1548) character.
let result = value;
if (numeralSystem === 'arab') {
result = result.replace(',', decimal);
result = result.replace(String.fromCharCode(1548), decimal);
result = result.replace('.', group);
} // fr-FR group character is char code 8239, but that's not a key on the french keyboard,
// so allow 'period' as a group char and replace it with a space
if (locale === 'fr-FR') {
result = result.replace('.', String.fromCharCode(8239));
}
/**
* Up until now we've replaced characters 1:1, not altering the length of the string.
* We are safe to restore the currency symbol now.
*/
if (currency && indexOfCurrency !== -1) {
result = result.substring(0, indexOfCurrency) + currency + result.substring(indexOfCurrency + currency.length, result.length);
}
let numerals = $d240dbdea722b9870443b6be31575$var$numberingSystems[numeralSystem || 'latn'].join('');
if (!numeralSystem) {
numerals = "" + numerals + $d240dbdea722b9870443b6be31575$var$numberingSystems['hanidec'].join('') + $d240dbdea722b9870443b6be31575$var$numberingSystems['arab'].join('');
} // 'u' flag is necessary for unicode
let invalidChars = new RegExp("[^" + minusSign + plusSign + numerals + validCharacters + "\\p{White_Space}]", 'gu');
let strippedValue = result.replace(invalidChars, '');
strippedValue = $d240dbdea722b9870443b6be31575$var$replaceAllButFirstOccurrence(strippedValue, minusSign);
strippedValue = $d240dbdea722b9870443b6be31575$var$replaceAllButFirstOccurrence(strippedValue, plusSign);
strippedValue = $d240dbdea722b9870443b6be31575$var$replaceAllButFirstOccurrence(strippedValue, decimal);
return strippedValue;
};
return {
parse,
round,
symbols: {
minusSign: symbols.minusSign,
plusSign: symbols.plusSign
},
clean
};
}
exports.useNumberParser = useNumberParser;
let $f2d7166fa8b4811bca7b68ebd673b$var$cache = new Map();

@@ -374,0 +571,0 @@ /**

import _babelRuntimeHelpersEsmExtends from "@babel/runtime/helpers/esm/extends";
import _intlMessageformat from "intl-messageformat";
import { useIsSSR } from "@react-aria/ssr";
import _react, { useEffect, useState, useContext, useCallback, useRef } from "react";
import _react, { useEffect, useState, useContext, useCallback, useMemo } from "react";

@@ -258,37 +258,2 @@ /*

}
/**
* Provides localized number parsing for the current locale.
* Idea from https://observablehq.com/@mbostock/localized-number-parsing.
*/
export function useNumberParser() {
let {
locale
} = useLocale();
const numberData = useRef({
group: null,
decimal: null,
numeral: null,
index: null
});
useEffect(() => {
const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);
const numerals = [...new Intl.NumberFormat(locale, {
useGrouping: false
}).format(9876543210)].reverse();
const index = new Map(numerals.map((d, i) => [d, i]));
numberData.current.group = new RegExp("[" + parts.find(d => d.type === 'group').value + "]", 'g');
numberData.current.decimal = new RegExp("[" + parts.find(d => d.type === 'decimal').value + "]");
numberData.current.numeral = new RegExp("[" + numerals.join('') + "]", 'g');
numberData.current.index = d => index.get(d);
}, [locale]);
const parse = useCallback(value => {
value = value.trim().replace(numberData.current.group, '').replace(numberData.current.decimal, '.').replace(numberData.current.numeral, numberData.current.index);
return value ? +value : NaN;
}, []);
return {
parse
};
}
let $ece3e138e83d330f42860705a2ec18a$var$formatterCache = new Map();

@@ -303,2 +268,3 @@ let $ece3e138e83d330f42860705a2ec18a$var$supportsSignDisplay = false;

} catch (e) {}
/**

@@ -309,8 +275,18 @@ * Provides localized number formatting for the current locale. Automatically updates when the locale changes,

*/
export function useNumberFormatter(options) {
if (options === void 0) {
options = {};
}
export function useNumberFormatter(options) {
let {
numeralSystem
} = options;
let {
locale
} = useLocale();
if (numeralSystem && locale.indexOf('-u-nu-') === -1) {
locale = locale + "-u-nu-" + numeralSystem;
}
let cacheKey = locale + (options ? Object.entries(options).sort((a, b) => a[0] < b[0] ? -1 : 1).join() : '');

@@ -339,2 +315,220 @@

}
// known supported numbering systems
let $faed188f6c4cda45a65dabbd27f06$var$numberingSystems = {
arab: [...'٠١٢٣٤٥٦٧٨٩'],
hanidec: [...'〇一二三四五六七八九'],
latn: [...'0123456789']
};
export function determineNumeralSystem(value) {
for (let i in [...value]) {
let char = value[i];
let system = Object.keys($faed188f6c4cda45a65dabbd27f06$var$numberingSystems).find(key => $faed188f6c4cda45a65dabbd27f06$var$numberingSystems[key].some(numeral => numeral === char));
if (system) {
return system;
}
}
return undefined;
}
let $faed188f6c4cda45a65dabbd27f06$var$CURRENCY_SIGN_REGEX = new RegExp('^.*\\(.*\\).*$');
let $faed188f6c4cda45a65dabbd27f06$var$replaceAllButFirstOccurrence = (val, char) => {
let first = val.indexOf(char);
let prefix = val.substring(0, first + 1);
let suffix = val.substring(first + 1).replace(char, '');
return prefix + suffix;
};
/**
* Provides localized number parsing for the current locale.
* Idea from https://observablehq.com/@mbostock/localized-number-parsing.
*/
export function useNumberParser(options) {
if (options === void 0) {
options = {};
}
let {
numeralSystem
} = options;
let {
locale
} = useLocale();
if (numeralSystem && locale.indexOf('-u-nu-') === -1) {
locale = locale + "-u-nu-" + numeralSystem;
}
let formatter = useNumberFormatter(options);
let intlOptions = useMemo(() => formatter.resolvedOptions(), [formatter]);
let symbols = useMemo(() => {
var _allParts$find, _allParts$find2, _posAllParts$find, _allParts$find3, _allParts$find4;
// Get the minus sign of the current locale to filter the input value
// Automatically updates the minus sign when numberFormatter changes
// won't work for currency accounting, but we have validCharacters for that in the pattern
// Note: some locale's don't add a group symbol until there is a ten thousands place
let allParts = formatter.formatToParts(-10000.1);
let minusSign = (_allParts$find = allParts.find(p => p.type === 'minusSign')) == null ? void 0 : _allParts$find.value;
let currency = (_allParts$find2 = allParts.find(p => p.type === 'currency')) == null ? void 0 : _allParts$find2.value;
let posAllParts = formatter.formatToParts(10000.1);
let plusSign = (_posAllParts$find = posAllParts.find(p => p.type === 'plusSign')) == null ? void 0 : _posAllParts$find.value;
let decimal = (_allParts$find3 = allParts.find(p => p.type === 'decimal')) == null ? void 0 : _allParts$find3.value;
let group = (_allParts$find4 = allParts.find(p => p.type === 'group')) == null ? void 0 : _allParts$find4.value; // this is a string ready for a regex so we can identify allowed to type characters, signs are excluded because
// the user needs to decide if it's allowed based on min/max values which parsing doesn't care about
let validCharacters = allParts.filter(p => {
if (p.type === 'decimal' && intlOptions.maximumFractionDigits === 0) {
return false;
}
if (p.type === 'fraction' || p.type === 'integer' || p.type === 'minusSign' || p.type === 'plusSign') {
return false;
}
return true;
}).map(p => p.value).join(''); // this set is also for a regex, it's all literals that might be in the string we want to eventually parse that
// don't contribute to the numerical value
let literals = allParts.filter(p => {
if (p.type === 'decimal' || p.type === 'fraction' || p.type === 'integer' || p.type === 'minusSign' || p.type === 'plusSign') {
return false;
}
return true;
}).map(p => p.value).join(''); // These are for replacing non-latn characters with the latn equivalent
let numerals = [...new Intl.NumberFormat(locale, {
useGrouping: false
}).format(9876543210)].reverse();
let indexes = new Map(numerals.map((d, i) => [d, i]));
let numeral = new RegExp("[" + numerals.join('') + "]", 'g');
let index = d => String(indexes.get(d));
return {
minusSign,
plusSign,
decimal,
group,
currency,
validCharacters,
literals,
numeral,
index
};
}, [formatter, intlOptions, locale]);
const parse = useCallback(value => {
// assuming a clean string
// to parse the number, we need to remove anything that isn't actually part of the number, for example we want '-10.40' not '-10.40 USD'
let fullySanitizedValue = value.replace(new RegExp("[" + symbols.literals + "\\p{White_Space}]", 'gu'), ''); // first match the prefix/suffix wrapper of everything that isn't a number
// then preserve that as we'll need to put it back together as the current input value
// with the actual numerals, parse them into a number
fullySanitizedValue = fullySanitizedValue.trim().replace(symbols.group, '').replace(symbols.decimal, '.').replace(symbols.numeral, symbols.index);
let newValue = fullySanitizedValue ? +fullySanitizedValue : NaN;
if (isNaN(newValue)) {
return NaN;
} // accounting will always be stripped to a positive number, so if it's accounting and has a () around everything, then we need to make it negative again
if ((intlOptions == null ? void 0 : intlOptions.currencySign) === 'accounting' && $faed188f6c4cda45a65dabbd27f06$var$CURRENCY_SIGN_REGEX.test(value)) {
newValue = -1 * newValue;
} // when reading the number, if it's a percent, then it should be interpreted as being divided by 100
if ((intlOptions == null ? void 0 : intlOptions.style) === 'percent') {
newValue = newValue / 100; // after dividing to get the percent value, javascript may get .0210999999 instead of .0211, so fix the number of fraction digits
newValue = +newValue.toFixed(intlOptions.maximumFractionDigits + 2);
}
return newValue;
}, [symbols, intlOptions]);
/**
* We could round by using toFixed and toPrecision with intlOptions fraction digits and significant digits
* respectively. However, it's hard to enforce the minimum's intlOptions also supports and it would take
* multiple trips from number to string and back again to accomplish.
* Instead, we format the number using the same format options that are passed in, strip it of everything
* that doesn't contribute to the numerical value, and parse it back to a number. This ensures the same
* rounding that the formatter will perform.
* @param value
*/
let round = value => parse(formatter.format(value));
let clean = value => {
let {
minusSign = '',
plusSign = '',
validCharacters,
group,
decimal,
currency
} = symbols;
/**
* Some currency symbols contain characters used in other parts of the number, like decimal characters.
* For example, the Bulgarian USD symbol is `щ.д.`. We don't want to remove those while we're cleaning
* the rest of the string. Store the start of the currency symbol so we can restore it later.
*/
let indexOfCurrency;
if (currency) {
indexOfCurrency = value.indexOf(currency);
} // In arab numeral system, their decimal character is 1643, but most keyboards don't type that
// instead they use the , (44) character or apparently the (1548) character.
let result = value;
if (numeralSystem === 'arab') {
result = result.replace(',', decimal);
result = result.replace(String.fromCharCode(1548), decimal);
result = result.replace('.', group);
} // fr-FR group character is char code 8239, but that's not a key on the french keyboard,
// so allow 'period' as a group char and replace it with a space
if (locale === 'fr-FR') {
result = result.replace('.', String.fromCharCode(8239));
}
/**
* Up until now we've replaced characters 1:1, not altering the length of the string.
* We are safe to restore the currency symbol now.
*/
if (currency && indexOfCurrency !== -1) {
result = result.substring(0, indexOfCurrency) + currency + result.substring(indexOfCurrency + currency.length, result.length);
}
let numerals = $faed188f6c4cda45a65dabbd27f06$var$numberingSystems[numeralSystem || 'latn'].join('');
if (!numeralSystem) {
numerals = "" + numerals + $faed188f6c4cda45a65dabbd27f06$var$numberingSystems['hanidec'].join('') + $faed188f6c4cda45a65dabbd27f06$var$numberingSystems['arab'].join('');
} // 'u' flag is necessary for unicode
let invalidChars = new RegExp("[^" + minusSign + plusSign + numerals + validCharacters + "\\p{White_Space}]", 'gu');
let strippedValue = result.replace(invalidChars, '');
strippedValue = $faed188f6c4cda45a65dabbd27f06$var$replaceAllButFirstOccurrence(strippedValue, minusSign);
strippedValue = $faed188f6c4cda45a65dabbd27f06$var$replaceAllButFirstOccurrence(strippedValue, plusSign);
strippedValue = $faed188f6c4cda45a65dabbd27f06$var$replaceAllButFirstOccurrence(strippedValue, decimal);
return strippedValue;
};
return {
parse,
round,
symbols: {
minusSign: symbols.minusSign,
plusSign: symbols.plusSign
},
clean
};
}
let $a4045a18d7252bf6de9312e613c4e$var$cache = new Map();

@@ -341,0 +535,0 @@ /**

33

dist/types.d.ts

@@ -43,5 +43,26 @@ import { Direction } from "@react-types/shared";

export function useDateFormatter(options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
type NumberParser = {
export type NumeralSystem = 'arab' | 'hanidec' | 'latn';
export interface NumberFormatOptions extends Intl.NumberFormatOptions {
/** Overrides the CLDR or browser default numeral system for the current locale. */
numeralSystem?: NumeralSystem;
}
/**
* Provides localized number formatting for the current locale. Automatically updates when the locale changes,
* and handles caching of the number formatter for performance.
* @param options - Formatting options.
*/
export function useNumberFormatter(options?: NumberFormatOptions): Intl.NumberFormat;
interface NumberParser {
/** Parses a cleaned string into the number it represents. */
parse: (value: string) => number;
};
/** Rounds a number using the current formatter. */
round: (value: number) => number;
symbols: {
minusSign: string;
plusSign: string;
};
/** This removes any not allowed characters from the string. */
clean: (value: string) => string;
}
export function determineNumeralSystem(value: string): NumeralSystem;
/**

@@ -51,10 +72,4 @@ * Provides localized number parsing for the current locale.

*/
export function useNumberParser(): NumberParser;
export function useNumberParser(options?: NumberFormatOptions): NumberParser;
/**
* Provides localized number formatting for the current locale. Automatically updates when the locale changes,
* and handles caching of the number formatter for performance.
* @param options - Formatting options.
*/
export function useNumberFormatter(options?: Intl.NumberFormatOptions): Intl.NumberFormat;
/**
* Provides localized string collation for the current locale. Automatically updates when the locale changes,

@@ -61,0 +76,0 @@ * and handles caching of the collator for performance.

{
"name": "@react-aria/i18n",
"version": "3.0.0-nightly.858+7326ad2a",
"version": "3.0.0-nightly.861+bd1afa1d",
"description": "Spectrum UI components in React",

@@ -21,4 +21,5 @@ "license": "Apache-2.0",

"@babel/runtime": "^7.6.2",
"@react-aria/ssr": "3.0.2-nightly.2536+7326ad2a",
"@react-types/shared": "3.0.0-nightly.858+7326ad2a",
"@react-aria/ssr": "3.0.2-nightly.2539+bd1afa1d",
"@react-aria/utils": "3.0.0-nightly.861+bd1afa1d",
"@react-types/shared": "3.0.0-nightly.861+bd1afa1d",
"intl-messageformat": "^2.2.0"

@@ -32,3 +33,3 @@ },

},
"gitHead": "7326ad2ab299a05c6786c1fc8e23df652b3d630d"
"gitHead": "bd1afa1d8f11893a1b3de13eaa3e99c19300d6bd"
}

@@ -13,2 +13,4 @@ /*

/// <reference types="intl-types-extension" />
export * from './context';

@@ -15,0 +17,0 @@ export * from './useMessageFormatter';

@@ -25,2 +25,9 @@ /*

export type NumeralSystem = 'arab' | 'hanidec' | 'latn';
export interface NumberFormatOptions extends Intl.NumberFormatOptions {
/** Overrides the CLDR or browser default numeral system for the current locale. */
numeralSystem?: NumeralSystem
}
/**

@@ -31,5 +38,10 @@ * Provides localized number formatting for the current locale. Automatically updates when the locale changes,

*/
export function useNumberFormatter(options?: Intl.NumberFormatOptions): Intl.NumberFormat {
export function useNumberFormatter(options: NumberFormatOptions = {}): Intl.NumberFormat {
let {numeralSystem} = options;
let {locale} = useLocale();
if (numeralSystem && locale.indexOf('-u-nu-') === -1) {
locale = `${locale}-u-nu-${numeralSystem}`;
}
let cacheKey = locale + (options ? Object.entries(options).sort((a, b) => a[0] < b[0] ? -1 : 1).join() : '');

@@ -36,0 +48,0 @@ if (formatterCache.has(cacheKey)) {

@@ -13,9 +13,47 @@ /*

import {useCallback, useEffect, useRef} from 'react';
import {NumberFormatOptions, NumeralSystem, useNumberFormatter} from './useNumberFormatter';
import {useCallback, useMemo} from 'react';
import {useLocale} from './context';
type NumberParser = {
parse: (value:string) => number
interface NumberParser {
/** Parses a cleaned string into the number it represents. */
parse: (value: string) => number,
/** Rounds a number using the current formatter. */
round: (value: number) => number,
symbols: {
minusSign: string,
plusSign: string
},
/** This removes any not allowed characters from the string. */
clean: (value: string) => string
}
// known supported numbering systems
let numberingSystems = {
arab: [...('٠١٢٣٤٥٦٧٨٩')],
hanidec: [...('〇一二三四五六七八九')],
latn: [...('0123456789')]
};
export function determineNumeralSystem(value: string): NumeralSystem {
for (let i in [...value]) {
let char = value[i];
let system = Object.keys(numberingSystems).find(key => numberingSystems[key].some(numeral => numeral === char));
if (system) {
return system as NumeralSystem;
}
}
return undefined;
}
let CURRENCY_SIGN_REGEX = new RegExp('^.*\\(.*\\).*$');
let replaceAllButFirstOccurrence = (val: string, char: string) => {
let first = val.indexOf(char);
let prefix = val.substring(0, first + 1);
let suffix = val.substring(first + 1).replace(char, '');
return prefix + suffix;
};
/**

@@ -25,26 +63,147 @@ * Provides localized number parsing for the current locale.

*/
export function useNumberParser(): NumberParser {
export function useNumberParser(options: NumberFormatOptions = {}): NumberParser {
let {numeralSystem} = options;
let {locale} = useLocale();
const numberData = useRef({group: null, decimal: null, numeral: null, index: null});
if (numeralSystem && locale.indexOf('-u-nu-') === -1) {
locale = `${locale}-u-nu-${numeralSystem}`;
}
useEffect(() => {
const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);
const numerals = [...new Intl.NumberFormat(locale, {useGrouping: false}).format(9876543210)].reverse();
const index = new Map(numerals.map((d, i) => [d, i]));
numberData.current.group = new RegExp(`[${parts.find(d => d.type === 'group').value}]`, 'g');
numberData.current.decimal = new RegExp(`[${parts.find(d => d.type === 'decimal').value}]`);
numberData.current.numeral = new RegExp(`[${numerals.join('')}]`, 'g');
numberData.current.index = d => index.get(d);
}, [locale]);
let formatter = useNumberFormatter(options);
let intlOptions = useMemo(() => formatter.resolvedOptions(), [formatter]);
const parse = useCallback((value:string) => {
value = value.trim()
.replace(numberData.current.group, '')
.replace(numberData.current.decimal, '.')
.replace(numberData.current.numeral, numberData.current.index);
return value ? +value : NaN;
}, []);
let symbols = useMemo(() => {
// Get the minus sign of the current locale to filter the input value
// Automatically updates the minus sign when numberFormatter changes
// won't work for currency accounting, but we have validCharacters for that in the pattern
// Note: some locale's don't add a group symbol until there is a ten thousands place
let allParts = formatter.formatToParts(-10000.1);
let minusSign = allParts.find(p => p.type === 'minusSign')?.value;
let currency = allParts.find(p => p.type === 'currency')?.value;
let posAllParts = formatter.formatToParts(10000.1);
let plusSign = posAllParts.find(p => p.type === 'plusSign')?.value;
return {parse};
let decimal = allParts.find(p => p.type === 'decimal')?.value;
let group = allParts.find(p => p.type === 'group')?.value;
// this is a string ready for a regex so we can identify allowed to type characters, signs are excluded because
// the user needs to decide if it's allowed based on min/max values which parsing doesn't care about
let validCharacters = allParts.filter(p => {
if (p.type === 'decimal' && intlOptions.maximumFractionDigits === 0) {
return false;
}
if (p.type === 'fraction' || p.type === 'integer' || p.type === 'minusSign' || p.type === 'plusSign') {
return false;
}
return true;
}).map(p => p.value).join('');
// this set is also for a regex, it's all literals that might be in the string we want to eventually parse that
// don't contribute to the numerical value
let literals = allParts.filter(p => {
if (p.type === 'decimal' || p.type === 'fraction' || p.type === 'integer' || p.type === 'minusSign' || p.type === 'plusSign') {
return false;
}
return true;
}).map(p => p.value).join('');
// These are for replacing non-latn characters with the latn equivalent
let numerals = [...new Intl.NumberFormat(locale, {useGrouping: false}).format(9876543210)].reverse();
let indexes = new Map(numerals.map((d, i) => [d, i]));
let numeral = new RegExp(`[${numerals.join('')}]`, 'g');
let index = d => String(indexes.get(d));
return {minusSign, plusSign, decimal, group, currency, validCharacters, literals, numeral, index};
}, [formatter, intlOptions, locale]);
const parse = useCallback((value:string): number => {
// assuming a clean string
// to parse the number, we need to remove anything that isn't actually part of the number, for example we want '-10.40' not '-10.40 USD'
let fullySanitizedValue = value.replace(new RegExp(`[${symbols.literals}\\p{White_Space}]`, 'gu'), '');
// first match the prefix/suffix wrapper of everything that isn't a number
// then preserve that as we'll need to put it back together as the current input value
// with the actual numerals, parse them into a number
fullySanitizedValue = fullySanitizedValue.trim()
.replace(symbols.group, '')
.replace(symbols.decimal, '.')
.replace(symbols.numeral, symbols.index);
let newValue = fullySanitizedValue ? +fullySanitizedValue : NaN;
if (isNaN(newValue)) {
return NaN;
}
// accounting will always be stripped to a positive number, so if it's accounting and has a () around everything, then we need to make it negative again
if (intlOptions?.currencySign === 'accounting' && CURRENCY_SIGN_REGEX.test(value)) {
newValue = -1 * newValue;
}
// when reading the number, if it's a percent, then it should be interpreted as being divided by 100
if (intlOptions?.style === 'percent') {
newValue = newValue / 100;
// after dividing to get the percent value, javascript may get .0210999999 instead of .0211, so fix the number of fraction digits
newValue = +newValue.toFixed(intlOptions.maximumFractionDigits + 2);
}
return newValue;
}, [symbols, intlOptions]);
/**
* We could round by using toFixed and toPrecision with intlOptions fraction digits and significant digits
* respectively. However, it's hard to enforce the minimum's intlOptions also supports and it would take
* multiple trips from number to string and back again to accomplish.
* Instead, we format the number using the same format options that are passed in, strip it of everything
* that doesn't contribute to the numerical value, and parse it back to a number. This ensures the same
* rounding that the formatter will perform.
* @param value
*/
let round = (value: number): number => parse(formatter.format(value));
let clean = (value: string): string => {
let {minusSign = '', plusSign = '', validCharacters, group, decimal, currency} = symbols;
/**
* Some currency symbols contain characters used in other parts of the number, like decimal characters.
* For example, the Bulgarian USD symbol is `щ.д.`. We don't want to remove those while we're cleaning
* the rest of the string. Store the start of the currency symbol so we can restore it later.
*/
let indexOfCurrency;
if (currency) {
indexOfCurrency = value.indexOf(currency);
}
// In arab numeral system, their decimal character is 1643, but most keyboards don't type that
// instead they use the , (44) character or apparently the (1548) character.
let result = value;
if (numeralSystem === 'arab') {
result = result.replace(',', decimal);
result = result.replace(String.fromCharCode(1548), decimal);
result = result.replace('.', group);
}
// fr-FR group character is char code 8239, but that's not a key on the french keyboard,
// so allow 'period' as a group char and replace it with a space
if (locale === 'fr-FR') {
result = result.replace('.', String.fromCharCode(8239));
}
/**
* Up until now we've replaced characters 1:1, not altering the length of the string.
* We are safe to restore the currency symbol now.
*/
if (currency && indexOfCurrency !== -1) {
result = result.substring(0, indexOfCurrency) + currency + result.substring(indexOfCurrency + currency.length, result.length);
}
let numerals = numberingSystems[numeralSystem || 'latn'].join('');
if (!numeralSystem) {
numerals = `${numerals}${numberingSystems['hanidec'].join('')}${numberingSystems['arab'].join('')}`;
}
// 'u' flag is necessary for unicode
let invalidChars = new RegExp(`[^${minusSign}${plusSign}${numerals}${validCharacters}\\p{White_Space}]`, 'gu');
let strippedValue = result.replace(invalidChars, '');
strippedValue = replaceAllButFirstOccurrence(strippedValue, minusSign);
strippedValue = replaceAllButFirstOccurrence(strippedValue, plusSign);
strippedValue = replaceAllButFirstOccurrence(strippedValue, decimal);
return strippedValue;
};
return {parse, round, symbols: {minusSign: symbols.minusSign, plusSign: symbols.plusSign}, clean};
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc