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
This commit is contained in:
Jack Short 2023-09-05 12:59:43 -04:00 committed by GitHub
parent cd520a9e2c
commit 60acc689ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 621 additions and 223 deletions

@ -279,16 +279,20 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
{totalBalance !== undefined ? ( {totalBalance !== undefined ? (
<FadeInColumn gap="xs"> <FadeInColumn gap="xs">
<ThemedText.HeadlineLarge fontWeight={535} data-testid="portfolio-total-balance"> <ThemedText.HeadlineLarge fontWeight={535} data-testid="portfolio-total-balance">
{formatNumber(totalBalance, NumberType.PortfolioBalance)} {formatNumber({
input: totalBalance,
type: NumberType.PortfolioBalance,
})}
</ThemedText.HeadlineLarge> </ThemedText.HeadlineLarge>
<AutoRow marginBottom="20px"> <AutoRow marginBottom="20px">
{absoluteChange !== 0 && percentChange && ( {absoluteChange !== 0 && percentChange && (
<> <>
<PortfolioArrow change={absoluteChange as number} /> <PortfolioArrow change={absoluteChange as number} />
<ThemedText.BodySecondary> <ThemedText.BodySecondary>
{`${formatNumber(Math.abs(absoluteChange as number), NumberType.PortfolioBalance)} (${formatDelta( {`${formatNumber({
percentChange input: Math.abs(absoluteChange as number),
)})`} type: NumberType.PortfolioBalance,
})} (${formatDelta(percentChange)})`}
</ThemedText.BodySecondary> </ThemedText.BodySecondary>
</> </>
)} )}

@ -167,18 +167,21 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
placement="left" placement="left"
text={ text={
<div style={{ padding: '4px 0px' }}> <div style={{ padding: '4px 0px' }}>
<ThemedText.BodySmall>{`${formatNumber( <ThemedText.BodySmall>{`${formatNumber({
liquidityValue, input: liquidityValue,
NumberType.PortfolioBalance type: NumberType.PortfolioBalance,
)} (liquidity) + ${formatNumber( })} (liquidity) + ${formatNumber({
feeValue, input: feeValue,
NumberType.PortfolioBalance type: NumberType.PortfolioBalance,
)} (fees)`}</ThemedText.BodySmall> })} (fees)`}</ThemedText.BodySmall>
</div> </div>
} }
> >
<ThemedText.SubHeader> <ThemedText.SubHeader>
{formatNumber((liquidityValue ?? 0) + (feeValue ?? 0), NumberType.PortfolioBalance)} {formatNumber({
input: (liquidityValue ?? 0) + (feeValue ?? 0),
type: NumberType.PortfolioBalance,
})}
</ThemedText.SubHeader> </ThemedText.SubHeader>
</MouseoverTooltip> </MouseoverTooltip>

@ -101,7 +101,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
title={<TokenNameText>{token?.name}</TokenNameText>} title={<TokenNameText>{token?.name}</TokenNameText>}
descriptor={ descriptor={
<TokenBalanceText> <TokenBalanceText>
{formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol} {formatNumber({ input: quantity, type: NumberType.TokenNonTx })} {token?.symbol}
</TokenBalanceText> </TokenBalanceText>
} }
onClick={navigateToTokenDetails} onClick={navigateToTokenDetails}
@ -109,7 +109,10 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
denominatedValue && ( denominatedValue && (
<> <>
<ThemedText.SubHeader> <ThemedText.SubHeader>
{formatNumber(denominatedValue?.value, NumberType.PortfolioBalance)} {formatNumber({
input: denominatedValue?.value,
type: NumberType.PortfolioBalance,
})}
</ThemedText.SubHeader> </ThemedText.SubHeader>
<Row justify="flex-end"> <Row justify="flex-end">
<PortfolioArrow change={percentChange} size={20} strokeWidth={1.75} /> <PortfolioArrow change={percentChange} size={20} strokeWidth={1.75} />

@ -39,7 +39,10 @@ export function FiatValue({
<Row gap="sm"> <Row gap="sm">
<ThemedText.BodySmall color="neutral2"> <ThemedText.BodySmall color="neutral2">
{fiatValue.data ? ( {fiatValue.data ? (
formatNumber(fiatValue.data, NumberType.FiatTokenPrice) formatNumber({
input: fiatValue.data,
type: NumberType.FiatTokenPrice,
})
) : ( ) : (
<MouseoverTooltip text={<Trans>Not enough liquidity to show accurate USD value.</Trans>}>-</MouseoverTooltip> <MouseoverTooltip text={<Trans>Not enough liquidity to show accurate USD value.</Trans>}>-</MouseoverTooltip>
)} )}

@ -62,7 +62,12 @@ function Stat({
return ( return (
<StatWrapper data-cy={`${dataCy}`}> <StatWrapper data-cy={`${dataCy}`}>
<MouseoverTooltip text={description}>{title}</MouseoverTooltip> <MouseoverTooltip text={description}>{title}</MouseoverTooltip>
<StatPrice>{formatNumber(value, NumberType.FiatTokenStats)}</StatPrice> <StatPrice>
{formatNumber({
input: value,
type: NumberType.FiatTokenStats,
})}
</StatPrice>
</StatWrapper> </StatWrapper>
) )
} }

@ -503,11 +503,19 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
} }
tvl={ tvl={
<ClickableContent> <ClickableContent>
{formatNumber(token.market?.totalValueLocked?.value, NumberType.FiatTokenStats)} {formatNumber({
input: token.market?.totalValueLocked?.value,
type: NumberType.FiatTokenStats,
})}
</ClickableContent> </ClickableContent>
} }
volume={ volume={
<ClickableContent>{formatNumber(token.market?.volume?.value, NumberType.FiatTokenStats)}</ClickableContent> <ClickableContent>
{formatNumber({
input: token.market?.volume?.value,
type: NumberType.FiatTokenStats,
})}
</ClickableContent>
} }
sparkLine={ sparkLine={
<SparkLine> <SparkLine>

@ -73,10 +73,10 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
> >
<TextWithLoadingPlaceholder syncing={syncing} width={50}> <TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall> <ThemedText.BodySmall>
{`${trade.totalGasUseEstimateUSD ? '~' : ''}${formatNumber( {`${trade.totalGasUseEstimateUSD ? '~' : ''}${formatNumber({
trade.totalGasUseEstimateUSD, input: trade.totalGasUseEstimateUSD,
NumberType.FiatGasPrice type: NumberType.FiatGasPrice,
)}`} })}`}
</ThemedText.BodySmall> </ThemedText.BodySmall>
</TextWithLoadingPlaceholder> </TextWithLoadingPlaceholder>
</MouseoverTooltip> </MouseoverTooltip>

@ -39,7 +39,11 @@ const GasCostItem = ({
<Row justify="space-between"> <Row justify="space-between">
<ThemedText.SubHeaderSmall>{title}</ThemedText.SubHeaderSmall> <ThemedText.SubHeaderSmall>{title}</ThemedText.SubHeaderSmall>
<ThemedText.SubHeaderSmall color="neutral1"> <ThemedText.SubHeaderSmall color="neutral1">
{itemValue ?? formatNumber(amount, NumberType.FiatGasPrice)} {itemValue ??
formatNumber({
input: amount,
type: NumberType.FiatGasPrice,
})}
</ThemedText.SubHeaderSmall> </ThemedText.SubHeaderSmall>
</Row> </Row>
) )

@ -47,10 +47,20 @@ export default function GasEstimateTooltip({ trade, loading }: { trade?: Interfa
{isUniswapXTrade(trade) ? <UniswapXRouterIcon testId="gas-estimate-uniswapx-icon" /> : <StyledGasIcon />} {isUniswapXTrade(trade) ? <UniswapXRouterIcon testId="gas-estimate-uniswapx-icon" /> : <StyledGasIcon />}
<ThemedText.BodySmall color="neutral2"> <ThemedText.BodySmall color="neutral2">
<Row gap="xs"> <Row gap="xs">
<div>{formatNumber(trade.totalGasUseEstimateUSD, NumberType.FiatGasPrice)}</div> <div>
{formatNumber({
input: trade.totalGasUseEstimateUSD,
type: NumberType.FiatGasPrice,
})}
</div>
{isUniswapXTrade(trade) && ( {isUniswapXTrade(trade) && (
<div> <div>
<s>{formatNumber(trade.classicGasUseEstimateUSD, NumberType.FiatGasPrice)}</s> <s>
{formatNumber({
input: trade.classicGasUseEstimateUSD,
type: NumberType.FiatGasPrice,
})}
</s>
</div> </div>
)} )}
</Row> </Row>

@ -109,7 +109,12 @@ export default function SwapModalFooter({
</Label> </Label>
</MouseoverTooltip> </MouseoverTooltip>
<MouseoverTooltip placement="right" size={TooltipSize.Small} text={<GasBreakdownTooltip trade={trade} />}> <MouseoverTooltip placement="right" size={TooltipSize.Small} text={<GasBreakdownTooltip trade={trade} />}>
<DetailRowValue>{formatNumber(trade.totalGasUseEstimateUSD, NumberType.FiatGasPrice)}</DetailRowValue> <DetailRowValue>
{formatNumber({
input: trade.totalGasUseEstimateUSD,
type: NumberType.FiatGasPrice,
})}
</DetailRowValue>
</MouseoverTooltip> </MouseoverTooltip>
</Row> </Row>
</ThemedText.BodySmall> </ThemedText.BodySmall>

@ -59,7 +59,10 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f
</ResponsiveHeadline> </ResponsiveHeadline>
{usdAmount && ( {usdAmount && (
<ThemedText.BodySmall color="neutral2"> <ThemedText.BodySmall color="neutral2">
{formatNumber(usdAmount, NumberType.FiatTokenQuantity)} {formatNumber({
input: usdAmount,
type: NumberType.FiatTokenQuantity,
})}
</ThemedText.BodySmall> </ThemedText.BodySmall>
)} )}
</Column> </Column>

@ -58,7 +58,14 @@ export default function TradePrice({ price }: TradePriceProps) {
<ThemedText.BodySmall>{text}</ThemedText.BodySmall>{' '} <ThemedText.BodySmall>{text}</ThemedText.BodySmall>{' '}
{usdPrice && ( {usdPrice && (
<ThemedText.BodySmall color="neutral2"> <ThemedText.BodySmall color="neutral2">
<Trans>({formatNumber(usdPrice, NumberType.FiatTokenPrice)})</Trans> <Trans>
(
{formatNumber({
input: usdPrice,
type: NumberType.FiatTokenPrice,
})}
)
</Trans>
</ThemedText.BodySmall> </ThemedText.BodySmall>
)} )}
</StyledPriceContainer> </StyledPriceContainer>

@ -46,6 +46,31 @@ export type SupportedLocalCurrency = (typeof SUPPORTED_LOCAL_CURRENCIES)[number]
export const DEFAULT_LOCAL_CURRENCY: SupportedLocalCurrency = 'USD' 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<SupportedLocalCurrency, 'narrowSymbol' | 'symbol'> = {
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 { export function getLocalCurrencyIcon(localCurrency: SupportedLocalCurrency, size = 20): ReactNode {
switch (localCurrency) { switch (localCurrency) {
case 'USD': case 'USD':

@ -14,122 +14,357 @@ import {
} from './formatNumbers' } from './formatNumbers'
it('formats token reference numbers correctly', () => { it('formats token reference numbers correctly', () => {
expect(formatNumber(1234567000000000, NumberType.TokenNonTx)).toBe('>999T') expect(formatNumber({ input: 1234567000000000, type: NumberType.TokenNonTx })).toBe('>999T')
expect(formatNumber(1002345, NumberType.TokenNonTx)).toBe('1.00M') expect(formatNumber({ input: 1234567000000000, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('>999\xa0Bio.')
expect(formatNumber(1234, NumberType.TokenNonTx)).toBe('1,234.00') expect(formatNumber({ input: 1002345, type: NumberType.TokenNonTx })).toBe('1.00M')
expect(formatNumber(0.00909, NumberType.TokenNonTx)).toBe('0.009') expect(formatNumber({ input: 1002345, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('1,00\xa0Mio.')
expect(formatNumber(0.09001, NumberType.TokenNonTx)).toBe('0.090') expect(formatNumber({ input: 1234, type: NumberType.TokenNonTx })).toBe('1,234.00')
expect(formatNumber(0.00099, NumberType.TokenNonTx)).toBe('<0.001') expect(formatNumber({ input: 1234, type: NumberType.TokenNonTx, locale: 'de-DE' })).toBe('1.234,00')
expect(formatNumber(0, NumberType.TokenNonTx)).toBe('0') 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', () => { it('formats token transaction numbers correctly', () => {
expect(formatNumber(1234567.8901, NumberType.TokenTx)).toBe('1,234,567.89') expect(formatNumber({ input: 1234567.8901, type: 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, 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({ input: 7654.321, type: NumberType.TokenTx })).toBe('7,654.32')
expect(formatNumber(765.4321, NumberType.TokenTx)).toBe('765.432') expect(formatNumber({ input: 7654.321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('7\xa0654,32')
expect(formatNumber(76.54321, NumberType.TokenTx)).toBe('76.5432') expect(formatNumber({ input: 765.4321, type: NumberType.TokenTx })).toBe('765.432')
expect(formatNumber(7.654321, NumberType.TokenTx)).toBe('7.65432') expect(formatNumber({ input: 765.4321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('765,432')
expect(formatNumber(7.60000054321, NumberType.TokenTx)).toBe('7.60') expect(formatNumber({ input: 76.54321, type: NumberType.TokenTx })).toBe('76.5432')
expect(formatNumber(7.6, NumberType.TokenTx)).toBe('7.60') expect(formatNumber({ input: 76.54321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('76,5432')
expect(formatNumber(7, NumberType.TokenTx)).toBe('7.00') 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({ input: 0.987654321, type: NumberType.TokenTx })).toBe('0.98765')
expect(formatNumber(0.9, NumberType.TokenTx)).toBe('0.90') expect(formatNumber({ input: 0.987654321, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('0,98765')
expect(formatNumber(0.901000123, NumberType.TokenTx)).toBe('0.901') expect(formatNumber({ input: 0.9, type: NumberType.TokenTx })).toBe('0.90')
expect(formatNumber(0.000000001, NumberType.TokenTx)).toBe('<0.00001') expect(formatNumber({ input: 0.9, type: NumberType.TokenTx, locale: 'ru-RU' })).toBe('0,90')
expect(formatNumber(0, NumberType.TokenTx)).toBe('0') 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', () => { it('formats fiat estimates on token details pages correctly', () => {
expect(formatNumber(1234567.891, NumberType.FiatTokenDetails)).toBe('$1.23M') expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenDetails })).toBe('$1.23M')
expect(formatNumber(1234.5678, NumberType.FiatTokenDetails)).toBe('$1,234.57') expect(
expect(formatNumber(1.048942, NumberType.FiatTokenDetails)).toBe('$1.049') 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({ input: 0.001231, type: NumberType.FiatTokenDetails })).toBe('$0.00123')
expect(formatNumber(0.00001231, NumberType.FiatTokenDetails)).toBe('$0.0000123') 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({ input: 0.0000001234, type: NumberType.FiatTokenDetails })).toBe('$0.000000123')
expect(formatNumber(0.000000009876, NumberType.FiatTokenDetails)).toBe('<$0.00000001') 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', () => { it('formats fiat estimates for tokens correctly', () => {
expect(formatNumber(1234567.891, NumberType.FiatTokenPrice)).toBe('$1.23M') expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenPrice })).toBe('$1.23M')
expect(formatNumber(1234.5678, NumberType.FiatTokenPrice)).toBe('$1,234.57') 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({ input: 0.010235, type: NumberType.FiatTokenPrice })).toBe('$0.0102')
expect(formatNumber(0.001231, NumberType.FiatTokenPrice)).toBe('$0.00123') expect(
expect(formatNumber(0.00001231, NumberType.FiatTokenPrice)).toBe('$0.0000123') 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({ input: 0.0000001234, type: NumberType.FiatTokenPrice })).toBe('$0.000000123')
expect(formatNumber(0.000000009876, NumberType.FiatTokenPrice)).toBe('<$0.00000001') expect(
expect(formatNumber(10000000000000000000000000000000, NumberType.FiatTokenPrice)).toBe('$1.000000E31') 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', () => { it('formats fiat estimates for token stats correctly', () => {
expect(formatNumber(1234576, NumberType.FiatTokenStats)).toBe('$1.2M') expect(formatNumber({ input: 1234576, type: NumberType.FiatTokenStats })).toBe('$1.2M')
expect(formatNumber(234567, NumberType.FiatTokenStats)).toBe('$234.6K') expect(formatNumber({ input: 1234576, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe(
expect(formatNumber(123.456, NumberType.FiatTokenStats)).toBe('$123.46') 'CA$123.5万'
expect(formatNumber(1.23, NumberType.FiatTokenStats)).toBe('$1.23') )
expect(formatNumber(0.123, NumberType.FiatTokenStats)).toBe('$0.12') expect(formatNumber({ input: 234567, type: NumberType.FiatTokenStats })).toBe('$234.6K')
expect(formatNumber(0.00123, NumberType.FiatTokenStats)).toBe('<$0.01') expect(formatNumber({ input: 234567, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe(
expect(formatNumber(0, NumberType.FiatTokenStats)).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(
'<CA$0.01'
)
expect(formatNumber({ input: 0, type: NumberType.FiatTokenStats })).toBe('-')
expect(formatNumber({ input: 0, type: NumberType.FiatTokenStats, locale: 'ja-JP', localCurrency: 'CAD' })).toBe('-')
}) })
it('formats gas USD prices correctly', () => { it('formats gas USD prices correctly', () => {
expect(formatNumber(1234567.891, NumberType.FiatGasPrice)).toBe('$1.23M') expect(formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice })).toBe('$1.23M')
expect(formatNumber(18.448, NumberType.FiatGasPrice)).toBe('$18.45') expect(
expect(formatNumber(0.0099, NumberType.FiatGasPrice)).toBe('<$0.01') formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice, locale: 'pt-PR', localCurrency: 'THB' })
expect(formatNumber(0, NumberType.FiatGasPrice)).toBe('$0.00') ).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', () => { it('formats USD token quantities prices correctly', () => {
expect(formatNumber(1234567.891, NumberType.FiatTokenQuantity)).toBe('$1.23M') expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenQuantity })).toBe('$1.23M')
expect(formatNumber(18.448, NumberType.FiatTokenQuantity)).toBe('$18.45') expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenQuantity, localCurrency: 'NGN' })).toBe('₦1.23M')
expect(formatNumber(0.0099, NumberType.FiatTokenQuantity)).toBe('<$0.01') expect(formatNumber({ input: 18.448, type: NumberType.FiatTokenQuantity })).toBe('$18.45')
expect(formatNumber(0, NumberType.FiatTokenQuantity)).toBe('$0.00') 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', () => { it('formats Swap text input/output numbers correctly', () => {
expect(formatNumber(1234567.8901, NumberType.SwapTradeAmount)).toBe('1234570') expect(formatNumber({ input: 1234567.8901, type: NumberType.SwapTradeAmount })).toBe('1234570')
expect(formatNumber(765432.1, NumberType.SwapTradeAmount)).toBe('765432') 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({ input: 7654.321, type: NumberType.SwapTradeAmount })).toBe('7654.32')
expect(formatNumber(765.4321, NumberType.SwapTradeAmount)).toBe('765.432') expect(formatNumber({ input: 7654.321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('7654.32')
expect(formatNumber(76.54321, NumberType.SwapTradeAmount)).toBe('76.5432') expect(formatNumber({ input: 765.4321, type: NumberType.SwapTradeAmount })).toBe('765.432')
expect(formatNumber(7.654321, NumberType.SwapTradeAmount)).toBe('7.65432') expect(formatNumber({ input: 765.4321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('765.432')
expect(formatNumber(7.60000054321, NumberType.SwapTradeAmount)).toBe('7.60') expect(formatNumber({ input: 76.54321, type: NumberType.SwapTradeAmount })).toBe('76.5432')
expect(formatNumber(7.6, NumberType.SwapTradeAmount)).toBe('7.60') expect(formatNumber({ input: 76.54321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('76.5432')
expect(formatNumber(7, NumberType.SwapTradeAmount)).toBe('7.00') 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({ input: 0.987654321, type: NumberType.SwapTradeAmount })).toBe('0.98765')
expect(formatNumber(0.9, NumberType.SwapTradeAmount)).toBe('0.90') expect(formatNumber({ input: 0.987654321, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('0.98765')
expect(formatNumber(0.901000123, NumberType.SwapTradeAmount)).toBe('0.901') expect(formatNumber({ input: 0.9, type: NumberType.SwapTradeAmount })).toBe('0.90')
expect(formatNumber(0.000000001, NumberType.SwapTradeAmount)).toBe('0.000000001') expect(formatNumber({ input: 0.9, type: NumberType.SwapTradeAmount, locale: 'ko-KR' })).toBe('0.90')
expect(formatNumber(0, NumberType.SwapTradeAmount)).toBe('0') 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', () => { it('formats NFT numbers correctly', () => {
expect(formatNumber(1234567000000000, NumberType.NFTTokenFloorPrice)).toBe('>999T') expect(formatNumber({ input: 1234567000000000, type: NumberType.NFTTokenFloorPrice })).toBe('>999T')
expect(formatNumber(1002345, NumberType.NFTTokenFloorPrice)).toBe('1M') expect(
expect(formatNumber(1234, NumberType.NFTTokenFloorPrice)).toBe('1.23K') formatNumber({
expect(formatNumber(12.34467, NumberType.NFTTokenFloorPrice)).toBe('12.34') input: 1234567000000000,
expect(formatNumber(12.1, NumberType.NFTTokenFloorPrice)).toBe('12.1') type: NumberType.NFTTokenFloorPrice,
expect(formatNumber(0.00909, NumberType.NFTTokenFloorPrice)).toBe('0.009') locale: 'pt-BR',
expect(formatNumber(0.09001, NumberType.NFTTokenFloorPrice)).toBe('0.09') localCurrency: 'BRL',
expect(formatNumber(0.00099, NumberType.NFTTokenFloorPrice)).toBe('<0.001') })
expect(formatNumber(0, NumberType.NFTTokenFloorPrice)).toBe('0') ).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({ input: 12.1, type: NumberType.NFTTokenFloorPriceTrailingZeros })).toBe('12.10')
expect(formatNumber(0.09001, NumberType.NFTTokenFloorPriceTrailingZeros)).toBe('0.090') 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({ input: 0.987654321, type: NumberType.NFTCollectionStats })).toBe('1')
expect(formatNumber(0.9, NumberType.NFTCollectionStats)).toBe('1') expect(
expect(formatNumber(76543.21, NumberType.NFTCollectionStats)).toBe('76.5K') formatNumber({ input: 0.987654321, type: NumberType.NFTCollectionStats, locale: 'pt-BR', localCurrency: 'BRL' })
expect(formatNumber(7.60000054321, NumberType.NFTCollectionStats)).toBe('8') ).toBe('1')
expect(formatNumber(1234567890, NumberType.NFTCollectionStats)).toBe('1.2B') expect(formatNumber({ input: 0.9, type: NumberType.NFTCollectionStats })).toBe('1')
expect(formatNumber(1234567000000000, NumberType.NFTCollectionStats)).toBe('1234.6T') 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', () => { describe('formatUSDPrice', () => {

@ -1,244 +1,302 @@
import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' 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> = T | null | undefined type Nullish<T> = T | null | undefined
type NumberFormatOptions = Intl.NumberFormatOptions
// Number formatting follows the standards laid out in this spec: // Number formatting follows the standards laid out in this spec:
// https://www.notion.so/uniswaplabs/Number-standards-fbb9f533f10e4e22820722c2f66d23c0 // 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', notation: 'standard',
maximumFractionDigits: 5, maximumFractionDigits: 5,
minimumFractionDigits: 2, 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', notation: 'standard',
maximumFractionDigits: 5, maximumFractionDigits: 5,
minimumFractionDigits: 2, minimumFractionDigits: 2,
useGrouping: false, useGrouping: false,
}) }
const NO_DECIMALS = new Intl.NumberFormat('en-US', { const NO_DECIMALS: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumFractionDigits: 0, maximumFractionDigits: 0,
minimumFractionDigits: 0, minimumFractionDigits: 0,
}) }
const THREE_DECIMALS_NO_TRAILING_ZEROS = new Intl.NumberFormat('en-US', { const THREE_DECIMALS_NO_TRAILING_ZEROS: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumFractionDigits: 3, maximumFractionDigits: 3,
minimumFractionDigits: 0, minimumFractionDigits: 0,
}) }
const THREE_DECIMALS = new Intl.NumberFormat('en-US', { const THREE_DECIMALS: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumFractionDigits: 3, maximumFractionDigits: 3,
minimumFractionDigits: 3, minimumFractionDigits: 3,
}) }
const THREE_DECIMALS_USD = new Intl.NumberFormat('en-US', { const THREE_DECIMALS_CURRENCY: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumFractionDigits: 3, maximumFractionDigits: 3,
minimumFractionDigits: 3, minimumFractionDigits: 3,
currency: 'USD', currency: 'USD',
style: 'currency', style: 'currency',
}) }
const TWO_DECIMALS_NO_TRAILING_ZEROS = new Intl.NumberFormat('en-US', { const TWO_DECIMALS_NO_TRAILING_ZEROS: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumFractionDigits: 2, maximumFractionDigits: 2,
}) }
const TWO_DECIMALS = new Intl.NumberFormat('en-US', { const TWO_DECIMALS: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2, minimumFractionDigits: 2,
}) }
const TWO_DECIMALS_USD = new Intl.NumberFormat('en-US', { const TWO_DECIMALS_CURRENCY: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2, minimumFractionDigits: 2,
currency: 'USD', currency: 'USD',
style: 'currency', style: 'currency',
}) }
const SHORTHAND_TWO_DECIMALS = new Intl.NumberFormat('en-US', { const SHORTHAND_TWO_DECIMALS: NumberFormatOptions = {
notation: 'compact', notation: 'compact',
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 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', notation: 'compact',
maximumFractionDigits: 2, maximumFractionDigits: 2,
}) }
const SHORTHAND_ONE_DECIMAL = new Intl.NumberFormat('en-US', { const SHORTHAND_ONE_DECIMAL: NumberFormatOptions = {
notation: 'compact', notation: 'compact',
minimumFractionDigits: 1, minimumFractionDigits: 1,
maximumFractionDigits: 1, maximumFractionDigits: 1,
}) }
const SHORTHAND_USD_TWO_DECIMALS = new Intl.NumberFormat('en-US', { const SHORTHAND_CURRENCY_TWO_DECIMALS: NumberFormatOptions = {
notation: 'compact', notation: 'compact',
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
currency: 'USD', currency: 'USD',
style: 'currency', style: 'currency',
}) }
const SHORTHAND_USD_ONE_DECIMAL = new Intl.NumberFormat('en-US', { const SHORTHAND_CURRENCY_ONE_DECIMAL: NumberFormatOptions = {
notation: 'compact', notation: 'compact',
minimumFractionDigits: 1, minimumFractionDigits: 1,
maximumFractionDigits: 1, maximumFractionDigits: 1,
currency: 'USD', currency: 'USD',
style: 'currency', style: 'currency',
}) }
const SIX_SIG_FIGS_TWO_DECIMALS = new Intl.NumberFormat('en-US', { const SIX_SIG_FIGS_TWO_DECIMALS: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumSignificantDigits: 6, maximumSignificantDigits: 6,
minimumSignificantDigits: 3, minimumSignificantDigits: 3,
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2, minimumFractionDigits: 2,
}) }
const SIX_SIG_FIGS_NO_COMMAS = new Intl.NumberFormat('en-US', { const SIX_SIG_FIGS_NO_COMMAS: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumSignificantDigits: 6, maximumSignificantDigits: 6,
useGrouping: false, 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', notation: 'standard',
maximumSignificantDigits: 6, maximumSignificantDigits: 6,
minimumSignificantDigits: 3, minimumSignificantDigits: 3,
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2, minimumFractionDigits: 2,
useGrouping: false, 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', notation: 'standard',
minimumSignificantDigits: 3, minimumSignificantDigits: 3,
maximumSignificantDigits: 3, maximumSignificantDigits: 3,
currency: 'USD', currency: 'USD',
style: 'currency', style: 'currency',
}) }
const SEVEN_SIG_FIGS__SCI_NOTATION_USD = new Intl.NumberFormat('en-US', { const SEVEN_SIG_FIGS__SCI_NOTATION_CURRENCY: NumberFormatOptions = {
notation: 'scientific', notation: 'scientific',
minimumSignificantDigits: 7, minimumSignificantDigits: 7,
maximumSignificantDigits: 7, maximumSignificantDigits: 7,
currency: 'USD', currency: 'USD',
style: 'currency', style: 'currency',
}) }
type Format = Intl.NumberFormat | string
// each rule must contain either an `upperBound` or an `exact` value. // each rule must contain either an `upperBound` or an `exact` value.
// upperBound => number will use that formatter as long as it is < upperBound // upperBound => number will use that formatter as long as it is < upperBound
// exact => number will use that formatter if it is === exact // exact => number will use that formatter if it is === exact
type FormatterRule = // if hardcodedinput is supplied it will override the input value or use the hardcoded output
| { upperBound?: undefined; exact: number; formatter: Format } type HardCodedInputFormat =
| { upperBound: number; exact?: undefined; formatter: Format } | {
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 // 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 // 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. // falls between 1 and 1e6, you'd use TWO_DECIMALS as the formatter.
const tokenNonTxFormatter: FormatterRule[] = [ const tokenNonTxFormatter: FormatterRule[] = [
{ exact: 0, formatter: '0' }, { exact: 0, formatterOptions: NO_DECIMALS },
{ upperBound: 0.001, formatter: '<0.001' }, { upperBound: 0.001, hardCodedInput: { input: 0.001, prefix: '<' }, formatterOptions: THREE_DECIMALS },
{ upperBound: 1, formatter: THREE_DECIMALS }, { upperBound: 1, formatterOptions: THREE_DECIMALS },
{ upperBound: 1e6, formatter: TWO_DECIMALS }, { upperBound: 1e6, formatterOptions: TWO_DECIMALS },
{ upperBound: 1e15, formatter: SHORTHAND_TWO_DECIMALS }, { upperBound: 1e15, formatterOptions: SHORTHAND_TWO_DECIMALS },
{ upperBound: Infinity, formatter: '>999T' }, {
upperBound: Infinity,
hardCodedInput: { input: 999_000_000_000_000, prefix: '>' },
formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS,
},
] ]
const tokenTxFormatter: FormatterRule[] = [ const tokenTxFormatter: FormatterRule[] = [
{ exact: 0, formatter: '0' }, { exact: 0, formatterOptions: NO_DECIMALS },
{ upperBound: 0.00001, formatter: '<0.00001' }, {
{ upperBound: 1, formatter: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN }, upperBound: 0.00001,
{ upperBound: 10000, formatter: SIX_SIG_FIGS_TWO_DECIMALS }, hardCodedInput: { input: 0.00001, prefix: '<' },
{ upperBound: Infinity, formatter: TWO_DECIMALS }, 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[] = [ const swapTradeAmountFormatter: FormatterRule[] = [
{ exact: 0, formatter: '0' }, { exact: 0, formatterOptions: NO_DECIMALS },
{ upperBound: 0.1, formatter: SIX_SIG_FIGS_NO_COMMAS }, { upperBound: 0.1, formatterOptions: SIX_SIG_FIGS_NO_COMMAS },
{ upperBound: 1, formatter: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN_NO_COMMAS }, { upperBound: 1, formatterOptions: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN_NO_COMMAS },
{ upperBound: Infinity, formatter: SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS }, { upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS },
] ]
const swapPriceFormatter: FormatterRule[] = [ const swapPriceFormatter: FormatterRule[] = [
{ exact: 0, formatter: '0' }, { exact: 0, formatterOptions: NO_DECIMALS },
{ upperBound: 0.00001, formatter: '<0.00001' }, {
upperBound: 0.00001,
hardCodedInput: { input: 0.00001, prefix: '<' },
formatterOptions: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN,
},
...swapTradeAmountFormatter, ...swapTradeAmountFormatter,
] ]
const fiatTokenDetailsFormatter: FormatterRule[] = [ const fiatTokenDetailsFormatter: FormatterRule[] = [
{ exact: 0, formatter: '$0.00' }, { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: 0.00000001, formatter: '<$0.00000001' }, {
{ upperBound: 0.1, formatter: THREE_SIG_FIGS_USD }, upperBound: 0.00000001,
{ upperBound: 1.05, formatter: THREE_DECIMALS_USD }, hardCodedInput: { input: 0.00000001, prefix: '<' },
{ upperBound: 1e6, formatter: TWO_DECIMALS_USD }, formatterOptions: ONE_SIG_FIG_CURRENCY,
{ upperBound: Infinity, formatter: SHORTHAND_USD_TWO_DECIMALS }, },
{ 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[] = [ const fiatTokenPricesFormatter: FormatterRule[] = [
{ exact: 0, formatter: '$0.00' }, { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: 0.00000001, formatter: '<$0.00000001' }, {
{ upperBound: 1, formatter: THREE_SIG_FIGS_USD }, upperBound: 0.00000001,
{ upperBound: 1e6, formatter: TWO_DECIMALS_USD }, hardCodedInput: { input: 0.00000001, prefix: '<' },
{ upperBound: 1e16, formatter: SHORTHAND_USD_TWO_DECIMALS }, formatterOptions: ONE_SIG_FIG_CURRENCY,
{ upperBound: Infinity, formatter: SEVEN_SIG_FIGS__SCI_NOTATION_USD }, },
{ 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[] = [ const fiatTokenStatsFormatter: FormatterRule[] = [
// if token stat value is 0, we probably don't have the data for it, so show '-' as a placeholder // if token stat value is 0, we probably don't have the data for it, so show '-' as a placeholder
{ exact: 0, formatter: '-' }, { exact: 0, hardCodedInput: { hardcodedOutput: '-' }, formatterOptions: ONE_SIG_FIG_CURRENCY },
{ upperBound: 0.01, formatter: '<$0.01' }, { upperBound: 0.01, hardCodedInput: { input: 0.01, prefix: '<' }, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: 1000, formatter: TWO_DECIMALS_USD }, { upperBound: 1000, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: Infinity, formatter: SHORTHAND_USD_ONE_DECIMAL }, { upperBound: Infinity, formatterOptions: SHORTHAND_CURRENCY_ONE_DECIMAL },
] ]
const fiatGasPriceFormatter: FormatterRule[] = [ const fiatGasPriceFormatter: FormatterRule[] = [
{ exact: 0, formatter: '$0.00' }, { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: 0.01, formatter: '<$0.01' }, { upperBound: 0.01, hardCodedInput: { input: 0.01, prefix: '<' }, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: 1e6, formatter: TWO_DECIMALS_USD }, { upperBound: 1e6, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: Infinity, formatter: SHORTHAND_USD_TWO_DECIMALS }, { 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[] = [ const portfolioBalanceFormatter: FormatterRule[] = [
{ exact: 0, formatter: '$0.00' }, { exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: Infinity, formatter: TWO_DECIMALS_USD }, { upperBound: Infinity, formatterOptions: TWO_DECIMALS_CURRENCY },
] ]
const ntfTokenFloorPriceFormatterTrailingZeros: FormatterRule[] = [ const ntfTokenFloorPriceFormatterTrailingZeros: FormatterRule[] = [
{ exact: 0, formatter: '0' }, { exact: 0, formatterOptions: NO_DECIMALS },
{ upperBound: 0.001, formatter: '<0.001' }, { upperBound: 0.001, hardCodedInput: { input: 0.001, prefix: '<' }, formatterOptions: THREE_DECIMALS },
{ upperBound: 1, formatter: THREE_DECIMALS }, { upperBound: 1, formatterOptions: THREE_DECIMALS },
{ upperBound: 1000, formatter: TWO_DECIMALS }, { upperBound: 1000, formatterOptions: TWO_DECIMALS },
{ upperBound: 1e15, formatter: SHORTHAND_TWO_DECIMALS }, { upperBound: 1e15, formatterOptions: SHORTHAND_TWO_DECIMALS },
{ upperBound: Infinity, formatter: '>999T' }, {
upperBound: Infinity,
hardCodedInput: { input: 999_000_000_000_000, prefix: '>' },
formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS,
},
] ]
const ntfTokenFloorPriceFormatter: FormatterRule[] = [ const ntfTokenFloorPriceFormatter: FormatterRule[] = [
{ exact: 0, formatter: '0' }, { exact: 0, formatterOptions: NO_DECIMALS },
{ upperBound: 0.001, formatter: '<0.001' }, { upperBound: 0.001, hardCodedInput: { input: 0.001, prefix: '<' }, formatterOptions: THREE_DECIMALS },
{ upperBound: 1, formatter: THREE_DECIMALS_NO_TRAILING_ZEROS }, { upperBound: 1, formatterOptions: THREE_DECIMALS_NO_TRAILING_ZEROS },
{ upperBound: 1000, formatter: TWO_DECIMALS_NO_TRAILING_ZEROS }, { upperBound: 1000, formatterOptions: TWO_DECIMALS_NO_TRAILING_ZEROS },
{ upperBound: 1e15, formatter: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS }, { upperBound: 1e15, formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS },
{ upperBound: Infinity, formatter: '>999T' }, {
upperBound: Infinity,
hardCodedInput: { input: 999_000_000_000_000, prefix: '>' },
formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS,
},
] ]
const ntfCollectionStatsFormatter: FormatterRule[] = [ const ntfCollectionStatsFormatter: FormatterRule[] = [
{ upperBound: 1000, formatter: NO_DECIMALS }, { upperBound: 1000, formatterOptions: NO_DECIMALS },
{ upperBound: Infinity, formatter: SHORTHAND_ONE_DECIMAL }, { upperBound: Infinity, formatterOptions: SHORTHAND_ONE_DECIMAL },
] ]
export enum NumberType { export enum NumberType {
@ -300,32 +358,57 @@ const TYPE_TO_FORMATTER_RULES = {
[NumberType.NFTCollectionStats]: ntfCollectionStatsFormatter, [NumberType.NFTCollectionStats]: ntfCollectionStatsFormatter,
} }
function getFormatterRule(input: number, type: NumberType): Format { function getFormatterRule(input: number, type: NumberType): FormatterRule {
const rules = TYPE_TO_FORMATTER_RULES[type] const rules = TYPE_TO_FORMATTER_RULES[type]
for (const rule of rules) { for (const rule of rules) {
if ( if (
(rule.exact !== undefined && input === rule.exact) || (rule.exact !== undefined && input === rule.exact) ||
(rule.upperBound !== undefined && input < rule.upperBound) (rule.upperBound !== undefined && input < rule.upperBound)
) { ) {
return rule.formatter return rule
} }
} }
throw new Error(`formatter for type ${type} not configured correctly`) throw new Error(`formatter for type ${type} not configured correctly`)
} }
export function formatNumber( interface FormatNumberOptions {
input: Nullish<number>, input: Nullish<number>
type: NumberType = NumberType.TokenNonTx, type?: NumberType
placeholder = '-' placeholder?: string
): 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) { if (input === null || input === undefined) {
return placeholder return placeholder
} }
const formatter = getFormatterRule(input, type) const { hardCodedInput, formatterOptions } = getFormatterRule(input, type)
if (typeof formatter === 'string') return formatter
return formatter.format(input) 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( export function formatCurrencyAmount(
@ -333,7 +416,7 @@ export function formatCurrencyAmount(
type: NumberType = NumberType.TokenNonTx, type: NumberType = NumberType.TokenNonTx,
placeholder?: string placeholder?: string
): 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 { export function formatPriceImpact(priceImpact: Percent | undefined): string {
@ -356,13 +439,13 @@ export function formatPrice(
return '-' return '-'
} }
return formatNumber(parseFloat(price.toSignificant()), type) return formatNumber({ input: parseFloat(price.toSignificant()), type })
} }
export function formatNumberOrString(price: Nullish<number | string>, type: NumberType): string { export function formatNumberOrString(price: Nullish<number | string>, type: NumberType): string {
if (price === null || price === undefined) return '-' if (price === null || price === undefined) return '-'
if (typeof price === 'string') return formatNumber(parseFloat(price), type) if (typeof price === 'string') return formatNumber({ input: parseFloat(price), type })
return formatNumber(price, type) return formatNumber({ input: price, type })
} }
export function formatUSDPrice(price: Nullish<number | string>, type: NumberType = NumberType.FiatTokenPrice): string { export function formatUSDPrice(price: Nullish<number | string>, type: NumberType = NumberType.FiatTokenPrice): string {