From 60acc689ee5f1323dfde89e52f266daad757cd74 Mon Sep 17 00:00:00 2001 From: Jack Short Date: Tue, 5 Sep 2023 12:59:43 -0400 Subject: [PATCH] chore: updating numberFormat for other currencies and languages (#7239) * adding currency settings option * moving menu item to shared component * adding supported currencies * currency menu items * currency url params * currency selector e2e tests * fixing tests * currency icons * removing eslint * removing another eslint disable * renaming to local currency * more name changes * design updates * adding currencies * changing formatter rules to formatter options * renaming file * fixing lint * custom currency symbol in number formatting * refactoring input to number formatter * locale selection * updating all format numbers with currency and locale * updating portfolio * better formatting symbols * removing adding locale and currency * upgrading constants to be translatable * refactoring * adding tests * updating comment * removing 0 from hardcoded input and comment --- .../AccountDrawer/AuthenticatedHeader.tsx | 12 +- .../MiniPortfolio/Pools/index.tsx | 19 +- .../MiniPortfolio/Tokens/index.tsx | 7 +- .../CurrencyInputPanel/FiatValue.tsx | 5 +- .../Tokens/TokenDetails/StatsSection.tsx | 7 +- src/components/Tokens/TokenTable/TokenRow.tsx | 12 +- src/components/swap/AdvancedSwapDetails.tsx | 8 +- src/components/swap/GasBreakdownTooltip.tsx | 6 +- src/components/swap/GasEstimateTooltip.tsx | 14 +- src/components/swap/SwapModalFooter.tsx | 7 +- src/components/swap/SwapModalHeaderAmount.tsx | 5 +- src/components/swap/TradePrice.tsx | 9 +- src/constants/localCurrencies.tsx | 25 ++ src/utils/formatNumbers.test.ts | 401 ++++++++++++++---- src/utils/formatNumbers.ts | 307 +++++++++----- 15 files changed, 621 insertions(+), 223 deletions(-) diff --git a/src/components/AccountDrawer/AuthenticatedHeader.tsx b/src/components/AccountDrawer/AuthenticatedHeader.tsx index 7636ad1f43..73b5fc25d2 100644 --- a/src/components/AccountDrawer/AuthenticatedHeader.tsx +++ b/src/components/AccountDrawer/AuthenticatedHeader.tsx @@ -279,16 +279,20 @@ export default function AuthenticatedHeader({ account, openSettings }: { account {totalBalance !== undefined ? ( - {formatNumber(totalBalance, NumberType.PortfolioBalance)} + {formatNumber({ + input: totalBalance, + type: NumberType.PortfolioBalance, + })} {absoluteChange !== 0 && percentChange && ( <> - {`${formatNumber(Math.abs(absoluteChange as number), NumberType.PortfolioBalance)} (${formatDelta( - percentChange - )})`} + {`${formatNumber({ + input: Math.abs(absoluteChange as number), + type: NumberType.PortfolioBalance, + })} (${formatDelta(percentChange)})`} )} diff --git a/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx b/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx index 1b8e16a6f5..414b1b165c 100644 --- a/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx +++ b/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx @@ -167,18 +167,21 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) { placement="left" text={
- {`${formatNumber( - liquidityValue, - NumberType.PortfolioBalance - )} (liquidity) + ${formatNumber( - feeValue, - NumberType.PortfolioBalance - )} (fees)`} + {`${formatNumber({ + input: liquidityValue, + type: NumberType.PortfolioBalance, + })} (liquidity) + ${formatNumber({ + input: feeValue, + type: NumberType.PortfolioBalance, + })} (fees)`}
} > - {formatNumber((liquidityValue ?? 0) + (feeValue ?? 0), NumberType.PortfolioBalance)} + {formatNumber({ + input: (liquidityValue ?? 0) + (feeValue ?? 0), + type: NumberType.PortfolioBalance, + })} diff --git a/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx b/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx index d3a75e1863..494f3d595d 100644 --- a/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx +++ b/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx @@ -101,7 +101,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok title={{token?.name}} descriptor={ - {formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol} + {formatNumber({ input: quantity, type: NumberType.TokenNonTx })} {token?.symbol} } onClick={navigateToTokenDetails} @@ -109,7 +109,10 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok denominatedValue && ( <> - {formatNumber(denominatedValue?.value, NumberType.PortfolioBalance)} + {formatNumber({ + input: denominatedValue?.value, + type: NumberType.PortfolioBalance, + })} diff --git a/src/components/CurrencyInputPanel/FiatValue.tsx b/src/components/CurrencyInputPanel/FiatValue.tsx index ee52cd4da8..1629389203 100644 --- a/src/components/CurrencyInputPanel/FiatValue.tsx +++ b/src/components/CurrencyInputPanel/FiatValue.tsx @@ -39,7 +39,10 @@ export function FiatValue({ {fiatValue.data ? ( - formatNumber(fiatValue.data, NumberType.FiatTokenPrice) + formatNumber({ + input: fiatValue.data, + type: NumberType.FiatTokenPrice, + }) ) : ( Not enough liquidity to show accurate USD value.}>- )} diff --git a/src/components/Tokens/TokenDetails/StatsSection.tsx b/src/components/Tokens/TokenDetails/StatsSection.tsx index 7f94e046fe..35f2f0f4e0 100644 --- a/src/components/Tokens/TokenDetails/StatsSection.tsx +++ b/src/components/Tokens/TokenDetails/StatsSection.tsx @@ -62,7 +62,12 @@ function Stat({ return ( {title} - {formatNumber(value, NumberType.FiatTokenStats)} + + {formatNumber({ + input: value, + type: NumberType.FiatTokenStats, + })} + ) } diff --git a/src/components/Tokens/TokenTable/TokenRow.tsx b/src/components/Tokens/TokenTable/TokenRow.tsx index 8fd0d7b232..f36ffb74b1 100644 --- a/src/components/Tokens/TokenTable/TokenRow.tsx +++ b/src/components/Tokens/TokenTable/TokenRow.tsx @@ -503,11 +503,19 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef - {formatNumber(token.market?.totalValueLocked?.value, NumberType.FiatTokenStats)} + {formatNumber({ + input: token.market?.totalValueLocked?.value, + type: NumberType.FiatTokenStats, + })} } volume={ - {formatNumber(token.market?.volume?.value, NumberType.FiatTokenStats)} + + {formatNumber({ + input: token.market?.volume?.value, + type: NumberType.FiatTokenStats, + })} + } sparkLine={ diff --git a/src/components/swap/AdvancedSwapDetails.tsx b/src/components/swap/AdvancedSwapDetails.tsx index 018a1ca33d..849c2b6001 100644 --- a/src/components/swap/AdvancedSwapDetails.tsx +++ b/src/components/swap/AdvancedSwapDetails.tsx @@ -73,10 +73,10 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: > - {`${trade.totalGasUseEstimateUSD ? '~' : ''}${formatNumber( - trade.totalGasUseEstimateUSD, - NumberType.FiatGasPrice - )}`} + {`${trade.totalGasUseEstimateUSD ? '~' : ''}${formatNumber({ + input: trade.totalGasUseEstimateUSD, + type: NumberType.FiatGasPrice, + })}`} diff --git a/src/components/swap/GasBreakdownTooltip.tsx b/src/components/swap/GasBreakdownTooltip.tsx index a7e5c012f6..611c4b660a 100644 --- a/src/components/swap/GasBreakdownTooltip.tsx +++ b/src/components/swap/GasBreakdownTooltip.tsx @@ -39,7 +39,11 @@ const GasCostItem = ({ {title} - {itemValue ?? formatNumber(amount, NumberType.FiatGasPrice)} + {itemValue ?? + formatNumber({ + input: amount, + type: NumberType.FiatGasPrice, + })} ) diff --git a/src/components/swap/GasEstimateTooltip.tsx b/src/components/swap/GasEstimateTooltip.tsx index 659521bfd4..0dad275e1b 100644 --- a/src/components/swap/GasEstimateTooltip.tsx +++ b/src/components/swap/GasEstimateTooltip.tsx @@ -47,10 +47,20 @@ export default function GasEstimateTooltip({ trade, loading }: { trade?: Interfa {isUniswapXTrade(trade) ? : } -
{formatNumber(trade.totalGasUseEstimateUSD, NumberType.FiatGasPrice)}
+
+ {formatNumber({ + input: trade.totalGasUseEstimateUSD, + type: NumberType.FiatGasPrice, + })} +
{isUniswapXTrade(trade) && (
- {formatNumber(trade.classicGasUseEstimateUSD, NumberType.FiatGasPrice)} + + {formatNumber({ + input: trade.classicGasUseEstimateUSD, + type: NumberType.FiatGasPrice, + })} +
)}
diff --git a/src/components/swap/SwapModalFooter.tsx b/src/components/swap/SwapModalFooter.tsx index c9987f75f8..32c16ae7a2 100644 --- a/src/components/swap/SwapModalFooter.tsx +++ b/src/components/swap/SwapModalFooter.tsx @@ -109,7 +109,12 @@ export default function SwapModalFooter({ }> - {formatNumber(trade.totalGasUseEstimateUSD, NumberType.FiatGasPrice)} + + {formatNumber({ + input: trade.totalGasUseEstimateUSD, + type: NumberType.FiatGasPrice, + })} +
diff --git a/src/components/swap/SwapModalHeaderAmount.tsx b/src/components/swap/SwapModalHeaderAmount.tsx index daa1c9f0af..304dfc9c69 100644 --- a/src/components/swap/SwapModalHeaderAmount.tsx +++ b/src/components/swap/SwapModalHeaderAmount.tsx @@ -59,7 +59,10 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f {usdAmount && ( - {formatNumber(usdAmount, NumberType.FiatTokenQuantity)} + {formatNumber({ + input: usdAmount, + type: NumberType.FiatTokenQuantity, + })} )} diff --git a/src/components/swap/TradePrice.tsx b/src/components/swap/TradePrice.tsx index b1574bd1c4..e52eb20d38 100644 --- a/src/components/swap/TradePrice.tsx +++ b/src/components/swap/TradePrice.tsx @@ -58,7 +58,14 @@ export default function TradePrice({ price }: TradePriceProps) { {text}{' '} {usdPrice && ( - ({formatNumber(usdPrice, NumberType.FiatTokenPrice)}) + + ( + {formatNumber({ + input: usdPrice, + type: NumberType.FiatTokenPrice, + })} + ) + )} diff --git a/src/constants/localCurrencies.tsx b/src/constants/localCurrencies.tsx index 63f2215a3c..616656252f 100644 --- a/src/constants/localCurrencies.tsx +++ b/src/constants/localCurrencies.tsx @@ -46,6 +46,31 @@ export type SupportedLocalCurrency = (typeof SUPPORTED_LOCAL_CURRENCIES)[number] export const DEFAULT_LOCAL_CURRENCY: SupportedLocalCurrency = 'USD' +// some currencies need to be forced to use the narrow symbol and others need to be forced to use symbol +// for example: when CAD is set to narrowSymbol it is displayed as $ which offers no differentiation from USD +// but when set to symbol it is displayed as CA$ which is correct +// On the other hand when TBH is set to symbol it is displayed as THB, but when set to narrowSymbol it is ฿ which is correct +export const LOCAL_CURRENCY_SYMBOL_DISPLAY_TYPE: Record = { + USD: 'narrowSymbol', + EUR: 'narrowSymbol', + RUB: 'narrowSymbol', + INR: 'narrowSymbol', + GBP: 'narrowSymbol', + JPY: 'narrowSymbol', + VND: 'narrowSymbol', + SGD: 'symbol', + BRL: 'symbol', + HKD: 'symbol', + CAD: 'symbol', + IDR: 'narrowSymbol', + TRY: 'narrowSymbol', + NGN: 'narrowSymbol', + UAH: 'narrowSymbol', + PKR: 'narrowSymbol', + AUD: 'symbol', + THB: 'narrowSymbol', +} + export function getLocalCurrencyIcon(localCurrency: SupportedLocalCurrency, size = 20): ReactNode { switch (localCurrency) { case 'USD': diff --git a/src/utils/formatNumbers.test.ts b/src/utils/formatNumbers.test.ts index 504c3c276c..e19529a8f7 100644 --- a/src/utils/formatNumbers.test.ts +++ b/src/utils/formatNumbers.test.ts @@ -14,122 +14,357 @@ import { } from './formatNumbers' it('formats token reference numbers correctly', () => { - expect(formatNumber(1234567000000000, NumberType.TokenNonTx)).toBe('>999T') - expect(formatNumber(1002345, NumberType.TokenNonTx)).toBe('1.00M') - expect(formatNumber(1234, NumberType.TokenNonTx)).toBe('1,234.00') - expect(formatNumber(0.00909, NumberType.TokenNonTx)).toBe('0.009') - expect(formatNumber(0.09001, NumberType.TokenNonTx)).toBe('0.090') - expect(formatNumber(0.00099, NumberType.TokenNonTx)).toBe('<0.001') - expect(formatNumber(0, NumberType.TokenNonTx)).toBe('0') + expect(formatNumber({ input: 1234567000000000, type: NumberType.TokenNonTx })).toBe('>999T') + expect(formatNumber({ input: 1234567000000000, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('>999\xa0Bio.') + expect(formatNumber({ input: 1002345, type: NumberType.TokenNonTx })).toBe('1.00M') + expect(formatNumber({ input: 1002345, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('1,00\xa0Mio.') + expect(formatNumber({ input: 1234, type: NumberType.TokenNonTx })).toBe('1,234.00') + expect(formatNumber({ input: 1234, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('1.234,00') + expect(formatNumber({ input: 0.00909, type: NumberType.TokenNonTx })).toBe('0.009') + expect(formatNumber({ input: 0.00909, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('0,009') + expect(formatNumber({ input: 0.09001, type: NumberType.TokenNonTx })).toBe('0.090') + expect(formatNumber({ input: 0.09001, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('0,090') + expect(formatNumber({ input: 0.00099, type: NumberType.TokenNonTx })).toBe('<0.001') + expect(formatNumber({ input: 0.00099, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('<0,001') + expect(formatNumber({ input: 0, type: NumberType.TokenNonTx })).toBe('0') + expect(formatNumber({ input: 0, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('0') }) - it('formats token transaction numbers correctly', () => { - expect(formatNumber(1234567.8901, NumberType.TokenTx)).toBe('1,234,567.89') - expect(formatNumber(765432.1, NumberType.TokenTx)).toBe('765,432.10') + expect(formatNumber({ input: 1234567.8901, type: NumberType.TokenTx })).toBe('1,234,567.89') + expect(formatNumber({ input: 1234567.8901, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('1\xa0234\xa0567,89') + expect(formatNumber({ input: 765432.1, type: NumberType.TokenTx })).toBe('765,432.10') + expect(formatNumber({ input: 765432.1, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('765\xa0432,10') - expect(formatNumber(7654.321, NumberType.TokenTx)).toBe('7,654.32') - expect(formatNumber(765.4321, NumberType.TokenTx)).toBe('765.432') - expect(formatNumber(76.54321, NumberType.TokenTx)).toBe('76.5432') - expect(formatNumber(7.654321, NumberType.TokenTx)).toBe('7.65432') - expect(formatNumber(7.60000054321, NumberType.TokenTx)).toBe('7.60') - expect(formatNumber(7.6, NumberType.TokenTx)).toBe('7.60') - expect(formatNumber(7, NumberType.TokenTx)).toBe('7.00') + expect(formatNumber({ input: 7654.321, type: NumberType.TokenTx })).toBe('7,654.32') + expect(formatNumber({ input: 7654.321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('7\xa0654,32') + expect(formatNumber({ input: 765.4321, type: NumberType.TokenTx })).toBe('765.432') + expect(formatNumber({ input: 765.4321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('765,432') + expect(formatNumber({ input: 76.54321, type: NumberType.TokenTx })).toBe('76.5432') + expect(formatNumber({ input: 76.54321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('76,5432') + expect(formatNumber({ input: 7.654321, type: NumberType.TokenTx })).toBe('7.65432') + expect(formatNumber({ input: 7.654321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('7,65432') + expect(formatNumber({ input: 7.60000054321, type: NumberType.TokenTx })).toBe('7.60') + expect(formatNumber({ input: 7.60000054321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('7,60') + expect(formatNumber({ input: 7.6, type: NumberType.TokenTx })).toBe('7.60') + expect(formatNumber({ input: 7.6, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('7,60') + expect(formatNumber({ input: 7, type: NumberType.TokenTx })).toBe('7.00') + expect(formatNumber({ input: 7, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('7,00') - expect(formatNumber(0.987654321, NumberType.TokenTx)).toBe('0.98765') - expect(formatNumber(0.9, NumberType.TokenTx)).toBe('0.90') - expect(formatNumber(0.901000123, NumberType.TokenTx)).toBe('0.901') - expect(formatNumber(0.000000001, NumberType.TokenTx)).toBe('<0.00001') - expect(formatNumber(0, NumberType.TokenTx)).toBe('0') + expect(formatNumber({ input: 0.987654321, type: NumberType.TokenTx })).toBe('0.98765') + expect(formatNumber({ input: 0.987654321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('0,98765') + expect(formatNumber({ input: 0.9, type: NumberType.TokenTx })).toBe('0.90') + expect(formatNumber({ input: 0.9, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('0,90') + expect(formatNumber({ input: 0.901000123, type: NumberType.TokenTx })).toBe('0.901') + expect(formatNumber({ input: 0.901000123, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('0,901') + expect(formatNumber({ input: 0.000000001, type: NumberType.TokenTx })).toBe('<0.00001') + expect(formatNumber({ input: 0.000000001, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('<0,00001') + expect(formatNumber({ input: 0, type: NumberType.TokenTx })).toBe('0') + expect(formatNumber({ input: 0, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('0') }) it('formats fiat estimates on token details pages correctly', () => { - expect(formatNumber(1234567.891, NumberType.FiatTokenDetails)).toBe('$1.23M') - expect(formatNumber(1234.5678, NumberType.FiatTokenDetails)).toBe('$1,234.57') - expect(formatNumber(1.048942, NumberType.FiatTokenDetails)).toBe('$1.049') + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenDetails })).toBe('$1.23M') + expect( + formatNumber({ input: 1234567.891, type: NumberType.FiatTokenDetails, locale: 'fr-FR', localCurrency: 'EUR' }) + ).toBe('1,23\xa0M\xa0€') + expect(formatNumber({ input: 1234.5678, type: NumberType.FiatTokenDetails })).toBe('$1,234.57') + expect( + formatNumber({ input: 1234.5678, type: NumberType.FiatTokenDetails, locale: 'fr-FR', localCurrency: 'EUR' }) + ).toBe('1\u202f234,57\xa0€') + expect(formatNumber({ input: 1.048942, type: NumberType.FiatTokenDetails })).toBe('$1.049') + expect( + formatNumber({ input: 1.048942, type: NumberType.FiatTokenDetails, locale: 'fr-FR', localCurrency: 'EUR' }) + ).toBe('1,049\xa0€') - expect(formatNumber(0.001231, NumberType.FiatTokenDetails)).toBe('$0.00123') - expect(formatNumber(0.00001231, NumberType.FiatTokenDetails)).toBe('$0.0000123') + expect(formatNumber({ input: 0.001231, type: NumberType.FiatTokenDetails })).toBe('$0.00123') + expect( + formatNumber({ input: 0.001231, type: NumberType.FiatTokenDetails, locale: 'fr-FR', localCurrency: 'EUR' }) + ).toBe('0,00123\xa0€') + expect(formatNumber({ input: 0.00001231, type: NumberType.FiatTokenDetails })).toBe('$0.0000123') + expect( + formatNumber({ input: 0.00001231, type: NumberType.FiatTokenDetails, locale: 'fr-FR', localCurrency: 'EUR' }) + ).toBe('0,0000123\xa0€') - expect(formatNumber(0.0000001234, NumberType.FiatTokenDetails)).toBe('$0.000000123') - expect(formatNumber(0.000000009876, NumberType.FiatTokenDetails)).toBe('<$0.00000001') + expect(formatNumber({ input: 0.0000001234, type: NumberType.FiatTokenDetails })).toBe('$0.000000123') + expect( + formatNumber({ input: 0.0000001234, type: NumberType.FiatTokenDetails, locale: 'fr-FR', localCurrency: 'EUR' }) + ).toBe('0,000000123\xa0€') + expect(formatNumber({ input: 0.000000009876, type: NumberType.FiatTokenDetails })).toBe('<$0.00000001') + expect( + formatNumber({ input: 0.000000009876, type: NumberType.FiatTokenDetails, locale: 'fr-FR', localCurrency: 'EUR' }) + ).toBe('<0,00000001\xa0€') }) it('formats fiat estimates for tokens correctly', () => { - expect(formatNumber(1234567.891, NumberType.FiatTokenPrice)).toBe('$1.23M') - expect(formatNumber(1234.5678, NumberType.FiatTokenPrice)).toBe('$1,234.57') + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenPrice })).toBe('$1.23M') + expect( + formatNumber({ input: 1234567.891, type: NumberType.FiatTokenPrice, locale: 'es-ES', localCurrency: 'JPY' }) + ).toBe('1,23\xa0M¥') + expect(formatNumber({ input: 1234.5678, type: NumberType.FiatTokenPrice })).toBe('$1,234.57') + expect( + formatNumber({ input: 1234.5678, type: NumberType.FiatTokenPrice, locale: 'es-ES', localCurrency: 'JPY' }) + ).toBe('1234,57\xa0¥') + expect( + formatNumber({ input: 12345.678, type: NumberType.FiatTokenPrice, locale: 'es-ES', localCurrency: 'JPY' }) + ).toBe('12.345,68\xa0¥') - expect(formatNumber(0.010235, NumberType.FiatTokenPrice)).toBe('$0.0102') - expect(formatNumber(0.001231, NumberType.FiatTokenPrice)).toBe('$0.00123') - expect(formatNumber(0.00001231, NumberType.FiatTokenPrice)).toBe('$0.0000123') + expect(formatNumber({ input: 0.010235, type: NumberType.FiatTokenPrice })).toBe('$0.0102') + expect( + formatNumber({ input: 0.010235, type: NumberType.FiatTokenPrice, locale: 'es-ES', localCurrency: 'JPY' }) + ).toBe('0,0102\xa0¥') + expect(formatNumber({ input: 0.001231, type: NumberType.FiatTokenPrice })).toBe('$0.00123') + expect( + formatNumber({ input: 0.001231, type: NumberType.FiatTokenPrice, locale: 'es-ES', localCurrency: 'JPY' }) + ).toBe('0,00123\xa0¥') + expect(formatNumber({ input: 0.00001231, type: NumberType.FiatTokenPrice })).toBe('$0.0000123') + expect( + formatNumber({ input: 0.00001231, type: NumberType.FiatTokenPrice, locale: 'es-ES', localCurrency: 'JPY' }) + ).toBe('0,0000123\xa0¥') - expect(formatNumber(0.0000001234, NumberType.FiatTokenPrice)).toBe('$0.000000123') - expect(formatNumber(0.000000009876, NumberType.FiatTokenPrice)).toBe('<$0.00000001') - expect(formatNumber(10000000000000000000000000000000, NumberType.FiatTokenPrice)).toBe('$1.000000E31') + expect(formatNumber({ input: 0.0000001234, type: NumberType.FiatTokenPrice })).toBe('$0.000000123') + expect( + formatNumber({ input: 0.0000001234, type: NumberType.FiatTokenPrice, locale: 'es-ES', localCurrency: 'JPY' }) + ).toBe('0,000000123\xa0¥') + expect(formatNumber({ input: 0.000000009876, type: NumberType.FiatTokenPrice })).toBe('<$0.00000001') + expect( + formatNumber({ input: 0.000000009876, type: NumberType.FiatTokenPrice, locale: 'es-ES', localCurrency: 'JPY' }) + ).toBe('<0,00000001\xa0¥') + expect(formatNumber({ input: 10000000000000000000000000000000, type: NumberType.FiatTokenPrice })).toBe( + '$1.000000E31' + ) + expect( + formatNumber({ + input: 10000000000000000000000000000000, + type: NumberType.FiatTokenPrice, + locale: 'es-ES', + localCurrency: 'JPY', + }) + ).toBe('1,000000E31\xa0¥') }) it('formats fiat estimates for token stats correctly', () => { - expect(formatNumber(1234576, NumberType.FiatTokenStats)).toBe('$1.2M') - expect(formatNumber(234567, NumberType.FiatTokenStats)).toBe('$234.6K') - expect(formatNumber(123.456, NumberType.FiatTokenStats)).toBe('$123.46') - expect(formatNumber(1.23, NumberType.FiatTokenStats)).toBe('$1.23') - expect(formatNumber(0.123, NumberType.FiatTokenStats)).toBe('$0.12') - expect(formatNumber(0.00123, NumberType.FiatTokenStats)).toBe('<$0.01') - expect(formatNumber(0, NumberType.FiatTokenStats)).toBe('-') + expect(formatNumber({ input: 1234576, type: NumberType.FiatTokenStats })).toBe('$1.2M') + expect(formatNumber({ input: 1234576, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe( + 'CA$123.5万' + ) + expect(formatNumber({ input: 234567, type: NumberType.FiatTokenStats })).toBe('$234.6K') + expect(formatNumber({ input: 234567, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe( + 'CA$23.5万' + ) + expect(formatNumber({ input: 123.456, type: NumberType.FiatTokenStats })).toBe('$123.46') + expect(formatNumber({ input: 123.456, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe( + 'CA$123.46' + ) + expect(formatNumber({ input: 1.23, type: NumberType.FiatTokenStats })).toBe('$1.23') + expect(formatNumber({ input: 1.23, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe( + 'CA$1.23' + ) + expect(formatNumber({ input: 0.123, type: NumberType.FiatTokenStats })).toBe('$0.12') + expect(formatNumber({ input: 0.123, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe( + 'CA$0.12' + ) + expect(formatNumber({ input: 0.00123, type: NumberType.FiatTokenStats })).toBe('<$0.01') + expect(formatNumber({ input: 0.00123, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe( + ' { - expect(formatNumber(1234567.891, NumberType.FiatGasPrice)).toBe('$1.23M') - expect(formatNumber(18.448, NumberType.FiatGasPrice)).toBe('$18.45') - expect(formatNumber(0.0099, NumberType.FiatGasPrice)).toBe('<$0.01') - expect(formatNumber(0, NumberType.FiatGasPrice)).toBe('$0.00') + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice })).toBe('$1.23M') + expect( + formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice, locale: 'pt-PR', localCurrency: 'THB' }) + ).toBe('฿\xa01,23\xa0mi') + expect(formatNumber({ input: 18.448, type: NumberType.FiatGasPrice })).toBe('$18.45') + expect(formatNumber({ input: 18.448, type: NumberType.FiatGasPrice, locale: 'pt-PR', localCurrency: 'THB' })).toBe( + '฿\xa018,45' + ) + expect(formatNumber({ input: 0.0099, type: NumberType.FiatGasPrice })).toBe('<$0.01') + expect(formatNumber({ input: 0.0099, type: NumberType.FiatGasPrice, locale: 'pt-PR', localCurrency: 'THB' })).toBe( + '<฿\xa00,01' + ) + expect(formatNumber({ input: 0, type: NumberType.FiatGasPrice })).toBe('$0.00') + expect(formatNumber({ input: 0, type: NumberType.FiatGasPrice, locale: 'pt-PR', localCurrency: 'THB' })).toBe( + '฿\xa00,00' + ) }) it('formats USD token quantities prices correctly', () => { - expect(formatNumber(1234567.891, NumberType.FiatTokenQuantity)).toBe('$1.23M') - expect(formatNumber(18.448, NumberType.FiatTokenQuantity)).toBe('$18.45') - expect(formatNumber(0.0099, NumberType.FiatTokenQuantity)).toBe('<$0.01') - expect(formatNumber(0, NumberType.FiatTokenQuantity)).toBe('$0.00') + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenQuantity })).toBe('$1.23M') + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenQuantity, localCurrency: 'NGN' })).toBe('₦1.23M') + expect(formatNumber({ input: 18.448, type: NumberType.FiatTokenQuantity })).toBe('$18.45') + expect(formatNumber({ input: 18.448, type: NumberType.FiatTokenQuantity, localCurrency: 'NGN' })).toBe('₦18.45') + expect(formatNumber({ input: 0.0099, type: NumberType.FiatTokenQuantity })).toBe('<$0.01') + expect(formatNumber({ input: 0.0099, type: NumberType.FiatTokenQuantity, localCurrency: 'NGN' })).toBe('<₦0.01') + expect(formatNumber({ input: 0, type: NumberType.FiatTokenQuantity })).toBe('$0.00') + expect(formatNumber({ input: 0, type: NumberType.FiatTokenQuantity, localCurrency: 'NGN' })).toBe('₦0.00') }) it('formats Swap text input/output numbers correctly', () => { - expect(formatNumber(1234567.8901, NumberType.SwapTradeAmount)).toBe('1234570') - expect(formatNumber(765432.1, NumberType.SwapTradeAmount)).toBe('765432') + expect(formatNumber({ input: 1234567.8901, type: NumberType.SwapTradeAmount })).toBe('1234570') + expect(formatNumber({ input: 1234567.8901, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('1234570') + expect(formatNumber({ input: 765432.1, type: NumberType.SwapTradeAmount })).toBe('765432') + expect(formatNumber({ input: 765432.1, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('765432') - expect(formatNumber(7654.321, NumberType.SwapTradeAmount)).toBe('7654.32') - expect(formatNumber(765.4321, NumberType.SwapTradeAmount)).toBe('765.432') - expect(formatNumber(76.54321, NumberType.SwapTradeAmount)).toBe('76.5432') - expect(formatNumber(7.654321, NumberType.SwapTradeAmount)).toBe('7.65432') - expect(formatNumber(7.60000054321, NumberType.SwapTradeAmount)).toBe('7.60') - expect(formatNumber(7.6, NumberType.SwapTradeAmount)).toBe('7.60') - expect(formatNumber(7, NumberType.SwapTradeAmount)).toBe('7.00') + expect(formatNumber({ input: 7654.321, type: NumberType.SwapTradeAmount })).toBe('7654.32') + expect(formatNumber({ input: 7654.321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('7654.32') + expect(formatNumber({ input: 765.4321, type: NumberType.SwapTradeAmount })).toBe('765.432') + expect(formatNumber({ input: 765.4321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('765.432') + expect(formatNumber({ input: 76.54321, type: NumberType.SwapTradeAmount })).toBe('76.5432') + expect(formatNumber({ input: 76.54321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('76.5432') + expect(formatNumber({ input: 7.654321, type: NumberType.SwapTradeAmount })).toBe('7.65432') + expect(formatNumber({ input: 7.654321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('7.65432') + expect(formatNumber({ input: 7.60000054321, type: NumberType.SwapTradeAmount })).toBe('7.60') + expect(formatNumber({ input: 7.60000054321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('7.60') + expect(formatNumber({ input: 7.6, type: NumberType.SwapTradeAmount })).toBe('7.60') + expect(formatNumber({ input: 7.6, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('7.60') + expect(formatNumber({ input: 7, type: NumberType.SwapTradeAmount })).toBe('7.00') + expect(formatNumber({ input: 7, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('7.00') - expect(formatNumber(0.987654321, NumberType.SwapTradeAmount)).toBe('0.98765') - expect(formatNumber(0.9, NumberType.SwapTradeAmount)).toBe('0.90') - expect(formatNumber(0.901000123, NumberType.SwapTradeAmount)).toBe('0.901') - expect(formatNumber(0.000000001, NumberType.SwapTradeAmount)).toBe('0.000000001') - expect(formatNumber(0, NumberType.SwapTradeAmount)).toBe('0') + expect(formatNumber({ input: 0.987654321, type: NumberType.SwapTradeAmount })).toBe('0.98765') + expect(formatNumber({ input: 0.987654321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('0.98765') + expect(formatNumber({ input: 0.9, type: NumberType.SwapTradeAmount })).toBe('0.90') + expect(formatNumber({ input: 0.9, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('0.90') + expect(formatNumber({ input: 0.901000123, type: NumberType.SwapTradeAmount })).toBe('0.901') + expect(formatNumber({ input: 0.901000123, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('0.901') + expect(formatNumber({ input: 0.000000001, type: NumberType.SwapTradeAmount })).toBe('0.000000001') + expect(formatNumber({ input: 0.000000001, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('0.000000001') + expect(formatNumber({ input: 0, type: NumberType.SwapTradeAmount })).toBe('0') + expect(formatNumber({ input: 0, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('0') }) it('formats NFT numbers correctly', () => { - expect(formatNumber(1234567000000000, NumberType.NFTTokenFloorPrice)).toBe('>999T') - expect(formatNumber(1002345, NumberType.NFTTokenFloorPrice)).toBe('1M') - expect(formatNumber(1234, NumberType.NFTTokenFloorPrice)).toBe('1.23K') - expect(formatNumber(12.34467, NumberType.NFTTokenFloorPrice)).toBe('12.34') - expect(formatNumber(12.1, NumberType.NFTTokenFloorPrice)).toBe('12.1') - expect(formatNumber(0.00909, NumberType.NFTTokenFloorPrice)).toBe('0.009') - expect(formatNumber(0.09001, NumberType.NFTTokenFloorPrice)).toBe('0.09') - expect(formatNumber(0.00099, NumberType.NFTTokenFloorPrice)).toBe('<0.001') - expect(formatNumber(0, NumberType.NFTTokenFloorPrice)).toBe('0') + expect(formatNumber({ input: 1234567000000000, type: NumberType.NFTTokenFloorPrice })).toBe('>999T') + expect( + formatNumber({ + input: 1234567000000000, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('>999\xa0tri') + expect(formatNumber({ input: 1002345, type: NumberType.NFTTokenFloorPrice })).toBe('1M') + expect( + formatNumber({ + input: 1002345, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('1\xa0mi') + expect(formatNumber({ input: 1234, type: NumberType.NFTTokenFloorPrice })).toBe('1.23K') + expect( + formatNumber({ + input: 1234, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('1,23\xa0mil') + expect(formatNumber({ input: 12.34467, type: NumberType.NFTTokenFloorPrice })).toBe('12.34') + expect( + formatNumber({ + input: 12.34467, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('12,34') + expect(formatNumber({ input: 12.1, type: NumberType.NFTTokenFloorPrice })).toBe('12.1') + expect( + formatNumber({ + input: 12.1, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('12,1') + expect(formatNumber({ input: 0.00909, type: NumberType.NFTTokenFloorPrice })).toBe('0.009') + expect( + formatNumber({ + input: 0.00909, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('0,009') + expect(formatNumber({ input: 0.09001, type: NumberType.NFTTokenFloorPrice })).toBe('0.09') + expect( + formatNumber({ + input: 0.09001, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('0,09') + expect(formatNumber({ input: 0.00099, type: NumberType.NFTTokenFloorPrice })).toBe('<0.001') + expect( + formatNumber({ + input: 0.00099, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('<0,001') + expect(formatNumber({ input: 0, type: NumberType.NFTTokenFloorPrice })).toBe('0') + expect( + formatNumber({ + input: 0, + type: NumberType.NFTTokenFloorPrice, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('0') - expect(formatNumber(12.1, NumberType.NFTTokenFloorPriceTrailingZeros)).toBe('12.10') - expect(formatNumber(0.09001, NumberType.NFTTokenFloorPriceTrailingZeros)).toBe('0.090') + expect(formatNumber({ input: 12.1, type: NumberType.NFTTokenFloorPriceTrailingZeros })).toBe('12.10') + expect( + formatNumber({ + input: 12.1, + type: NumberType.NFTTokenFloorPriceTrailingZeros, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('12,10') + expect(formatNumber({ input: 0.09001, type: NumberType.NFTTokenFloorPriceTrailingZeros })).toBe('0.090') + expect( + formatNumber({ + input: 0.09001, + type: NumberType.NFTTokenFloorPriceTrailingZeros, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('0,090') - expect(formatNumber(0.987654321, NumberType.NFTCollectionStats)).toBe('1') - expect(formatNumber(0.9, NumberType.NFTCollectionStats)).toBe('1') - expect(formatNumber(76543.21, NumberType.NFTCollectionStats)).toBe('76.5K') - expect(formatNumber(7.60000054321, NumberType.NFTCollectionStats)).toBe('8') - expect(formatNumber(1234567890, NumberType.NFTCollectionStats)).toBe('1.2B') - expect(formatNumber(1234567000000000, NumberType.NFTCollectionStats)).toBe('1234.6T') + expect(formatNumber({ input: 0.987654321, type: NumberType.NFTCollectionStats })).toBe('1') + expect( + formatNumber({ input: 0.987654321, type: NumberType.NFTCollectionStats, locale: 'pt-BR', localCurrency: 'BRL' }) + ).toBe('1') + expect(formatNumber({ input: 0.9, type: NumberType.NFTCollectionStats })).toBe('1') + expect(formatNumber({ input: 0.9, type: NumberType.NFTCollectionStats, locale: 'pt-BR', localCurrency: 'BRL' })).toBe( + '1' + ) + expect(formatNumber({ input: 76543.21, type: NumberType.NFTCollectionStats })).toBe('76.5K') + expect( + formatNumber({ input: 76543.21, type: NumberType.NFTCollectionStats, locale: 'pt-BR', localCurrency: 'BRL' }) + ).toBe('76,5\xa0mil') + expect(formatNumber({ input: 7.60000054321, type: NumberType.NFTCollectionStats })).toBe('8') + expect( + formatNumber({ input: 7.60000054321, type: NumberType.NFTCollectionStats, locale: 'pt-BR', localCurrency: 'BRL' }) + ).toBe('8') + expect(formatNumber({ input: 1234567890, type: NumberType.NFTCollectionStats })).toBe('1.2B') + expect( + formatNumber({ input: 1234567890, type: NumberType.NFTCollectionStats, locale: 'pt-BR', localCurrency: 'BRL' }) + ).toBe('1,2\xa0bi') + expect(formatNumber({ input: 1234567000000000, type: NumberType.NFTCollectionStats })).toBe('1234.6T') + expect( + formatNumber({ + input: 1234567000000000, + type: NumberType.NFTCollectionStats, + locale: 'pt-BR', + localCurrency: 'BRL', + }) + ).toBe('1234,6\xa0tri') }) describe('formatUSDPrice', () => { diff --git a/src/utils/formatNumbers.ts b/src/utils/formatNumbers.ts index b5226e9fce..19679bfdb1 100644 --- a/src/utils/formatNumbers.ts +++ b/src/utils/formatNumbers.ts @@ -1,244 +1,302 @@ import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' -import { DEFAULT_LOCALE } from 'constants/locales' +import { + DEFAULT_LOCAL_CURRENCY, + LOCAL_CURRENCY_SYMBOL_DISPLAY_TYPE, + SupportedLocalCurrency, +} from 'constants/localCurrencies' +import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales' type Nullish = T | null | undefined +type NumberFormatOptions = Intl.NumberFormatOptions // Number formatting follows the standards laid out in this spec: // https://www.notion.so/uniswaplabs/Number-standards-fbb9f533f10e4e22820722c2f66d23c0 -const FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN = new Intl.NumberFormat('en-US', { +const FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 5, minimumFractionDigits: 2, -}) +} -const FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN_NO_COMMAS = new Intl.NumberFormat('en-US', { +const FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN_NO_COMMAS: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 5, minimumFractionDigits: 2, useGrouping: false, -}) +} -const NO_DECIMALS = new Intl.NumberFormat('en-US', { +const NO_DECIMALS: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 0, minimumFractionDigits: 0, -}) +} -const THREE_DECIMALS_NO_TRAILING_ZEROS = new Intl.NumberFormat('en-US', { +const THREE_DECIMALS_NO_TRAILING_ZEROS: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 3, minimumFractionDigits: 0, -}) +} -const THREE_DECIMALS = new Intl.NumberFormat('en-US', { +const THREE_DECIMALS: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 3, minimumFractionDigits: 3, -}) +} -const THREE_DECIMALS_USD = new Intl.NumberFormat('en-US', { +const THREE_DECIMALS_CURRENCY: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 3, minimumFractionDigits: 3, currency: 'USD', style: 'currency', -}) +} -const TWO_DECIMALS_NO_TRAILING_ZEROS = new Intl.NumberFormat('en-US', { +const TWO_DECIMALS_NO_TRAILING_ZEROS: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 2, -}) +} -const TWO_DECIMALS = new Intl.NumberFormat('en-US', { +const TWO_DECIMALS: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 2, minimumFractionDigits: 2, -}) +} -const TWO_DECIMALS_USD = new Intl.NumberFormat('en-US', { +const TWO_DECIMALS_CURRENCY: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 2, minimumFractionDigits: 2, currency: 'USD', style: 'currency', -}) +} -const SHORTHAND_TWO_DECIMALS = new Intl.NumberFormat('en-US', { +const SHORTHAND_TWO_DECIMALS: NumberFormatOptions = { notation: 'compact', minimumFractionDigits: 2, maximumFractionDigits: 2, -}) +} -const SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS = new Intl.NumberFormat('en-US', { +const SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS: NumberFormatOptions = { notation: 'compact', maximumFractionDigits: 2, -}) +} -const SHORTHAND_ONE_DECIMAL = new Intl.NumberFormat('en-US', { +const SHORTHAND_ONE_DECIMAL: NumberFormatOptions = { notation: 'compact', minimumFractionDigits: 1, maximumFractionDigits: 1, -}) +} -const SHORTHAND_USD_TWO_DECIMALS = new Intl.NumberFormat('en-US', { +const SHORTHAND_CURRENCY_TWO_DECIMALS: NumberFormatOptions = { notation: 'compact', minimumFractionDigits: 2, maximumFractionDigits: 2, currency: 'USD', style: 'currency', -}) +} -const SHORTHAND_USD_ONE_DECIMAL = new Intl.NumberFormat('en-US', { +const SHORTHAND_CURRENCY_ONE_DECIMAL: NumberFormatOptions = { notation: 'compact', minimumFractionDigits: 1, maximumFractionDigits: 1, currency: 'USD', style: 'currency', -}) +} -const SIX_SIG_FIGS_TWO_DECIMALS = new Intl.NumberFormat('en-US', { +const SIX_SIG_FIGS_TWO_DECIMALS: NumberFormatOptions = { notation: 'standard', maximumSignificantDigits: 6, minimumSignificantDigits: 3, maximumFractionDigits: 2, minimumFractionDigits: 2, -}) +} -const SIX_SIG_FIGS_NO_COMMAS = new Intl.NumberFormat('en-US', { +const SIX_SIG_FIGS_NO_COMMAS: NumberFormatOptions = { notation: 'standard', maximumSignificantDigits: 6, useGrouping: false, -}) +} -const SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS = new Intl.NumberFormat('en-US', { +const SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS: NumberFormatOptions = { notation: 'standard', maximumSignificantDigits: 6, minimumSignificantDigits: 3, maximumFractionDigits: 2, minimumFractionDigits: 2, useGrouping: false, -}) +} -const THREE_SIG_FIGS_USD = new Intl.NumberFormat('en-US', { +const ONE_SIG_FIG_CURRENCY: NumberFormatOptions = { + notation: 'standard', + minimumSignificantDigits: 1, + maximumSignificantDigits: 1, + currency: 'USD', + style: 'currency', +} + +const THREE_SIG_FIGS_CURRENCY: NumberFormatOptions = { notation: 'standard', minimumSignificantDigits: 3, maximumSignificantDigits: 3, currency: 'USD', style: 'currency', -}) +} -const SEVEN_SIG_FIGS__SCI_NOTATION_USD = new Intl.NumberFormat('en-US', { +const SEVEN_SIG_FIGS__SCI_NOTATION_CURRENCY: NumberFormatOptions = { notation: 'scientific', minimumSignificantDigits: 7, maximumSignificantDigits: 7, currency: 'USD', style: 'currency', -}) - -type Format = Intl.NumberFormat | string +} // each rule must contain either an `upperBound` or an `exact` value. // upperBound => number will use that formatter as long as it is < upperBound // exact => number will use that formatter if it is === exact -type FormatterRule = - | { upperBound?: undefined; exact: number; formatter: Format } - | { upperBound: number; exact?: undefined; formatter: Format } +// if hardcodedinput is supplied it will override the input value or use the hardcoded output +type HardCodedInputFormat = + | { + input: number + prefix?: string + hardcodedOutput?: undefined + } + | { + input?: undefined + prefix?: undefined + hardcodedOutput: string + } + +type FormatterBaseRule = { formatterOptions: NumberFormatOptions } +type FormatterExactRule = { upperBound?: undefined; exact: number } & FormatterBaseRule +type FormatterUpperBoundRule = { upperBound: number; exact?: undefined } & FormatterBaseRule + +type FormatterRule = (FormatterExactRule | FormatterUpperBoundRule) & { hardCodedInput?: HardCodedInputFormat } // these formatter objects dictate which formatter rule to use based on the interval that // the number falls into. for example, based on the rule set below, if your number // falls between 1 and 1e6, you'd use TWO_DECIMALS as the formatter. const tokenNonTxFormatter: FormatterRule[] = [ - { exact: 0, formatter: '0' }, - { upperBound: 0.001, formatter: '<0.001' }, - { upperBound: 1, formatter: THREE_DECIMALS }, - { upperBound: 1e6, formatter: TWO_DECIMALS }, - { upperBound: 1e15, formatter: SHORTHAND_TWO_DECIMALS }, - { upperBound: Infinity, formatter: '>999T' }, + { exact: 0, formatterOptions: NO_DECIMALS }, + { upperBound: 0.001, hardCodedInput: { input: 0.001, prefix: '<' }, formatterOptions: THREE_DECIMALS }, + { upperBound: 1, formatterOptions: THREE_DECIMALS }, + { upperBound: 1e6, formatterOptions: TWO_DECIMALS }, + { upperBound: 1e15, formatterOptions: SHORTHAND_TWO_DECIMALS }, + { + upperBound: Infinity, + hardCodedInput: { input: 999_000_000_000_000, prefix: '>' }, + formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS, + }, ] const tokenTxFormatter: FormatterRule[] = [ - { exact: 0, formatter: '0' }, - { upperBound: 0.00001, formatter: '<0.00001' }, - { upperBound: 1, formatter: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN }, - { upperBound: 10000, formatter: SIX_SIG_FIGS_TWO_DECIMALS }, - { upperBound: Infinity, formatter: TWO_DECIMALS }, + { exact: 0, formatterOptions: NO_DECIMALS }, + { + upperBound: 0.00001, + hardCodedInput: { input: 0.00001, prefix: '<' }, + formatterOptions: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN, + }, + { upperBound: 1, formatterOptions: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN }, + { upperBound: 10000, formatterOptions: SIX_SIG_FIGS_TWO_DECIMALS }, + { upperBound: Infinity, formatterOptions: TWO_DECIMALS }, ] const swapTradeAmountFormatter: FormatterRule[] = [ - { exact: 0, formatter: '0' }, - { upperBound: 0.1, formatter: SIX_SIG_FIGS_NO_COMMAS }, - { upperBound: 1, formatter: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN_NO_COMMAS }, - { upperBound: Infinity, formatter: SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS }, + { exact: 0, formatterOptions: NO_DECIMALS }, + { upperBound: 0.1, formatterOptions: SIX_SIG_FIGS_NO_COMMAS }, + { upperBound: 1, formatterOptions: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN_NO_COMMAS }, + { upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS }, ] const swapPriceFormatter: FormatterRule[] = [ - { exact: 0, formatter: '0' }, - { upperBound: 0.00001, formatter: '<0.00001' }, + { exact: 0, formatterOptions: NO_DECIMALS }, + { + upperBound: 0.00001, + hardCodedInput: { input: 0.00001, prefix: '<' }, + formatterOptions: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN, + }, ...swapTradeAmountFormatter, ] const fiatTokenDetailsFormatter: FormatterRule[] = [ - { exact: 0, formatter: '$0.00' }, - { upperBound: 0.00000001, formatter: '<$0.00000001' }, - { upperBound: 0.1, formatter: THREE_SIG_FIGS_USD }, - { upperBound: 1.05, formatter: THREE_DECIMALS_USD }, - { upperBound: 1e6, formatter: TWO_DECIMALS_USD }, - { upperBound: Infinity, formatter: SHORTHAND_USD_TWO_DECIMALS }, + { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY }, + { + upperBound: 0.00000001, + hardCodedInput: { input: 0.00000001, prefix: '<' }, + formatterOptions: ONE_SIG_FIG_CURRENCY, + }, + { upperBound: 0.1, formatterOptions: THREE_SIG_FIGS_CURRENCY }, + { upperBound: 1.05, formatterOptions: THREE_DECIMALS_CURRENCY }, + { upperBound: 1e6, formatterOptions: TWO_DECIMALS_CURRENCY }, + { upperBound: Infinity, formatterOptions: SHORTHAND_CURRENCY_TWO_DECIMALS }, ] const fiatTokenPricesFormatter: FormatterRule[] = [ - { exact: 0, formatter: '$0.00' }, - { upperBound: 0.00000001, formatter: '<$0.00000001' }, - { upperBound: 1, formatter: THREE_SIG_FIGS_USD }, - { upperBound: 1e6, formatter: TWO_DECIMALS_USD }, - { upperBound: 1e16, formatter: SHORTHAND_USD_TWO_DECIMALS }, - { upperBound: Infinity, formatter: SEVEN_SIG_FIGS__SCI_NOTATION_USD }, + { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY }, + { + upperBound: 0.00000001, + hardCodedInput: { input: 0.00000001, prefix: '<' }, + formatterOptions: ONE_SIG_FIG_CURRENCY, + }, + { upperBound: 1, formatterOptions: THREE_SIG_FIGS_CURRENCY }, + { upperBound: 1e6, formatterOptions: TWO_DECIMALS_CURRENCY }, + { upperBound: 1e16, formatterOptions: SHORTHAND_CURRENCY_TWO_DECIMALS }, + { upperBound: Infinity, formatterOptions: SEVEN_SIG_FIGS__SCI_NOTATION_CURRENCY }, ] const fiatTokenStatsFormatter: FormatterRule[] = [ // if token stat value is 0, we probably don't have the data for it, so show '-' as a placeholder - { exact: 0, formatter: '-' }, - { upperBound: 0.01, formatter: '<$0.01' }, - { upperBound: 1000, formatter: TWO_DECIMALS_USD }, - { upperBound: Infinity, formatter: SHORTHAND_USD_ONE_DECIMAL }, + { exact: 0, hardCodedInput: { hardcodedOutput: '-' }, formatterOptions: ONE_SIG_FIG_CURRENCY }, + { upperBound: 0.01, hardCodedInput: { input: 0.01, prefix: '<' }, formatterOptions: TWO_DECIMALS_CURRENCY }, + { upperBound: 1000, formatterOptions: TWO_DECIMALS_CURRENCY }, + { upperBound: Infinity, formatterOptions: SHORTHAND_CURRENCY_ONE_DECIMAL }, ] const fiatGasPriceFormatter: FormatterRule[] = [ - { exact: 0, formatter: '$0.00' }, - { upperBound: 0.01, formatter: '<$0.01' }, - { upperBound: 1e6, formatter: TWO_DECIMALS_USD }, - { upperBound: Infinity, formatter: SHORTHAND_USD_TWO_DECIMALS }, + { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY }, + { upperBound: 0.01, hardCodedInput: { input: 0.01, prefix: '<' }, formatterOptions: TWO_DECIMALS_CURRENCY }, + { upperBound: 1e6, formatterOptions: TWO_DECIMALS_CURRENCY }, + { upperBound: Infinity, formatterOptions: SHORTHAND_CURRENCY_TWO_DECIMALS }, ] -const fiatTokenQuantityFormatter = [{ exact: 0, formatter: '$0.00' }, ...fiatGasPriceFormatter] +const fiatTokenQuantityFormatter: FormatterRule[] = [ + { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY }, + ...fiatGasPriceFormatter, +] const portfolioBalanceFormatter: FormatterRule[] = [ - { exact: 0, formatter: '$0.00' }, - { upperBound: Infinity, formatter: TWO_DECIMALS_USD }, + { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY }, + { upperBound: Infinity, formatterOptions: TWO_DECIMALS_CURRENCY }, ] const ntfTokenFloorPriceFormatterTrailingZeros: FormatterRule[] = [ - { exact: 0, formatter: '0' }, - { upperBound: 0.001, formatter: '<0.001' }, - { upperBound: 1, formatter: THREE_DECIMALS }, - { upperBound: 1000, formatter: TWO_DECIMALS }, - { upperBound: 1e15, formatter: SHORTHAND_TWO_DECIMALS }, - { upperBound: Infinity, formatter: '>999T' }, + { exact: 0, formatterOptions: NO_DECIMALS }, + { upperBound: 0.001, hardCodedInput: { input: 0.001, prefix: '<' }, formatterOptions: THREE_DECIMALS }, + { upperBound: 1, formatterOptions: THREE_DECIMALS }, + { upperBound: 1000, formatterOptions: TWO_DECIMALS }, + { upperBound: 1e15, formatterOptions: SHORTHAND_TWO_DECIMALS }, + { + upperBound: Infinity, + hardCodedInput: { input: 999_000_000_000_000, prefix: '>' }, + formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS, + }, ] const ntfTokenFloorPriceFormatter: FormatterRule[] = [ - { exact: 0, formatter: '0' }, - { upperBound: 0.001, formatter: '<0.001' }, - { upperBound: 1, formatter: THREE_DECIMALS_NO_TRAILING_ZEROS }, - { upperBound: 1000, formatter: TWO_DECIMALS_NO_TRAILING_ZEROS }, - { upperBound: 1e15, formatter: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS }, - { upperBound: Infinity, formatter: '>999T' }, + { exact: 0, formatterOptions: NO_DECIMALS }, + { upperBound: 0.001, hardCodedInput: { input: 0.001, prefix: '<' }, formatterOptions: THREE_DECIMALS }, + { upperBound: 1, formatterOptions: THREE_DECIMALS_NO_TRAILING_ZEROS }, + { upperBound: 1000, formatterOptions: TWO_DECIMALS_NO_TRAILING_ZEROS }, + { upperBound: 1e15, formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS }, + { + upperBound: Infinity, + hardCodedInput: { input: 999_000_000_000_000, prefix: '>' }, + formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS, + }, ] const ntfCollectionStatsFormatter: FormatterRule[] = [ - { upperBound: 1000, formatter: NO_DECIMALS }, - { upperBound: Infinity, formatter: SHORTHAND_ONE_DECIMAL }, + { upperBound: 1000, formatterOptions: NO_DECIMALS }, + { upperBound: Infinity, formatterOptions: SHORTHAND_ONE_DECIMAL }, ] export enum NumberType { @@ -300,32 +358,57 @@ const TYPE_TO_FORMATTER_RULES = { [NumberType.NFTCollectionStats]: ntfCollectionStatsFormatter, } -function getFormatterRule(input: number, type: NumberType): Format { +function getFormatterRule(input: number, type: NumberType): FormatterRule { const rules = TYPE_TO_FORMATTER_RULES[type] for (const rule of rules) { if ( (rule.exact !== undefined && input === rule.exact) || (rule.upperBound !== undefined && input < rule.upperBound) ) { - return rule.formatter + return rule } } throw new Error(`formatter for type ${type} not configured correctly`) } -export function formatNumber( - input: Nullish, - type: NumberType = NumberType.TokenNonTx, - placeholder = '-' -): string { +interface FormatNumberOptions { + input: Nullish + type?: NumberType + placeholder?: string + locale?: SupportedLocale + localCurrency?: SupportedLocalCurrency +} + +export function formatNumber({ + input, + type = NumberType.TokenNonTx, + placeholder = '-', + locale = DEFAULT_LOCALE, + localCurrency = DEFAULT_LOCAL_CURRENCY, +}: FormatNumberOptions): string { if (input === null || input === undefined) { return placeholder } - const formatter = getFormatterRule(input, type) - if (typeof formatter === 'string') return formatter - return formatter.format(input) + const { hardCodedInput, formatterOptions } = getFormatterRule(input, type) + + if (formatterOptions.currency) { + formatterOptions.currency = localCurrency + formatterOptions.currencyDisplay = LOCAL_CURRENCY_SYMBOL_DISPLAY_TYPE[localCurrency] + } + + if (!hardCodedInput) { + return new Intl.NumberFormat(locale, formatterOptions).format(input) + } + + if (hardCodedInput.hardcodedOutput) { + return hardCodedInput.hardcodedOutput + } + + const { input: hardCodedInputValue, prefix } = hardCodedInput + if (hardCodedInputValue === undefined) return placeholder + return (prefix ?? '') + new Intl.NumberFormat(locale, formatterOptions).format(hardCodedInputValue) } export function formatCurrencyAmount( @@ -333,7 +416,7 @@ export function formatCurrencyAmount( type: NumberType = NumberType.TokenNonTx, placeholder?: string ): string { - return formatNumber(amount ? parseFloat(amount.toSignificant()) : undefined, type, placeholder) + return formatNumber({ input: amount ? parseFloat(amount.toSignificant()) : undefined, type, placeholder }) } export function formatPriceImpact(priceImpact: Percent | undefined): string { @@ -356,13 +439,13 @@ export function formatPrice( return '-' } - return formatNumber(parseFloat(price.toSignificant()), type) + return formatNumber({ input: parseFloat(price.toSignificant()), type }) } export function formatNumberOrString(price: Nullish, type: NumberType): string { if (price === null || price === undefined) return '-' - if (typeof price === 'string') return formatNumber(parseFloat(price), type) - return formatNumber(price, type) + if (typeof price === 'string') return formatNumber({ input: parseFloat(price), type }) + return formatNumber({ input: price, type }) } export function formatUSDPrice(price: Nullish, type: NumberType = NumberType.FiatTokenPrice): string {