diff --git a/src/hooks/useLocaleSeparators.ts b/src/hooks/useLocaleSeparators.ts deleted file mode 100644 index 048e393..0000000 --- a/src/hooks/useLocaleSeparators.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { getSeparator } from '@/lib/numbers'; - -export const useLocaleSeparators = () => { - const [locale, setLocale] = useState(navigator.language || 'en-US'); - - useEffect(() => { - const handler = () => setLocale(navigator.language || 'en-US'); - globalThis.addEventListener('languagechange', handler); - return () => globalThis.removeEventListener('languagechange', handler); - }, []); - - return useMemo(() => { - return { - group: getSeparator({ locale, separatorType: 'group' }), - decimal: getSeparator({ locale, separatorType: 'decimal' }), - }; - }, [locale]); -}; diff --git a/src/hooks/useLocaleSeparators.tsx b/src/hooks/useLocaleSeparators.tsx new file mode 100644 index 0000000..870f747 --- /dev/null +++ b/src/hooks/useLocaleSeparators.tsx @@ -0,0 +1,55 @@ +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { SUPPORTED_BASE_TAGS_LOCALE_MAPPING } from '@/constants/localization'; + +import { getSelectedLocale } from '@/state/localizationSelectors'; + +import abacusStateManager from '@/lib/abacus'; +import { getSeparator } from '@/lib/numbers'; + +type LocaleContextType = ReturnType; +const LocaleContext = createContext({} as LocaleContextType); +LocaleContext.displayName = 'Locale'; + +export const LocaleProvider = ({ ...props }) => ( + +); + +const useLocaleContext = () => { + const selectedLocale = useSelector(getSelectedLocale); + const [browserLanguage, setBrowserLanguage] = useState(navigator.language || 'en-US'); + + useEffect(() => { + const handler = () => setBrowserLanguage(navigator.language || 'en-US'); + globalThis.addEventListener('languagechange', handler); + return () => globalThis.removeEventListener('languagechange', handler); + }, []); + + useEffect(() => { + if (selectedLocale) { + const updatedBrowserLanguage = Object.entries(SUPPORTED_BASE_TAGS_LOCALE_MAPPING).find( + ([, value]) => value === selectedLocale + ); + + if (updatedBrowserLanguage) { + setBrowserLanguage(selectedLocale); + } + } + }, [selectedLocale]); + + const separators = useMemo(() => { + return { + group: getSeparator({ browserLanguage, separatorType: 'group' }), + decimal: getSeparator({ browserLanguage, separatorType: 'decimal' }), + }; + }, [browserLanguage]); + + useEffect(() => { + abacusStateManager.setLocaleSeparators(separators); + }, [separators]); + + return separators; +}; + +export const useLocaleSeparators = () => useContext(LocaleContext); diff --git a/src/lib/abacus/formatter.ts b/src/lib/abacus/formatter.ts index 08f70cb..bebfe0d 100644 --- a/src/lib/abacus/formatter.ts +++ b/src/lib/abacus/formatter.ts @@ -1,12 +1,25 @@ import type { AbacusFormatterProtocol } from '@/constants/abacus'; +import { type LocaleSeparators, MustBigNumber, getFractionDigits } from '../numbers'; + class AbacusFormatter implements AbacusFormatterProtocol { + localeSeparators: LocaleSeparators; + + constructor() { + this.localeSeparators = { group: ',', decimal: '.' }; + } + + setLocaleSeparators({ group, decimal }: LocaleSeparators) { + this.localeSeparators = { group, decimal }; + } + percent(value: number, digits: number): string { - return value.toString(); + return MustBigNumber(value).toFixed(digits); } dollar(value: number, tickSize: string): string { - return value.toString(); + const tickSizeDecimals = getFractionDigits(tickSize); + return MustBigNumber(value).toFixed(tickSizeDecimals); } } diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index 80212b6..1afef4b 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -34,6 +34,7 @@ import AbacusStateNotifier from './stateNotification'; import AbacusLocalizer from './localizer'; import AbacusFormatter from './formatter'; import AbacusThreading from './threading'; +import { LocaleSeparators } from '../numbers'; class AbacusStateManager { private store: RootStore | undefined; @@ -42,12 +43,14 @@ class AbacusStateManager { stateManager: InstanceType; websocket: AbacusWebsocket; stateNotifier: AbacusStateNotifier; + abacusFormatter: AbacusFormatter; constructor() { this.store = undefined; this.currentMarket = undefined; this.stateNotifier = new AbacusStateNotifier(); this.websocket = new AbacusWebsocket(); + this.abacusFormatter = new AbacusFormatter(); const ioImplementations = new IOImplementations( // @ts-ignore @@ -63,7 +66,7 @@ class AbacusStateManager { const uiImplementations = new UIImplementations( // @ts-ignore new AbacusLocalizer(), - new AbacusFormatter() + this.abacusFormatter ); this.stateManager = new AsyncAbacusStateManager( @@ -179,6 +182,10 @@ class AbacusStateManager { this.stateManager.closePosition(value, field); }; + setLocaleSeparators = ({ group, decimal }: LocaleSeparators) => { + this.abacusFormatter.setLocaleSeparators({ group, decimal }); + }; + // ------ Utils ------ // placeOrderPayload = (): Nullable => this.stateManager.placeOrderPayload(); diff --git a/src/lib/numbers.ts b/src/lib/numbers.ts index 6df2b65..8676d88 100644 --- a/src/lib/numbers.ts +++ b/src/lib/numbers.ts @@ -1,6 +1,7 @@ import { BigNumber } from 'bignumber.js'; export type BigNumberish = BigNumber | string | number; +export type LocaleSeparators = { group?: string; decimal?: string }; export const BIG_NUMBERS = { ZERO: new BigNumber(0), @@ -56,12 +57,12 @@ export const shorternNumberForDisplay = (num?: number) => * @returns separator for the given locale and separator type */ export const getSeparator = ({ - locale = navigator.language || 'en-US', + browserLanguage = navigator.language || 'en-US', separatorType, }: { - locale?: string; + browserLanguage?: string; separatorType: Intl.NumberFormatPartTypes; }) => - Intl.NumberFormat(locale) + Intl.NumberFormat(browserLanguage) .formatToParts(1000.1) .find?.((part) => part.type === separatorType)?.value; diff --git a/src/lib/tradeData.ts b/src/lib/tradeData.ts index 0a790d8..416cc8c 100644 --- a/src/lib/tradeData.ts +++ b/src/lib/tradeData.ts @@ -13,7 +13,7 @@ import { } from '@/constants/abacus'; import { AlertType } from '@/constants/alerts'; -import { PERCENT_DECIMALS } from '@/constants/numbers'; +import { PERCENT_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; import { TRADE_ROUTE } from '@/constants/routes'; import { PositionSide, TradeTypes } from '@/constants/trade'; @@ -65,9 +65,13 @@ export const hasPositionSideChanged = ({ const formatErrorParam = ({ value, format, + stepSizeDecimals, + tickSizeDecimals, }: { value: Nullable; format?: Nullable; + stepSizeDecimals: Nullable; + tickSizeDecimals: Nullable; }) => { switch (format) { case 'percent': { @@ -76,7 +80,11 @@ const formatErrorParam = ({ } case 'size': { const sizeBN = MustBigNumber(value); - return sizeBN.toFixed(0); + return sizeBN.toFixed(stepSizeDecimals ?? 0); + } + case 'price': { + const dollarBN = MustBigNumber(value); + return `$${dollarBN.toFixed(tickSizeDecimals ?? USD_DECIMALS)}`; } default: { return value || ''; @@ -90,9 +98,13 @@ const formatErrorParam = ({ export const getTradeInputAlert = ({ abacusInputErrors, stringGetter, + stepSizeDecimals, + tickSizeDecimals, }: { abacusInputErrors: ValidationError[]; stringGetter: StringGetterFunction; + stepSizeDecimals: Nullable; + tickSizeDecimals: Nullable; }) => { const inputAlerts = abacusInputErrors.map(({ action: errorAction, resources, type }) => { const { action, text } = resources || {}; @@ -104,7 +116,10 @@ export const getTradeInputAlert = ({ Object.fromEntries( stringParams .toArray() - .map(({ key, value, format }) => [key, formatErrorParam({ value, format })]) + .map(({ key, value, format }) => [ + key, + formatErrorParam({ value, format, stepSizeDecimals, tickSizeDecimals }), + ]) ); return { diff --git a/src/main.tsx b/src/main.tsx index a6a9b80..2a29686 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -11,12 +11,15 @@ import { ErrorBoundary } from './components/ErrorBoundary'; import './index.css'; import App from './App'; +import { LocaleProvider } from './hooks/useLocaleSeparators'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - } /> + + } /> + diff --git a/src/views/forms/TradeForm.tsx b/src/views/forms/TradeForm.tsx index 013936d..476986d 100644 --- a/src/views/forms/TradeForm.tsx +++ b/src/views/forms/TradeForm.tsx @@ -99,7 +99,8 @@ export const TradeForm = ({ const { limitPrice, triggerPrice, trailingPercent } = price || {}; const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders); const currentInput = useSelector(getCurrentInput); - const { tickSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {}; + const { tickSizeDecimals, stepSizeDecimals } = + useSelector(getCurrentMarketConfig, shallowEqual) || {}; const needsAdvancedOptions = needsGoodUntil || timeInForceOptions || executionOptions || needsPostOnly || needsReduceOnly; @@ -120,6 +121,8 @@ export const TradeForm = ({ const inputAlert = getTradeInputAlert({ abacusInputErrors: tradeErrors ?? [], stringGetter, + stepSizeDecimals, + tickSizeDecimals, }); if (placeOrderError) {