diff --git a/packages/components/src/composite/Table/components/HeaderColumn.tsx b/packages/components/src/composite/Table/components/HeaderColumn.tsx index 3a1a98eb78a4..89adb11c5a17 100644 --- a/packages/components/src/composite/Table/components/HeaderColumn.tsx +++ b/packages/components/src/composite/Table/components/HeaderColumn.tsx @@ -15,6 +15,10 @@ interface IHeaderColumnProps { selectedColumnName: string; onChangeSelectedName: (columnName: string) => void; onHeaderRow?: ITableProps['onHeaderRow']; + // Controlled mode: the order comes from the parent and no internal sort + // state is kept, so external sort actions stay reflected in the arrows. + isSortControlled?: boolean; + controlledOrder?: ETableSortType; } function HeaderColumn({ @@ -23,6 +27,8 @@ function HeaderColumn({ onHeaderRow, selectedColumnName, onChangeSelectedName, + isSortControlled, + controlledOrder, }: IHeaderColumnProps) { const { title, @@ -39,11 +45,21 @@ function HeaderColumn({ events?.initialSortOrder, ); + let currentSortOrder: ETableSortType | undefined; + if (isSortControlled) { + currentSortOrder = controlledOrder; + } else if (dataIndex === selectedColumnName) { + currentSortOrder = sortOrder; + } + useEffect(() => { + if (isSortControlled) { + return; + } if (selectedColumnName !== dataIndex) { setSortOrder(undefined); } - }, [dataIndex, selectedColumnName]); + }, [dataIndex, selectedColumnName, isSortControlled]); const handleColumnPress = useCallback(() => { events?.onPress?.(); @@ -51,7 +67,14 @@ function HeaderColumn({ return; } const disabledSorts = events?.disableSort || []; - const order = getNextSortOrder(sortOrder, disabledSorts); + const order = getNextSortOrder(currentSortOrder, disabledSorts); + + // Controlled mode owns no local state: just report the next order and let + // the parent's re-render drive the arrow. + if (isSortControlled) { + events?.onSortTypeChange?.(order); + return; + } // When resetting to undefined, clear the selected column to allow default sorting if (order === undefined) { @@ -68,12 +91,17 @@ function HeaderColumn({ setTimeout(() => { events?.onSortTypeChange?.(order); }); - }, [dataIndex, enableSortType, events, onChangeSelectedName, sortOrder]); + }, [ + dataIndex, + enableSortType, + events, + onChangeSelectedName, + currentSortOrder, + isSortControlled, + ]); const cursor = enableSortType ? 'pointer' : undefined; const showSortIcon = enableSortType && !renderTitle; - const currentSortOrder = - dataIndex === selectedColumnName ? sortOrder : undefined; const { renderSortIcon: renderInlineSortIcon } = useSortIcon({ showSortIcon: enableSortType && !!renderTitle, @@ -91,7 +119,12 @@ function HeaderColumn({ let titleContent; if (renderTitle) { - titleContent = renderTitle(renderInlineSortIcon()); + titleContent = renderTitle(renderInlineSortIcon(), { + order: currentSortOrder, + onSortPress: enableSortType + ? (handleColumnPress as () => void) + : undefined, + }); } else if (typeof title === 'string') { titleContent = ( ({ onHeaderRow, rowProps, headerRowProps, + controlledSort, }: { columns: ITableProps['columns']; onHeaderRow?: ITableProps['onHeaderRow']; rowProps?: ITableProps['rowProps']; headerRowProps?: ITableProps['headerRowProps']; + controlledSort?: ITableProps['controlledSort']; }) { + const isSortControlled = controlledSort !== undefined; const initialSelectedColumn = useMemo(() => { if (!onHeaderRow) return ''; for (let i = 0; i < columns.length; i += 1) { @@ -328,6 +331,9 @@ function TableHeaderRow({ const [selectedColumnName, setSelectedColumnName] = useState( initialSelectedColumn, ); + const activeColumnName = isSortControlled + ? (controlledSort?.dataIndex ?? '') + : selectedColumnName; return ( ({ column ? ( ) : null, )} @@ -358,6 +370,7 @@ function BasicTable({ TableFooterComponent, TableEmptyComponent, onHeaderRow, + controlledSort, onRow, rowProps, keyExtractor, @@ -445,9 +458,17 @@ function BasicTable({ rowProps={rowProps} headerRowProps={headerRowProps} onHeaderRow={onHeaderRow} + controlledSort={controlledSort} /> ) : null, - [columns, headerRowProps, onHeaderRow, rowProps, showHeader], + [ + columns, + headerRowProps, + onHeaderRow, + controlledSort, + rowProps, + showHeader, + ], ); const renderPlaceholder = useCallback( @@ -810,5 +831,10 @@ export const Table = withStaticProperties(BasicTable, { }); // Export types -export type { ITableProps, ITableColumn } from './types'; +export type { + ITableProps, + ITableColumn, + ITableColumnSortContext, + ITableControlledSort, +} from './types'; export { ETableSortType } from './types'; diff --git a/packages/components/src/composite/Table/types.ts b/packages/components/src/composite/Table/types.ts index df61ddb2fa0b..5ac40f0e2e29 100644 --- a/packages/components/src/composite/Table/types.ts +++ b/packages/components/src/composite/Table/types.ts @@ -9,11 +9,30 @@ export enum ETableSortType { DESC = 'desc', } +// Sort state handed to renderTitle so a custom header can draw its own sort +// affordance and make the whole title (text + icon) the click target. +export interface ITableColumnSortContext { + order?: ETableSortType; + onSortPress?: () => void; +} + +// Externally-owned sort state. `null` means "nothing sorted". Passing this +// switches the header from its internal per-column state to controlled mode, +// which is what lets an outside control (e.g. a sort shortcut chip) and the +// header arrows stay on one source of truth. +export type ITableControlledSort = { + dataIndex: string; + order: ETableSortType; +} | null; + export interface ITableColumn { title: ReactNode; // When provided, renders the title with an inline sort icon instead of // the default external sort icon rendered by Column. - renderTitle?: (sortIcon: ReactNode) => ReactNode; + renderTitle?: ( + sortIcon: ReactNode, + sortContext: ITableColumnSortContext, + ) => ReactNode; dataIndex: string; titleProps?: ISizableTextProps; columnProps?: Omit; @@ -55,6 +74,8 @@ export interface ITableProps { onDragBegin?: ISortableListViewProps['onDragBegin']; onDragEnd?: ISortableListViewProps['onDragEnd']; keyExtractor: (item: T, index: number) => string; + // Opt into controlled header sort; omit to keep the internal state behavior. + controlledSort?: ITableControlledSort; onHeaderRow?: ( column: ITableColumn, index: number, diff --git a/packages/kit-bg/src/services/ServiceMarketV2.ts b/packages/kit-bg/src/services/ServiceMarketV2.ts index 752ffaa1a831..047409b22c06 100644 --- a/packages/kit-bg/src/services/ServiceMarketV2.ts +++ b/packages/kit-bg/src/services/ServiceMarketV2.ts @@ -67,6 +67,9 @@ type IMarketTokenListRequestParams = { type?: string; category?: string; timeFrame?: string; + // hot-token v6 filter params (marketCapMin, liquidityMin, volumeMin, ...), + // already flattened by buildHotTokenFilterParams on the caller side. + filterParams?: Record; }; type INormalizedMarketTokenListRequestParams = IMarketTokenListRequestParams & { @@ -184,6 +187,7 @@ class ServiceMarketV2 extends ServiceBase { type, category, timeFrame, + filterParams, }: INormalizedMarketTokenListRequestParams) { const client = await this.getClient(EServiceEndpointEnum.Utility); const response = await client.get<{ @@ -202,6 +206,7 @@ class ServiceMarketV2 extends ServiceBase { type, category, timeFrame, + ...filterParams, currency: 'usd', }, }); @@ -341,6 +346,7 @@ class ServiceMarketV2 extends ServiceBase { type, category, timeFrame, + filterParams, }: IMarketTokenListRequestParams, options?: IFetchMarketTokenListOptions, ) { @@ -355,6 +361,7 @@ class ServiceMarketV2 extends ServiceBase { type, category, timeFrame, + filterParams, }); if (options?.forceRemote) { return this._fetchMarketTokenListFromApi(normalizedParams); diff --git a/packages/kit/src/views/Market/MarketBannerDetail/BannerDetailListColumnHeader.tsx b/packages/kit/src/views/Market/MarketBannerDetail/BannerDetailListColumnHeader.tsx index f6c447fdb4cd..f1c93deaa12e 100644 --- a/packages/kit/src/views/Market/MarketBannerDetail/BannerDetailListColumnHeader.tsx +++ b/packages/kit/src/views/Market/MarketBannerDetail/BannerDetailListColumnHeader.tsx @@ -37,8 +37,8 @@ export function BannerDetailListColumnHeader({ {`${intl.formatMessage({ id: ETranslations.global_name, - })} / ${intl.formatMessage({ - id: ETranslations.dexmarket_turnover, + })} / 24h ${intl.formatMessage({ + id: ETranslations.perp_token_selector_volume, })}`} diff --git a/packages/kit/src/views/Market/MarketBannerDetail/PerpsTokenListSection.tsx b/packages/kit/src/views/Market/MarketBannerDetail/PerpsTokenListSection.tsx index c2fede9e6ee4..de0ad49d1215 100644 --- a/packages/kit/src/views/Market/MarketBannerDetail/PerpsTokenListSection.tsx +++ b/packages/kit/src/views/Market/MarketBannerDetail/PerpsTokenListSection.tsx @@ -16,6 +16,7 @@ import { type IMarketPerpsToken, mapServerToken, } from '../MarketHomeV2/components/MarketPerpsList/hooks/useMarketPerpsTokenList'; +import { usePerpsClientSort } from '../MarketHomeV2/components/MarketPerpsList/hooks/usePerpsClientSort'; import { usePerpsColumns } from '../MarketHomeV2/components/MarketPerpsList/hooks/usePerpsColumns'; import { BannerDetailTokenFlatList } from './BannerDetailTokenFlatList'; @@ -98,6 +99,14 @@ export function PerpsTokenListSection({ [tokens], ); + // Same header sorting the standalone perps list uses, so the two perps + // tables behave identically wherever they appear. + const { + sortedTokens, + onHeaderRow: handleHeaderRow, + controlledSort, + } = usePerpsClientSort({ tokens }); + const showSkeleton = Boolean(isLoading) && tokens.length === 0; const handleMobileItemPress = useCallback( @@ -155,7 +164,9 @@ export function PerpsTokenListSection({ stickyHeader columns={perpsColumns} - dataSource={tokens} + dataSource={sortedTokens} + onHeaderRow={handleHeaderRow} + controlledSort={controlledSort} keyExtractor={(item) => item.name} estimatedItemSize="$14" extraData={tokens.length} diff --git a/packages/kit/src/views/Market/MarketBannerDetail/useMarketBannerDetail/index.ts b/packages/kit/src/views/Market/MarketBannerDetail/useMarketBannerDetail/index.ts index 08ccd7c27cae..b674184d2878 100644 --- a/packages/kit/src/views/Market/MarketBannerDetail/useMarketBannerDetail/index.ts +++ b/packages/kit/src/views/Market/MarketBannerDetail/useMarketBannerDetail/index.ts @@ -4,6 +4,7 @@ import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/background import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; import { useMarketBannerListSortAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; +import { MARKET_CLIENT_SORT_FIELD_MAP } from '../../MarketHomeV2/components/MarketTokenList/utils/marketListClientSort'; import { getNetworkLogoUri, transformApiItemToToken, @@ -12,12 +13,13 @@ import { import type { IMarketTokenListResult } from '../../MarketHomeV2/components/MarketTokenList/MarketTokenListBase'; const BANNER_DETAIL_CHANGE_SORT_BY = 'change24h'; -type IBannerDetailSortBy = typeof BANNER_DETAIL_CHANGE_SORT_BY; -function isBannerDetailSortBy( - sortBy: string | undefined, -): sortBy is IBannerDetailSortBy { - return sortBy === BANNER_DETAIL_CHANGE_SORT_BY; +// Any column the shared client sort can express is accepted, so the desktop +// table's headers all sort here like they do on the home lists. The mobile +// change toggle still reads `changeSortType` below, which narrows back to the +// change column on its own. +function isBannerDetailSortBy(sortBy: string | undefined): sortBy is string { + return Boolean(sortBy && MARKET_CLIENT_SORT_FIELD_MAP[sortBy]); } type IUseMarketBannerDetailParams = { @@ -89,7 +91,7 @@ export function useMarketBannerDetail({ ); const toggleSort = useCallback( - (sortBy: IBannerDetailSortBy) => { + (sortBy: string) => { const activeSortType = sortRef.current.sortBy === sortBy ? sortRef.current.sortType diff --git a/packages/kit/src/views/Market/MarketDetailV2/components/TokenSelector/MarketTokenSelectorList.tsx b/packages/kit/src/views/Market/MarketDetailV2/components/TokenSelector/MarketTokenSelectorList.tsx index 5f9625da3e18..20d69c7bcbab 100644 --- a/packages/kit/src/views/Market/MarketDetailV2/components/TokenSelector/MarketTokenSelectorList.tsx +++ b/packages/kit/src/views/Market/MarketDetailV2/components/TokenSelector/MarketTokenSelectorList.tsx @@ -315,7 +315,11 @@ const MarketTokenSelectorList = memo( size="$bodySm" color="$textSubdued" > - {intl.formatMessage({ id: ETranslations.dexmarket_turnover })} + {/* The selector requests a fixed 1h window (MarketTokenSelector + passes timeRange="1h"), so the header names that window. */} + {`1h ${intl.formatMessage({ + id: ETranslations.perp_token_selector_volume, + })}`} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterBarSmall/MarketFilterBarSmall.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterBarSmall/MarketFilterBarSmall.tsx index 88f81db02f5d..992a90f199f6 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterBarSmall/MarketFilterBarSmall.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterBarSmall/MarketFilterBarSmall.tsx @@ -1,15 +1,24 @@ import { XStack, YStack } from '@onekeyhq/components'; +import { MarketFiltersIconTrigger } from '../MarketFilterChipsBar'; import { MobileNetworkDropdown } from '../MobileNetworkDropdown'; import { TimeRangeDropdown } from '../TimeRangeDropdown'; import type { ITimeRangeSelectorValue } from '../TimeRangeSelector'; +// Figma 25169-43731: the time-range dropdown and the filters trigger form one +// right-aligned group. +const TRAILING_CONTROLS_GAP = 10; + export interface IMarketFilterBarSmallProps { selectedNetworkId?: string; timeRange?: ITimeRangeSelectorValue; onNetworkIdChange?: (networkId: string) => void; onTimeRangeChange?: (value: ITimeRangeSelectorValue) => void; + // Only the caller knows whether its list consumes filterState, so the entry + // point is opt-in: a trigger on a tab that ignores filters would light up its + // badge and change nothing. See isMarketTrendingList. + showFiltersTrigger?: boolean; } function MarketFilterBarSmall({ @@ -17,6 +26,7 @@ function MarketFilterBarSmall({ timeRange = '1h', onNetworkIdChange, onTimeRangeChange, + showFiltersTrigger = false, }: IMarketFilterBarSmallProps) { return ( @@ -31,13 +41,25 @@ function MarketFilterBarSmall({ selectedNetworkId={selectedNetworkId} onNetworkIdChange={onNetworkIdChange} /> - {onTimeRangeChange ? ( - - ) : null} + + {onTimeRangeChange ? ( + + ) : null} + {showFiltersTrigger && onTimeRangeChange ? ( + + ) : null} + ); diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketFilterChipsBar.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketFilterChipsBar.tsx new file mode 100644 index 000000000000..08db5a9576cf --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketFilterChipsBar.tsx @@ -0,0 +1,735 @@ +import { useState } from 'react'; +import type { ReactNode } from 'react'; + +import { useIntl } from 'react-intl'; + +import { + Divider, + Icon, + LinearGradient, + Popover, + SizableText, + Stack, + XStack, + YStack, +} from '@onekeyhq/components'; +import type { IKeyOfIcons } from '@onekeyhq/components'; +import { LazyTooltip } from '@onekeyhq/components/src/actions/LazyTooltip'; +import { ANIMATE_ONLY_OPACITY_TRANSFORM } from '@onekeyhq/components/src/utils/animationConstants'; +import { useThemeVariant } from '@onekeyhq/kit/src/hooks/useThemeVariant'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; + +import { + MARKET_TOOLBAR_GAP_BOTTOM, + MARKET_TOOLBAR_GAP_TOP, + MARKET_TOOLBAR_ITEM_HEIGHT, + marketToolbarFrameProps, +} from '../marketToolbarFrame'; + +import { + MARKET_FILTER_CHIPS, + MARKET_FILTER_DIMENSION_MAP, + findActiveMarketFilterChip, + formatMarketFilterOptionLabel, + getMarketFilterOption, +} from './marketListFilterConfig'; +import { useMarketListFilter } from './MarketListFilterContext'; +import { TierPill } from './TierPill'; + +import type { + EMarketFilterDimension, + IMarketFilterChip, + IMarketListSortState, +} from './marketListFilterTypes'; +import type { IMarketTimeRangeValue } from '../../types'; + +const TIME_RANGE_OPTIONS: IMarketTimeRangeValue[] = ['5m', '1h', '4h', '24h']; + +// The Popover's own Trigger wrapper drives the open-on-press behavior; this +// noop only exists so the trigger XStack keeps a Pressable press state for +// hoverStyle/pressStyle to animate against. +const noop = () => undefined; + +// Sortable column dataIndex -> chip copy. The chip has no column header next +// to it, so it has to name the column itself. +const SORT_COLUMN_LABEL_KEYS: Record = { + name: ETranslations.dexmarket_token_age, + price: ETranslations.global_price, + change24h: ETranslations.dexmarket_token_change, + marketCap: ETranslations.dexmarket_market_cap, + liquidity: ETranslations.dexmarket_liquidity, + transactions: ETranslations.dexmarket_txns, + holders: ETranslations.dexmarket_holders, + turnover: ETranslations.perp_token_selector_volume, +}; + +const SORT_DIRECTION_LABEL_KEYS: Record<'asc' | 'desc', ETranslations> = { + desc: ETranslations.high_to_low__action, + asc: ETranslations.low_to_high__action, +}; + +// Rounded pill shared by time-range buttons, quick chips and the Filters +// trigger. Figma: min-w 32 / px 7 / py 5 / radius full / bodySm-medium +// subdued; selected state = bg strong + primary text color. +export function MarketToolbarPill({ + label, + icon, + selected, + trailing, + onPress, + testID, +}: { + label: string; + icon?: IKeyOfIcons; + selected?: boolean; + trailing?: ReactNode; + onPress?: () => void; + testID?: string; +}) { + return ( + + {icon ? : null} + + {label} + + {trailing} + + ); +} + +// 16px icon in a 24px round hover hit-zone (Figma IconButton spec). +function SmallRoundIconButton({ + icon, + onPress, + testID, +}: { + icon: IKeyOfIcons; + onPress: () => void; + testID?: string; +}) { + return ( + + + + ); +} + +// Figma 25053-6035: text-button whose hover state recolors the label and +// broom icon (no background), with a pointer cursor. +function ClearTextButton({ + onPress, + testID, +}: { + onPress: () => void; + testID?: string; +}) { + const intl = useIntl(); + const [isHovered, setIsHovered] = useState(false); + return ( + setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} + onPress={onPress} + role="button" + testID={testID} + > + + + {intl.formatMessage({ id: ETranslations.global_clear })} + + + ); +} + +// The fade must hide the text under the × — it fades to the chip's EFFECTIVE +// surface (its translucent bg token composited over bgApp), precomputed per +// theme as a hex (theme.*.val is not a reliable color string on web). The fade +// only appears while the chip is highlighted (hover/open), so the target is the +// hovered surface: bgStrongHover over bgApp = light #E8E8E8 / dark #282828 +// (grayA4 over bgApp). Recompute if grayA4 changes. +const CHIP_MASK_HEX_HOVER = { light: '#e8e8e8', dark: '#282828' }; + +// 4px inset, matching the category bars: a 30px control row keeps the box at +// MARKET_TOOLBAR_HEIGHT (30 + 4*2 + 1px border each side). +const TOOLBAR_PADDING = 4; + +// Remove hit-target + fade geometry. The overlays sit ON TOP of the value +// text, so revealing the × never changes the chip width (no layout jitter). +// The gradient keeps a solid plateau over the right ~55% so the × always sits +// on an opaque patch; only the left edge fades, blending into the text. +const CHIP_REMOVE_WIDTH = 26; +const CHIP_FADE_WIDTH = 34; +const CHIP_HORIZONTAL_PADDING = 10; +const CHIP_FADE_START: [number, number] = [0, 0]; +const CHIP_FADE_END: [number, number] = [1, 0]; +const CHIP_FADE_LOCATIONS: [number, number, number] = [0, 0.45, 1]; + +// Figma 25099-6710 / 25116-6318: bg-strong capsule, h 26 / radius full, label +// regular + value medium; the body opens a popover on press (highlighting to +// bg-strong-hover with a pointer), and hover fades in a remove control over the +// right edge. Both the sort chip and the filter chips use this, so they read +// as one consistent control. +function ConditionChipShell({ + label, + value, + isOpen, + onRemove, + testID, + removeTestID, +}: { + label: string; + value: string; + isOpen?: boolean; + onRemove: () => void; + testID?: string; + removeTestID?: string; +}) { + const [isHovered, setIsHovered] = useState(false); + const [isRemoveHovered, setIsRemoveHovered] = useState(false); + const themeVariant = useThemeVariant(); + const themeKey = themeVariant === 'dark' ? 'dark' : 'light'; + const showRemove = isHovered || isOpen; + // The fade only shows while the body is highlighted (hover/open), so it + // targets the hover surface. + const maskHex = CHIP_MASK_HEX_HOVER[themeKey]; + return ( + setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} + onPress={noop} + role="button" + testID={testID} + > + + {label} + + + {value} + + {showRemove ? ( + <> + + setIsRemoveHovered(true)} + onHoverOut={() => setIsRemoveHovered(false)} + onPress={(event) => { + // The chip body opens the tier popover; the × must not. + event.stopPropagation(); + onRemove(); + }} + role="button" + testID={removeTestID} + > + {/* Recolor on hover, matching the popover's Clear button, so the + close target reads as distinct from the pill body. */} + + + + ) : null} + + ); +} + +type ITierPopoverOption = { id: string; label: string; disabled?: boolean }; + +// Wide enough that three tiers share a row without the widest floor label +// ("100K+"/"500K+") getting truncated: 3 pills (~72px incl. border + px 11) +// + 2 gaps (8) + popover px (14) each side. +const TIER_POPOVER_WIDTH = 264; + +// Figma tier popover (25117-6453 / 25117-6494): 240 wide, header = dimension +// label + Clear text button, then pills that share the row equally. Three or +// fewer options sit on one line; four wrap to a 2x2 grid (the pill floor drops +// to 0 so three can fit, matching the design's min-w-px pills). +function TierPopoverContent({ + title, + options, + selectedOptionId, + onSelect, + onClear, + testIdPrefix, +}: { + title: string; + options: ITierPopoverOption[]; + selectedOptionId?: string; + onSelect: (optionId: string) => void; + onClear: () => void; + testIdPrefix: string; +}) { + const columns = options.length === 4 ? 2 : options.length; + const rows: ITierPopoverOption[][] = []; + for (let i = 0; i < options.length; i += columns) { + rows.push(options.slice(i, i + columns)); + } + return ( + + + + {title} + + + + + {rows.map((row) => ( + + {row.map((option) => ( + onSelect(option.id)} + testID={`${testIdPrefix}-${option.id}`} + /> + ))} + + ))} + + + ); +} + +// A dimension's applied tier. Body opens the tier popover (single-select +// snap), × removes the dimension. +function FilterConditionChip({ + dimensionId, + optionId, + isOpen, + onOpenChange, + onSelectOption, + onRemove, +}: { + dimensionId: EMarketFilterDimension; + optionId: string; + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onSelectOption: (optionId: string) => void; + onRemove: () => void; +}) { + const intl = useIntl(); + const dimension = MARKET_FILTER_DIMENSION_MAP.get(dimensionId); + const option = getMarketFilterOption(dimensionId, optionId); + if (!dimension || !option) { + return null; + } + const dimensionLabel = intl.formatMessage({ id: dimension.labelKey }); + return ( + + } + renderContent={ + ({ + id: item.id, + label: formatMarketFilterOptionLabel(intl, item), + }))} + selectedOptionId={option.id} + onSelect={onSelectOption} + onClear={onRemove} + testIdPrefix={`market-filter-tier-${dimensionId}`} + /> + } + /> + ); +} + +// The active sort, shown as a peer of the filter conditions: the sort is part +// of "how you are seeing this table", so leaving it implicit would hide half +// the applied state. Same state machine as the column header (P1-10). It opens +// a direction dropdown for consistency with the filter chips; ascending is +// disabled only while a quick chip owns the sort, since those are +// descending-only by definition. A sort the user drove from the column header +// is theirs to reverse. +function SortConditionChip({ + sortState, + isOpen, + lockAscending, + onOpenChange, + onSelectDirection, + onRemove, +}: { + sortState: IMarketListSortState; + isOpen: boolean; + lockAscending: boolean; + onOpenChange: (open: boolean) => void; + onSelectDirection: (direction: 'asc' | 'desc') => void; + onRemove: () => void; +}) { + const intl = useIntl(); + const { sortBy, sortType } = sortState; + if (!sortBy || !sortType) { + return null; + } + const columnLabelKey = SORT_COLUMN_LABEL_KEYS[sortBy]; + const columnLabel = columnLabelKey + ? intl.formatMessage({ id: columnLabelKey }) + : sortBy; + const directionLabel = (direction: 'asc' | 'desc') => + intl.formatMessage({ id: SORT_DIRECTION_LABEL_KEYS[direction] }); + return ( + + } + renderContent={ + onSelectDirection(id as 'asc' | 'desc')} + onClear={onRemove} + testIdPrefix="market-filter-tier-sort" + /> + } + /> + ); +} + +// Quick chips look alike whether they sort or filter; the difference is +// disclosed in the tooltip rather than encoded in the styling (P2-9). +function QuickChip({ + chip, + onPress, +}: { + chip: IMarketFilterChip; + onPress: () => void; +}) { + const intl = useIntl(); + return ( + + } + renderContent={intl.formatMessage({ id: chip.tooltipKey })} + /> + ); +} + +export function MarketFilterChipsBar({ + timeRange, + onTimeRangeChange, + filtersTrigger, +}: { + timeRange: IMarketTimeRangeValue; + onTimeRangeChange: (v: IMarketTimeRangeValue) => void; + // Filters modal trigger slot rendered at the right edge. + filtersTrigger?: ReactNode; +}) { + const { filterState, sortState, applyConditions, setSortState } = + useMarketListFilter(); + const [openPopover, setOpenPopover] = useState(); + + const conditionEntries = Object.entries(filterState.conditions) as [ + EMarketFilterDimension, + string, + ][]; + const hasSort = Boolean(sortState.sortBy && sortState.sortType); + const isExpanded = conditionEntries.length > 0 || hasSort; + // Derived, not stored: whatever chip the live state happens to match. + const activeChip = findActiveMarketFilterChip( + filterState.conditions, + sortState, + ); + + const handleQuickChipPress = (chip: IMarketFilterChip) => { + // A chip carrying `sort` reorders the list instead of narrowing it, and it + // writes the shared sort state directly rather than going through the + // header's dexSort path. Tracked as its own action, with the dimension and + // direction kept, so sort shortcuts stay separable from filter presets. + defaultLogger.dex.list.dexFilterChip( + chip.sort + ? { + action: 'sortChipClick', + presetId: chip.id, + field: chip.sort.sortBy, + value: chip.sort.sortType, + conditionCount: Object.keys(chip.conditions).length, + } + : { + action: 'presetClick', + presetId: chip.id, + conditionCount: Object.keys(chip.conditions).length, + }, + ); + // The chip applies to whatever window the user is on: market cap and + // liquidity do not move with the window at all, and the volume floor only + // changes how strict the third condition is. Overriding an explicit + // selection buys less than it costs (measured 2026-08-17, PRD 8/P2-9). + applyConditions(chip.conditions, { sort: chip.sort }); + }; + + const removeDimension = (dimensionId: EMarketFilterDimension) => { + defaultLogger.dex.list.dexFilterChip({ + action: 'conditionRemove', + field: dimensionId, + }); + const next = { ...filterState.conditions }; + delete next[dimensionId]; + applyConditions(next); + setOpenPopover(undefined); + }; + + return ( + // Figma 25141-48128: the trending controls sit in the same bordered + // toolbar the Favorites/Stocks/Perps sub-category bars use, sharing their + // frame geometry so all four tabs line up (see marketToolbarFrame). + + + {/* Fixed row height so swapping quick chips <-> condition chips + (different intrinsic pill heights) never shifts the table below. */} + + + {TIME_RANGE_OPTIONS.map((option) => ( + onTimeRangeChange(option)} + testID={`market-filter-time-range-${option}`} + /> + ))} + + + {isExpanded ? ( + + {/* Group anchor: the matched chip's icon, or a generic filter + glyph once the conditions no longer spell out a chip. */} + + + + + + setOpenPopover(open ? 'sort' : undefined) + } + onSelectDirection={(direction) => { + defaultLogger.dex.list.dexFilterChip({ + action: 'conditionChange', + field: 'sort', + value: direction, + }); + setSortState((prev) => ({ ...prev, sortType: direction })); + setOpenPopover(undefined); + }} + onRemove={() => { + defaultLogger.dex.list.dexFilterChip({ + action: 'conditionRemove', + field: 'sort', + }); + setSortState({}); + setOpenPopover(undefined); + }} + /> + {conditionEntries.map(([dimensionId, optionId]) => ( + + setOpenPopover(open ? dimensionId : undefined) + } + onSelectOption={(nextOptionId) => { + defaultLogger.dex.list.dexFilterChip({ + action: 'conditionChange', + field: dimensionId, + value: nextOptionId, + }); + applyConditions({ + ...filterState.conditions, + [dimensionId]: nextOptionId, + }); + setOpenPopover(undefined); + }} + onRemove={() => removeDimension(dimensionId)} + /> + ))} + + + { + defaultLogger.dex.list.dexFilterChip({ + action: 'clearAll', + }); + applyConditions({}); + }} + testID="market-filter-chips-clear-all" + /> + + + ) : ( + + {MARKET_FILTER_CHIPS.map((chip) => ( + handleQuickChipPress(chip)} + /> + ))} + + )} + + {filtersTrigger} + + + ); +} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketFiltersModal.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketFiltersModal.tsx new file mode 100644 index 000000000000..ead1f2690ca4 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketFiltersModal.tsx @@ -0,0 +1,488 @@ +import { useCallback, useState } from 'react'; +import type { ReactNode } from 'react'; + +import { useIntl } from 'react-intl'; + +import { + Button, + Dialog, + Divider, + Icon, + ScrollView, + SizableText, + XStack, + YStack, +} from '@onekeyhq/components'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; + +import { MARKET_TOOLBAR_ITEM_HEIGHT } from '../marketToolbarFrame'; + +import { + MARKET_FILTER_DIMENSIONS, + MARKET_FILTER_GROUP_LABELS, + MARKET_FILTER_GROUP_ORDER, + formatMarketFilterOptionLabel, +} from './marketListFilterConfig'; +import { useMarketListFilter } from './MarketListFilterContext'; +import { EMarketFilterGroup } from './marketListFilterTypes'; +import { TierPill } from './TierPill'; + +import type { + IMarketFilterDimensionConfig, + IMarketListFilterConditions, +} from './marketListFilterTypes'; +import type { IMarketTimeRangeValue } from '../../types'; + +const TIME_RANGE_OPTIONS: IMarketTimeRangeValue[] = ['5m', '1h', '4h', '24h']; + +// Figma 25060-6052: label takes the remaining width on the left, controls sit +// in a fixed 232px column that wraps. Three pills per row (min 72px each); +// a four-option dimension wraps to 2x2 with wider pills. +const CONTROL_COLUMN_WIDTH = 232; +const ROW_LABEL_HEIGHT = 30; +const TIER_GAP = 6; +const TIME_RANGE_GAP = 4; +const TIME_RANGE_PILL_MIN_WIDTH = 40; +const TIER_COLUMNS_DEFAULT = 3; +const TIER_COLUMNS_FOR_FOUR_OPTIONS = 2; +const TIER_MIN_WIDTH_DEFAULT = 72; +const TIER_MIN_WIDTH_WIDE = 80; + +function getTierColumns(optionCount: number) { + return optionCount === 4 + ? TIER_COLUMNS_FOR_FOUR_OPTIONS + : TIER_COLUMNS_DEFAULT; +} + +// Figma 25169-44190: the icon-only mobile trigger draws an invisible +// -4/-8 inset around itself so the 18px glyph still has a comfortable tap area. +const MOBILE_TRIGGER_HIT_SLOP = { top: 4, bottom: 4, left: 8, right: 8 }; + +const AUDIT_TIER_LABELS = ['≤ 10%', '≤ 30%', '≤ 50%']; + +// Short labels: the row's label column is ~104px wide, so the full +// "... holding %" wording wrapped onto a second line. The Audit group header +// already supplies the "holding" context. Final copy lands with Spike A#8. +const AUDIT_ROWS = [ + { label: 'Top 10 %', testId: 'top10-holding' }, + { label: 'Dev %', testId: 'dev-holding' }, + { label: 'Suspicious %', testId: 'suspicious-holding' }, + { label: 'Bundler %', testId: 'bundle-holding' }, +]; + +// Equal-width pills laid out on a fixed column grid so every row lines up. +function TierGrid({ + items, + columns, +}: { + items: { + key: string; + label: string; + selected?: boolean; + disabled?: boolean; + onPress?: () => void; + testID?: string; + }[]; + columns: number; +}) { + const rows: (typeof items)[] = []; + for (let i = 0; i < items.length; i += columns) { + rows.push(items.slice(i, i + columns)); + } + const minWidth = + columns === TIER_COLUMNS_FOR_FOUR_OPTIONS + ? TIER_MIN_WIDTH_WIDE + : TIER_MIN_WIDTH_DEFAULT; + return ( + + {rows.map((row) => ( + + {row.map((item) => ( + + ))} + {row.length < columns + ? Array.from({ length: columns - row.length }).map((_, index) => ( + + )) + : null} + + ))} + + ); +} + +// Label on the left, controls in the fixed-width column on the right. +function FilterRow({ + label, + note, + children, +}: { + label: string; + note?: string; + children: ReactNode; +}) { + return ( + + + + {label} + + {note ? ( + + {note} + + ) : null} + + {children} + + ); +} + +function DimensionRow({ + dimension, + selectedOptionId, + onSelect, +}: { + dimension: IMarketFilterDimensionConfig; + selectedOptionId?: string; + onSelect: (optionId: string | undefined) => void; +}) { + const intl = useIntl(); + const label = intl.formatMessage({ id: dimension.labelKey }); + return ( + + ({ + key: option.id, + label: formatMarketFilterOptionLabel(intl, option), + selected: option.id === selectedOptionId, + onPress: () => + onSelect(option.id === selectedOptionId ? undefined : option.id), + testID: `market-filters-modal-field-${dimension.id}-${option.id}`, + }))} + /> + + ); +} + +function GroupHeader({ label }: { label: string }) { + return {label}; +} + +// Modal body. Receives the committed state as a snapshot instead of reading +// the filter context: Dialog portals render outside the Market provider +// subtree, so context would resolve to the empty default there. +function MarketFiltersModalContent({ + initialConditions, + initialTimeRange, + onApply, + onApplyTimeRange, + onClose, +}: { + initialConditions: IMarketListFilterConditions; + initialTimeRange: IMarketTimeRangeValue; + onApply: (next: IMarketListFilterConditions) => void; + onApplyTimeRange: (v: IMarketTimeRangeValue) => void; + onClose: () => void; +}) { + const intl = useIntl(); + const [draft, setDraft] = useState({ + ...initialConditions, + }); + // Time frame is drafted alongside the conditions: Dialog.show snapshots its + // props, so local state is what keeps the selection visibly in sync, and + // committing on Confirm matches how every other row in this modal behaves. + const [draftTimeRange, setDraftTimeRange] = + useState(initialTimeRange); + + const handleSelect = ( + dimension: IMarketFilterDimensionConfig, + optionId: string | undefined, + ) => { + setDraft((prev) => { + const next = { ...prev }; + if (optionId === undefined) { + delete next[dimension.id]; + } else { + next[dimension.id] = optionId; + } + return next; + }); + }; + + return ( + + {/* The Dialog frame has no built-in scroll; cap the section list so + long group stacks scroll internally and the footer stays pinned. */} + + + {MARKET_FILTER_GROUP_ORDER.map((group, groupIndex) => { + const dimensions = MARKET_FILTER_DIMENSIONS.filter( + (dimension) => dimension.group === group, + ); + return ( + + {groupIndex > 0 ? : null} + + + {group === EMarketFilterGroup.Metrics ? ( + + + {TIME_RANGE_OPTIONS.map((option) => ( + setDraftTimeRange(option)} + testID={`market-filters-modal-time-range-${option}`} + /> + ))} + + + ) : null} + {group === EMarketFilterGroup.Audit + ? AUDIT_ROWS.map((row) => ( + + ({ + key: tierLabel, + label: tierLabel, + disabled: true, + testID: `market-filters-modal-audit-${row.testId}-${tierLabel}`, + }))} + /> + + )) + : dimensions.map((dimension) => ( + + handleSelect(dimension, optionId) + } + /> + ))} + + + ); + })} + + + {/* Figma footer: two equal-width actions, secondary Reset + primary + Confirm (the dialog's own close button covers dismissal). */} + + + + + + ); +} + +// Opens the filter dialog. Must be called from inside the Market provider +// subtree: Dialog portals render outside it, so the current state and the +// setters are snapshotted here and handed to the content as props. +// Desktop renders it as a centered modal, native as a bottom sheet — that is +// Dialog's own platform behavior, so both entry points share this one path. +function useOpenMarketFiltersDialog({ + timeRange, + onTimeRangeChange, +}: { + timeRange: IMarketTimeRangeValue; + onTimeRangeChange: (v: IMarketTimeRangeValue) => void; +}) { + const intl = useIntl(); + const { filterState, applyConditions } = useMarketListFilter(); + return useCallback(() => { + const dialog = Dialog.show({ + title: intl.formatMessage({ id: ETranslations.market_filters }), + showFooter: false, + renderContent: ( + { + void dialog.close(); + }} + /> + ), + }); + }, [ + filterState.conditions, + timeRange, + applyConditions, + onTimeRangeChange, + intl, + ]); +} + +// Applied-condition count badge (Figma 24980-5127 / 25169-44218). +function FiltersCountBadge({ count }: { count: number }) { + if (count <= 0) { + return null; + } + return ( + + + {count} + + + ); +} + +// Mobile toolbar entry (Figma 25169-43731 / 25169-44211): icon only, with the +// count badge beside it once conditions are applied. The 18px icon is a small +// touch target, so hitSlop widens it to the design's -4/-8 inset. +export function MarketFiltersIconTrigger({ + timeRange, + onTimeRangeChange, +}: { + timeRange: IMarketTimeRangeValue; + onTimeRangeChange: (v: IMarketTimeRangeValue) => void; +}) { + const { activeConditionCount } = useMarketListFilter(); + const openDialog = useOpenMarketFiltersDialog({ + timeRange, + onTimeRangeChange, + }); + return ( + + + + + ); +} + +// Desktop toolbar entry pill. +export function MarketFiltersTrigger({ + timeRange, + onTimeRangeChange, +}: { + timeRange: IMarketTimeRangeValue; + onTimeRangeChange: (v: IMarketTimeRangeValue) => void; +}) { + const intl = useIntl(); + const { activeConditionCount } = useMarketListFilter(); + const openDialog = useOpenMarketFiltersDialog({ + timeRange, + onTimeRangeChange, + }); + + return ( + + + {intl.formatMessage({ id: ETranslations.market_filters })} + + + + + ); +} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketListFilterContext.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketListFilterContext.tsx new file mode 100644 index 000000000000..dcf75b248f18 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/MarketListFilterContext.tsx @@ -0,0 +1,94 @@ +import { + type PropsWithChildren, + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from 'react'; + +import { sameConditions } from './marketListFilterConfig'; + +import type { + IMarketListFilterConditions, + IMarketListFilterContextValue, + IMarketListFilterState, + IMarketListSortState, +} from './marketListFilterTypes'; + +const EMPTY_STATE: IMarketListFilterState = { conditions: {} }; +const EMPTY_SORT: IMarketListSortState = {}; + +const EMPTY_CONTEXT: IMarketListFilterContextValue = { + filterState: EMPTY_STATE, + sortState: EMPTY_SORT, + applyConditions: () => undefined, + setSortState: () => undefined, + activeConditionCount: 0, +}; + +const MarketListFilterContext = + createContext(EMPTY_CONTEXT); + +// The single source of truth for "how you want to see this table". Chip +// selection, the chip row's condition chips, the popover's tier selection and +// the Filters badge are all read off this one state — none of them keeps a +// copy, so they cannot drift apart. +export function MarketListFilterProvider({ children }: PropsWithChildren) { + const [filterState, setFilterState] = + useState(EMPTY_STATE); + const [sortState, setSortState] = useState(EMPTY_SORT); + + // Mirrors filterState so the callback below can compare against the current + // conditions without taking them as a dependency and churning its identity. + const conditionsRef = useRef(filterState.conditions); + conditionsRef.current = filterState.conditions; + + const applyConditions = useCallback( + ( + conditions: IMarketListFilterConditions, + options?: { sort?: IMarketListSortState }, + ) => { + // A caller applying both at once (a chip carrying its own sort) always + // wins, whether or not the conditions moved. + if (options?.sort) { + setFilterState({ conditions }); + setSortState(options.sort); + return; + } + // Every entry point funnels through here — the chip row, the tier + // popover, clear-all and the Filters modal — and several of them re-apply + // the value that is already selected. Resetting the sort belongs to an + // actual change of slice: the ordering is only invalidated when the rows + // being ordered change. A no-op re-apply must leave the sort alone. + if (sameConditions(conditionsRef.current, conditions)) { + return; + } + setFilterState({ conditions }); + setSortState(EMPTY_SORT); + }, + [], + ); + + const value = useMemo( + () => ({ + filterState, + sortState, + applyConditions, + setSortState, + activeConditionCount: Object.keys(filterState.conditions).length, + }), + [filterState, sortState, applyConditions], + ); + + return ( + + {children} + + ); +} + +export function useMarketListFilter() { + return useContext(MarketListFilterContext); +} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/TierPill.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/TierPill.tsx new file mode 100644 index 000000000000..98674e233d06 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/TierPill.tsx @@ -0,0 +1,75 @@ +import { SizableText, XStack } from '@onekeyhq/components'; + +// Figma (25060-6052) tier pill. +// - `filled` (default): bg-strong capsule, px 11 / py 5 / bodyMd-medium; the +// selected state swaps to bg-active with an active border ring. +// - `plain`: transparent until selected (time-frame segment style), where the +// selected option gets the bg-strong fill instead. +export function TierPill({ + label, + selected, + disabled, + grow, + width, + minWidth = 72, + variant = 'filled', + onPress, + testID, +}: { + label: string; + selected?: boolean; + disabled?: boolean; + // Stretch to share the row width evenly (tier grid layout). + grow?: boolean; + // Fixed width (time-frame segment). + width?: number; + minWidth?: number; + variant?: 'filled' | 'plain'; + onPress?: () => void; + testID?: string; +}) { + const isPlain = variant === 'plain'; + let backgroundColor = '$bgStrong'; + if (isPlain) { + backgroundColor = selected ? '$bgStrong' : '$transparent'; + } else if (selected) { + backgroundColor = '$bgActive'; + } + let textColor = '$text'; + if (disabled) { + textColor = '$textDisabled'; + } else if (isPlain && !selected) { + textColor = '$textSubdued'; + } + + return ( + + + {label} + + + ); +} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/applyMarketListLocalFilter.test.ts b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/applyMarketListLocalFilter.test.ts new file mode 100644 index 000000000000..9a88def1c537 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/applyMarketListLocalFilter.test.ts @@ -0,0 +1,195 @@ +import { applyMarketListLocalFilter } from './applyMarketListLocalFilter'; +import { + MARKET_FILTER_CHIPS, + buildHotTokenFilterParams, + findActiveMarketFilterChip, + getMarketFilterOption, +} from './marketListFilterConfig'; +import { EMarketFilterDimension } from './marketListFilterTypes'; + +import type { IMarketToken } from '../MarketTokenList/MarketTokenData'; + +const now = 1_800_000_000_000; +const hours = (n: number) => n * 60 * 60 * 1000; +const tokens = [ + { + id: 'young-small', + firstTradeTime: now - hours(10), + marketCap: 500_000, + liquidity: 100_000, + }, + { + id: 'old-large', + firstTradeTime: now - hours(1000), + marketCap: 200_000_000, + liquidity: 900_000, + }, + { id: 'no-age', firstTradeTime: undefined, marketCap: 1000 }, +] as IMarketToken[]; + +describe('applyMarketListLocalFilter', () => { + it('returns input array as-is when no conditions', () => { + expect(applyMarketListLocalFilter(tokens, {}, now)).toBe(tokens); + }); + + it('applies min-only threshold tiers', () => { + expect( + applyMarketListLocalFilter( + tokens, + { [EMarketFilterDimension.Liquidity]: 'min-500000' }, + now, + ).map((t) => t.id), + ).toEqual(['old-large']); + }); + + it('applies market cap floors', () => { + expect( + applyMarketListLocalFilter( + tokens, + { [EMarketFilterDimension.MarketCap]: 'min-100000000' }, + now, + ).map((t) => t.id), + ).toEqual(['old-large']); + }); + + it('applies age ceilings and drops rows missing the field', () => { + expect( + applyMarketListLocalFilter( + tokens, + { [EMarketFilterDimension.TokenAge]: 'under-48h' }, + now, + ).map((t) => t.id), + ).toEqual(['young-small']); + }); + + it('ANDs multiple conditions', () => { + expect( + applyMarketListLocalFilter( + tokens, + { + [EMarketFilterDimension.MarketCap]: 'min-10000000', + [EMarketFilterDimension.Liquidity]: 'min-500000', + }, + now, + ).map((t) => t.id), + ).toEqual(['old-large']); + }); + + it('ignores unknown option ids', () => { + expect( + applyMarketListLocalFilter( + tokens, + { [EMarketFilterDimension.MarketCap]: 'nope' }, + now, + ).map((t) => t.id), + ).toEqual(['young-small', 'old-large', 'no-age']); + }); +}); + +describe('buildHotTokenFilterParams', () => { + it('expands buckets into min/max hot-token params', () => { + expect( + buildHotTokenFilterParams({ + [EMarketFilterDimension.MarketCap]: 'min-500000', + [EMarketFilterDimension.Liquidity]: 'min-10000', + }), + ).toEqual({ + marketCapMin: 500_000, + liquidityMin: 10_000, + }); + }); + + it('emits nothing for dimensions without server params', () => { + expect( + buildHotTokenFilterParams({ + [EMarketFilterDimension.TokenAge]: 'under-48h', + }), + ).toEqual({}); + }); +}); + +describe('MARKET_FILTER_CHIPS', () => { + // "chip 值 ⊆ 檔位集": a chip threshold with no matching tier could not be + // shown as selected in the popover, which is where chip<->popover sync + // would silently break. + it('only uses conditions the popover tier table can express', () => { + MARKET_FILTER_CHIPS.forEach((chip) => { + Object.entries(chip.conditions).forEach(([dimensionId, optionId]) => { + expect( + getMarketFilterOption( + dimensionId as EMarketFilterDimension, + optionId, + ), + ).toBeDefined(); + }); + }); + }); +}); + +describe('findActiveMarketFilterChip', () => { + const largeCap = MARKET_FILTER_CHIPS.find((c) => c.id === 'largeCap'); + const topTurnover = MARKET_FILTER_CHIPS.find((c) => c.id === 'topTurnover'); + + it('lights a chip whose exact condition set is applied', () => { + expect(findActiveMarketFilterChip(largeCap?.conditions ?? {}, {})?.id).toBe( + 'largeCap', + ); + }); + + it('lights a chip when the same set is assembled by hand', () => { + expect( + findActiveMarketFilterChip( + { + [EMarketFilterDimension.Turnover]: 'min-100000', + [EMarketFilterDimension.Liquidity]: 'min-50000', + [EMarketFilterDimension.MarketCap]: 'min-1000000', + }, + {}, + )?.id, + ).toBe('largeCap'); + }); + + it('dims the chip once any tier deviates', () => { + expect( + findActiveMarketFilterChip( + { + ...largeCap?.conditions, + [EMarketFilterDimension.MarketCap]: 'min-10000000', + }, + {}, + ), + ).toBeUndefined(); + }); + + it('dims a superset of the chip conditions', () => { + expect( + findActiveMarketFilterChip( + { + ...largeCap?.conditions, + [EMarketFilterDimension.Txns]: 'min-100', + }, + {}, + ), + ).toBeUndefined(); + }); + + // The sort-bearing chip needs both halves to match, so sorting another + // column from the table header dims it with no extra bookkeeping. + it('requires the sort to match for a sort-bearing chip', () => { + expect( + findActiveMarketFilterChip(topTurnover?.conditions ?? {}, { + sortBy: 'turnover', + sortType: 'desc', + })?.id, + ).toBe('topTurnover'); + expect( + findActiveMarketFilterChip(topTurnover?.conditions ?? {}, { + sortBy: 'marketCap', + sortType: 'desc', + }), + ).toBeUndefined(); + expect( + findActiveMarketFilterChip(topTurnover?.conditions ?? {}, {}), + ).toBeUndefined(); + }); +}); diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/applyMarketListLocalFilter.ts b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/applyMarketListLocalFilter.ts new file mode 100644 index 000000000000..f2703fcbdf24 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/applyMarketListLocalFilter.ts @@ -0,0 +1,49 @@ +import { + MARKET_FILTER_DIMENSION_MAP, + getMarketFilterOption, +} from './marketListFilterConfig'; + +import type { + EMarketFilterDimension, + IMarketListFilterConditions, +} from './marketListFilterTypes'; +import type { IMarketToken } from '../MarketTokenList/MarketTokenData'; + +export function applyMarketListLocalFilter( + tokens: IMarketToken[], + conditions: IMarketListFilterConditions, + nowMs: number = Date.now(), +): IMarketToken[] { + const entries = Object.entries(conditions).filter(([dimensionId]) => { + const dimension = MARKET_FILTER_DIMENSION_MAP.get( + dimensionId as EMarketFilterDimension, + ); + return dimension?.localField !== undefined; + }) as [EMarketFilterDimension, string][]; + if (entries.length === 0) { + return tokens; + } + return tokens.filter((token) => + entries.every(([dimensionId, optionId]) => { + const dimension = MARKET_FILTER_DIMENSION_MAP.get(dimensionId); + const option = getMarketFilterOption(dimensionId, optionId); + if (!dimension?.localField || !option) { + return true; + } + const raw = token[dimension.localField] as number | undefined; + if (raw === undefined || raw === null || Number.isNaN(raw)) { + return false; + } + // Age dimensions store a ms epoch timestamp (same clock as Date.now()); + // convert to an age before comparing against the option range. + const value = dimension.isAge ? nowMs - raw : raw; + if (option.min !== undefined && value < option.min) { + return false; + } + if (option.max !== undefined && value > option.max) { + return false; + } + return true; + }), + ); +} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/index.ts b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/index.ts new file mode 100644 index 000000000000..a204c08b7761 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/index.ts @@ -0,0 +1,30 @@ +export { MarketFilterChipsBar } from './MarketFilterChipsBar'; +export { + MarketFiltersIconTrigger, + MarketFiltersTrigger, +} from './MarketFiltersModal'; +export { + MarketListFilterProvider, + useMarketListFilter, +} from './MarketListFilterContext'; +export { + MARKET_FILTER_CHIPS, + MARKET_FILTER_DIMENSIONS, + MARKET_FILTER_DIMENSION_MAP, + buildHotTokenFilterParams, + findActiveMarketFilterChip, + getMarketFilterOption, + pickLocalOnlyConditions, + sameConditions, +} from './marketListFilterConfig'; +export { EMarketFilterDimension } from './marketListFilterTypes'; +export type { + IMarketFilterChip, + IMarketFilterDimensionConfig, + IMarketFilterOption, + IMarketListFilterConditions, + IMarketListFilterContextValue, + IMarketListFilterState, + IMarketListSortState, +} from './marketListFilterTypes'; +export { applyMarketListLocalFilter } from './applyMarketListLocalFilter'; diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterConfig.ts b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterConfig.ts new file mode 100644 index 000000000000..34b1c07bf476 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterConfig.ts @@ -0,0 +1,376 @@ +import { ETranslations } from '@onekeyhq/shared/src/locale'; + +import { + EMarketFilterDimension, + EMarketFilterGroup, +} from './marketListFilterTypes'; + +import type { + IMarketFilterChip, + IMarketFilterDimensionConfig, + IMarketFilterOption, + IMarketListFilterConditions, + IMarketListSortState, +} from './marketListFilterTypes'; +import type { IntlShape } from 'react-intl'; + +const H = 60 * 60 * 1000; +const D = 24 * H; + +function formatUsd(value: number) { + if (value >= 1_000_000) { + return `${value / 1_000_000}M`; + } + return `${value / 1000}K`; +} + +function formatCount(value: number) { + return value >= 1000 ? `${value / 1000}K` : `${value}`; +} + +// Floor tiers ("at least X"). The popover row title carries the unit, so the +// pill stays short; the toolbar chip has no row title and repeats it. +function usdFloors(values: number[]): IMarketFilterOption[] { + return values.map((value) => ({ + id: `min-${value}`, + label: `${formatUsd(value)}+`, + chipLabel: `$${formatUsd(value)}+`, + min: value, + })); +} + +function countFloors(values: number[]): IMarketFilterOption[] { + return values.map((value) => ({ + id: `min-${value}`, + label: `${formatCount(value)}+`, + chipLabel: `${formatCount(value)}+`, + min: value, + })); +} + +// Tier values are the frozen P2-9 table (2026-07-21), calibrated against a +// single-day rankBy=15 pool snapshot: each tier had to move the view enough to +// be worth a tap, so tiers whose in-pool retention was ~90+/100 (barely a +// change) or near 0 (a dead button) were cut. Values stay centralized here +// because the PRD expects them to become server-configurable and to be +// calibrated again once P1-11 lands. Param names track hot-token v6. +// +// Row order borrows the OKX screener's split between stock metrics and the +// ones the time frame rewrites, inverted for our trust-first positioning: +// the "is this substantial?" levers (market cap / liquidity / holders) lead, +// then the time-frame-driven metrics (turnover / change / txns), then token +// age, which serves the secondary new-launch hunt. +export const MARKET_FILTER_DIMENSIONS: IMarketFilterDimensionConfig[] = [ + { + id: EMarketFilterDimension.MarketCap, + group: EMarketFilterGroup.Metrics, + labelKey: ETranslations.dexmarket_market_cap, + unit: '$', + minParam: 'marketCapMin', + maxParam: 'marketCapMax', + localField: 'marketCap', + // 88/82/55/~30 retention; 100K cut as redundant (93 = view barely moves). + options: usdFloors([500_000, 1_000_000, 10_000_000, 100_000_000]), + }, + { + id: EMarketFilterDimension.Liquidity, + labelKey: ETranslations.dexmarket_liquidity, + group: EMarketFilterGroup.Metrics, + unit: '$', + minParam: 'liquidityMin', + maxParam: 'liquidityMax', + localField: 'liquidity', + // Trimmed to three so the row fits one line (matching Holders/Change/Txns). + // 10K is the Mid-cap chip anchor, 50K the Large-cap anchor; 5M+ (~22 + // retention, rarely tapped) dropped as the least-used end of the ladder. + options: usdFloors([10_000, 50_000, 500_000]), + }, + { + id: EMarketFilterDimension.Holders, + labelKey: ETranslations.dexmarket_holders, + group: EMarketFilterGroup.Metrics, + minParam: 'holdersMin', + maxParam: 'holdersMax', + localField: 'holders', + // 91/71/~15; the 1K floor doubles as the fake-market-cap defense. + options: countFloors([1000, 10_000, 100_000]), + }, + { + id: EMarketFilterDimension.Turnover, + labelKey: ETranslations.perp_token_selector_volume, + group: EMarketFilterGroup.Metrics, + unit: '$', + minParam: 'volumeMin', + maxParam: 'volumeMax', + localField: 'turnover', + // Trimmed to three (one-line row). 50K is the Mid-cap anchor, 100K the + // Large-cap anchor; 1M+ (~4 retention, extreme) dropped. Window-sensitive: + // the remaining tiers stop discriminating at 24h (PRD open question 8). + options: usdFloors([10_000, 50_000, 100_000]), + }, + { + id: EMarketFilterDimension.Change, + labelKey: ETranslations.dexmarket_token_change, + group: EMarketFilterGroup.Metrics, + minParam: 'priceChangePercentMin', + maxParam: 'priceChangePercentMax', + localField: 'change24h', + // Kept deliberately: self-serve filtering is the user's call, unlike the + // curated chips. Expect in-pool meme spikes; the copy stays honest. + options: [10, 50, 100].map((value) => ({ + id: `min-${value}`, + label: `${value}%+`, + chipLabel: `${value}%+`, + min: value, + })), + }, + { + id: EMarketFilterDimension.Txns, + labelKey: ETranslations.dexmarket_txns, + group: EMarketFilterGroup.Metrics, + minParam: 'txsMin', + maxParam: 'txsMax', + localField: 'transactions', + // 72/~35/~10; 10K cut on quality, not reach — it returned 14 rows topped + // by a wash-trading token's transaction count. + options: countFloors([100, 500, 2000]), + }, + { + id: EMarketFilterDimension.TokenAge, + labelKey: ETranslations.dexmarket_token_age, + group: EMarketFilterGroup.Metrics, + // No server-side age param exists, so this row filters the slice already + // fetched instead of the upstream pool — the only row that does, hence the + // separate marking in the popover. + localField: 'firstTradeTime', + isAge: true, + isLocalOnly: true, + // Finer tiers (≤5m…≤24h) are dead in every pool: tokens take ≥24h to + // enter the hot list at all. + options: [ + // `label`/`chipLabel` are the untranslated fallback; `age` is what the + // renderer actually formats, through the existing token-age keys. + { + id: 'under-48h', + label: '≤ 48h', + chipLabel: '≤ 48h', + age: { amount: 48, unit: 'h' }, + max: 2 * D, + }, + { + id: 'under-7d', + label: '≤ 7d', + chipLabel: '≤ 7d', + age: { amount: 7, unit: 'd' }, + max: 7 * D, + }, + { + id: 'under-30d', + label: '≤ 30d', + chipLabel: '≤ 30d', + age: { amount: 30, unit: 'd' }, + max: 30 * D, + }, + ], + }, + // Traders (uniqueTraderMin/Max) and Net inflow (inflowUsdMin/Max) are + // intentionally absent: the redesigned table no longer shows a Traders + // column and inflow has no list column or local data source — filters + // must not reference metrics the user cannot see in the table. +]; + +export const MARKET_FILTER_GROUP_LABELS: Record< + EMarketFilterGroup, + ETranslations +> = { + [EMarketFilterGroup.Metrics]: ETranslations.market_filters_group_metrics, + // Names the metric these rows measure, so the rows themselves can stay + // short enough to fit the label column ("Top 10 %", "Dev %", ...). + // Hidden this wave, so it has no key yet — reuses the single-word Audit + // string until the group is restored. + [EMarketFilterGroup.Audit]: ETranslations.dexmarket_audit, +}; + +const TOKEN_AGE_UNIT_KEYS = { + h: ETranslations.dexmarket_token_age_h, + d: ETranslations.dexmarket_token_age_d, +} as const; + +// Token age tiers read as "≤ 48H"; every other tier is a numeric format that +// is identical in all locales and needs no translation. +export function formatMarketFilterOptionLabel( + intl: IntlShape, + option: IMarketFilterOption, + variant: 'label' | 'chipLabel' = 'label', +): string { + if (!option.age) { + return option[variant]; + } + // No space after the operator: the tier pill is only wide enough for three + // per row, and CJK units ("48小時") need every pixel. + return `≤${intl.formatMessage( + { id: TOKEN_AGE_UNIT_KEYS[option.age.unit] }, + { amount: option.age.amount }, + )}`; +} + +// Holdings audit is out of scope for this delivery: its rows are placeholders +// pending Spike A#8 and `top10HoldPercentMax` is a confirmed dead param +// upstream. Add EMarketFilterGroup.Audit back here to restore the section — +// its labels, rows and rendering are all still in place. +export const MARKET_FILTER_GROUP_ORDER: EMarketFilterGroup[] = [ + EMarketFilterGroup.Metrics, +]; + +export const MARKET_FILTER_DIMENSION_MAP = new Map( + MARKET_FILTER_DIMENSIONS.map((dimension) => [dimension.id, dimension]), +); + +export function getMarketFilterOption( + dimensionId: EMarketFilterDimension, + optionId: string | undefined, +): IMarketFilterOption | undefined { + if (!optionId) { + return undefined; + } + return MARKET_FILTER_DIMENSION_MAP.get(dimensionId)?.options.find( + (option) => option.id === optionId, + ); +} + +// Expands selected options into the flat hot-token v6 param object the +// future server passthrough will send. Kept here so the passthrough swap +// stays mechanical even while the demo filters locally. +export function buildHotTokenFilterParams( + conditions: IMarketListFilterConditions, +): Record { + const params: Record = {}; + Object.entries(conditions).forEach(([dimensionId, optionId]) => { + const dimension = MARKET_FILTER_DIMENSION_MAP.get( + dimensionId as EMarketFilterDimension, + ); + const option = getMarketFilterOption( + dimensionId as EMarketFilterDimension, + optionId, + ); + if (!dimension || !option) { + return; + } + if (dimension.minParam && option.min !== undefined) { + params[dimension.minParam] = option.min; + } + if (dimension.maxParam && option.max !== undefined) { + params[dimension.maxParam] = option.max; + } + }); + return params; +} + +// Splits the applied conditions into the half the server can filter and the +// half it cannot. Token age has no upstream param, so it stays a client-side +// pass over the rows already fetched; everything else goes through +// buildHotTokenFilterParams and is filtered before the pool is sliced. +export function pickLocalOnlyConditions( + conditions: IMarketListFilterConditions, +): IMarketListFilterConditions { + const localOnly: IMarketListFilterConditions = {}; + Object.entries(conditions).forEach(([dimensionId, optionId]) => { + const dimension = MARKET_FILTER_DIMENSION_MAP.get( + dimensionId as EMarketFilterDimension, + ); + if (dimension?.isLocalOnly) { + localOnly[dimensionId as EMarketFilterDimension] = optionId; + } + }); + return localOnly; +} + +// P2-9 chip roster (frozen 2026-07-21). Three chips; Early tokens moved to +// P2-12 because a view switch was judged too heavy for the first wave. +// Every chip anchors the time frame to 1h — the popover stays unanchored so +// self-serve filtering keeps free choice. +export const MARKET_FILTER_CHIPS: IMarketFilterChip[] = [ + { + // Variant B: a quality floor passed upstream PLUS an in-pool turnover + // sort. The sort half runs through the P1-10 header state machine, so the + // chip and the column arrow are one state, and the expanded form shows + // both halves rather than hiding the floor. + id: 'topTurnover', + labelKey: ETranslations.market_filter_chip_top_turnover, + icon: 'ChartColumnarOutline', + conditions: { + [EMarketFilterDimension.Holders]: 'min-1000', + }, + sort: { sortBy: 'turnover', sortType: 'desc' }, + tooltipKey: ETranslations.market_filter_chip_top_turnover_tips, + }, + { + id: 'midCap', + labelKey: ETranslations.market_filter_chip_mid_cap, + icon: 'WorldOutline', + conditions: { + [EMarketFilterDimension.MarketCap]: 'min-500000', + [EMarketFilterDimension.Liquidity]: 'min-10000', + [EMarketFilterDimension.Turnover]: 'min-50000', + }, + tooltipKey: ETranslations.market_filter_chip_conditions_tips, + }, + { + // Holders floor dropped per PM 2026-07-21 (was the PRD's optional + // quality-floor note, open question 5). Large-cap now filters on the three + // size/liquidity/turnover floors only. + id: 'largeCap', + labelKey: ETranslations.market_filter_chip_large_cap, + icon: 'GalaxyOutline', + conditions: { + [EMarketFilterDimension.MarketCap]: 'min-1000000', + [EMarketFilterDimension.Liquidity]: 'min-50000', + [EMarketFilterDimension.Turnover]: 'min-100000', + }, + tooltipKey: ETranslations.market_filter_chip_conditions_tips, + }, +]; + +// Also the "did the user actually change anything" test for the Filters modal: +// applying conditions resets the sort, so an unchanged Confirm must not apply. +export function sameConditions( + a: IMarketListFilterConditions, + b: IMarketListFilterConditions, +) { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) { + return false; + } + return aKeys.every( + (key) => + a[key as EMarketFilterDimension] === b[key as EMarketFilterDimension], + ); +} + +// Chip selection is DERIVED, never stored: a chip is lit exactly when the +// live condition set (plus sort, for the sort-bearing chip) equals what that +// chip applies. So assembling the same combination by hand in the popover +// lights the chip, and nudging any tier dissolves the preset back into plain +// conditions — one truth, four presentations. +// A chip is its conditions plus its sort. The time frame is deliberately not +// part of the identity: chips carry no window of their own (2026-08-17), so +// changing the toolbar window re-runs the same preset over a different window +// and must leave the chip lit. +export function findActiveMarketFilterChip( + conditions: IMarketListFilterConditions, + sortState: IMarketListSortState, +): IMarketFilterChip | undefined { + return MARKET_FILTER_CHIPS.find((chip) => { + if (!sameConditions(chip.conditions, conditions)) { + return false; + } + if (!chip.sort) { + return true; + } + return ( + chip.sort.sortBy === sortState.sortBy && + chip.sort.sortType === sortState.sortType + ); + }); +} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterPassthrough.test.ts b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterPassthrough.test.ts new file mode 100644 index 000000000000..28562fbe29d4 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterPassthrough.test.ts @@ -0,0 +1,141 @@ +import { + MARKET_FILTER_CHIPS, + MARKET_FILTER_DIMENSIONS, + buildHotTokenFilterParams, + findActiveMarketFilterChip, + pickLocalOnlyConditions, + sameConditions, +} from './marketListFilterConfig'; +import { EMarketFilterDimension } from './marketListFilterTypes'; + +import type { IMarketListFilterConditions } from './marketListFilterTypes'; + +// Selects the first option of every dimension, so the assertions below cover +// the whole roster rather than a hand-picked subset. +const allConditions: IMarketListFilterConditions = Object.fromEntries( + MARKET_FILTER_DIMENSIONS.map((dimension) => [ + dimension.id, + dimension.options[0].id, + ]), +); + +describe('market filter server passthrough split', () => { + it('marks token age as the only client-side dimension', () => { + expect( + MARKET_FILTER_DIMENSIONS.filter((dimension) => dimension.isLocalOnly).map( + (dimension) => dimension.id, + ), + ).toEqual([EMarketFilterDimension.TokenAge]); + }); + + it('gives every server-side dimension a param, so none can filter nothing', () => { + MARKET_FILTER_DIMENSIONS.filter( + (dimension) => !dimension.isLocalOnly, + ).forEach((dimension) => { + const params = buildHotTokenFilterParams({ + [dimension.id]: dimension.options[0].id, + }); + expect(Object.keys(params)).not.toHaveLength(0); + }); + }); + + it('never sends a param for a client-side dimension', () => { + const localOnly = MARKET_FILTER_DIMENSIONS.filter( + (dimension) => dimension.isLocalOnly, + ); + localOnly.forEach((dimension) => { + expect(dimension.minParam).toBeUndefined(); + expect(dimension.maxParam).toBeUndefined(); + expect( + buildHotTokenFilterParams({ + [dimension.id]: dimension.options[0].id, + }), + ).toEqual({}); + }); + }); + + it('keeps only token age for the local pass when everything is selected', () => { + expect(Object.keys(pickLocalOnlyConditions(allConditions))).toEqual([ + EMarketFilterDimension.TokenAge, + ]); + }); + + it('routes each selected dimension to exactly one of the two passes', () => { + const serverParamCount = Object.keys( + buildHotTokenFilterParams(allConditions), + ).length; + const localCount = Object.keys( + pickLocalOnlyConditions(allConditions), + ).length; + expect(serverParamCount + localCount).toBe(MARKET_FILTER_DIMENSIONS.length); + }); +}); + +// The Filters modal calls onApply only when this reports a change, because +// applying conditions resets the sort. A false negative here silently drops +// the user's sort on an unchanged Confirm. +describe('sameConditions', () => { + const base: IMarketListFilterConditions = { + [EMarketFilterDimension.Holders]: 'min-1000', + }; + + it('treats an untouched draft as unchanged', () => { + expect(sameConditions({ ...base }, base)).toBe(true); + expect(sameConditions({}, {})).toBe(true); + }); + + it('detects an added, removed or retargeted dimension', () => { + expect(sameConditions({}, base)).toBe(false); + expect(sameConditions(base, {})).toBe(false); + expect( + sameConditions({ [EMarketFilterDimension.Holders]: 'min-10000' }, base), + ).toBe(false); + }); + + it('ignores key order', () => { + const a: IMarketListFilterConditions = { + [EMarketFilterDimension.Holders]: 'min-1000', + [EMarketFilterDimension.MarketCap]: 'min-500k', + }; + const b: IMarketListFilterConditions = { + [EMarketFilterDimension.MarketCap]: 'min-500k', + [EMarketFilterDimension.Holders]: 'min-1000', + }; + expect(sameConditions(a, b)).toBe(true); + }); +}); + +describe('findActiveMarketFilterChip window independence', () => { + // 2026-08-17: chips carry no window of their own. Market cap and liquidity + // do not move with the window, and the volume floor only shifts how strict + // one of three conditions is, so a preset stays itself across windows. + it('no chip declares a time frame', () => { + expect( + MARKET_FILTER_CHIPS.filter( + (chip) => (chip as { timeRange?: string }).timeRange !== undefined, + ), + ).toEqual([]); + }); + + // Guards the behavior this replaced: matching once took the window into + // account, so switching the toolbar window silently unlit the chip. + it('matches on conditions and sort alone', () => { + for (const chip of MARKET_FILTER_CHIPS) { + expect(findActiveMarketFilterChip(chip.conditions, chip.sort ?? {})).toBe( + chip, + ); + } + }); + + it('still refuses a chip whose sort no longer matches', () => { + const sorting = MARKET_FILTER_CHIPS.find((chip) => chip.sort); + expect(sorting).toBeDefined(); + if (!sorting) return; + expect( + findActiveMarketFilterChip(sorting.conditions, { + sortBy: 'marketCap', + sortType: 'asc', + }), + ).toBeUndefined(); + }); +}); diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterTypes.ts b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterTypes.ts new file mode 100644 index 000000000000..9cbc66a1a487 --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketFilterChipsBar/marketListFilterTypes.ts @@ -0,0 +1,110 @@ +import type { Dispatch, SetStateAction } from 'react'; + +import type { IKeyOfIcons } from '@onekeyhq/components'; +import type { ETranslations } from '@onekeyhq/shared/src/locale'; + +import type { IMarketToken } from '../MarketTokenList/MarketTokenData'; + +// Filter dimensions shown to the user. Each dimension maps to a pair of +// hot-token v6 params (min/max) so bucket options can pass ranges upstream. +export enum EMarketFilterDimension { + TokenAge = 'tokenAge', + MarketCap = 'marketCap', + Liquidity = 'liquidity', + Turnover = 'turnover', + Holders = 'holders', + Change = 'change', + Txns = 'txns', +} + +// A selectable tier. Threshold dimensions use only `min` (floor semantics, +// label like "$5K+"); token age uses only `max`, read as an age ceiling. +export type IMarketFilterOption = { + id: string; + // Copy shown inside the tier pill (e.g. "500K+"). Numeric tiers are the same + // in every locale, so they stay literal; token age carries `age` instead. + label: string; + // Copy for the condition chip in the toolbar, which carries no row title + // and so has to spell out the unit (e.g. "$500K+"). + chipLabel: string; + // Token age tiers read as "≤ 48H" and have to go through the existing + // dexmarket_token_age_* keys, so the amount and unit are kept apart from the + // copy and composed at render time. + age?: { amount: number; unit: 'h' | 'd' }; + min?: number; + max?: number; +}; + +// Section groups in the Filters modal: named groups on one scrolling page +// instead of tabs. +export enum EMarketFilterGroup { + Metrics = 'metrics', + Audit = 'audit', +} + +export type IMarketFilterDimensionConfig = { + id: EMarketFilterDimension; + labelKey: ETranslations; + // Optional unit shown after the label in the Filters popover row title. + unit?: string; + // hot-token v6 param names backing the future server passthrough. + minParam?: string; + maxParam?: string; + // IMarketToken field backing the local demo filter; undefined = the + // dimension is selectable but has no local data source and is skipped by + // applyMarketListLocalFilter. + localField?: keyof IMarketToken; + // firstTradeTime is a timestamp; compare (now - value) against min/max. + isAge?: boolean; + // Filters the slice already in hand rather than the upstream pool. Token age + // is the only such row (no server-side age param exists), and the PRD + // requires the popover to mark it as behaving differently. + isLocalOnly?: boolean; + group: EMarketFilterGroup; + // Extra note rendered next to the row (demo caveats). + note?: string; + options: IMarketFilterOption[]; +}; + +// Selected tier option id per dimension. Chip conditions must be expressible +// here too ("chip 值 ⊆ 檔位集"): a chip whose threshold has no tier could not +// stay in sync with the popover, so every chip value exists in the tier table. +export type IMarketListFilterConditions = Partial< + Record +>; + +export type IMarketListFilterState = { + conditions: IMarketListFilterConditions; +}; + +export type IMarketListSortState = { + sortBy?: string; + sortType?: 'asc' | 'desc'; +}; + +export type IMarketFilterChip = { + id: string; + labelKey: ETranslations; + // Leading icon; also reused as the group anchor icon once expanded. + icon: IKeyOfIcons; + // Honest disclosure of what the chip does to the list (P1-2 wording). + tooltipKey: ETranslations; + // Conditions the chip applies. Every value is a tier option id. + conditions: IMarketListFilterConditions; + // Sort the chip dispatches, if any. Same state machine as the column header + // (P1-10), so the header arrow and the chip can never disagree. + sort?: IMarketListSortState; +}; + +export type IMarketListFilterContextValue = { + filterState: IMarketListFilterState; + sortState: IMarketListSortState; + // Applies conditions. Sort resets unless `sort` is given, because switching + // the filtered slice invalidates the previous ordering (P2-9 × P1-10). + applyConditions: ( + conditions: IMarketListFilterConditions, + options?: { sort?: IMarketListSortState }, + ) => void; + setSortState: Dispatch>; + activeConditionCount: number; +}; diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketListColumnHeader.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketListColumnHeader.tsx index 2b0162b9ab46..786d8771ad04 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketListColumnHeader.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketListColumnHeader.tsx @@ -5,10 +5,15 @@ import { useIntl } from 'react-intl'; import { Icon, SizableText, XStack } from '@onekeyhq/components'; import { ETranslations } from '@onekeyhq/shared/src/locale'; +import type { IMarketTimeRangeValue } from '../types'; + type IMarketListColumnHeaderProps = { changeSortType?: 'asc' | 'desc'; changeSortTestID?: string; change24hColumnTitle?: string; + // Window the second line's number covers. Spot crypto follows the toolbar's + // time range; stocks and perps always report 24h, so they leave it unset. + volumeTimeRange?: IMarketTimeRangeValue; onChangeSortPress?: () => void; }; @@ -16,6 +21,7 @@ function MarketListColumnHeaderBase({ changeSortType, changeSortTestID, change24hColumnTitle, + volumeTimeRange, onChangeSortPress, }: IMarketListColumnHeaderProps) { const intl = useIntl(); @@ -35,8 +41,8 @@ function MarketListColumnHeaderBase({ {`${intl.formatMessage({ id: ETranslations.global_name, - })} / ${intl.formatMessage({ - id: ETranslations.dexmarket_turnover, + })} / ${volumeTimeRange ?? '24h'} ${intl.formatMessage({ + id: ETranslations.perp_token_selector_volume, })}`} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MarketPerpsCategorySelectorDesktop.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MarketPerpsCategorySelectorDesktop.tsx index 2eb6fd75ed5e..0cb793b3432c 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MarketPerpsCategorySelectorDesktop.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MarketPerpsCategorySelectorDesktop.tsx @@ -2,6 +2,12 @@ import { GradientMask, ScrollView, XStack } from '@onekeyhq/components'; import { useNetworkFilterScroll } from '../../hooks/useNetworkFilterScroll'; import { CategoryFilterItem } from '../CategoryFilterItem'; +import { + MARKET_TOOLBAR_GAP_BOTTOM, + MARKET_TOOLBAR_GAP_TOP, + MARKET_TOOLBAR_ITEM_HEIGHT, + marketToolbarFrameProps, +} from '../marketToolbarFrame'; import type { IMarketPerpsCategorySelectorProps } from './MarketPerpsCategorySelector.types'; @@ -29,13 +35,9 @@ export function MarketPerpsCategorySelectorDesktop({ position="relative" p="$1" gap="$1" - mt="$3" - mb="$2" - maxWidth="100%" - overflow="hidden" - borderWidth={1} - borderColor="$neutral4" - borderRadius="$3" + mt={MARKET_TOOLBAR_GAP_TOP} + mb={MARKET_TOOLBAR_GAP_BOTTOM} + {...marketToolbarFrameProps} > {categories.map((category) => ( ) => + handleHeaderRowRef.current(...args), + [], + ); + const CategorySelector = useMemo( () => ( - + {/* Same bottom padding as the spot lists, so the first row clears the + pinned header by the same amount on every tab. */} + {CategorySelector} - + ); @@ -121,6 +143,8 @@ function MarketPerpsTokenListImpl({ stickyPortalTarget, CategorySelector, perpsColumns, + stableHandleHeaderRow, + controlledSort, ]); let integratedContentPaddingBottom = tabBarHeight; @@ -133,7 +157,9 @@ function MarketPerpsTokenListImpl({ const tableContentContainerStyle = tabIntegrated ? { - paddingTop: 8 + (platformEnv.isNative ? 150 : 0), + // 4px like the spot lists (MarketTokenListBase); together with the + // header's pb this keeps the first row at the same y on every tab. + paddingTop: 4 + (platformEnv.isNative ? 150 : 0), paddingBottom: integratedContentPaddingBottom, } : { @@ -158,7 +184,7 @@ function MarketPerpsTokenListImpl({ ) : ( @@ -168,9 +194,14 @@ function MarketPerpsTokenListImpl({ tabIntegrated={tabIntegrated} scrollEnabled={!webTabIntegrated} columns={perpsColumns} - dataSource={tokens} + dataSource={sortedTokens} + onHeaderRow={stableHandleHeaderRow} + controlledSort={controlledSort} keyExtractor={(item) => item.name} - estimatedItemSize="$14" + // Same row box as the spot tables, so switching tabs does not + // shift every row below the first. + rowProps={{ minHeight: REDESIGN_ROW_HEIGHT }} + estimatedItemSize={REDESIGN_ROW_HEIGHT} extraData={hasRealTimeData} TableEmptyComponent={TableEmptyComponent} TableFooterComponent={TableFooterComponent} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MarketPerpsTokenListItem.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MarketPerpsTokenListItem.tsx index 55b7fa527d81..63f68d9b628a 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MarketPerpsTokenListItem.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MarketPerpsTokenListItem.tsx @@ -15,6 +15,7 @@ import { PerpDexBadge, SubtitleText, } from '../../../components/PerpsBadges'; +import { MARKET_MOBILE_ROW_HEIGHT } from '../../layouts/mobileLayoutUtils'; import { PriceChangeBadge } from '../PriceChangeBadge'; import type { IMarketPerpsToken } from './hooks/useMarketPerpsTokenList'; @@ -34,8 +35,10 @@ const BasicMarketPerpsTokenListItem: FC = ({ @@ -44,7 +47,7 @@ const BasicMarketPerpsTokenListItem: FC = ({ flexGrow={1} flexBasis={0} alignItems="center" - gap="$2" + gap="$3" minWidth={0} overflow="hidden" > diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MobileMarketPerpsFlatList.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MobileMarketPerpsFlatList.tsx index c4cdc8c4f26f..71e04a1d3395 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MobileMarketPerpsFlatList.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/MobileMarketPerpsFlatList.tsx @@ -103,7 +103,9 @@ function MobileMarketPerpsFlatListImpl({ ListEmptyComponent={ListEmptyComponent} ListFooterComponent={ListFooterComponent} contentContainerStyle={{ - ...(platformEnv.isNative ? {} : { paddingTop: 8 }), + // Same top inset as the spot lists (MobileMarketTokenFlatList), or the + // first row sits 4px lower here and the table jumps on tab switch. + ...(platformEnv.isNative ? {} : { paddingTop: 4 }), paddingBottom: platformEnv.isNativeAndroid ? listContainerProps.paddingBottom : tabBarHeight, diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/hooks/usePerpsClientSort.ts b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/hooks/usePerpsClientSort.ts new file mode 100644 index 000000000000..f734ac63742e --- /dev/null +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/hooks/usePerpsClientSort.ts @@ -0,0 +1,98 @@ +import { useCallback, useMemo, useState } from 'react'; + +import type { ITableColumn, ITableControlledSort } from '@onekeyhq/components'; +import { ETableSortType } from '@onekeyhq/components'; + +import type { IMarketPerpsToken } from './marketPerpsTokenUtils'; + +// dataIndex (table column) -> the perps field it renders. `name` is absent on +// purpose: the first column is not sortable outside trending. +const PERPS_CLIENT_SORT_FIELD_MAP: Record = { + price: 'markPrice', + change24h: 'change24hPercent', + volume24h: 'volume24h', + openInterest: 'openInterest', + fundingRate: 'fundingRate', +}; + +function isMissingSortValue(value: unknown): boolean { + return ( + value === undefined || value === null || !Number.isFinite(Number(value)) + ); +} + +// The perps list arrives as one full category pool, so sorting locally covers +// every row the tab can show. Values are numeric strings off the wire. +function sortPerpsTokens( + tokens: IMarketPerpsToken[], + field: keyof IMarketPerpsToken, + direction: 'asc' | 'desc', +): IMarketPerpsToken[] { + return tokens.toSorted((a, b) => { + const aVal = a[field]; + const bVal = b[field]; + const aMissing = isMissingSortValue(aVal); + const bMissing = isMissingSortValue(bVal); + if (aMissing || bMissing) { + if (aMissing && bMissing) return 0; + // Missing values always sink to the bottom in both directions. + return aMissing ? 1 : -1; + } + const result = Number(aVal) - Number(bVal); + return direction === 'asc' ? result : -result; + }); +} + +export function usePerpsClientSort({ + tokens, +}: { + tokens: IMarketPerpsToken[]; +}) { + const [sortBy, setSortBy] = useState(undefined); + const [sortType, setSortType] = useState<'asc' | 'desc' | undefined>( + undefined, + ); + + const sortedTokens = useMemo(() => { + if (!sortBy || !sortType) { + return tokens; + } + const field = PERPS_CLIENT_SORT_FIELD_MAP[sortBy]; + if (!field) { + return tokens; + } + return sortPerpsTokens(tokens, field, sortType); + }, [sortBy, sortType, tokens]); + + const onHeaderRow = useCallback( + (column: ITableColumn) => { + const columnKey = String(column.dataIndex); + if (!PERPS_CLIENT_SORT_FIELD_MAP[columnKey]) { + return undefined; + } + return { + onSortTypeChange: (order: 'asc' | 'desc' | undefined) => { + setSortBy(order ? columnKey : undefined); + setSortType(order); + }, + initialSortOrder: + sortBy === columnKey ? (sortType as ETableSortType) : undefined, + }; + }, + [sortBy, sortType], + ); + + // The desktop layout renders a portalled header alongside the table's own, + // so the order has to come from here rather than each header's local state. + const controlledSort = useMemo(() => { + if (!sortBy || !sortType) { + return null; + } + return { + dataIndex: sortBy, + order: sortType === 'asc' ? ETableSortType.ASC : ETableSortType.DESC, + }; + }, [sortBy, sortType]); + + return { sortedTokens, onHeaderRow, controlledSort }; +} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/hooks/usePerpsColumns.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/hooks/usePerpsColumns.tsx index 7fd60165f017..dc322636a771 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/hooks/usePerpsColumns.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketPerpsList/hooks/usePerpsColumns.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import type { ReactNode } from 'react'; import { useIntl } from 'react-intl'; @@ -11,7 +12,10 @@ import { YStack, useMedia, } from '@onekeyhq/components'; -import type { ITableColumn } from '@onekeyhq/components'; +import type { + ITableColumn, + ITableColumnSortContext, +} from '@onekeyhq/components'; import { Token } from '@onekeyhq/kit/src/components/Token'; import { MarketPerpsStarV2 } from '@onekeyhq/kit/src/views/Market/components/MarketStarV2'; import { @@ -21,6 +25,13 @@ import { } from '@onekeyhq/kit/src/views/Market/components/PerpsBadges'; import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { + REDESIGN_NAME_ICON_GAP, + REDESIGN_STAR_COLUMN_WIDTH, + REDESIGN_STAR_ICON_SIZE, + renderRedesignHeaderTitle, +} from '../../marketListRedesignVisuals'; + import { usePerpsColumnsMobile } from './usePerpsColumnsMobile'; import type { IMarketPerpsToken } from './useMarketPerpsTokenList'; @@ -40,10 +51,14 @@ export function usePerpsColumnsDesktop(): ITableColumn[] { ) as any, dataIndex: 'star', - columnWidth: 50, + columnWidth: REDESIGN_STAR_COLUMN_WIDTH, render: (_: unknown, record: IMarketPerpsToken) => ( - - + + ), renderSkeleton: () => ( @@ -57,9 +72,14 @@ export function usePerpsColumnsDesktop(): ITableColumn[] { dataIndex: 'name', columnWidth: gtXl ? 340 : 260, render: (_: unknown, record: IMarketPerpsToken) => ( - + [] { // Column 5: 24h Volume { - title: intl.formatMessage({ - id: ETranslations.dexmarket_turnover, - }), + title: `24h ${intl.formatMessage({ + id: ETranslations.perp_token_selector_volume, + })}`, dataIndex: 'volume24h', columnProps: { flex: 1 }, render: (_: unknown, record: IMarketPerpsToken) => ( @@ -235,7 +255,27 @@ export function usePerpsColumnsDesktop(): ITableColumn[] { renderSkeleton: () => , } : undefined, - ].filter(Boolean) as ITableColumn[], + ] + .filter(Boolean) + .map((column) => { + const typed = column as ITableColumn; + // Same header chrome as the spot lists. Perps has no sorting, so + // sortContext carries no onSortPress and no glyph is drawn. + if ( + String(typed.dataIndex) === 'star' || + typeof typed.title !== 'string' + ) { + return typed; + } + const label = typed.title; + return { + ...typed, + renderTitle: ( + _sortIcon: ReactNode, + sortContext: ITableColumnSortContext, + ) => renderRedesignHeaderTitle({ label, sortContext }), + }; + }) as ITableColumn[], [intl, gtXl], ); } diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketNormalTokenList.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketNormalTokenList.tsx index 64175a0e20e7..a05ac9200ca0 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketNormalTokenList.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketNormalTokenList.tsx @@ -7,10 +7,19 @@ import { markMarketReactPerf, useMarketRenderCommitProbe, } from '../../../utils/marketReactPerf'; +import { isMarketTrendingList } from '../../utils'; +import { applyMarketListLocalFilter } from '../MarketFilterChipsBar/applyMarketListLocalFilter'; +import { + buildHotTokenFilterParams, + pickLocalOnlyConditions, +} from '../MarketFilterChipsBar/marketListFilterConfig'; +import { useMarketListFilter } from '../MarketFilterChipsBar/MarketListFilterContext'; +import { useClientSortResult } from './hooks/useClientSortResult'; import { useMarketTokenList } from './hooks/useMarketTokenList'; import { type IMarketToken } from './MarketTokenData'; import { MarketTokenListBase } from './MarketTokenListBase'; +import { getTokenAgeSortValue } from './utils/marketListClientSort'; import { shouldUseStockMetadataColumnsForTokens } from './utils/tokenListHelpers'; import type { IMarketTokenListLiveOverride } from './MarketTokenListBase'; @@ -65,6 +74,25 @@ function MarketNormalTokenList({ stockCategory, timeRange, }); + const { filterState, sortState, setSortState } = useMarketListFilter(); + + // Filters, the chip-driven sort and the fixed column roster are trending + // features; stocks keep their own columns and server-driven behavior. + const isTrendingList = isMarketTrendingList({ + categoryId: selectedCategory, + isStockCategory: Boolean(stockCategory), + }); + + // Server-side passthrough for every dimension the API supports; the local + // pass below only handles what it cannot (token age). + const filterParams = useMemo( + () => + isTrendingList + ? buildHotTokenFilterParams(filterState.conditions) + : undefined, + [isTrendingList, filterState.conditions], + ); + const normalResult = useMarketTokenList({ networkId, initialSortBy, @@ -73,6 +101,7 @@ function MarketNormalTokenList({ type: selectedCategory, category: stockCategory, timeRange, + filterParams, pollingInterval, }); @@ -81,6 +110,34 @@ function MarketNormalTokenList({ [normalResult.data], ); + const filteredData = useMemo(() => { + if (!isTrendingList) { + return normalResult.data; + } + // Only the conditions the server cannot express — the rest already + // narrowed the pool upstream, before it was sliced. + return applyMarketListLocalFilter( + normalResult.data, + pickLocalOnlyConditions(filterState.conditions), + ); + }, [isTrendingList, normalResult.data, filterState.conditions]); + + // Only the trending view has a chip row to stay in sync with; everywhere + // else the hook keeps its own private sort state. + const externalSort = useMemo( + () => + isTrendingList ? { ...sortState, onChange: setSortState } : undefined, + [isTrendingList, sortState, setSortState], + ); + const clientSortResult = useClientSortResult( + useMemo( + () => ({ ...normalResult, data: filteredData }), + [normalResult, filteredData], + ), + { externalSort }, + ); + const listResult = clientSortResult; + useEffect(() => { if (selectedCategory) { onStockDataChange?.(selectedCategory, isStockData); @@ -113,8 +170,16 @@ function MarketNormalTokenList({ networkId={networkId} onItemPress={onItemPress} toolbar={toolbar} - result={normalResult} + result={listResult} isWatchlistMode={false} + clientSort + clientSortFieldMapOverride={ + // Trending merges Token Age into the Name column, so pressing that + // header sorts by age — the same reader the standalone column uses. + isTrendingList ? { name: getTokenAgeSortValue } : undefined + } + redesignColumnOrderEnabled={isTrendingList} + volumeTimeRange={timeRange} showEndReachedIndicator tabIntegrated={tabIntegrated} tabName={tabName} diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketStockCategorySelector.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketStockCategorySelector.tsx index 914e450c6ef5..0fce165c79b9 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketStockCategorySelector.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketStockCategorySelector.tsx @@ -14,6 +14,12 @@ import { CategoryFilterItem, CategoryFilterItemWithLayout, } from '../CategoryFilterItem'; +import { + MARKET_TOOLBAR_GAP_BOTTOM, + MARKET_TOOLBAR_GAP_TOP, + MARKET_TOOLBAR_ITEM_HEIGHT, + marketToolbarFrameProps, +} from '../marketToolbarFrame'; import type { IMarketCategoryItem } from '../../types'; @@ -51,13 +57,9 @@ function MarketStockCategorySelectorImpl({ position="relative" p="$1" gap="$1" - mt="$3" - mb="$2" - maxWidth="100%" - overflow="hidden" - borderWidth={1} - borderColor="$neutral4" - borderRadius="$3" + mt={MARKET_TOOLBAR_GAP_TOP} + mb={MARKET_TOOLBAR_GAP_BOTTOM} + {...marketToolbarFrameProps} > {categories.map((category) => ( = { - change24h: 'change24h', -}; - -// Sort key → IMarketToken field mapping for client-side sorting -const CLIENT_SORT_FIELD_MAP: Record = { - change24h: 'change24h', -}; - -// Map sort keys to ESortWay enum values for logging +// Map sort keys to ESortWay enum values for logging. +// clientSort mode keys sortBy by dataIndex, so this also covers the full +// client-sortable column set (see MARKET_CLIENT_SORT_FIELD_MAP). const SORT_KEY_TO_ENUM: Record = { liquidity: ESortWay.Liquidity, mc: ESortWay.MC, v24hUSD: ESortWay.Volume, + price: ESortWay.Price, + change24h: ESortWay.Change, + marketCap: ESortWay.MC, + turnover: ESortWay.Volume, + transactions: ESortWay.Txns, + uniqueTraders: ESortWay.Traders, + holders: ESortWay.Holders, + tokenAge: ESortWay.TokenAge, + // Task 10 redesign mode merges Name/Token Age into one column; bucket its + // analytics under TokenAge. No-op while that column stays unsortable. + name: ESortWay.TokenAge, }; const STOCK_METADATA_COLUMN_DATA_INDEXES = new Set([ @@ -164,9 +168,11 @@ function getLimitedSubscriptionRange({ function getMarketHomeVisibleSubscriptionRange({ rootElement, tokenCount, + rowHeight, }: { rootElement: HTMLElement | null; tokenCount: number; + rowHeight: number; }): IMarketHomeSubscriptionRange { if (tokenCount <= 0) { return { start: 0, end: 0 }; @@ -202,8 +208,8 @@ function getMarketHomeVisibleSubscriptionRange({ return getLimitedSubscriptionRange({ tokenCount, - visibleStartIndex: Math.floor(visibleTop / MARKET_HOME_WS_ROW_HEIGHT_PX), - visibleEndIndex: Math.ceil(visibleBottom / MARKET_HOME_WS_ROW_HEIGHT_PX), + visibleStartIndex: Math.floor(visibleTop / rowHeight), + visibleEndIndex: Math.ceil(visibleBottom / rowHeight), }); } @@ -230,6 +236,15 @@ type IMarketTokenListBaseProps = { result: IMarketTokenListResult; isWatchlistMode?: boolean; clientSort?: boolean; + clientSortFieldMapOverride?: Record< + string, + keyof IMarketToken | IMarketClientSortValueGetter + >; + // Restricts which dataIndexes are sortable in clientSort mode (intersected + // with clientSortFieldMap). Omit to keep all mapped columns sortable + // (trending behavior). Banner detail passes ['change24h'] only, since its + // sort state (useMarketBannerDetail) supports nothing else. + clientSortableColumns?: readonly string[]; showEndReachedIndicator?: boolean; hideTokenAge?: boolean; watchlistFrom?: EWatchlistFrom; @@ -257,6 +272,11 @@ type IMarketTokenListBaseProps = { enableWebSocket?: boolean; rowBg?: string; testID?: string; + // Trending-only column roster. Other lists keep their own columns while + // still using the shared row/header visuals. + redesignColumnOrderEnabled?: boolean; + // Window the volume column reports. Omit for lists that resolve 24h. + volumeTimeRange?: IMarketTimeRangeValue; }; function MarketTokenListBase({ @@ -266,6 +286,8 @@ function MarketTokenListBase({ result, isWatchlistMode = false, clientSort = false, + clientSortFieldMapOverride, + clientSortableColumns, showEndReachedIndicator = false, hideTokenAge = false, watchlistFrom, @@ -287,6 +309,8 @@ function MarketTokenListBase({ enableWebSocket, rowBg, testID, + redesignColumnOrderEnabled, + volumeTimeRange, }: IMarketTokenListBaseProps) { useMarketRenderCommitProbe('MarketTokenListBase', { tabName, @@ -328,22 +352,47 @@ function MarketTokenListBase({ canEnableWebSocket && (!platformEnv.isWeb || !webTabIntegrated || enableDeferredWebFeatures), ); + const clientSortFieldMap = useMemo( + () => ({ ...MARKET_CLIENT_SORT_FIELD_MAP, ...clientSortFieldMapOverride }), + [clientSortFieldMapOverride], + ); + const clientSortableColumnSet = useMemo( + () => (clientSortableColumns ? new Set(clientSortableColumns) : undefined), + [clientSortableColumns], + ); + // Declared before the sort below: stock rows sort by their own metadata. + const useStockMetadataColumns = useMemo( + () => + shouldUseStockMetadataColumnsForTokens(rawData, { + forceStockMetadataColumns, + enableAutoDetection: + showStockSubtitle === 'auto' || + (isWatchlistMode && showStockSubtitle !== false), + }), + [forceStockMetadataColumns, isWatchlistMode, rawData, showStockSubtitle], + ); const orderedData = useMemo(() => { if (!clientSort || !currentSortBy || !currentSortType) { return rawData; } - const field = CLIENT_SORT_FIELD_MAP[currentSortBy]; + const stockGetter = useStockMetadataColumns + ? MARKET_STOCK_CLIENT_SORT_VALUE_GETTERS[currentSortBy] + : undefined; + const field = stockGetter ?? clientSortFieldMap[currentSortBy]; if (!field) { return rawData; } - return [...rawData].toSorted((a, b) => { - const aVal = (a[field] as number) ?? 0; - const bVal = (b[field] as number) ?? 0; - return currentSortType === 'asc' ? aVal - bVal : bVal - aVal; - }); - }, [clientSort, currentSortBy, currentSortType, rawData]); + return sortMarketTokensClient(rawData, field, currentSortType); + }, [ + clientSort, + clientSortFieldMap, + currentSortBy, + currentSortType, + rawData, + useStockMetadataColumns, + ]); const [subscriptionRange, setSubscriptionRange] = useState({ start: 0, end: 0 }); const updateSubscriptionRange = useCallback(() => { @@ -351,6 +400,7 @@ function MarketTokenListBase({ ? getMarketHomeVisibleSubscriptionRange({ rootElement: listRootRef.current, tokenCount: orderedData.length, + rowHeight: REDESIGN_ROW_HEIGHT, }) : { start: 0, end: 0 }; @@ -449,16 +499,6 @@ function MarketTokenListBase({ return shouldShowStockSubtitleForTokens(rawData); }, [rawData, showStockSubtitle]); - const useStockMetadataColumns = useMemo( - () => - shouldUseStockMetadataColumnsForTokens(rawData, { - forceStockMetadataColumns, - enableAutoDetection: - showStockSubtitle === 'auto' || - (isWatchlistMode && showStockSubtitle !== false), - }), - [forceStockMetadataColumns, isWatchlistMode, rawData, showStockSubtitle], - ); // Web tab integration gives the inner FlatList the full tab height so the // outer Tabs.Container can own vertical scroll. During cold start, keep only // the first rows rich and defer extra media/interactive decoration until @@ -480,6 +520,7 @@ function MarketTokenListBase({ change24hColumnTitle, useStockMetadataColumns, deferRichRowAfterIndex, + { redesignColumnOrderEnabled, volumeTimeRange }, ); const data = useMemo(() => { @@ -551,23 +592,30 @@ function MarketTokenListBase({ const handleHeaderRow = useCallback( (column: ITableColumn) => { - if (!isWatchlistMode && !clientSort) { + // The watchlist is ordered by drag, so a header sort would silently + // overwrite the arrangement the user built by hand. It never sorts. + if (isWatchlistMode || !clientSort) { return undefined; } + // Stock metadata columns are not server-sortable, but they sort locally + // off the displayed `record.stock` value. if ( useStockMetadataColumns && - STOCK_METADATA_COLUMN_DATA_INDEXES.has(String(column.dataIndex)) + STOCK_METADATA_COLUMN_DATA_INDEXES.has(String(column.dataIndex)) && + !MARKET_STOCK_CLIENT_SORT_VALUE_GETTERS[String(column.dataIndex)] ) { return undefined; } - // Client sort mode is used by banner detail for 24h change sorting, - // watchlist mode uses restricted server-side sortable columns. - const columnsMap = clientSort - ? CLIENT_SORTABLE_COLUMNS - : SORTABLE_COLUMNS; - const sortKey = columnsMap[column.dataIndex as keyof typeof columnsMap]; + // Any column mapped by clientSortFieldMap is sortable (sortKey === + // dataIndex), narrowed further by clientSortableColumns when the caller + // supports fewer of them than the map does. + const columnKey = String(column.dataIndex); + const isColumnClientSortable = + Boolean(clientSortFieldMap[columnKey]) && + (!clientSortableColumnSet || clientSortableColumnSet.has(columnKey)); + const sortKey = isColumnClientSortable ? columnKey : undefined; if (sortKey) { const isCurrentSort = currentSortBy === sortKey; @@ -587,6 +635,8 @@ function MarketTokenListBase({ handleSortChange, isWatchlistMode, clientSort, + clientSortFieldMap, + clientSortableColumnSet, currentSortBy, currentSortType, useStockMetadataColumns, @@ -761,11 +811,24 @@ function MarketTokenListBase({ const stickyPortalTarget = stickyHeaderCtx?.portalTarget ?? null; const useDesktopPortal = webTabIntegrated && !!stickyPortalTarget && !md; + // Client-sort mode hands the header its order from our own state, so a sort + // triggered outside the table (the Top turnover chip) still moves the arrow. + const controlledSort = useMemo(() => { + if (!clientSort) { + return undefined; + } + return currentSortBy && currentSortType + ? { dataIndex: currentSortBy, order: currentSortType as ETableSortType } + : null; + }, [clientSort, currentSortBy, currentSortType]); + const portalContent = useMemo(() => { if (!useDesktopPortal || !isTabFocused || !stickyPortalTarget) return null; return ( - + {/* Bottom padding keeps the first row clear of the pinned header once + the list scrolls under it. */} + {toolbar ? ( {toolbar} @@ -774,6 +837,7 @@ function MarketTokenListBase({ @@ -785,6 +849,7 @@ function MarketTokenListBase({ toolbar, marketTokenColumns, stableHandleHeaderRow, + controlledSort, ]); let integratedContentPaddingBottom = tabBarHeight; @@ -815,13 +880,12 @@ function MarketTokenListBase({ }), [], ); - const tableRowProps = useMemo(() => { + const tableRowProps = useMemo(() => { const hasWebRowStyle = platformEnv.isWeb && webTabIntegrated; - if (!rowBg && !hasWebRowStyle) { - return undefined; - } return { ...(rowBg ? { bg: rowBg } : undefined), + // Figma: 12px vertical padding around a 44px identity block = 68px rows. + minHeight: REDESIGN_ROW_HEIGHT, ...(hasWebRowStyle ? { style: MARKET_HOME_WEB_ROW_CONTENT_VISIBILITY_STYLE } : undefined), @@ -880,9 +944,10 @@ function MarketTokenListBase({ keyExtractor={(item) => item.id} extraData={networkId} onHeaderRow={stableHandleHeaderRow} + controlledSort={controlledSort} TableEmptyComponent={TableEmptyComponent} TableFooterComponent={TableFooterComponent} - estimatedItemSize={60} + estimatedItemSize={REDESIGN_ROW_HEIGHT} onRow={stableOnRow} rowProps={tableRowProps} /> diff --git a/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketWatchlistCategorySelector.tsx b/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketWatchlistCategorySelector.tsx index 82b58006ca57..a5686977d254 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketWatchlistCategorySelector.tsx +++ b/packages/kit/src/views/Market/MarketHomeV2/components/MarketTokenList/MarketWatchlistCategorySelector.tsx @@ -17,6 +17,12 @@ import { CategoryFilterItem, CategoryFilterItemWithLayout, } from '../CategoryFilterItem'; +import { + MARKET_TOOLBAR_GAP_BOTTOM, + MARKET_TOOLBAR_GAP_TOP, + MARKET_TOOLBAR_ITEM_HEIGHT, + marketToolbarFrameProps, +} from '../marketToolbarFrame'; export type IWatchlistFilterType = 'all' | 'spot' | 'perps'; @@ -103,13 +109,9 @@ function MarketWatchlistCategorySelectorDesktop({ position="relative" p="$1" gap="$1" - mt="$3" - mb="$2" - maxWidth="100%" - overflow="hidden" - borderWidth={1} - borderColor="$neutral4" - borderRadius="$3" + mt={MARKET_TOOLBAR_GAP_TOP} + mb={MARKET_TOOLBAR_GAP_BOTTOM} + {...marketToolbarFrameProps} > {categories.map((c) => ( - - {tabElements} - - + // Wraps the whole container so the tab bar's filters trigger and the tab + // content's lists read the same condition state. + + + + {tabElements} + + + ); } diff --git a/packages/kit/src/views/Market/MarketHomeV2/layouts/mobileLayoutUtils.ts b/packages/kit/src/views/Market/MarketHomeV2/layouts/mobileLayoutUtils.ts index 965405beec13..759dfab7d7ca 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/layouts/mobileLayoutUtils.ts +++ b/packages/kit/src/views/Market/MarketHomeV2/layouts/mobileLayoutUtils.ts @@ -1,4 +1,9 @@ export const MARKET_MOBILE_SECONDARY_HEADER_HEIGHT = 74; + +// Every mobile list row is this tall. Pinned rather than left to fall out of +// the text metrics, so a copy or font-size change cannot silently make one +// list's rows a few pixels shorter than another's. +export const MARKET_MOBILE_ROW_HEIGHT = 68; export const MARKET_MOBILE_COLUMN_HEADER_HEIGHT = 32; export const MARKET_MOBILE_CONTENT_TOP_GAP = 16; diff --git a/packages/kit/src/views/Market/MarketHomeV2/utils.test.ts b/packages/kit/src/views/Market/MarketHomeV2/utils.test.ts index 308affe438ad..55b3c73fad57 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/utils.test.ts +++ b/packages/kit/src/views/Market/MarketHomeV2/utils.test.ts @@ -3,6 +3,7 @@ import { COMPACT_SPOT_HIDDEN_DESKTOP_COLUMNS, isMarketStockCategory, isMarketStockCategoryById, + isMarketTrendingList, parseValueToNumber, shouldHideSpotExtendedStats, validateLiquidityInput, @@ -414,3 +415,25 @@ describe('Market Home API Defaults', () => { expect(TIME_RANGE_TO_API_MAP['1h']).toBe('2'); }); }); + +describe('isMarketTrendingList', () => { + it('accepts trending only', () => { + expect(isMarketTrendingList({ categoryId: 'trending' })).toBe(true); + expect(isMarketTrendingList({ categoryId: 'x_mentioned' })).toBe(false); + expect(isMarketTrendingList({ categoryId: undefined })).toBe(false); + }); + + it('rejects trending rendered as a stock category', () => { + expect( + isMarketTrendingList({ categoryId: 'trending', isStockCategory: true }), + ).toBe(false); + }); + + // Guards the drift the review caught: the toolbar showed the Filters entry + // for any non-stock category while only trending consumed filterState. + it('is false for a non-stock category that is not trending', () => { + expect( + isMarketTrendingList({ categoryId: 'meme', isStockCategory: false }), + ).toBe(false); + }); +}); diff --git a/packages/kit/src/views/Market/MarketHomeV2/utils.ts b/packages/kit/src/views/Market/MarketHomeV2/utils.ts index cdf8f80fc98f..c392c38dbfc1 100644 --- a/packages/kit/src/views/Market/MarketHomeV2/utils.ts +++ b/packages/kit/src/views/Market/MarketHomeV2/utils.ts @@ -138,6 +138,25 @@ export const shouldHideSpotExtendedStats = ( return !SPOT_CATEGORIES_WITH_FULL_STATS.has(normalizedCategory); }; +/** + * Trending is the only spot list wired to the filter chips, the Filters panel + * and the fixed nine-column roster; every other category is server-ordered and + * ignores filterState. + * + * Anything that *shows* one of those controls has to gate on this same + * predicate, not on a lookalike condition. The two drifted before: the compact + * mobile toolbar showed the Filters trigger for any non-stock category while + * only trending consumed it, which currently coincides simply because the + * server happens to return no other non-stock spot category. + */ +export const isMarketTrendingList = ({ + categoryId, + isStockCategory, +}: { + categoryId?: string; + isStockCategory?: boolean; +}): boolean => categoryId === 'trending' && !isStockCategory; + export const isMarketStockCategory = ( category?: Pick, ): boolean => { diff --git a/packages/kit/src/views/Market/components/MarketStarV2.tsx b/packages/kit/src/views/Market/components/MarketStarV2.tsx index 5fc1684ca1bd..767e16c9944d 100644 --- a/packages/kit/src/views/Market/components/MarketStarV2.tsx +++ b/packages/kit/src/views/Market/components/MarketStarV2.tsx @@ -161,10 +161,12 @@ export const usePerpsStarV2Checked = ({ perpsCoin }: { perpsCoin: string }) => { function BasicMarketPerpsStarV2({ perpsCoin, size, + customIconSize, ...props }: { perpsCoin: string; size?: IIconButtonProps['size']; + customIconSize?: string; } & IStackProps) { const intl = useIntl(); const { onPress, checked } = usePerpsStarV2Checked({ perpsCoin }); @@ -183,6 +185,7 @@ function BasicMarketPerpsStarV2({ iconSize={size ? undefined : '$5'} iconProps={{ color: checked ? '$iconActive' : '$iconSubdued', + ...(customIconSize ? { size: customIconSize } : {}), }} onPress={onPress} {...(props as IXStackProps)} diff --git a/packages/kit/src/views/Market/utils/marketLightApi.ts b/packages/kit/src/views/Market/utils/marketLightApi.ts index 541b8d1aec18..7f6f7aaa8cdf 100644 --- a/packages/kit/src/views/Market/utils/marketLightApi.ts +++ b/packages/kit/src/views/Market/utils/marketLightApi.ts @@ -32,6 +32,8 @@ type IMarketTokenListRequestParams = { type?: string; category?: string; timeFrame?: string; + // hot-token v6 filter params, flattened by buildHotTokenFilterParams. + filterParams?: Record; }; type INormalizedMarketTokenListRequestParams = IMarketTokenListRequestParams & { @@ -77,8 +79,12 @@ const shouldUseMarketHomeTokenListSeed = ({ type, category, timeFrame, + filterParams, }: INormalizedMarketTokenListRequestParams) => shouldUseMarketHomeTokenListBootstrapSeed() && + // The seed is a snapshot of the unfiltered first page; any filter makes it + // the wrong answer. + filterParams === undefined && networkId === '' && sortBy === 'v24hUSD' && sortType === 'desc' && @@ -101,6 +107,7 @@ const fetchMarketTokenListFromApi = async ({ type, category, timeFrame, + filterParams, }: INormalizedMarketTokenListRequestParams) => { markMarketPerf('market-light-api-token-list-start', { networkId, @@ -129,6 +136,7 @@ const fetchMarketTokenListFromApi = async ({ category, timeFrame, currency: 'usd', + ...filterParams, }, }); const data = response.data.data; diff --git a/packages/shared/src/locale/enum/translations.ts b/packages/shared/src/locale/enum/translations.ts index 87eb88643f22..9c0a2e4c10fc 100644 --- a/packages/shared/src/locale/enum/translations.ts +++ b/packages/shared/src/locale/enum/translations.ts @@ -350,6 +350,10 @@ export enum ETranslations { browser_unable_to_pay = 'browser_unable_to_pay', btc_trade_btc_slow_to_confirm = 'btc_trade.btc_slow_to_confirm', btc_trade_set_to_1_percent = 'btc_trade.set_to_1_percent', + button_apply_slippage_all_future_orders = 'button_apply_slippage_all_future_orders', + button_apply_slippage_this_order = 'button_apply_slippage_this_order', + button_convert_tokens_estimated_amount = 'button_convert_tokens_estimated_amount', + button_stop_conversion = 'button_stop_conversion', buy_and_sell = 'buy_and_sell', chase__action = 'chase__action', chase_order_confirmation__desc = 'chase_order_confirmation__desc', @@ -2247,8 +2251,11 @@ export enum ETranslations { kyt_supported_assets__title = 'kyt_supported_assets__title', kyt_supported_assets_load_error__desc = 'kyt_supported_assets_load_error__desc', kyt_view_report__action = 'kyt_view_report__action', + label_conversion_progress = 'label_conversion_progress', label_converted = 'label_converted', label_market_awaiting_open = 'label_market_awaiting_open', + label_select_all = 'label_select_all', + label_skipped = 'label_skipped', last_1_month__action = 'last_1_month__action', last_3_months__action = 'last_3_months__action', learn_more_about_qr_code_wallet = 'learn_more_about_qr_code_wallet', @@ -2382,6 +2389,7 @@ export enum ETranslations { market_chart_settings__y_axis = 'market.chart_settings__y_axis', market_chart_style = 'market.chart_style', market_close_abbr = 'market.close_abbr', + market_column_name_token_age = 'market.column_name_token_age', market_data_display = 'market.data_display', market_days_since_launch = 'market.days_since_launch', market_edit_preferred_intervals = 'market.edit_preferred_intervals', @@ -2389,18 +2397,29 @@ export enum ETranslations { market_empty_watchlist_title = 'market.empty_watchlist_title', market_favorites_empty = 'market.favorites_empty', market_favorites_empty_desc = 'market.favorites_empty_desc', + market_filter_chip_conditions_tips = 'market.filter_chip_conditions_tips', + market_filter_chip_large_cap = 'market.filter_chip_large_cap', + market_filter_chip_mid_cap = 'market.filter_chip_mid_cap', + market_filter_chip_top_turnover = 'market.filter_chip_top_turnover', + market_filter_chip_top_turnover_tips = 'market.filter_chip_top_turnover_tips', + market_filters = 'market.filters', + market_filters_group_metrics = 'market.filters_group_metrics', + market_filters_time_frame = 'market.filters_time_frame', market_heikin_ashi = 'market.heikin_ashi', market_high = 'market.high', market_high_abbr = 'market.high_abbr', + market_holders_tips = 'market.holders_tips', market_indicators = 'market.indicators', market_intervals = 'market.intervals', market_last_price = 'market.last_price', market_last_seven_days = 'market.last_seven_days', market_last_updated = 'market.last_updated', market_line = 'market.line', + market_liquidity_tips = 'market.liquidity_tips', market_low = 'market.low', market_low_abbr = 'market.low_abbr', market_main_chart_indicators = 'market.main_chart_indicators', + market_market_cap_tips = 'market.market_cap_tips', market_minus_2_percent_depth = 'market.minus_2_percent_depth', market_most_24h_volume = 'market.most_24h_volume', market_most_market_cap = 'market.most_market_cap', @@ -2428,8 +2447,10 @@ export enum ETranslations { market_sub_chart_indicators = 'market.sub_chart_indicators', market_trending = 'market.trending', market_trust_score = 'market.trust_score', + market_turnover_tips = 'market.turnover_tips', market_twenty_four_hour_percentage = 'market.twenty_four_hour_percentage', market_twenty_four_hour_volume = 'market.twenty_four_hour_volume', + market_txns_tips = 'market.txns_tips', market_volume_percentage = 'market.volume_percentage', market_preset_priority_fee_price_unit = 'market_preset.priority_fee_price_unit', market_status_closed = 'market_status.closed', @@ -3340,6 +3361,7 @@ export enum ETranslations { perps_alerts__title = 'perps_alerts__title', perps_back_to_default_layout__action = 'perps_back_to_default_layout__action', perps_chart_do_not_show__action = 'perps_chart_do_not_show__action', + perps_desktop_resize_panels__desc = 'perps_desktop_resize_panels__desc', perps_footer_help_us_better = 'perps_footer.help_us_better', perps_hide_other_pairs = 'perps_hide_other_pairs', perps_layout_reset__msg = 'perps_layout_reset__msg', @@ -4661,11 +4683,15 @@ export enum ETranslations { terms_privacy = 'terms_privacy', third_party_hw_adapter_not_registered__msg = 'third_party_hw_adapter_not_registered__msg', title_403 = 'title_403', + title_dust_sweep = 'title_dust_sweep', title_dust_sweep_completed = 'title_dust_sweep_completed', + title_dust_swept = 'title_dust_swept', + title_edit_slippage = 'title_edit_slippage', to_chain__title = 'to_chain__title', toast_web_auth = 'toast.web_auth', toast_create_address_failed_message = 'toast_create_address_failed_message', toast_create_address_failed_title = 'toast_create_address_failed_title', + toast_price_impact_exceeded_skipped = 'toast_price_impact_exceeded_skipped', token_manage_custom_token_address_failed = 'token_manage.custom_token_address_failed', token_no_search_results_desc = 'token_no_search_results_desc', token_selector_network = 'token_selector.network', diff --git a/packages/shared/src/locale/json/bn.json b/packages/shared/src/locale/json/bn.json index b460c6eb7e53..85bc9e11fc4d 100644 --- a/packages/shared/src/locale/json/bn.json +++ b/packages/shared/src/locale/json/bn.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "পেমেন্ট খুলতে অক্ষম। দয়া করে Bitrefill থেকে পুনরায় চেষ্টা করুন।", "btc_trade.btc_slow_to_confirm": "BTC নিশ্চিত করতে ধীর — ১% স্লিপেজের নিচে অর্ডার ব্যর্থ হতে পারে", "btc_trade.set_to_1_percent": "১% এ সেট করুন", + "button_apply_slippage_all_future_orders": "সব ভবিষ্যৎ অর্ডার", + "button_apply_slippage_this_order": "এই অর্ডার", + "button_convert_tokens_estimated_amount": "{count} টোকেন কনভার্ট (~{amount})", + "button_stop_conversion": "থামান", "buy_and_sell": "কেনা-বেচা", "chase__action": "চেজ", "chase_order_confirmation__desc": "চেজ করলে অর্ডারের দাম বর্তমান সেরা দামে আপডেট হবে, যাতে অর্ডারটি দ্রুত পূরণ হতে পারে।", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "কভার করা অ্যাসেট", "kyt_supported_assets_load_error__desc": "সমর্থিত অ্যাসেট লোড করা যায়নি। সংযোগ পরীক্ষা করে আবার চেষ্টা করুন", "kyt_view_report__action": "রিপোর্ট দেখুন", + "label_conversion_progress": "কনভার্সনের অগ্রগতি", "label_converted": "রূপান্তরিত", "label_market_awaiting_open": "খোলার অপেক্ষা", + "label_select_all": "সব নির্বাচন করুন", + "label_skipped": "বাদ দেওয়া হয়েছে", "last_1_month__action": "গত মাসে", "last_3_months__action": "গত ৩ মাস", "learn_more_about_qr_code_wallet": "QR-based ওয়ালেট সম্পর্কে আরও জানুন", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y-অক্ষ", "market.chart_style": "চার্ট স্টাইল", "market.close_abbr": "C", + "market.column_name_token_age": "নাম/টোকেনের বয়স", "market.data_display": "ডেটা প্রদর্শন", "market.days_since_launch": "লঞ্চের পর দিন", "market.edit_preferred_intervals": "পছন্দের ইন্টারভাল নির্বাচন করুন", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "আপনার দর্শনতালিকা খালি", "market.favorites_empty": "আপনার প্রিয় তালিকা খালি", "market.favorites_empty_desc": "আপনার প্রিয় টোকেনগুলি তালিকায় যোগ করুন", + "market.filter_chip_conditions_tips": "এই শর্তগুলি পূরণ করে এমন জনপ্রিয় টোকেন", + "market.filter_chip_large_cap": "লার্জ-ক্যাপ টোকেন", + "market.filter_chip_mid_cap": "মিড-ক্যাপ টোকেন", + "market.filter_chip_top_turnover": "সর্বোচ্চ ভলিউম", + "market.filter_chip_top_turnover_tips": "বর্তমান জনপ্রিয় তালিকা, ভলিউম অনুসারে সাজানো", + "market.filters": "ফিল্টার", + "market.filters_group_metrics": "মেট্রিক", + "market.filters_time_frame": "সময়সীমা", "market.heikin_ashi": "Heikin Ashi", "market.high": "উচ্চ", "market.high_abbr": "H", + "market.holders_tips": "টোকেন ধারণকারী অ্যাড্রেসের সংখ্যা।", "market.indicators": "ইন্ডিকেটর", "market.intervals": "ইন্টারভাল", "market.last_price": "সর্বশেষ মূল্য", "market.last_seven_days": "গত ৭ দিন", "market.last_updated": "সর্বশেষ আপডেট করা হয়েছে", "market.line": "লাইন", + "market.liquidity_tips": "পুলে উপলব্ধ মোট লিকুইডিটি।", "market.low": "নিম্ন", "market.low_abbr": "L", "market.main_chart_indicators": "মেইন-চার্ট ইন্ডিকেটর", + "market.market_cap_tips": "টোকেনের প্রচলিত বাজারমূল্য।", "market.minus_2_percent_depth": "-2% গভীরতা", "market.most_24h_volume": "সর্বাধিক 24 ঘন্টার আয়তন", "market.most_market_cap": "সর্বাধিক বাজার মূল্য", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "সাব-চার্ট ইন্ডিকেটর", "market.trending": "ট্রেন্ডিং", "market.trust_score": "ট্রাস্ট স্কোর", + "market.turnover_tips": "নির্বাচিত সময়সীমার লেনদেনের পরিমাণ।", "market.twenty_four_hour_percentage": "২৪ ঘণ্টা %", "market.twenty_four_hour_volume": "২৪ ঘণ্টার ভলিউম", + "market.txns_tips": "নির্বাচিত সময়সীমায় ক্রয় ও বিক্রয় লেনদেনের সংখ্যা।", "market.volume_percentage": "ট্রেডিং ভলিউম %", "market_preset.priority_fee_price_unit": "অগ্রাধিকার ফি-র দরের একক। ফি বেশি দিলে সাধারণত লেনদেন তাড়াতাড়ি নিশ্চিত হওয়ার সম্ভাবনা বাড়ে। নির্ধারণের সময় বর্তমান রিয়েল-টাইম নেটওয়ার্ক ফি দেখে নিন।", "market_status.closed": "বন্ধ", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "ফি ছাড় সক্রিয় হয়েছে", "perps__snooze_remind_later__action": "এখন নয়, পরে মনে করিয়ে দিন", "perps_alerts__title": "পার্পস সতর্কতা", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "ডিফল্ট লেআউটে রিসেট করুন", + "perps_chart_do_not_show__action": "দেখাবেন না", + "perps_desktop_resize_panels__desc": "চার্ট ও পজিশন প্যানেলের আকার পরিবর্তন করতে বিভাজকটি টেনে আনুন।", "perps_footer.help_us_better": "Perps কমিউনিটি", "perps_hide_other_pairs": "অন্যান্য পেয়ার লুকান", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "লেআউট রিসেট করা হয়েছে", + "perps_layout_settings__title": "লেআউট সেটিংস", "perps_referral_campaign__desc": "বন্ধুদের আমন্ত্রণ জানান, একসাথে 99U জিতুন। লিডারবোর্ডে উঠুন এবং গ্র্যান্ড প্রাইজের সুযোগ নিন!", "perps_spot_add_funds__title": "ফান্ড যোগ করুন", "perps_spot_buy_token_spot__action": "{token} স্পট কিনুন", "perps_spot_deposit_to_usdc__desc": "আপনার অ্যাকাউন্টে অন-চেইন সম্পদ জমা দিন", "perps_spot_deposit_to_usdc__title": "USDC-তে জমা দিন", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "ট্রেডিং পেজের ক্যান্ডেলস্টিক চার্ট", "perps_trigger_price_equal_current": "ট্রিগার মূল্য বর্তমান মূল্য থেকে আলাদা হতে হবে", "photo_library_access_denied__desc": "ছবি সংরক্ষণ করতে OneKey-এর ফটো লাইব্রেরি অ্যাক্সেস প্রয়োজন। অনুগ্রহ করে সেটিংসে গিয়ে ফটো লাইব্রেরি অনুমতি সক্ষম করুন।", "photo_library_access_denied__title": "ফটো লাইব্রেরি অ্যাক্সেস অস্বীকৃত", @@ -4656,11 +4678,15 @@ "terms_privacy": "চালিয়ে গেলে, আপনি নিশ্চিত করছেন যে আপনার বয়স ১৮+ এবং আপনি আমাদের TermsPrivacy-এ সম্মত হচ্ছেন", "third_party_hw_adapter_not_registered__msg": "তৃতীয় পক্ষের হার্ডওয়্যার অ্যাডাপ্টার নিবন্ধিত নয়।", "title_403": "403 নিষিদ্ধ", + "title_dust_sweep": "ডাস্ট কনভার্ট", "title_dust_sweep_completed": "ডাস্ট সুইপ সম্পন্ন", + "title_dust_swept": "ডাস্ট কনভার্ট হয়েছে", + "title_edit_slippage": "স্লিপেজ সম্পাদনা", "to_chain__title": "গন্তব্য চেইন", "toast.web_auth": "সেট ব্যর্থ হয়েছে", "toast_create_address_failed_message": "দয়া করে প্রথমে একটি অ্যাকাউন্ট নির্বাচন করুন", "toast_create_address_failed_title": "ঠিকানা তৈরি করতে অক্ষম", + "toast_price_impact_exceeded_skipped": "প্রাইস ইমপ্যাক্ট বেশি—বাদ", "token_manage.custom_token_address_failed": "চুক্তির ঠিকানা খুঁজে পাওয়া যায়নি। আবার চেষ্টা করুন। ", "token_no_search_results_desc": "আপনার অনুসন্ধানের জন্য কোন মিল পাওয়া যায়নি। দয়া করে পুনরায় প্রবেশ করুন।", "token_selector.network": "নেটওয়ার্ক:", diff --git a/packages/shared/src/locale/json/de.json b/packages/shared/src/locale/json/de.json index 28c24c00b952..2346a9747cb1 100644 --- a/packages/shared/src/locale/json/de.json +++ b/packages/shared/src/locale/json/de.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Zahlung kann nicht geöffnet werden. Bitte versuchen Sie es erneut von Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC ist langsam bei der Bestätigung — Aufträge mit weniger als 1 % Slippage können fehlschlagen", "btc_trade.set_to_1_percent": "1 % nutzen", + "button_apply_slippage_all_future_orders": "Alle Folgeorders", + "button_apply_slippage_this_order": "Diese Order", + "button_convert_tokens_estimated_amount": "{count} Token umwandeln (~{amount})", + "button_stop_conversion": "Stoppen", "buy_and_sell": "Kauf/Verkauf", "chase__action": "Nachziehen", "chase_order_confirmation__desc": "Beim Nachziehen wird der Orderpreis auf den aktuell besten Preis aktualisiert, damit die Order schneller ausgeführt wird.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Abgedeckte Assets", "kyt_supported_assets_load_error__desc": "Unterstützte Assets konnten nicht geladen werden. Bitte Verbindung prüfen und erneut versuchen", "kyt_view_report__action": "Bericht anzeigen", + "label_conversion_progress": "Umwandlungsfortschritt", "label_converted": "Konvertiert", "label_market_awaiting_open": "Vor Eröffnung", + "label_select_all": "Alle auswählen", + "label_skipped": "Übersprungen", "last_1_month__action": "Letzten Monat", "last_3_months__action": "Letzte 3 Monate", "learn_more_about_qr_code_wallet": "Mehr über QR-Code-Wallets erfahren", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y-Achse", "market.chart_style": "Chart-Stil", "market.close_abbr": "C", + "market.column_name_token_age": "Name/Token-Alter", "market.data_display": "Datenanzeige", "market.days_since_launch": "Tage seit dem Start", "market.edit_preferred_intervals": "Bevorzugte Intervalle auswählen", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Ihre Beobachtungsliste ist leer", "market.favorites_empty": "Ihre Favoriten sind leer", "market.favorites_empty_desc": "Fügen Sie Ihre Lieblings-Token zur Liste hinzu", + "market.filter_chip_conditions_tips": "Beliebte Token, die diese Kriterien erfüllen", + "market.filter_chip_large_cap": "Token mit hoher Marktkapitalisierung", + "market.filter_chip_mid_cap": "Token mit mittlerer Marktkapitalisierung", + "market.filter_chip_top_turnover": "Höchstes Volumen", + "market.filter_chip_top_turnover_tips": "Aktuelle Trendliste, sortiert nach Volumen", + "market.filters": "Filter", + "market.filters_group_metrics": "Kennzahlen", + "market.filters_time_frame": "Zeitraum", "market.heikin_ashi": "Heikin Ashi", "market.high": "Hoch", "market.high_abbr": "H", + "market.holders_tips": "Anzahl der Adressen, die den Token halten.", "market.indicators": "Indikatoren", "market.intervals": "Intervalle", "market.last_price": "Letzter Preis", "market.last_seven_days": "Letzte 7 Tage", "market.last_updated": "Zuletzt aktualisiert", "market.line": "Linie", + "market.liquidity_tips": "Gesamte in den Pools verfügbare Liquidität.", "market.low": "Niedrig", "market.low_abbr": "L", "market.main_chart_indicators": "Hauptchart-Indikatoren", + "market.market_cap_tips": "Umlaufende Marktkapitalisierung des Tokens.", "market.minus_2_percent_depth": "-2% tiefe", "market.most_24h_volume": "Meiste 24h Volumen", "market.most_market_cap": "Größte Marktkapitalisierung", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Subchart-Indikatoren", "market.trending": "Im Trend", "market.trust_score": "Vertrauensscore", + "market.turnover_tips": "Handelsvolumen im ausgewählten Zeitraum.", "market.twenty_four_hour_percentage": "24h%", "market.twenty_four_hour_volume": "24h Volumen", + "market.txns_tips": "Anzahl der Kauf- und Verkaufstransaktionen im ausgewählten Zeitraum.", "market.volume_percentage": "Volumen %", "market_preset.priority_fee_price_unit": "Prioritätsgebühr-Preiseinheit. Eine höhere Gebühr erhöht normalerweise die Wahrscheinlichkeit einer früheren Bestätigung. Beachten Sie die aktuelle Echtzeit-Netzwerkgebühr, wenn Sie diese festlegen.", "market_status.closed": "Geschlossen", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Gebührenrabatt aktiviert", "perps__snooze_remind_later__action": "Nicht jetzt, später erinnern", "perps_alerts__title": "Perps-Warnungen", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Auf Standardlayout zurücksetzen", + "perps_chart_do_not_show__action": "Nicht anzeigen", + "perps_desktop_resize_panels__desc": "Ziehe die Trennlinie, um die Größe der Chart- und Positionsbereiche anzupassen.", "perps_footer.help_us_better": "Perps-Community", "perps_hide_other_pairs": "Andere Paare ausblenden", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "Layout wurde zurückgesetzt", + "perps_layout_settings__title": "Layout-Einstellungen", "perps_referral_campaign__desc": "Lade Freunde ein und gewinne gemeinsam 99U. Klettere in der Rangliste nach oben und erhalte die Chance auf den Hauptpreis!", "perps_spot_add_funds__title": "Geld hinzufügen", "perps_spot_buy_token_spot__action": "{token} Spot kaufen", "perps_spot_deposit_to_usdc__desc": "Zahlen Sie On-Chain-Assets auf Ihr Konto ein", "perps_spot_deposit_to_usdc__title": "In USDC einzahlen", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Candlestick-Chart auf der Handelsseite", "perps_trigger_price_equal_current": "Auslösepreis muss sich vom aktuellen Preis unterscheiden", "photo_library_access_denied__desc": "OneKey benötigt Zugriff auf die Fotomediathek, um Bilder zu speichern. Bitte gehen Sie zu den Einstellungen und aktivieren Sie die Berechtigungen für die Fotomediathek.", "photo_library_access_denied__title": "Zugriff auf die Fotomediathek verweigert", @@ -4656,11 +4678,15 @@ "terms_privacy": "Wenn Sie fortfahren, bestätigen Sie, dass Sie 18+ sind und unseren Nutzungsbedingungen und Datenschutzbestimmungen zustimmen", "third_party_hw_adapter_not_registered__msg": "Der Drittanbieter-Hardware-Adapter ist nicht registriert.", "title_403": "403 Kein Zugriff", + "title_dust_sweep": "Kleinstbeträge umwandeln", "title_dust_sweep_completed": "Dust bereinigt", + "title_dust_swept": "Kleinstbeträge umgewandelt", + "title_edit_slippage": "Slippage ändern", "to_chain__title": "Ziel-Chain", "toast.web_auth": "Einstellung fehlgeschlagen", "toast_create_address_failed_message": "Bitte wählen Sie zuerst ein Konto aus", "toast_create_address_failed_title": "Adresse kann nicht erstellt werden", + "toast_price_impact_exceeded_skipped": "Preiseffekt zu hoch – übersprungen", "token_manage.custom_token_address_failed": "Die Vertragsadresse konnte nicht gefunden werden. Versuchen Sie es erneut. ", "token_no_search_results_desc": "Keine Übereinstimmung für Ihre Suche gefunden. Bitte erneut eingeben.", "token_selector.network": "Netz:", diff --git a/packages/shared/src/locale/json/en_US.json b/packages/shared/src/locale/json/en_US.json index e132f9e089f8..8f7b08e20658 100644 --- a/packages/shared/src/locale/json/en_US.json +++ b/packages/shared/src/locale/json/en_US.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Unable to open payment. Please retry from Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC is slow to confirm — Orders below 1% slippage may fail", "btc_trade.set_to_1_percent": "Set to 1%", + "button_apply_slippage_all_future_orders": "All future orders", + "button_apply_slippage_this_order": "This order", + "button_convert_tokens_estimated_amount": "Convert {count} Tokens (~{amount})", + "button_stop_conversion": "Stop", "buy_and_sell": "Buy & Sell", "chase__action": "Chase", "chase_order_confirmation__desc": "Chasing updates the order price to the current best price to help the order fill faster.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Assets covered", "kyt_supported_assets_load_error__desc": "Failed to load supported assets. Please check your connection and try again", "kyt_view_report__action": "View report", + "label_conversion_progress": "Conversion progress", "label_converted": "Converted", "label_market_awaiting_open": "Awaiting Open", + "label_select_all": "Select all", + "label_skipped": "Skipped", "last_1_month__action": "Last Month", "last_3_months__action": "Last 3 Months", "learn_more_about_qr_code_wallet": "Learn more about QR-based wallet", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y-axis", "market.chart_style": "Chart style", "market.close_abbr": "C", + "market.column_name_token_age": "Name/Token Age", "market.data_display": "Data display", "market.days_since_launch": "Days since launch", "market.edit_preferred_intervals": "Select preferred intervals", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Your watchlist is empty", "market.favorites_empty": "Your favorites is empty", "market.favorites_empty_desc": "Add your favorite tokens to the list", + "market.filter_chip_conditions_tips": "Hot tokens matching these conditions", + "market.filter_chip_large_cap": "Large-cap tokens", + "market.filter_chip_mid_cap": "Mid-cap tokens", + "market.filter_chip_top_turnover": "Top volume", + "market.filter_chip_top_turnover_tips": "Current hot list sorted by volume", + "market.filters": "Filters", + "market.filters_group_metrics": "Metrics", + "market.filters_time_frame": "Time frame", "market.heikin_ashi": "Heikin Ashi", "market.high": "High", "market.high_abbr": "H", + "market.holders_tips": "Number of addresses holding the token.", "market.indicators": "Indicators", "market.intervals": "Intervals", "market.last_price": "Last price", "market.last_seven_days": "Last 7 days", "market.last_updated": "Last updated", "market.line": "Line", + "market.liquidity_tips": "Total liquidity available in pools.", "market.low": "Low", "market.low_abbr": "L", "market.main_chart_indicators": "Main-chart indicators", + "market.market_cap_tips": "Circulating market value of the token.", "market.minus_2_percent_depth": "-2% depth", "market.most_24h_volume": "Most 24h volume", "market.most_market_cap": "Most market cap", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Sub-chart indicators", "market.trending": "Trending", "market.trust_score": "Trust core", + "market.turnover_tips": "Trading volume in the selected time range.", "market.twenty_four_hour_percentage": "24h%", "market.twenty_four_hour_volume": "24h volume", + "market.txns_tips": "Buy and sell transactions in the selected time range.", "market.volume_percentage": "Volume %", "market_preset.priority_fee_price_unit": "Priority fee price unit. A higher fee usually makes earlier confirmation more likely. Refer to the current real-time network fee when setting it.", "market_status.closed": "Closed", @@ -3333,8 +3354,9 @@ "perps__fee_discount_activated__msg": "Fee discount activated", "perps__snooze_remind_later__action": "Not now, remind me later", "perps_alerts__title": "Perps alerts", - "perps_back_to_default_layout__action": "Back to default layout", + "perps_back_to_default_layout__action": "Reset to default layout", "perps_chart_do_not_show__action": "Do not show", + "perps_desktop_resize_panels__desc": "Drag the divider to resize the chart and positions panels.", "perps_footer.help_us_better": "Perps Community", "perps_hide_other_pairs": "Hide other pairs", "perps_layout_reset__msg": "Layout has been reset", @@ -4656,11 +4678,15 @@ "terms_privacy": "By continuing, you confirm that you are 18+ and agree to our Terms & Privacy", "third_party_hw_adapter_not_registered__msg": "Third-party hardware adapter is not registered.", "title_403": "403 Forbidden", + "title_dust_sweep": "Dust sweep", "title_dust_sweep_completed": "Dust swept", + "title_dust_swept": "Dust swept", + "title_edit_slippage": "Edit slippage", "to_chain__title": "To chain", "toast.web_auth": "Set failed", "toast_create_address_failed_message": "Please select an account first", "toast_create_address_failed_title": "Unable to create address", + "toast_price_impact_exceeded_skipped": "Price impact exceeded cap — skipped", "token_manage.custom_token_address_failed": "Failed to find the contract address. Try again.", "token_no_search_results_desc": "No match found for your search. Please re-enter.", "token_selector.network": "Network:", diff --git a/packages/shared/src/locale/json/es.json b/packages/shared/src/locale/json/es.json index 22a5bbd48f9e..c964397551d4 100644 --- a/packages/shared/src/locale/json/es.json +++ b/packages/shared/src/locale/json/es.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "No se puede abrir el pago. por favor, inténtelo de nuevo desde Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC es lento para confirmar: los pedidos con un deslizamiento inferior al 1% pueden fallar", "btc_trade.set_to_1_percent": "Usar 1%", + "button_apply_slippage_all_future_orders": "Órdenes futuras", + "button_apply_slippage_this_order": "Esta orden", + "button_convert_tokens_estimated_amount": "Convertir {count} tokens (~{amount})", + "button_stop_conversion": "Detener", "buy_and_sell": "Compra/venta", "chase__action": "Seguir", "chase_order_confirmation__desc": "El seguimiento actualiza el precio de la orden al mejor precio actual para facilitar una ejecución más rápida.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Activos cubiertos", "kyt_supported_assets_load_error__desc": "No se pudieron cargar los activos compatibles. Comprueba tu conexión e inténtalo de nuevo", "kyt_view_report__action": "Ver informe", + "label_conversion_progress": "Progreso de conversión", "label_converted": "Convertido", "label_market_awaiting_open": "Por abrir", + "label_select_all": "Seleccionar todo", + "label_skipped": "Omitidos", "last_1_month__action": "El mes pasado", "last_3_months__action": "Últimos 3 meses", "learn_more_about_qr_code_wallet": "Más información sobre la billetera basada en códigos QR", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Eje Y", "market.chart_style": "Estilo del gráfico", "market.close_abbr": "C", + "market.column_name_token_age": "Nombre/Antigüedad del token", "market.data_display": "Visualización de datos", "market.days_since_launch": "Días desde el lanzamiento", "market.edit_preferred_intervals": "Selecciona los intervalos preferidos", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Tu lista de seguimiento está vacía", "market.favorites_empty": "Tus favoritos están vacíos", "market.favorites_empty_desc": "Agrega tus tokens favoritos a la lista", + "market.filter_chip_conditions_tips": "Tokens populares que cumplen estos criterios", + "market.filter_chip_large_cap": "Tokens de gran capitalización", + "market.filter_chip_mid_cap": "Tokens de mediana capitalización", + "market.filter_chip_top_turnover": "Mayor volumen", + "market.filter_chip_top_turnover_tips": "Lista de tendencias actual ordenada por volumen", + "market.filters": "Filtros", + "market.filters_group_metrics": "Métricas", + "market.filters_time_frame": "Periodo", "market.heikin_ashi": "Heikin Ashi", "market.high": "Alto", "market.high_abbr": "H", + "market.holders_tips": "Número de direcciones que poseen el token.", "market.indicators": "Indicadores", "market.intervals": "Intervalos", "market.last_price": "Último precio", "market.last_seven_days": "Últimos 7 días", "market.last_updated": "Última actualización", "market.line": "Línea", + "market.liquidity_tips": "Liquidez total disponible en los pools.", "market.low": "Bajo", "market.low_abbr": "L", "market.main_chart_indicators": "Indicadores del gráfico principal", + "market.market_cap_tips": "Valor de mercado circulante del token.", "market.minus_2_percent_depth": "-2% prof.", "market.most_24h_volume": "Mayor volumen en 24h", "market.most_market_cap": "La mayoría del capital de mercado", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Indicadores del subgráfico", "market.trending": "De tendencia", "market.trust_score": "Puntuación de confianza", + "market.turnover_tips": "Volumen de operaciones en el periodo seleccionado.", "market.twenty_four_hour_percentage": "24h%", "market.twenty_four_hour_volume": "Volumen de 24h", + "market.txns_tips": "Número de transacciones de compra y venta en el periodo seleccionado.", "market.volume_percentage": "Volumen %", "market_preset.priority_fee_price_unit": "Unidad de precio de tarifa prioritaria. Una tarifa más alta generalmente hace que la confirmación temprana sea más probable. Consulte la tarifa de red en tiempo real actual al configurarla.", "market_status.closed": "Cerrado", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Descuento de comisión activado", "perps__snooze_remind_later__action": "Ahora no, recuérdame más tarde", "perps_alerts__title": "Alertas de Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Restablecer el diseño predeterminado", + "perps_chart_do_not_show__action": "No mostrar", + "perps_desktop_resize_panels__desc": "Arrastra el separador para cambiar el tamaño de los paneles del gráfico y de posiciones.", "perps_footer.help_us_better": "Comunidad de Perps", "perps_hide_other_pairs": "Ocultar otros pares", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "El diseño se ha restablecido", + "perps_layout_settings__title": "Configuración del diseño", "perps_referral_campaign__desc": "¡Invita a amigos, gana 99U juntos. Escala en la clasificación y opta al gran premio!", "perps_spot_add_funds__title": "Añadir fondos", "perps_spot_buy_token_spot__action": "Comprar {token} al contado", "perps_spot_deposit_to_usdc__desc": "Deposita activos on-chain en tu cuenta", "perps_spot_deposit_to_usdc__title": "Depositar a USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Gráfico de velas de la página de trading", "perps_trigger_price_equal_current": "El precio de activación debe ser diferente al precio actual", "photo_library_access_denied__desc": "OneKey requiere acceso a la fototeca para guardar imágenes. Vaya a Ajustes y habilite los permisos de la fototeca.", "photo_library_access_denied__title": "Acceso a la fototeca denegado", @@ -4656,11 +4678,15 @@ "terms_privacy": "Al continuar, confirmas que tienes 18+ y aceptas nuestros Términos y Privacidad", "third_party_hw_adapter_not_registered__msg": "El adaptador de hardware de terceros no está registrado.", "title_403": "403 Prohibido", + "title_dust_sweep": "Conversión de dust", "title_dust_sweep_completed": "Polvo barrido", + "title_dust_swept": "Dust convertido", + "title_edit_slippage": "Editar tolerancia", "to_chain__title": "A la cadena", "toast.web_auth": "Error en la configuración", "toast_create_address_failed_message": "Por favor selecciona una cuenta primero", "toast_create_address_failed_title": "No se puede crear la dirección", + "toast_price_impact_exceeded_skipped": "Supera límite de impacto: omitido", "token_manage.custom_token_address_failed": "No se pudo encontrar la dirección del contrato. Inténtalo de nuevo. ", "token_no_search_results_desc": "No se encontró ninguna coincidencia para su búsqueda. Por favor, vuelva a ingresar.", "token_selector.network": "Red:", diff --git a/packages/shared/src/locale/json/fr_FR.json b/packages/shared/src/locale/json/fr_FR.json index 45f964eb95de..37cdaf1d36a8 100644 --- a/packages/shared/src/locale/json/fr_FR.json +++ b/packages/shared/src/locale/json/fr_FR.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Impossible d'ouvrir le paiement. veuillez réessayer depuis Bitrefill.", "btc_trade.btc_slow_to_confirm": "Le BTC est lent à confirmer — Les ordres avec un glissement inférieur à 1 % peuvent échouer", "btc_trade.set_to_1_percent": "Définir à 1 %", + "button_apply_slippage_all_future_orders": "Ordres à venir", + "button_apply_slippage_this_order": "Cet ordre", + "button_convert_tokens_estimated_amount": "Convertir {count} jetons (~{amount})", + "button_stop_conversion": "Arrêter", "buy_and_sell": "Acheter et vendre", "chase__action": "Suivre", "chase_order_confirmation__desc": "Le suivi actualise le prix de l’ordre au meilleur prix actuel afin de favoriser une exécution plus rapide.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Actifs couverts", "kyt_supported_assets_load_error__desc": "Impossible de charger les actifs pris en charge. Vérifiez votre connexion et réessayez", "kyt_view_report__action": "Voir le rapport", + "label_conversion_progress": "Conversion en cours", "label_converted": "Converti", "label_market_awaiting_open": "Préouverture", + "label_select_all": "Tout sélectionner", + "label_skipped": "Ignorés", "last_1_month__action": "Le mois dernier", "last_3_months__action": "3 derniers mois", "learn_more_about_qr_code_wallet": "En savoir plus sur le portefeuille à code QR", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Axe Y", "market.chart_style": "Style du graphique", "market.close_abbr": "C", + "market.column_name_token_age": "Nom/Ancienneté du token", "market.data_display": "Affichage des données", "market.days_since_launch": "Jours depuis le lancement", "market.edit_preferred_intervals": "Sélectionner les intervalles préférés", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Votre liste de surveillance est vide", "market.favorites_empty": "Vos favoris sont vides", "market.favorites_empty_desc": "Ajoutez vos jetons préférés à la liste", + "market.filter_chip_conditions_tips": "Tokens populaires correspondant à ces critères", + "market.filter_chip_large_cap": "Tokens à grande capitalisation", + "market.filter_chip_mid_cap": "Tokens à moyenne capitalisation", + "market.filter_chip_top_turnover": "Volume le plus élevé", + "market.filter_chip_top_turnover_tips": "Liste des tokens en vogue, triée par volume", + "market.filters": "Filtres", + "market.filters_group_metrics": "Indicateurs", + "market.filters_time_frame": "Période", "market.heikin_ashi": "Heikin Ashi", "market.high": "Haut", "market.high_abbr": "H", + "market.holders_tips": "Nombre d'adresses détenant le token.", "market.indicators": "Indicateurs", "market.intervals": "Intervalles", "market.last_price": "Dernier prix", "market.last_seven_days": "Les 7 derniers jours", "market.last_updated": "Dernière mise à jour", "market.line": "Ligne", + "market.liquidity_tips": "Liquidité totale disponible dans les pools.", "market.low": "Faible", "market.low_abbr": "L", "market.main_chart_indicators": "Indicateurs du graphique principal", + "market.market_cap_tips": "Valeur de marché en circulation du token.", "market.minus_2_percent_depth": "-2% prof.", "market.most_24h_volume": "Volume le plus élevé en 24h", "market.most_market_cap": "Plus forte capitalisation", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Indicateurs du sous-graphique", "market.trending": "En tendance", "market.trust_score": "Score de confiance", + "market.turnover_tips": "Volume d'échanges sur la période sélectionnée.", "market.twenty_four_hour_percentage": "24h%", "market.twenty_four_hour_volume": "Volume sur 24h", + "market.txns_tips": "Nombre de transactions d'achat et de vente sur la période sélectionnée.", "market.volume_percentage": "Volume %", "market_preset.priority_fee_price_unit": "Unité de prix des frais prioritaires. Un frais plus élevé augmente généralement la probabilité d'une confirmation plus rapide. Consultez les frais de réseau en temps réel actuels lors de sa définition.", "market_status.closed": "Fermé", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Réduction des frais activée", "perps__snooze_remind_later__action": "Pas maintenant, me rappeler plus tard", "perps_alerts__title": "Alertes Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Réinitialiser la disposition par défaut", + "perps_chart_do_not_show__action": "Ne pas afficher", + "perps_desktop_resize_panels__desc": "Faites glisser le séparateur pour redimensionner les panneaux du graphique et des positions.", "perps_footer.help_us_better": "Communauté Perps", "perps_hide_other_pairs": "Masquer les autres paires", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "La disposition a été réinitialisée", + "perps_layout_settings__title": "Paramètres de mise en page", "perps_referral_campaign__desc": "Invitez des amis et gagnez 99U ensemble. Montez dans le classement pour tenter de remporter le grand prix !", "perps_spot_add_funds__title": "Ajouter des fonds", "perps_spot_buy_token_spot__action": "Acheter le spot {token}", "perps_spot_deposit_to_usdc__desc": "Déposez des actifs on-chain sur votre compte", "perps_spot_deposit_to_usdc__title": "Déposer vers USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Graphique en chandeliers de la page de trading", "perps_trigger_price_equal_current": "Le prix déclencheur doit être différent du prix actuel", "photo_library_access_denied__desc": "OneKey nécessite l'accès à la photothèque pour enregistrer des images. Veuillez aller dans les Réglages et activer les autorisations de la photothèque.", "photo_library_access_denied__title": "Accès à la photothèque refusé", @@ -4656,11 +4678,15 @@ "terms_privacy": "En continuant, vous confirmez avoir 18 ans ou plus et acceptez nos Conditions et Politique de confidentialité", "third_party_hw_adapter_not_registered__msg": "L'adaptateur matériel tiers n'est pas enregistré.", "title_403": "403 Interdit", + "title_dust_sweep": "Petits soldes", "title_dust_sweep_completed": "Dust regroupée", + "title_dust_swept": "Soldes convertis", + "title_edit_slippage": "Modifier le slippage", "to_chain__title": "Chaîne de destination", "toast.web_auth": "Échec de l'installation", "toast_create_address_failed_message": "Veuillez d'abord sélectionner un compte", "toast_create_address_failed_title": "Impossible de créer une adresse", + "toast_price_impact_exceeded_skipped": "Impact prix trop élevé — ignoré", "token_manage.custom_token_address_failed": "Échec de la recherche de l'adresse du contrat. Essayez à nouveau. ", "token_no_search_results_desc": "Aucun résultat trouvé pour votre recherche. Veuillez réessayer.", "token_selector.network": "Réseau:", diff --git a/packages/shared/src/locale/json/hi_IN.json b/packages/shared/src/locale/json/hi_IN.json index 53f5a566fa01..5702ab1e3454 100644 --- a/packages/shared/src/locale/json/hi_IN.json +++ b/packages/shared/src/locale/json/hi_IN.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "भुगतान खोलने में असमर्थ। कृपया Bitrefill से पुनः प्रयास करें।", "btc_trade.btc_slow_to_confirm": "BTC की पुष्टि में देरी हो रही है — 1% स्लिपेज से कम के ऑर्डर विफल हो सकते हैं", "btc_trade.set_to_1_percent": "1% पर सेट करें", + "button_apply_slippage_all_future_orders": "सभी भावी ऑर्डर", + "button_apply_slippage_this_order": "यह ऑर्डर", + "button_convert_tokens_estimated_amount": "{count} टोकन बदलें (~{amount})", + "button_stop_conversion": "रोकें", "buy_and_sell": "खरीदें & बेचें", "chase__action": "चेज़", "chase_order_confirmation__desc": "चेज़ करने पर ऑर्डर की कीमत मौजूदा सर्वश्रेष्ठ कीमत पर अपडेट हो जाती है, जिससे ऑर्डर जल्दी पूरा हो सके।", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "कवर किए गए एसेट", "kyt_supported_assets_load_error__desc": "समर्थित एसेट लोड नहीं हो सके। कृपया अपना कनेक्शन जाँचें और फिर से कोशिश करें", "kyt_view_report__action": "रिपोर्ट देखें", + "label_conversion_progress": "कन्वर्ज़न की प्रगति", "label_converted": "कन्वर्ट किया गया", "label_market_awaiting_open": "खुलना बाकी", + "label_select_all": "सभी चुनें", + "label_skipped": "स्किप किए", "last_1_month__action": "पिछले महीने", "last_3_months__action": "पिछले 3 महीने", "learn_more_about_qr_code_wallet": "QR-based वॉलेट के बारे में और जानें", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y-अक्ष", "market.chart_style": "चार्ट स्टाइल", "market.close_abbr": "C", + "market.column_name_token_age": "नाम/टोकन आयु", "market.data_display": "डेटा डिस्प्ले", "market.days_since_launch": "लॉन्च के बाद से दिन", "market.edit_preferred_intervals": "पसंदीदा अंतराल चुनें", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "आपकी वॉचलिस्ट खाली है", "market.favorites_empty": "आपकी पसंदीदा सूची खाली है", "market.favorites_empty_desc": "अपनी पसंदीदा टोकन को सूची में जोड़ें", + "market.filter_chip_conditions_tips": "इन शर्तों को पूरा करने वाले लोकप्रिय टोकन", + "market.filter_chip_large_cap": "लार्ज-कैप टोकन", + "market.filter_chip_mid_cap": "मिड-कैप टोकन", + "market.filter_chip_top_turnover": "सर्वाधिक वॉल्यूम", + "market.filter_chip_top_turnover_tips": "मौजूदा हॉट लिस्ट, वॉल्यूम के अनुसार क्रमबद्ध", + "market.filters": "फ़िल्टर", + "market.filters_group_metrics": "मेट्रिक्स", + "market.filters_time_frame": "समयावधि", "market.heikin_ashi": "Heikin Ashi", "market.high": "उच्च", "market.high_abbr": "H", + "market.holders_tips": "टोकन रखने वाले पतों की संख्या।", "market.indicators": "इंडिकेटर", "market.intervals": "अंतराल", "market.last_price": "अंतिम मूल्य", "market.last_seven_days": "पिछले 7 दिन", "market.last_updated": "आखरी अपडेट", "market.line": "लाइन", + "market.liquidity_tips": "पूल में उपलब्ध कुल लिक्विडिटी।", "market.low": "निम्न", "market.low_abbr": "L", "market.main_chart_indicators": "मुख्य चार्ट इंडिकेटर", + "market.market_cap_tips": "टोकन का प्रचलनशील बाज़ार मूल्य।", "market.minus_2_percent_depth": "-2% गहराई", "market.most_24h_volume": "सबसे अधिक 24 घंटे की मात्रा", "market.most_market_cap": "सबसे अधिक मार्केट कैप", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "सब-चार्ट इंडिकेटर", "market.trending": "ट्रेंडिंग", "market.trust_score": "विश्वस्तता की परख", + "market.turnover_tips": "चुनी गई समयावधि में ट्रेडिंग वॉल्यूम।", "market.twenty_four_hour_percentage": "24 घंटे %", "market.twenty_four_hour_volume": "24 घंटे का वॉल्यूम", + "market.txns_tips": "चुनी गई समयावधि में खरीद और बिक्री लेनदेन की संख्या।", "market.volume_percentage": "ट्रेडिंग वॉल्यूम %", "market_preset.priority_fee_price_unit": "प्राथमिकता शुल्क मूल्य इकाई। अधिक शुल्क आमतौर पर पहले पुष्टि की संभावना को बढ़ाता है। इसे सेट करते समय वर्तमान वास्तविक समय नेटवर्क शुल्क का संदर्भ लें।", "market_status.closed": "बंद", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "शुल्क छूट सक्रिय हो गई", "perps__snooze_remind_later__action": "अभी नहीं, बाद में याद दिलाएं", "perps_alerts__title": "Perps अलर्ट", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "डिफ़ॉल्ट लेआउट पर रीसेट करें", + "perps_chart_do_not_show__action": "न दिखाएं", + "perps_desktop_resize_panels__desc": "चार्ट और पोज़िशन पैनल का आकार बदलने के लिए विभाजक को खींचें।", "perps_footer.help_us_better": "Perps समुदाय", "perps_hide_other_pairs": "अन्य पेयर छिपाएँ", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "लेआउट रीसेट कर दिया गया है", + "perps_layout_settings__title": "लेआउट सेटिंग्स", "perps_referral_campaign__desc": "दोस्तों को आमंत्रित करें, मिलकर 99U जीतें। लीडरबोर्ड पर चढ़ें और ग्रैंड प्राइज़ का मौका पाएं!", "perps_spot_add_funds__title": "फंड जोड़ें", "perps_spot_buy_token_spot__action": "{token} स्पॉट खरीदें", "perps_spot_deposit_to_usdc__desc": "अपने खाते में ऑन-चेन एसेट जमा करें", "perps_spot_deposit_to_usdc__title": "USDC में जमा करें", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "ट्रेडिंग पेज कैंडलस्टिक चार्ट", "perps_trigger_price_equal_current": "ट्रिगर मूल्य वर्तमान मूल्य से अलग होना चाहिए", "photo_library_access_denied__desc": "इमेज सेव करने के लिए OneKey को फ़ोटो लाइब्रेरी का एक्सेस चाहिए। कृपया सेटिंग्स में जाकर फ़ोटो लाइब्रेरी की अनुमति चालू करें।", "photo_library_access_denied__title": "फ़ोटो लाइब्रेरी एक्सेस अस्वीकृत", @@ -4656,11 +4678,15 @@ "terms_privacy": "आगे बढ़ते हुए, आप पुष्टि करते हैं कि आपकी आयु 18+ है और आप हमारे Terms और Privacy से सहमत हैं", "third_party_hw_adapter_not_registered__msg": "थर्ड-पार्टी हार्डवेयर अडैप्टर पंजीकृत नहीं है।", "title_403": "403 निषिद्ध", + "title_dust_sweep": "डस्ट स्वीप", "title_dust_sweep_completed": "डस्ट स्वीप पूरा हुआ", + "title_dust_swept": "डस्ट स्वीप पूरा", + "title_edit_slippage": "स्लिपेज बदलें", "to_chain__title": "लक्ष्य श्रृंखला", "toast.web_auth": "सेट करना विफल रहा", "toast_create_address_failed_message": "कृपया पहले एक खाता चुनें", "toast_create_address_failed_title": "पता बनाने में असमर्थ", + "toast_price_impact_exceeded_skipped": "प्राइस इम्पैक्ट सीमा पार—स्किप किया", "token_manage.custom_token_address_failed": "अनुबंध का पता लगाने में विफल रहे। पुनः प्रयास करें। ", "token_no_search_results_desc": "आपकी खोज के लिए कोई मिलान नहीं मिला। कृपया पुनः दर्ज करें।", "token_selector.network": "नेटवर्क:", diff --git a/packages/shared/src/locale/json/id.json b/packages/shared/src/locale/json/id.json index 5d9f318f1452..612cc1a35df6 100644 --- a/packages/shared/src/locale/json/id.json +++ b/packages/shared/src/locale/json/id.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Tidak dapat membuka pembayaran. Silakan coba lagi dari Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC lambat untuk dikonfirmasi — Pesanan di bawah 1% slippage mungkin gagal", "btc_trade.set_to_1_percent": "Atur ke 1%", + "button_apply_slippage_all_future_orders": "Order mendatang", + "button_apply_slippage_this_order": "Order ini", + "button_convert_tokens_estimated_amount": "Konversi {count} Token (~{amount})", + "button_stop_conversion": "Stop", "buy_and_sell": "Beli & jual", "chase__action": "Kejar", "chase_order_confirmation__desc": "Mengejar memperbarui harga order ke harga terbaik saat ini agar order lebih cepat terisi.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Aset yang dicakup", "kyt_supported_assets_load_error__desc": "Gagal memuat aset yang didukung. Periksa koneksi Anda lalu coba lagi", "kyt_view_report__action": "Lihat laporan", + "label_conversion_progress": "Progres konversi", "label_converted": "Dikonversi", "label_market_awaiting_open": "Belum Dibuka", + "label_select_all": "Pilih semua", + "label_skipped": "Dilewati", "last_1_month__action": "Bulan lalu", "last_3_months__action": "3 Bulan Terakhir", "learn_more_about_qr_code_wallet": "Pelajari lebih lanjut tentang dompet berbasis kode QR", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Sumbu Y", "market.chart_style": "Gaya grafik", "market.close_abbr": "C", + "market.column_name_token_age": "Nama/Usia token", "market.data_display": "Tampilan data", "market.days_since_launch": "Hari sejak peluncuran", "market.edit_preferred_intervals": "Pilih interval pilihan", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Daftar pantauan Anda kosong", "market.favorites_empty": "Favorit Anda kosong", "market.favorites_empty_desc": "Tambahkan token favorit Anda ke daftar", + "market.filter_chip_conditions_tips": "Token populer yang memenuhi kriteria ini", + "market.filter_chip_large_cap": "Token kapitalisasi besar", + "market.filter_chip_mid_cap": "Token kapitalisasi menengah", + "market.filter_chip_top_turnover": "Volume tertinggi", + "market.filter_chip_top_turnover_tips": "Daftar token populer saat ini, diurutkan berdasarkan volume", + "market.filters": "Filter", + "market.filters_group_metrics": "Metrik", + "market.filters_time_frame": "Rentang waktu", "market.heikin_ashi": "Heikin Ashi", "market.high": "Tinggi", "market.high_abbr": "H", + "market.holders_tips": "Jumlah alamat yang memegang token ini.", "market.indicators": "Indikator", "market.intervals": "Interval", "market.last_price": "Harga terakhir", "market.last_seven_days": "7 hari terakhir", "market.last_updated": "Terakhir diperbarui", "market.line": "Garis", + "market.liquidity_tips": "Total likuiditas yang tersedia di pool.", "market.low": "Rendah", "market.low_abbr": "L", "market.main_chart_indicators": "Indikator grafik utama", + "market.market_cap_tips": "Nilai pasar token yang beredar.", "market.minus_2_percent_depth": "Kedalaman -2%", "market.most_24h_volume": "Volume terbanyak dalam 24 jam", "market.most_market_cap": "Kapitalisasi pasar terbesar", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Indikator subgrafik", "market.trending": "Tren", "market.trust_score": "Skor kepercayaan", + "market.turnover_tips": "Nilai transaksi dalam rentang waktu yang dipilih.", "market.twenty_four_hour_percentage": "24j%", "market.twenty_four_hour_volume": "Volume 24 jam", + "market.txns_tips": "Jumlah transaksi beli dan jual dalam rentang waktu yang dipilih.", "market.volume_percentage": "Volume %", "market_preset.priority_fee_price_unit": "Unit harga biaya prioritas. Biaya yang lebih tinggi biasanya membuat konfirmasi lebih awal lebih mungkin terjadi. Lihat biaya jaringan waktu nyata saat ini saat mengaturnya.", "market_status.closed": "Tutup", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Diskon biaya diaktifkan", "perps__snooze_remind_later__action": "Nanti saja, ingatkan lagi nanti", "perps_alerts__title": "Peringatan Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Atur ulang ke tata letak default", + "perps_chart_do_not_show__action": "Jangan tampilkan", + "perps_desktop_resize_panels__desc": "Seret pemisah untuk mengubah ukuran panel grafik dan posisi.", "perps_footer.help_us_better": "Komunitas Perps", "perps_hide_other_pairs": "Sembunyikan pasangan lain", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "Tata letak telah diatur ulang", + "perps_layout_settings__title": "Pengaturan tata letak", "perps_referral_campaign__desc": "Undang teman, menangkan 99U bersama. Daki papan peringkat untuk kesempatan meraih hadiah utama!", "perps_spot_add_funds__title": "Tambahkan dana", "perps_spot_buy_token_spot__action": "Beli Spot {token}", "perps_spot_deposit_to_usdc__desc": "Deposit aset on-chain ke akun Anda", "perps_spot_deposit_to_usdc__title": "Setor ke USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Grafik candlestick halaman trading", "perps_trigger_price_equal_current": "Harga trigger harus berbeda dari harga saat ini", "photo_library_access_denied__desc": "OneKey memerlukan akses ke perpustakaan foto untuk menyimpan gambar. Silakan buka Pengaturan dan aktifkan izin perpustakaan foto.", "photo_library_access_denied__title": "Akses ke Perpustakaan Foto Ditolak", @@ -4656,11 +4678,15 @@ "terms_privacy": "Dengan melanjutkan, Anda menyatakan bahwa Anda berusia 18+ dan menyetujui Ketentuan & Privasi kami", "third_party_hw_adapter_not_registered__msg": "Adaptor perangkat keras pihak ketiga belum terdaftar.", "title_403": "403 Dilarang", + "title_dust_sweep": "Konversi Dust", "title_dust_sweep_completed": "Dust berhasil dikumpulkan", + "title_dust_swept": "Dust dikonversi", + "title_edit_slippage": "Edit slippage", "to_chain__title": "Ke chain", "toast.web_auth": "Pengaturan gagal", "toast_create_address_failed_message": "Silakan pilih akun terlebih dahulu", "toast_create_address_failed_title": "Tidak dapat membuat alamat", + "toast_price_impact_exceeded_skipped": "Dampak harga > batas — dilewati", "token_manage.custom_token_address_failed": "Gagal menemukan alamat kontrak. Coba lagi. ", "token_no_search_results_desc": "Tidak ada hasil yang cocok dengan pencarian Anda. Silakan masukkan kembali.", "token_selector.network": "Jaringan:", diff --git a/packages/shared/src/locale/json/it_IT.json b/packages/shared/src/locale/json/it_IT.json index 031179cf5df2..6ed0144b72a9 100644 --- a/packages/shared/src/locale/json/it_IT.json +++ b/packages/shared/src/locale/json/it_IT.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Impossibile aprire il pagamento. Si prega di riprovare da Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC è lento a confermare — Gli ordini con slippage inferiore all'1% potrebbero fallire", "btc_trade.set_to_1_percent": "Imposta all'1%", + "button_apply_slippage_all_future_orders": "Ordini futuri", + "button_apply_slippage_this_order": "Questo ordine", + "button_convert_tokens_estimated_amount": "Converti {count} token (~{amount})", + "button_stop_conversion": "Ferma", "buy_and_sell": "Compra/vendi", "chase__action": "Insegui", "chase_order_confirmation__desc": "L’inseguimento aggiorna il prezzo dell’ordine al miglior prezzo attuale per favorire un’esecuzione più rapida.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Asset coperti", "kyt_supported_assets_load_error__desc": "Impossibile caricare gli asset supportati. Controlla la connessione e riprova", "kyt_view_report__action": "Visualizza report", + "label_conversion_progress": "Conversione in corso", "label_converted": "Convertito", "label_market_awaiting_open": "Preapertura", + "label_select_all": "Seleziona tutto", + "label_skipped": "Saltati", "last_1_month__action": "Lo scorso mese", "last_3_months__action": "Ultimi 3 mesi", "learn_more_about_qr_code_wallet": "Scopri di più sul portafoglio basato su codice QR", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Asse Y", "market.chart_style": "Stile del grafico", "market.close_abbr": "C", + "market.column_name_token_age": "Nome/Età del token", "market.data_display": "Visualizzazione dei dati", "market.days_since_launch": "Giorni dal lancio", "market.edit_preferred_intervals": "Seleziona gli intervalli preferiti", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "La tua lista di osservazione è vuota", "market.favorites_empty": "I tuoi preferiti sono vuoti", "market.favorites_empty_desc": "Aggiungi i tuoi token preferiti alla lista", + "market.filter_chip_conditions_tips": "Token popolari che soddisfano questi criteri", + "market.filter_chip_large_cap": "Token a grande capitalizzazione", + "market.filter_chip_mid_cap": "Token a media capitalizzazione", + "market.filter_chip_top_turnover": "Volume più alto", + "market.filter_chip_top_turnover_tips": "Elenco dei token di tendenza, ordinato per volume", + "market.filters": "Filtri", + "market.filters_group_metrics": "Metriche", + "market.filters_time_frame": "Periodo", "market.heikin_ashi": "Heikin Ashi", "market.high": "Alto", "market.high_abbr": "H", + "market.holders_tips": "Numero di indirizzi che detengono il token.", "market.indicators": "Indicatori", "market.intervals": "Intervalli", "market.last_price": "Ultimo prezzo", "market.last_seven_days": "Ultimi 7 giorni", "market.last_updated": "Ultimo aggiornamento", "market.line": "Linea", + "market.liquidity_tips": "Liquidità totale disponibile nei pool.", "market.low": "Basso", "market.low_abbr": "L", "market.main_chart_indicators": "Indicatori del grafico principale", + "market.market_cap_tips": "Capitalizzazione di mercato circolante del token.", "market.minus_2_percent_depth": "-2% prof.", "market.most_24h_volume": "Volume massimo in 24 ore", "market.most_market_cap": "Capitalizzazione più alta", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Indicatori del sottografico", "market.trending": "In tendenza", "market.trust_score": "Punteggio di affidabilità", + "market.turnover_tips": "Volume di scambi nel periodo selezionato.", "market.twenty_four_hour_percentage": "24h%", "market.twenty_four_hour_volume": "Volume 24h", + "market.txns_tips": "Numero di transazioni di acquisto e vendita nel periodo selezionato.", "market.volume_percentage": "Volume %", "market_preset.priority_fee_price_unit": "Unità di prezzo della commissione prioritaria. Una commissione più alta di solito rende più probabile una conferma anticipata. Fare riferimento alla commissione di rete in tempo reale attuale quando la si imposta.", "market_status.closed": "Chiuso", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Sconto commissioni attivato", "perps__snooze_remind_later__action": "Non ora, ricordamelo dopo", "perps_alerts__title": "Avvisi Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Ripristina il layout predefinito", + "perps_chart_do_not_show__action": "Non mostrare", + "perps_desktop_resize_panels__desc": "Trascina il divisore per ridimensionare i pannelli del grafico e delle posizioni.", "perps_footer.help_us_better": "Community Perps", "perps_hide_other_pairs": "Nascondi altre coppie", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "Il layout è stato ripristinato", + "perps_layout_settings__title": "Impostazioni del layout", "perps_referral_campaign__desc": "Invita amici, vinci 99U insieme. Scala la classifica per avere una chance al premio principale!", "perps_spot_add_funds__title": "Aggiungi fondi", "perps_spot_buy_token_spot__action": "Acquista Spot {token}", "perps_spot_deposit_to_usdc__desc": "Deposita asset on-chain nel tuo account", "perps_spot_deposit_to_usdc__title": "Deposita in USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Grafico a candele della pagina di trading", "perps_trigger_price_equal_current": "Il prezzo trigger deve differire dal prezzo attuale", "photo_library_access_denied__desc": "OneKey richiede l'accesso alla libreria foto per salvare le immagini. Vai su Impostazioni e abilita i permessi della libreria foto.", "photo_library_access_denied__title": "Accesso alla libreria foto negato", @@ -4656,11 +4678,15 @@ "terms_privacy": "Continuando, confermi di avere 18+ anni e di accettare i nostri Termini e Privacy", "third_party_hw_adapter_not_registered__msg": "L'adattatore hardware di terze parti non è registrato.", "title_403": "403 Proibito", + "title_dust_sweep": "Converti dust", "title_dust_sweep_completed": "Residui raccolti", + "title_dust_swept": "Dust convertita", + "title_edit_slippage": "Modifica slippage", "to_chain__title": "Chain di destinazione", "toast.web_auth": "Impostazione fallita", "toast_create_address_failed_message": "Per favore seleziona prima un account", "toast_create_address_failed_title": "Impossibile creare indirizzo", + "toast_price_impact_exceeded_skipped": "Impatto prezzo eccessivo — saltato", "token_manage.custom_token_address_failed": "Impossibile trovare l'indirizzo del contratto. Prova di nuovo. ", "token_no_search_results_desc": "Non sono stati trovati risultati per la tua ricerca. Si prega di reinserire.", "token_selector.network": "Rete:", diff --git a/packages/shared/src/locale/json/ja_JP.json b/packages/shared/src/locale/json/ja_JP.json index 37e5acd62517..03a4448d008b 100644 --- a/packages/shared/src/locale/json/ja_JP.json +++ b/packages/shared/src/locale/json/ja_JP.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "支払いを開けません。Bitrefillから再試行してください。", "btc_trade.btc_slow_to_confirm": "BTC の確認が遅い — 1% 未満のスリッページの注文は失敗する可能性があります", "btc_trade.set_to_1_percent": "1% に設定", + "button_apply_slippage_all_future_orders": "今後の全注文", + "button_apply_slippage_this_order": "この注文のみ", + "button_convert_tokens_estimated_amount": "{count}トークンを変換(約{amount})", + "button_stop_conversion": "停止", "buy_and_sell": "購入と販売", "chase__action": "追跡", "chase_order_confirmation__desc": "追跡すると注文価格が現在の最良価格に更新され、注文がより早く約定しやすくなります。", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "対象資産", "kyt_supported_assets_load_error__desc": "対応アセットを読み込めませんでした。接続を確認して再試行してください", "kyt_view_report__action": "レポートを表示", + "label_conversion_progress": "変換の進捗", "label_converted": "変換済み", "label_market_awaiting_open": "取引開始待ち", + "label_select_all": "すべて選択", + "label_skipped": "スキップ", "last_1_month__action": "先月", "last_3_months__action": "過去3か月", "learn_more_about_qr_code_wallet": "QRコードウォレットの詳細を見る", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y 軸", "market.chart_style": "チャートスタイル", "market.close_abbr": "終", + "market.column_name_token_age": "名称/作成日時", "market.data_display": "データ表示", "market.days_since_launch": "発売からの日数", "market.edit_preferred_intervals": "お気に入りの時間足を選択", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "あなたのウォッチリストは空です", "market.favorites_empty": "お気に入りは空です", "market.favorites_empty_desc": "お気に入りのトークンをリストに追加してください", + "market.filter_chip_conditions_tips": "これらの条件に一致する人気トークン", + "market.filter_chip_large_cap": "大型時価総額トークン", + "market.filter_chip_mid_cap": "中型時価総額トークン", + "market.filter_chip_top_turnover": "出来高上位", + "market.filter_chip_top_turnover_tips": "現在の人気リストを出来高順に表示", + "market.filters": "フィルター", + "market.filters_group_metrics": "指標", + "market.filters_time_frame": "期間", "market.heikin_ashi": "平均足", "market.high": "高", "market.high_abbr": "高", + "market.holders_tips": "そのトークンを保有しているアドレスの数。", "market.indicators": "インジケーター", "market.intervals": "時間足", "market.last_price": "最終価格", "market.last_seven_days": "過去7日間", "market.last_updated": "最終更新日", "market.line": "ライン", + "market.liquidity_tips": "流動性プール内で利用できる流動性の合計。", "market.low": "低", "market.low_abbr": "安", "market.main_chart_indicators": "メインチャートインジケーター", + "market.market_cap_tips": "トークンの流通時価総額。", "market.minus_2_percent_depth": "深さ -2%", "market.most_24h_volume": "最も24時間の取引量", "market.most_market_cap": "最大の時価総額", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "サブチャートインジケーター", "market.trending": "トレンド", "market.trust_score": "信頼スコア", + "market.turnover_tips": "選択した期間内の取引高。", "market.twenty_four_hour_percentage": "24時間%", "market.twenty_four_hour_volume": "24時間の取引量", + "market.txns_tips": "選択した期間内の買いと売りの取引件数。", "market.volume_percentage": "取引量 %", "market_preset.priority_fee_price_unit": "優先手数料単価。手数料が高いほど、早期に確認される可能性が高くなります。設定時には現在のリアルタイムネットワーク手数料を参照してください。", "market_status.closed": "クローズ", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "手数料割引が有効になりました", "perps__snooze_remind_later__action": "今はいい、後でもう一度", "perps_alerts__title": "Perpsアラート", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "デフォルトのレイアウトにリセット", + "perps_chart_do_not_show__action": "表示しない", + "perps_desktop_resize_panels__desc": "区切り線をドラッグして、チャートとポジションのパネルサイズを変更できます。", "perps_footer.help_us_better": "Perpsコミュニティ", "perps_hide_other_pairs": "他のペアを非表示", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "レイアウトをリセットしました", + "perps_layout_settings__title": "レイアウト設定", "perps_referral_campaign__desc": "友達を招待して、一緒に 99U を当てよう。ランキングを上げてグランドプライズを狙え!", "perps_spot_add_funds__title": "資金を追加", "perps_spot_buy_token_spot__action": "{token} 現物を購入", "perps_spot_deposit_to_usdc__desc": "オンチェーン資産を口座に入金", "perps_spot_deposit_to_usdc__title": "USDC に入金", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "取引ページのローソク足チャート", "perps_trigger_price_equal_current": "トリガー価格は現在の価格と異なる必要があります", "photo_library_access_denied__desc": "OneKey で画像を保存するには、フォトライブラリへのアクセスが必要です。設定に移動して、フォトライブラリの権限を有効にしてください。", "photo_library_access_denied__title": "フォトライブラリへのアクセスが拒否されました", @@ -4656,11 +4678,15 @@ "terms_privacy": "続行すると、18歳以上であることを確認し、利用規約プライバシーに同意したものとみなされます", "third_party_hw_adapter_not_registered__msg": "サードパーティのハードウェアアダプターが登録されていません。", "title_403": "403 アクセス禁止", + "title_dust_sweep": "少額トークン変換", "title_dust_sweep_completed": "少額資産の変換完了", + "title_dust_swept": "少額トークンを変換しました", + "title_edit_slippage": "スリッページを編集", "to_chain__title": "送信先チェーン", "toast.web_auth": "設定に失敗しました", "toast_create_address_failed_message": "最初にアカウントを選択してください", "toast_create_address_failed_title": "アドレスを作成できません", + "toast_price_impact_exceeded_skipped": "価格影響が上限を超えたためスキップ", "token_manage.custom_token_address_failed": "コントラクトアドレスが見つかりませんでした。もう一度お試しください。", "token_no_search_results_desc": "検索に一致する結果が見つかりませんでした。入力し直してください。", "token_selector.network": "ネットワーク:", diff --git a/packages/shared/src/locale/json/ko_KR.json b/packages/shared/src/locale/json/ko_KR.json index 8f708f03f731..f69f40817da4 100644 --- a/packages/shared/src/locale/json/ko_KR.json +++ b/packages/shared/src/locale/json/ko_KR.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "결제할 수 없습니다. Bitrefill에서 다시 시도해 주세요.", "btc_trade.btc_slow_to_confirm": "BTC 확인이 느림 — 1% 미만의 슬리피지 주문은 실패할 수 있음", "btc_trade.set_to_1_percent": "1%로 설정", + "button_apply_slippage_all_future_orders": "향후 모든 주문", + "button_apply_slippage_this_order": "이번 주문", + "button_convert_tokens_estimated_amount": "토큰 {count}개 전환 (~{amount})", + "button_stop_conversion": "중지", "buy_and_sell": "구매·판매", "chase__action": "추적", "chase_order_confirmation__desc": "추적하면 주문 가격이 현재 최적가로 업데이트되어 주문이 더 빠르게 체결될 수 있습니다.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "지원되는 자산", "kyt_supported_assets_load_error__desc": "지원되는 자산을 불러오지 못했습니다. 연결을 확인하고 다시 시도하세요", "kyt_view_report__action": "보고서 보기", + "label_conversion_progress": "전환 진행률", "label_converted": "전환 완료", "label_market_awaiting_open": "개장 대기", + "label_select_all": "전체 선택", + "label_skipped": "건너뜀", "last_1_month__action": "지난달", "last_3_months__action": "최근 3개월", "learn_more_about_qr_code_wallet": "QR 코드 지갑 자세히 알아보기", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y축", "market.chart_style": "차트 스타일", "market.close_abbr": "종", + "market.column_name_token_age": "이름/생성 시간", "market.data_display": "데이터 표시", "market.days_since_launch": "출시 이후 일수", "market.edit_preferred_intervals": "선호하는 간격 선택", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "관심목록이 비어 있네요", "market.favorites_empty": "즐겨찾기가 비어 있습니다", "market.favorites_empty_desc": "목록에 좋아하는 토큰을 추가하세요", + "market.filter_chip_conditions_tips": "이 조건에 해당하는 인기 토큰", + "market.filter_chip_large_cap": "대형 시가총액 토큰", + "market.filter_chip_mid_cap": "중형 시가총액 토큰", + "market.filter_chip_top_turnover": "거래량 상위", + "market.filter_chip_top_turnover_tips": "현재 인기 목록을 거래량순으로 정렬", + "market.filters": "필터", + "market.filters_group_metrics": "지표", + "market.filters_time_frame": "기간", "market.heikin_ashi": "헤이킨 아시", "market.high": "높음", "market.high_abbr": "고", + "market.holders_tips": "해당 토큰을 보유한 주소 수입니다.", "market.indicators": "지표", "market.intervals": "간격", "market.last_price": "마지막 가격", "market.last_seven_days": "지난 7일", "market.last_updated": "마지막 업데이트", "market.line": "라인", + "market.liquidity_tips": "풀에서 이용 가능한 총 유동성입니다.", "market.low": "낮음", "market.low_abbr": "저", "market.main_chart_indicators": "메인 차트 지표", + "market.market_cap_tips": "토큰의 유통 시가총액입니다.", "market.minus_2_percent_depth": "-2% 깊이", "market.most_24h_volume": "가장 많은 24시간 거래량", "market.most_market_cap": "시가총액이 가장 많은", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "보조 차트 지표", "market.trending": "트렌딩", "market.trust_score": "신용점수", + "market.turnover_tips": "선택한 기간의 거래대금입니다.", "market.twenty_four_hour_percentage": "24시간%", "market.twenty_four_hour_volume": "24시간 거래량", + "market.txns_tips": "선택한 기간의 매수 및 매도 거래 수입니다.", "market.volume_percentage": "거래량 %", "market_preset.priority_fee_price_unit": "우선 수수료 가격 단위. 더 높은 수수료는 일반적으로 더 빠른 확인을 가능하게 합니다. 설정 시 현재 실시간 네트워크 수수료를 참조하세요.", "market_status.closed": "마감", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "수수료 할인이 적용되었습니다", "perps__snooze_remind_later__action": "지금은 괜찮아요, 나중에 다시 알려주세요", "perps_alerts__title": "Perps 알림", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "기본 레이아웃으로 재설정", + "perps_chart_do_not_show__action": "표시 안 함", + "perps_desktop_resize_panels__desc": "구분선을 드래그하여 차트 및 포지션 패널의 크기를 조정하세요.", "perps_footer.help_us_better": "Perps 커뮤니티", "perps_hide_other_pairs": "다른 페어 숨기기", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "레이아웃이 재설정되었습니다", + "perps_layout_settings__title": "레이아웃 설정", "perps_referral_campaign__desc": "친구를 초대하고 함께 99U를 받아가세요. 리더보드를 올라 대상을 노려보세요!", "perps_spot_add_funds__title": "자금 추가", "perps_spot_buy_token_spot__action": "{token} 현물 구매", "perps_spot_deposit_to_usdc__desc": "온체인 자산을 계정에 입금", "perps_spot_deposit_to_usdc__title": "USDC로 입금", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "거래 페이지 캔들스틱 차트", "perps_trigger_price_equal_current": "트리거 가격은 현재 가격과 달라야 합니다", "photo_library_access_denied__desc": "OneKey는 이미지를 저장하려면 사진 보관함 접근 권한이 필요합니다. 설정으로 이동하여 사진 보관함 권한을 활성화해 주세요.", "photo_library_access_denied__title": "사진 보관함 접근이 거부되었습니다", @@ -4656,11 +4678,15 @@ "terms_privacy": "계속하면 본인이 18세 이상이며 이용약관개인정보 처리방침에 동의함을 확인합니다", "third_party_hw_adapter_not_registered__msg": "서드파티 하드웨어 어댑터가 등록되지 않았습니다.", "title_403": "403 금지", + "title_dust_sweep": "소액 토큰 정리", "title_dust_sweep_completed": "소액 자산 정리 완료", + "title_dust_swept": "소액 토큰 정리 완료", + "title_edit_slippage": "슬리피지 수정", "to_chain__title": "도착 체인", "toast.web_auth": "설정 실패", "toast_create_address_failed_message": "먼저 계정을 선택하세요", "toast_create_address_failed_title": "주소를 생성할 수 없습니다", + "toast_price_impact_exceeded_skipped": "가격 영향 한도 초과로 건너뜀", "token_manage.custom_token_address_failed": "컨트랙트 주소를 찾을 수 없습니다. 다시 시도해 주세요.", "token_no_search_results_desc": "검색 결과가 없습니다. 다시 입력해 주세요.", "token_selector.network": "네트워크:", diff --git a/packages/shared/src/locale/json/pt.json b/packages/shared/src/locale/json/pt.json index b85d70c1c002..e849cd0cadd8 100644 --- a/packages/shared/src/locale/json/pt.json +++ b/packages/shared/src/locale/json/pt.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Impossível abrir o pagamento. Por favor, tente novamente a partir do Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC é lento para confirmar — Pedidos com menos de 1% de slippage podem falhar", "btc_trade.set_to_1_percent": "Usar 1%", + "button_apply_slippage_all_future_orders": "Ordens futuras", + "button_apply_slippage_this_order": "Esta ordem", + "button_convert_tokens_estimated_amount": "Converter {count} (~{amount})", + "button_stop_conversion": "Parar", "buy_and_sell": "Compra/venda", "chase__action": "Acompanhar", "chase_order_confirmation__desc": "O acompanhamento atualiza o preço da ordem para o melhor preço atual, ajudando a executá-la mais rapidamente.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Ativos abrangidos", "kyt_supported_assets_load_error__desc": "Falha ao carregar os ativos suportados. Verifique a ligação e tente novamente", "kyt_view_report__action": "Ver relatório", + "label_conversion_progress": "Progresso da conversão", "label_converted": "Convertido", "label_market_awaiting_open": "Pré-abertura", + "label_select_all": "Selecionar tudo", + "label_skipped": "Ignorados", "last_1_month__action": "Mês passado", "last_3_months__action": "Últimos 3 meses", "learn_more_about_qr_code_wallet": "Saiba mais sobre a carteira baseada em QR", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Eixo Y", "market.chart_style": "Estilo do gráfico", "market.close_abbr": "C", + "market.column_name_token_age": "Nome/Antiguidade do token", "market.data_display": "Apresentação de dados", "market.days_since_launch": "Dias desde o lançamento", "market.edit_preferred_intervals": "Selecionar intervalos preferidos", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "A sua lista de observação está vazia", "market.favorites_empty": "Seus favoritos estão vazios", "market.favorites_empty_desc": "Adicione seus tokens favoritos à lista", + "market.filter_chip_conditions_tips": "Tokens populares que cumprem estes critérios", + "market.filter_chip_large_cap": "Tokens de grande capitalização", + "market.filter_chip_mid_cap": "Tokens de média capitalização", + "market.filter_chip_top_turnover": "Maior volume", + "market.filter_chip_top_turnover_tips": "Lista de tendências atual ordenada por volume", + "market.filters": "Filtros", + "market.filters_group_metrics": "Métricas", + "market.filters_time_frame": "Período", "market.heikin_ashi": "Heikin Ashi", "market.high": "Alto", "market.high_abbr": "H", + "market.holders_tips": "Número de endereços que detêm o token.", "market.indicators": "Indicadores", "market.intervals": "Intervalos", "market.last_price": "Último preço", "market.last_seven_days": "Últimos 7 dias", "market.last_updated": "Última atualização", "market.line": "Linha", + "market.liquidity_tips": "Liquidez total disponível nos pools.", "market.low": "Baixo", "market.low_abbr": "L", "market.main_chart_indicators": "Indicadores do gráfico principal", + "market.market_cap_tips": "Valor de mercado em circulação do token.", "market.minus_2_percent_depth": "-2% prof.", "market.most_24h_volume": "Maior volume em 24h", "market.most_market_cap": "Maior capitalização de mercado", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Indicadores do subgráfico", "market.trending": "Em alta", "market.trust_score": "Pontuação de crédito", + "market.turnover_tips": "Volume de negociação no período selecionado.", "market.twenty_four_hour_percentage": "24h%", "market.twenty_four_hour_volume": "Volume de 24h", + "market.txns_tips": "Número de transações de compra e venda no período selecionado.", "market.volume_percentage": "Volume %", "market_preset.priority_fee_price_unit": "Unidade de preço da taxa prioritária. Uma taxa mais alta geralmente torna a confirmação antecipada mais provável. Consulte a taxa de rede em tempo real atual ao defini-la.", "market_status.closed": "Fechado", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Desconto de taxa ativado", "perps__snooze_remind_later__action": "Agora não, lembrar mais tarde", "perps_alerts__title": "Alertas de Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Repor o layout predefinido", + "perps_chart_do_not_show__action": "Não mostrar", + "perps_desktop_resize_panels__desc": "Arraste o separador para redimensionar os painéis do gráfico e das posições.", "perps_footer.help_us_better": "Comunidade Perps", "perps_hide_other_pairs": "Ocultar outros pares", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "O layout foi reposto", + "perps_layout_settings__title": "Definições de layout", "perps_referral_campaign__desc": "Convida amigos, ganhem 99U juntos. Sobe no ranking e concorre ao grande prémio!", "perps_spot_add_funds__title": "Adicionar fundos", "perps_spot_buy_token_spot__action": "Comprar spot de {token}", "perps_spot_deposit_to_usdc__desc": "Deposite ativos on-chain na sua conta", "perps_spot_deposit_to_usdc__title": "Depositar em USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Gráfico de velas da página de negociação", "perps_trigger_price_equal_current": "O preço de gatilho deve ser diferente do preço atual", "photo_library_access_denied__desc": "O OneKey requer acesso à biblioteca de fotografias para guardar imagens. Vá a Definições e ative as permissões da biblioteca de fotografias.", "photo_library_access_denied__title": "Acesso à biblioteca de fotografias negado", @@ -4656,11 +4678,15 @@ "terms_privacy": "Ao continuar, confirma que tem 18 anos ou mais e concorda com os nossos Termos e Privacidade", "third_party_hw_adapter_not_registered__msg": "O adaptador de hardware de terceiros não está registado.", "title_403": "403 Proibido", + "title_dust_sweep": "Limpar resíduos", "title_dust_sweep_completed": "Dust coletado", + "title_dust_swept": "Resíduos limpos", + "title_edit_slippage": "Editar slippage", "to_chain__title": "Para a cadeia", "toast.web_auth": "Falha na definição", "toast_create_address_failed_message": "Por favor, selecione uma conta primeiro", "toast_create_address_failed_title": "Incapaz de criar endereço", + "toast_price_impact_exceeded_skipped": "Impacto acima do limite — ignorado", "token_manage.custom_token_address_failed": "Falha ao encontrar o endereço do contrato. Tente novamente. ", "token_no_search_results_desc": "Nenhuma correspondência encontrada para a sua pesquisa. Por favor, digite novamente.", "token_selector.network": "Rede:", diff --git a/packages/shared/src/locale/json/pt_BR.json b/packages/shared/src/locale/json/pt_BR.json index 5a4783a93f25..1c65930f89df 100644 --- a/packages/shared/src/locale/json/pt_BR.json +++ b/packages/shared/src/locale/json/pt_BR.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Não foi possível abrir o pagamento. Por favor, tente novamente a partir do Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC é lento para confirmar — Pedidos com desvio abaixo de 1% podem falhar", "btc_trade.set_to_1_percent": "Usar 1%", + "button_apply_slippage_all_future_orders": "Ordens futuras", + "button_apply_slippage_this_order": "Esta ordem", + "button_convert_tokens_estimated_amount": "Converter {count} tokens (~{amount})", + "button_stop_conversion": "Parar", "buy_and_sell": "Compra/venda", "chase__action": "Chase", "chase_order_confirmation__desc": "O chase atualiza o preço da ordem para o melhor preço atual, ajudando a executá-la mais rapidamente.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Ativos cobertos", "kyt_supported_assets_load_error__desc": "Falha ao carregar ativos compatíveis. Verifique sua conexão e tente novamente", "kyt_view_report__action": "Ver relatório", + "label_conversion_progress": "Progresso da conversão", "label_converted": "Convertido", "label_market_awaiting_open": "Pré-abertura", + "label_select_all": "Selecionar tudo", + "label_skipped": "Ignorados", "last_1_month__action": "Mês passado", "last_3_months__action": "Últimos 3 meses", "learn_more_about_qr_code_wallet": "Saiba mais sobre a carteira baseada em QR", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Eixo Y", "market.chart_style": "Estilo do gráfico", "market.close_abbr": "C", + "market.column_name_token_age": "Nome/Idade do token", "market.data_display": "Exibição de dados", "market.days_since_launch": "Dias desde o lançamento", "market.edit_preferred_intervals": "Selecionar intervalos preferidos", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Sua lista de observação está vazia", "market.favorites_empty": "Seus favoritos estão vazios", "market.favorites_empty_desc": "Adicione seus tokens favoritos à lista", + "market.filter_chip_conditions_tips": "Tokens populares que atendem a esses critérios", + "market.filter_chip_large_cap": "Tokens de grande capitalização", + "market.filter_chip_mid_cap": "Tokens de média capitalização", + "market.filter_chip_top_turnover": "Maior volume", + "market.filter_chip_top_turnover_tips": "Lista de tendências atual ordenada por volume", + "market.filters": "Filtros", + "market.filters_group_metrics": "Métricas", + "market.filters_time_frame": "Período", "market.heikin_ashi": "Heikin Ashi", "market.high": "Alto", "market.high_abbr": "H", + "market.holders_tips": "Número de endereços que possuem o token.", "market.indicators": "Indicadores", "market.intervals": "Intervalos", "market.last_price": "Último preço", "market.last_seven_days": "Últimos 7 dias", "market.last_updated": "Última atualização", "market.line": "Linha", + "market.liquidity_tips": "Liquidez total disponível nos pools.", "market.low": "Baixo", "market.low_abbr": "L", "market.main_chart_indicators": "Indicadores do gráfico principal", + "market.market_cap_tips": "Valor de mercado em circulação do token.", "market.minus_2_percent_depth": "-2% prof.", "market.most_24h_volume": "Maior volume em 24h", "market.most_market_cap": "Maior capitalização de mercado", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Indicadores do subgráfico", "market.trending": "Em alta", "market.trust_score": "Pontuação de confiança", + "market.turnover_tips": "Volume de negociação no período selecionado.", "market.twenty_four_hour_percentage": "24h%", "market.twenty_four_hour_volume": "Volume de 24h", + "market.txns_tips": "Número de transações de compra e venda no período selecionado.", "market.volume_percentage": "Volume %", "market_preset.priority_fee_price_unit": "Unidade de preço da taxa prioritária. Uma taxa mais alta geralmente torna a confirmação antecipada mais provável. Consulte a taxa de rede em tempo real atual ao defini-la.", "market_status.closed": "Fechado", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Desconto de taxa ativado", "perps__snooze_remind_later__action": "Agora não, me lembre depois", "perps_alerts__title": "Alertas de Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Redefinir para o layout padrão", + "perps_chart_do_not_show__action": "Não exibir", + "perps_desktop_resize_panels__desc": "Arraste o divisor para redimensionar os painéis do gráfico e das posições.", "perps_footer.help_us_better": "Comunidade Perps", "perps_hide_other_pairs": "Ocultar outros pares", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "O layout foi redefinido", + "perps_layout_settings__title": "Configurações de layout", "perps_referral_campaign__desc": "Convide amigos, ganhe 99U juntos. Suba no ranking e concorra ao grande prêmio!", "perps_spot_add_funds__title": "Adicionar fundos", "perps_spot_buy_token_spot__action": "Comprar spot de {token}", "perps_spot_deposit_to_usdc__desc": "Deposite ativos on-chain na sua conta", "perps_spot_deposit_to_usdc__title": "Depositar em USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Gráfico de velas da página de negociação", "perps_trigger_price_equal_current": "O preço de gatilho deve ser diferente do preço atual", "photo_library_access_denied__desc": "O OneKey precisa de acesso à biblioteca de fotos para salvar imagens. Acesse Ajustes e ative as permissões da biblioteca de fotos.", "photo_library_access_denied__title": "Acesso à biblioteca de fotos negado", @@ -4656,11 +4678,15 @@ "terms_privacy": "Ao continuar, você confirma que tem 18 anos ou mais e concorda com nossos Termos e Privacidade", "third_party_hw_adapter_not_registered__msg": "O adaptador de hardware de terceiros não está registrado.", "title_403": "403 Proibido", + "title_dust_sweep": "Limpar dust", "title_dust_sweep_completed": "Poeira varrida", + "title_dust_swept": "Dust convertido", + "title_edit_slippage": "Editar slippage", "to_chain__title": "Para a rede", "toast.web_auth": "Falha na configuração", "toast_create_address_failed_message": "Por favor, selecione uma conta primeiro", "toast_create_address_failed_title": "Não foi possível criar endereço", + "toast_price_impact_exceeded_skipped": "Impacto acima do limite — ignorado", "token_manage.custom_token_address_failed": "Falha ao encontrar o endereço do contrato. Tente novamente. ", "token_no_search_results_desc": "Nenhuma correspondência encontrada para a sua pesquisa. Por favor, digite novamente.", "token_selector.network": "Rede:", diff --git a/packages/shared/src/locale/json/ru.json b/packages/shared/src/locale/json/ru.json index d6bc630dcd2b..5ad28f4bb0a9 100644 --- a/packages/shared/src/locale/json/ru.json +++ b/packages/shared/src/locale/json/ru.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Невозможно открыть платеж. Пожалуйста, повторите попытку из Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC медленно подтверждается — Заказы с проскальзыванием ниже 1% могут не выполниться", "btc_trade.set_to_1_percent": "Задать 1%", + "button_apply_slippage_all_future_orders": "Для всех ордеров", + "button_apply_slippage_this_order": "Этот ордер", + "button_convert_tokens_estimated_amount": "Конвертировать {count} (~{amount})", + "button_stop_conversion": "Стоп", "buy_and_sell": "Купи/продай", "chase__action": "Сопровождать", "chase_order_confirmation__desc": "Сопровождение обновляет цену ордера до текущей лучшей цены, чтобы ускорить исполнение.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Поддерживаемые активы", "kyt_supported_assets_load_error__desc": "Не удалось загрузить поддерживаемые активы. Проверьте подключение и повторите попытку", "kyt_view_report__action": "Посмотреть отчет", + "label_conversion_progress": "Ход конвертации", "label_converted": "Конвертировано", "label_market_awaiting_open": "Ждёт торгов", + "label_select_all": "Выбрать все", + "label_skipped": "Пропущено", "last_1_month__action": "Прошлый месяц", "last_3_months__action": "Последние 3 месяца", "learn_more_about_qr_code_wallet": "Узнайте больше о кошельке на основе QR-кода", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Ось Y", "market.chart_style": "Стиль графика", "market.close_abbr": "З", + "market.column_name_token_age": "Название/Возраст токена", "market.data_display": "Отображение данных", "market.days_since_launch": "Дней с момента запуска", "market.edit_preferred_intervals": "Выберите предпочитаемые интервалы", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Ваш список наблюдения пуст", "market.favorites_empty": "Ваши избранные пусты", "market.favorites_empty_desc": "Добавьте ваши любимые токены в список", + "market.filter_chip_conditions_tips": "Популярные токены, соответствующие этим условиям", + "market.filter_chip_large_cap": "Токены крупной капитализации", + "market.filter_chip_mid_cap": "Токены средней капитализации", + "market.filter_chip_top_turnover": "Наибольший объём", + "market.filter_chip_top_turnover_tips": "Текущий список популярных токенов, отсортированный по объёму торгов", + "market.filters": "Фильтры", + "market.filters_group_metrics": "Показатели", + "market.filters_time_frame": "Период", "market.heikin_ashi": "Хейкин Аши", "market.high": "Высокий", "market.high_abbr": "Макс", + "market.holders_tips": "Количество адресов, владеющих токеном.", "market.indicators": "Индикаторы", "market.intervals": "Интервалы", "market.last_price": "Последняя цена", "market.last_seven_days": "Последние 7 дней", "market.last_updated": "Последнее обновление", "market.line": "Линия", + "market.liquidity_tips": "Общая ликвидность, доступная в пулах.", "market.low": "Низкий", "market.low_abbr": "Мин", "market.main_chart_indicators": "Индикаторы основного графика", + "market.market_cap_tips": "Рыночная стоимость токенов в обращении.", "market.minus_2_percent_depth": "-2% Глубина", "market.most_24h_volume": "Наибольший объем за 24 часа", "market.most_market_cap": "Наибольшая рыночная капитализация", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Индикаторы дополнительного графика", "market.trending": "Тренды", "market.trust_score": "Оценка доверия", + "market.turnover_tips": "Оборот торгов за выбранный период.", "market.twenty_four_hour_percentage": "24ч %", "market.twenty_four_hour_volume": "24-часовой объем", + "market.txns_tips": "Количество сделок на покупку и продажу за выбранный период.", "market.volume_percentage": "Объем %", "market_preset.priority_fee_price_unit": "Единица цены приоритетной комиссии. Более высокая комиссия обычно делает более вероятным раннее подтверждение. При установке обращайтесь к текущей сетевой комиссии в реальном времени.", "market_status.closed": "Закрыт", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Скидка на комиссию активирована", "perps__snooze_remind_later__action": "Не сейчас, напомнить позже", "perps_alerts__title": "Оповещения о Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Сбросить макет по умолчанию", + "perps_chart_do_not_show__action": "Не показывать", + "perps_desktop_resize_panels__desc": "Перетащите разделитель, чтобы изменить размер панелей графика и позиций.", "perps_footer.help_us_better": "Сообщество Perps", "perps_hide_other_pairs": "Скрыть другие пары", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "Макет сброшен", + "perps_layout_settings__title": "Настройки макета", "perps_referral_campaign__desc": "Приглашайте друзей и выигрывайте 99U вместе. Поднимайтесь в рейтинге и боритесь за главный приз!", "perps_spot_add_funds__title": "Добавить средства", "perps_spot_buy_token_spot__action": "Купить спот {token}", "perps_spot_deposit_to_usdc__desc": "Внесите ончейн-активы на свой счет", "perps_spot_deposit_to_usdc__title": "Пополнить в USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Свечной график на странице торговли", "perps_trigger_price_equal_current": "Цена триггера должна отличаться от текущей цены", "photo_library_access_denied__desc": "OneKey требуется доступ к фототеке, чтобы сохранять изображения. Перейдите в Настройки и включите разрешения на доступ к фототеке.", "photo_library_access_denied__title": "Доступ к фототеке запрещен", @@ -4656,11 +4678,15 @@ "terms_privacy": "Продолжая, вы подтверждаете, что вам 18+ лет, и соглашаетесь с нашими Условиями и Политикой конфиденциальности", "third_party_hw_adapter_not_registered__msg": "Сторонний аппаратный адаптер не зарегистрирован.", "title_403": "403 Запрещено", + "title_dust_sweep": "Сбор пыли", "title_dust_sweep_completed": "Пыль собрана", + "title_dust_swept": "Пыль собрана", + "title_edit_slippage": "Проскальзывание", "to_chain__title": "Сеть назначения", "toast.web_auth": "Установка не удалась", "toast_create_address_failed_message": "Пожалуйста, сначала выберите аккаунт", "toast_create_address_failed_title": "Не удалось создать адрес", + "toast_price_impact_exceeded_skipped": "Лимит влияния превышен — пропуск", "token_manage.custom_token_address_failed": "Не удалось найти адрес контракта. Попробуйте снова. ", "token_no_search_results_desc": "Не найдено совпадений для вашего поиска. Пожалуйста, введите заново.", "token_selector.network": "Сеть:", diff --git a/packages/shared/src/locale/json/th_TH.json b/packages/shared/src/locale/json/th_TH.json index 7648469e4d9d..b41a9dff2625 100644 --- a/packages/shared/src/locale/json/th_TH.json +++ b/packages/shared/src/locale/json/th_TH.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "ไม่สามารถเปิดการชำระเงินได้ กรุณาลองอีกครั้งจาก Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC ยืนยันช้า — คำสั่งที่มี slippage ต่ำกว่า 1% อาจล้มเหลว", "btc_trade.set_to_1_percent": "ตั้งค่าเป็น 1%", + "button_apply_slippage_all_future_orders": "ทุกออเดอร์ถัดไป", + "button_apply_slippage_this_order": "ออเดอร์นี้", + "button_convert_tokens_estimated_amount": "แปลง {count} โทเค็น (~{amount})", + "button_stop_conversion": "หยุด", "buy_and_sell": "ซื้อ & ขาย", "chase__action": "ไล่ราคา", "chase_order_confirmation__desc": "การไล่ราคาจะอัปเดตราคาออร์เดอร์เป็นราคาที่ดีที่สุดในปัจจุบัน เพื่อช่วยให้ออร์เดอร์จับคู่ได้เร็วขึ้น", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "สินทรัพย์ที่ครอบคลุม", "kyt_supported_assets_load_error__desc": "โหลดสินทรัพย์ที่รองรับไม่สำเร็จ โปรดตรวจสอบการเชื่อมต่อแล้วลองอีกครั้ง", "kyt_view_report__action": "ดูรายงาน", + "label_conversion_progress": "ความคืบหน้าการแปลง", "label_converted": "แปลงแล้ว", "label_market_awaiting_open": "รอเปิด", + "label_select_all": "เลือกทั้งหมด", + "label_skipped": "ข้ามแล้ว", "last_1_month__action": "เดือนที่ผ่านมา", "last_3_months__action": "3 เดือนที่ผ่านมา", "learn_more_about_qr_code_wallet": "เรียนรู้เพิ่มเติมเกี่ยวกับกระเป๋าเงินที่ใช้ QR Code", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "แกน Y", "market.chart_style": "รูปแบบกราฟ", "market.close_abbr": "C", + "market.column_name_token_age": "ชื่อ/อายุโทเค็น", "market.data_display": "การแสดงข้อมูล", "market.days_since_launch": "วันนับตั้งแต่เปิดตัว", "market.edit_preferred_intervals": "เลือกช่วงเวลาที่ต้องการ", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "รายการที่คุณติดตามว่างเปล่า", "market.favorites_empty": "รายการโปรดของคุณว่างเปล่า", "market.favorites_empty_desc": "เพิ่มโทเค็นที่คุณชื่นชอบลงในรายการ", + "market.filter_chip_conditions_tips": "โทเค็นยอดนิยมที่ตรงตามเงื่อนไขเหล่านี้", + "market.filter_chip_large_cap": "โทเค็นมูลค่าตลาดสูง", + "market.filter_chip_mid_cap": "โทเค็นมูลค่าตลาดปานกลาง", + "market.filter_chip_top_turnover": "ปริมาณซื้อขายสูงสุด", + "market.filter_chip_top_turnover_tips": "รายการยอดนิยมปัจจุบัน เรียงตามปริมาณซื้อขาย", + "market.filters": "ตัวกรอง", + "market.filters_group_metrics": "ตัวชี้วัด", + "market.filters_time_frame": "ช่วงเวลา", "market.heikin_ashi": "เฮคินอาชิ", "market.high": "สูง", "market.high_abbr": "H", + "market.holders_tips": "จำนวนที่อยู่ที่ถือโทเค็นนี้", "market.indicators": "อินดิเคเตอร์", "market.intervals": "ช่วงเวลา", "market.last_price": "ราคาล่าสุด", "market.last_seven_days": "7 วันที่ผ่านมา", "market.last_updated": "อัปเดตล่าสุด", "market.line": "เส้น", + "market.liquidity_tips": "สภาพคล่องทั้งหมดที่มีในพูล", "market.low": "ต่ำ", "market.low_abbr": "L", "market.main_chart_indicators": "อินดิเคเตอร์กราฟหลัก", + "market.market_cap_tips": "มูลค่าตลาดของโทเค็นที่หมุนเวียนอยู่", "market.minus_2_percent_depth": "-2% ความลึก", "market.most_24h_volume": "ปริมาณธุรกรรมสูงสุดใน 24 ชั่วโมง", "market.most_market_cap": "มูลค่าตามราคาตลาดสูงสุด", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "อินดิเคเตอร์กราฟย่อย", "market.trending": "เทรนด์", "market.trust_score": "เครดิตสกอร์", + "market.turnover_tips": "มูลค่าการซื้อขายในช่วงเวลาที่เลือก", "market.twenty_four_hour_percentage": "24 ชั่วโมง %", "market.twenty_four_hour_volume": "ปริมาณการซื้อขายใน 24 ชั่วโมง", + "market.txns_tips": "จำนวนธุรกรรมซื้อและขายในช่วงเวลาที่เลือก", "market.volume_percentage": "ปริมาณ %", "market_preset.priority_fee_price_unit": "หน่วยราคาค่าธรรมเนียมลำดับความสำคัญ ค่าธรรมเนียมที่สูงกว่ามักจะทำให้การยืนยันเร็วขึ้นได้ง่ายขึ้น อ้างอิงค่าธรรมเนียมเครือข่ายแบบเรียลไทม์ในปัจจุบันเมื่อทำการตั้งค่า", "market_status.closed": "ปิด", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "ส่วนลดค่าธรรมเนียมเปิดใช้งานแล้ว", "perps__snooze_remind_later__action": "ยังไม่ต้อง เตือนฉันทีหลัง", "perps_alerts__title": "การแจ้งเตือน Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "รีเซ็ตเป็นเลย์เอาต์เริ่มต้น", + "perps_chart_do_not_show__action": "ไม่แสดง", + "perps_desktop_resize_panels__desc": "ลากเส้นแบ่งเพื่อปรับขนาดแผงกราฟและโพซิชัน", "perps_footer.help_us_better": "ชุมชน Perps", "perps_hide_other_pairs": "ซ่อนคู่เทรดอื่น", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "รีเซ็ตเลย์เอาต์แล้ว", + "perps_layout_settings__title": "การตั้งค่าเลย์เอาต์", "perps_referral_campaign__desc": "ชวนเพื่อน ลุ้น 99U ด้วยกัน ปีนอันดับลีดเดอร์บอร์ดเพื่อลุ้นรางวัลใหญ่!", "perps_spot_add_funds__title": "เพิ่มเงินทุน", "perps_spot_buy_token_spot__action": "ซื้อสปอต {token}", "perps_spot_deposit_to_usdc__desc": "ฝากสินทรัพย์ออนเชนเข้าบัญชีของคุณ", "perps_spot_deposit_to_usdc__title": "ฝากเข้า USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "กราฟแท่งเทียนในหน้าการซื้อขาย", "perps_trigger_price_equal_current": "ราคาทริกเกอร์ต้องแตกต่างจากราคาปัจจุบัน", "photo_library_access_denied__desc": "OneKey ต้องการสิทธิ์เข้าถึงคลังรูปภาพเพื่อบันทึกรูปภาพ โปรดไปที่การตั้งค่าและเปิดใช้งานสิทธิ์คลังรูปภาพ.", "photo_library_access_denied__title": "ถูกปฏิเสธการเข้าถึงคลังรูปภาพ", @@ -4656,11 +4678,15 @@ "terms_privacy": "เมื่อดำเนินการต่อ คุณยืนยันว่าคุณมีอายุ 18 ปีขึ้นไป และยอมรับ ข้อตกลงการใช้บริการ และ นโยบายความเป็นส่วนตัว ของเรา", "third_party_hw_adapter_not_registered__msg": "อะแดปเตอร์ฮาร์ดแวร์ของบุคคลที่สามยังไม่ได้ลงทะเบียน", "title_403": "403 สิ่งต้องห้าม", + "title_dust_sweep": "แปลงเหรียญเศษ", "title_dust_sweep_completed": "รวมเศษเหรียญแล้ว", + "title_dust_swept": "แปลงเหรียญเศษแล้ว", + "title_edit_slippage": "แก้ไข Slippage", "to_chain__title": "ไปยังเชน", "toast.web_auth": "การตั้งค่าล้มเหลว", "toast_create_address_failed_message": "กรุณาเลือกบัญชีก่อน", "toast_create_address_failed_title": "ไม่สามารถสร้างที่อยู่ได้", + "toast_price_impact_exceeded_skipped": "ผลกระทบราคาเกินกำหนด—ข้ามแล้ว", "token_manage.custom_token_address_failed": "ไม่สามารถหาที่อยู่ของสัญญา ลองอีกครั้ง", "token_no_search_results_desc": "ไม่พบผลลัพธ์ที่ตรงกับการค้นหาของคุณ โปรดป้อนใหม่", "token_selector.network": "เครือข่าย:", diff --git a/packages/shared/src/locale/json/uk_UA.json b/packages/shared/src/locale/json/uk_UA.json index ab8e9c94a45c..8a28781c5fac 100644 --- a/packages/shared/src/locale/json/uk_UA.json +++ b/packages/shared/src/locale/json/uk_UA.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Неможливо відкрити платіж. Будь ласка, спробуйте ще раз з Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC повільно підтверджується — Замовлення з прослизанням нижче 1% можуть не виконатися", "btc_trade.set_to_1_percent": "Задати 1%", + "button_apply_slippage_all_future_orders": "Майбутні ордери", + "button_apply_slippage_this_order": "Цей ордер", + "button_convert_tokens_estimated_amount": "Конвертувати {count} (~{amount})", + "button_stop_conversion": "Зупинити", "buy_and_sell": "Купи/продай", "chase__action": "Відстежити", "chase_order_confirmation__desc": "Відстеження оновлює ціну ордера до поточної найкращої ціни, щоб пришвидшити виконання.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Підтримувані активи", "kyt_supported_assets_load_error__desc": "Не вдалося завантажити підтримувані активи. Перевірте підключення й повторіть спробу", "kyt_view_report__action": "Переглянути звіт", + "label_conversion_progress": "Хід конвертації", "label_converted": "Конвертовано", "label_market_awaiting_open": "До відкриття", + "label_select_all": "Вибрати все", + "label_skipped": "Пропущено", "last_1_month__action": "Минулого місяця", "last_3_months__action": "Останні 3 місяці", "learn_more_about_qr_code_wallet": "Дізнайтеся більше про гаманець на основі QR-коду", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Вісь Y", "market.chart_style": "Стиль графіка", "market.close_abbr": "Закр", + "market.column_name_token_age": "Назва/Вік токена", "market.data_display": "Відображення даних", "market.days_since_launch": "Днів з моменту запуску", "market.edit_preferred_intervals": "Виберіть бажані інтервали", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Ваш список спостереження порожній", "market.favorites_empty": "Ваші улюблені порожні", "market.favorites_empty_desc": "Додайте свої улюблені токени до списку", + "market.filter_chip_conditions_tips": "Популярні токени, що відповідають цим умовам", + "market.filter_chip_large_cap": "Токени великої капіталізації", + "market.filter_chip_mid_cap": "Токени середньої капіталізації", + "market.filter_chip_top_turnover": "Найбільший обсяг", + "market.filter_chip_top_turnover_tips": "Поточний список популярних токенів, відсортований за обсягом торгів", + "market.filters": "Фільтри", + "market.filters_group_metrics": "Показники", + "market.filters_time_frame": "Період", "market.heikin_ashi": "Хейкін Аші", "market.high": "Високий", "market.high_abbr": "Макс", + "market.holders_tips": "Кількість адрес, які володіють токеном.", "market.indicators": "Індикатори", "market.intervals": "Інтервали", "market.last_price": "Остання ціна", "market.last_seven_days": "Останні 7 днів", "market.last_updated": "Останнє оновлення", "market.line": "Лінія", + "market.liquidity_tips": "Загальна ліквідність, доступна в пулах.", "market.low": "Низький", "market.low_abbr": "Мін", "market.main_chart_indicators": "Індикатори основного графіка", + "market.market_cap_tips": "Ринкова вартість токенів в обігу.", "market.minus_2_percent_depth": "-2% Глибина", "market.most_24h_volume": "Найбільший обсяг за 24 години", "market.most_market_cap": "Найбільша капіталізація ринку", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Індикатори допоміжного графіка", "market.trending": "Тренди", "market.trust_score": "Кредитна оцінка", + "market.turnover_tips": "Оборот торгів за вибраний період.", "market.twenty_four_hour_percentage": "24г %", "market.twenty_four_hour_volume": "Обсяг торгів за 24 години", + "market.txns_tips": "Кількість операцій купівлі та продажу за вибраний період.", "market.volume_percentage": "Обсяг %", "market_preset.priority_fee_price_unit": "Одиниця ціни пріоритетної комісії. Вища комісія зазвичай підвищує ймовірність швидшого підтвердження. Зверніться до поточної реальної комісії мережі при її встановленні.", "market_status.closed": "Закрито", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Знижку на комісію активовано", "perps__snooze_remind_later__action": "Не зараз, нагадати пізніше", "perps_alerts__title": "Сповіщення про перпи", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Скинути до макета за замовчуванням", + "perps_chart_do_not_show__action": "Не показувати", + "perps_desktop_resize_panels__desc": "Перетягніть роздільник, щоб змінити розмір панелей графіка та позицій.", "perps_footer.help_us_better": "Спільнота Perps", "perps_hide_other_pairs": "Приховати інші пари", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "Макет скинуто", + "perps_layout_settings__title": "Налаштування макета", "perps_referral_campaign__desc": "Запрошуйте друзів і вигравайте 99U разом. Підіймайтеся в рейтингу та змагайтеся за головний приз!", "perps_spot_add_funds__title": "Додати кошти", "perps_spot_buy_token_spot__action": "Купити спот {token}", "perps_spot_deposit_to_usdc__desc": "Внесіть ончейн-активи на свій рахунок", "perps_spot_deposit_to_usdc__title": "Поповнити в USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Свічковий графік на сторінці торгівлі", "perps_trigger_price_equal_current": "Ціна тригера має відрізнятися від поточної ціни", "photo_library_access_denied__desc": "OneKey потрібен доступ до фотобібліотеки, щоб зберігати зображення. Будь ласка, перейдіть у Налаштування та ввімкніть дозвіл на доступ до фотобібліотеки.", "photo_library_access_denied__title": "Доступ до фотобібліотеки заборонено", @@ -4656,11 +4678,15 @@ "terms_privacy": "Продовжуючи, ви підтверджуєте, що вам 18+ і погоджуєтеся з нашими Умовами та Політикою конфіденційності", "third_party_hw_adapter_not_registered__msg": "Сторонній апаратний адаптер не зареєстрований.", "title_403": "403 Заборонено", + "title_dust_sweep": "Конвертація пилу", "title_dust_sweep_completed": "Пил зібрано", + "title_dust_swept": "Пил конвертовано", + "title_edit_slippage": "Зміна прослизання", "to_chain__title": "До мережі", "toast.web_auth": "Налаштування не вдалося", "toast_create_address_failed_message": "Будь ласка, спочатку виберіть обліковий запис", "toast_create_address_failed_title": "Неможливо створити адресу", + "toast_price_impact_exceeded_skipped": "Вплив завеликий — пропущено", "token_manage.custom_token_address_failed": "Не вдалося знайти адресу контракту. Спробуйте ще раз. ", "token_no_search_results_desc": "Не знайдено жодного результату за вашим запитом. Будь ласка, введіть знову.", "token_selector.network": "Мережа:", diff --git a/packages/shared/src/locale/json/vi.json b/packages/shared/src/locale/json/vi.json index d761c35f19e9..05576a838959 100644 --- a/packages/shared/src/locale/json/vi.json +++ b/packages/shared/src/locale/json/vi.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "Không thể mở thanh toán. Vui lòng thử lại từ Bitrefill.", "btc_trade.btc_slow_to_confirm": "BTC xác nhận chậm — Các lệnh dưới 1% trượt giá có thể thất bại", "btc_trade.set_to_1_percent": "Đặt thành 1%", + "button_apply_slippage_all_future_orders": "Mọi lệnh sau này", + "button_apply_slippage_this_order": "Lệnh này", + "button_convert_tokens_estimated_amount": "Đổi {count} token (~{amount})", + "button_stop_conversion": "Dừng", "buy_and_sell": "Mua & bán", "chase__action": "Theo đuổi", "chase_order_confirmation__desc": "Tính năng theo đuổi cập nhật giá lệnh thành mức giá tốt nhất hiện tại để giúp lệnh được khớp nhanh hơn.", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "Tài sản được hỗ trợ", "kyt_supported_assets_load_error__desc": "Không tải được tài sản được hỗ trợ. Vui lòng kiểm tra kết nối rồi thử lại", "kyt_view_report__action": "Xem báo cáo", + "label_conversion_progress": "Tiến độ chuyển đổi", "label_converted": "Đã quy đổi", "label_market_awaiting_open": "Chờ mở", + "label_select_all": "Chọn tất cả", + "label_skipped": "Đã bỏ qua", "last_1_month__action": "Tháng trước", "last_3_months__action": "3 tháng qua", "learn_more_about_qr_code_wallet": "Tìm hiểu thêm về ví dựa trên mã QR", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Trục Y", "market.chart_style": "Kiểu biểu đồ", "market.close_abbr": "C", + "market.column_name_token_age": "Tên/Tuổi token", "market.data_display": "Hiển thị dữ liệu", "market.days_since_launch": "Ngày kể từ khi ra mắt", "market.edit_preferred_intervals": "Chọn khoảng thời gian ưa thích", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "Danh sách theo dõi của bạn đang trống", "market.favorites_empty": "Danh sách yêu thích của bạn trống", "market.favorites_empty_desc": "Thêm các token yêu thích của bạn vào danh sách", + "market.filter_chip_conditions_tips": "Các token thịnh hành khớp với những điều kiện này", + "market.filter_chip_large_cap": "Token vốn hóa lớn", + "market.filter_chip_mid_cap": "Token vốn hóa trung bình", + "market.filter_chip_top_turnover": "Khối lượng cao nhất", + "market.filter_chip_top_turnover_tips": "Danh sách thịnh hành hiện tại, sắp xếp theo khối lượng giao dịch", + "market.filters": "Bộ lọc", + "market.filters_group_metrics": "Chỉ số", + "market.filters_time_frame": "Khung thời gian", "market.heikin_ashi": "Heikin Ashi", "market.high": "Cao", "market.high_abbr": "H", + "market.holders_tips": "Số địa chỉ đang nắm giữ token.", "market.indicators": "Chỉ báo", "market.intervals": "Khoảng thời gian", "market.last_price": "Giá cuối cùng", "market.last_seven_days": "7 ngày qua", "market.last_updated": "Cập nhật lần cuối", "market.line": "Đường", + "market.liquidity_tips": "Tổng thanh khoản khả dụng trong các pool.", "market.low": "Thấp", "market.low_abbr": "L", "market.main_chart_indicators": "Chỉ báo biểu đồ chính", + "market.market_cap_tips": "Giá trị vốn hóa lưu hành của token.", "market.minus_2_percent_depth": "-2% Độ sâu", "market.most_24h_volume": "Khối lượng giao dịch nhiều nhất trong 24h", "market.most_market_cap": "Phần lớn vốn hóa thị trường", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "Chỉ báo biểu đồ phụ", "market.trending": "Đang thịnh hành", "market.trust_score": "Điểm tin cậy", + "market.turnover_tips": "Giá trị giao dịch trong khung thời gian đã chọn.", "market.twenty_four_hour_percentage": "24 giờ %", "market.twenty_four_hour_volume": "Khối lượng 24h", + "market.txns_tips": "Số giao dịch mua và bán trong khung thời gian đã chọn.", "market.volume_percentage": "Khối lượng%", "market_preset.priority_fee_price_unit": "Đơn vị giá phí ưu tiên. Phí cao hơn thường làm tăng khả năng xác nhận sớm hơn. Tham khảo phí mạng thời gian thực hiện tại khi thiết lập.", "market_status.closed": "Đóng cửa", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "Đã kích hoạt giảm phí", "perps__snooze_remind_later__action": "Không phải bây giờ, nhắc tôi sau", "perps_alerts__title": "Cảnh báo Perps", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "Đặt lại về bố cục mặc định", + "perps_chart_do_not_show__action": "Không hiển thị", + "perps_desktop_resize_panels__desc": "Kéo đường phân cách để thay đổi kích thước bảng biểu đồ và vị thế.", "perps_footer.help_us_better": "Cộng đồng Perps", "perps_hide_other_pairs": "Ẩn các cặp khác", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "Đã đặt lại bố cục", + "perps_layout_settings__title": "Cài đặt bố cục", "perps_referral_campaign__desc": "Mời bạn bè, cùng nhau giành 99U. Leo bảng xếp hạng để có cơ hội giành giải lớn!", "perps_spot_add_funds__title": "Thêm tiền", "perps_spot_buy_token_spot__action": "Mua giao ngay {token}", "perps_spot_deposit_to_usdc__desc": "Nạp tài sản on-chain vào tài khoản của bạn", "perps_spot_deposit_to_usdc__title": "Nạp vào USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "Biểu đồ nến trên trang giao dịch", "perps_trigger_price_equal_current": "Giá kích hoạt phải khác với giá hiện tại", "photo_library_access_denied__desc": "OneKey cần quyền truy cập thư viện ảnh để lưu ảnh. Vui lòng vào Cài đặt và bật quyền truy cập thư viện ảnh.", "photo_library_access_denied__title": "Truy cập thư viện ảnh bị từ chối", @@ -4656,11 +4678,15 @@ "terms_privacy": "Bằng cách tiếp tục, bạn xác nhận rằng mình đã đủ 18+ và đồng ý với Điều khoản & Quyền riêng tư của chúng tôi", "third_party_hw_adapter_not_registered__msg": "Bộ điều hợp phần cứng của bên thứ ba chưa được đăng ký.", "title_403": "403 bị cấm", + "title_dust_sweep": "Gom token lẻ", "title_dust_sweep_completed": "Đã gom token lẻ", + "title_dust_swept": "Đã gom token lẻ", + "title_edit_slippage": "Chỉnh độ trượt giá", "to_chain__title": "Chuỗi đích", "toast.web_auth": "Thiết lập thất bại", "toast_create_address_failed_message": "Vui lòng chọn một tài khoản trước tiên", "toast_create_address_failed_title": "Không thể tạo địa chỉ", + "toast_price_impact_exceeded_skipped": "Tác động giá vượt mức — bỏ qua", "token_manage.custom_token_address_failed": "Không tìm thấy địa chỉ hợp đồng. Hãy thử lại. ", "token_no_search_results_desc": "Không tìm thấy kết quả phù hợp với tìm kiếm của bạn. Vui lòng nhập lại.", "token_selector.network": "Mạng lưới:", diff --git a/packages/shared/src/locale/json/zh_CN.json b/packages/shared/src/locale/json/zh_CN.json index 49118503b379..3fc60c1126c9 100644 --- a/packages/shared/src/locale/json/zh_CN.json +++ b/packages/shared/src/locale/json/zh_CN.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "无法打开支付。请从 Bitrefill 重试。", "btc_trade.btc_slow_to_confirm": "BTC 网络确认较慢 — 滑点低于 1% 容易失败", "btc_trade.set_to_1_percent": "设为 1%", + "button_apply_slippage_all_future_orders": "后续所有订单", + "button_apply_slippage_this_order": "仅此订单", + "button_convert_tokens_estimated_amount": "兑换 {count} 个代币(约 {amount})", + "button_stop_conversion": "停止", "buy_and_sell": "买卖币", "chase__action": "追单", "chase_order_confirmation__desc": "追单会将委托价格更新至当前最优价,帮助订单更快成交。", @@ -823,9 +827,9 @@ "dexmarket.stock_status_open": "交易中", "dexmarket.switch_to_trade": "如果您希望交易其他代币,请切换到", "dexmarket.tax_rate": "税率", - "dexmarket.token_age": "创建时间", + "dexmarket.token_age": "创建时长", "dexmarket.token_age_d": "{amount}日", - "dexmarket.token_age_h": "{amount}小时", + "dexmarket.token_age_h": "{amount}时", "dexmarket.token_age_m": "{amount}月", "dexmarket.token_age_y": "{amount}年", "dexmarket.token_amount": "代币数量", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "支持监控的资产", "kyt_supported_assets_load_error__desc": "支持的资产加载失败,请检查网络后重试", "kyt_view_report__action": "查看报告", + "label_conversion_progress": "兑换进度", "label_converted": "已兑换", "label_market_awaiting_open": "待开盘", + "label_select_all": "全选", + "label_skipped": "已跳过", "last_1_month__action": "最近 1 个月", "last_3_months__action": "最近 3 个月", "learn_more_about_qr_code_wallet": "了解二维码钱包", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y 轴", "market.chart_style": "图表样式", "market.close_abbr": "收", + "market.column_name_token_age": "名称/创建时长", "market.data_display": "数据显示", "market.days_since_launch": "上线时间", "market.edit_preferred_intervals": "选择偏好时间周期", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "您的观察列表为空", "market.favorites_empty": "您的自选列表为空", "market.favorites_empty_desc": "将您喜欢的代币添加到列表中", + "market.filter_chip_conditions_tips": "符合这些条件的热门代币", + "market.filter_chip_large_cap": "大市值代币", + "market.filter_chip_mid_cap": "中市值代币", + "market.filter_chip_top_turnover": "成交量最高", + "market.filter_chip_top_turnover_tips": "当前热门列表按成交量排序", + "market.filters": "筛选", + "market.filters_group_metrics": "指标", + "market.filters_time_frame": "时间范围", "market.heikin_ashi": "平均 K 线", "market.high": "高", "market.high_abbr": "高", + "market.holders_tips": "持有该代币的地址数量。", "market.indicators": "指标", "market.intervals": "时间间隔", "market.last_price": "最新价格", "market.last_seven_days": "过去 7 天", "market.last_updated": "最后更新", "market.line": "折线图", + "market.liquidity_tips": "资金池中可用的总流动性。", "market.low": "低", "market.low_abbr": "低", "market.main_chart_indicators": "主图指标", + "market.market_cap_tips": "代币的流通市值。", "market.minus_2_percent_depth": "-2% 深度", "market.most_24h_volume": "24 小时最多交易量", "market.most_market_cap": "市值最大", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "副图指标", "market.trending": "热门", "market.trust_score": "信用评分", + "market.turnover_tips": "所选时间范围内的交易额。", "market.twenty_four_hour_percentage": "24 小时%", "market.twenty_four_hour_volume": "24 小时交易量", + "market.txns_tips": "所选时间范围内的买入与卖出交易数。", "market.volume_percentage": "交易量 %", "market_preset.priority_fee_price_unit": "优先费价格单位,较高的费用通常会增加更早确认的可能性,设置时请参考当前网络费用。", "market_status.closed": "休市", @@ -3333,8 +3354,9 @@ "perps__fee_discount_activated__msg": "手续费优惠已生效", "perps__snooze_remind_later__action": "暂不需要,下次再提醒我", "perps_alerts__title": "永续合约提醒", - "perps_back_to_default_layout__action": "返回默认布局", + "perps_back_to_default_layout__action": "重置为默认布局", "perps_chart_do_not_show__action": "不展示", + "perps_desktop_resize_panels__desc": "拖动分隔线可调整图表和持仓区域的高度。", "perps_footer.help_us_better": "合约社区", "perps_hide_other_pairs": "隐藏其他交易对", "perps_layout_reset__msg": "布局已重置", @@ -4656,11 +4678,15 @@ "terms_privacy": "继续即表示您确认已年满 18 岁,并同意我们的使用条款隐私政策", "third_party_hw_adapter_not_registered__msg": "第三方硬件适配器未注册。", "title_403": "403 禁止访问", + "title_dust_sweep": "小额资产兑换", "title_dust_sweep_completed": "小额资产归集完成", + "title_dust_swept": "小额资产兑换完成", + "title_edit_slippage": "编辑滑点", "to_chain__title": "目标链", "toast.web_auth": "设置失败", "toast_create_address_failed_message": "请先选择一个账户", "toast_create_address_failed_title": "无法创建地址", + "toast_price_impact_exceeded_skipped": "价格影响超限,已跳过", "token_manage.custom_token_address_failed": "未能找到合约地址。请再试一次。", "token_no_search_results_desc": "未找到匹配项,请重新输入。", "token_selector.network": "网络:", diff --git a/packages/shared/src/locale/json/zh_HK.json b/packages/shared/src/locale/json/zh_HK.json index e25d236fe943..72afe3ed0090 100644 --- a/packages/shared/src/locale/json/zh_HK.json +++ b/packages/shared/src/locale/json/zh_HK.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "無法開啟付款。請從 Bitrefill 再次嘗試。", "btc_trade.btc_slow_to_confirm": "BTC 網路確認較慢 — 滑點低於 1% 容易失敗", "btc_trade.set_to_1_percent": "設為 1%", + "button_apply_slippage_all_future_orders": "往後所有訂單", + "button_apply_slippage_this_order": "此訂單", + "button_convert_tokens_estimated_amount": "兌換 {count} 種代幣(約 {amount})", + "button_stop_conversion": "停止", "buy_and_sell": "買賣幣", "chase__action": "追單", "chase_order_confirmation__desc": "追單會將委託價格更新為當前最優價,協助訂單更快成交。", @@ -823,9 +827,9 @@ "dexmarket.stock_status_open": "交易中", "dexmarket.switch_to_trade": "如果您希望交易其他代幣,請切換至", "dexmarket.tax_rate": "稅率", - "dexmarket.token_age": "建立時間", + "dexmarket.token_age": "建立時長", "dexmarket.token_age_d": "{amount}日", - "dexmarket.token_age_h": "{amount}小時", + "dexmarket.token_age_h": "{amount}時", "dexmarket.token_age_m": "{amount}月", "dexmarket.token_age_y": "{amount}年", "dexmarket.token_amount": "代幣數量", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "支援監控的資產", "kyt_supported_assets_load_error__desc": "支援的資產載入失敗,請檢查網絡後重試", "kyt_view_report__action": "查看報告", + "label_conversion_progress": "兌換進度", "label_converted": "已兌換", "label_market_awaiting_open": "待開市", + "label_select_all": "全選", + "label_skipped": "已略過", "last_1_month__action": "最近 1 個月", "last_3_months__action": "最近 3 個月", "learn_more_about_qr_code_wallet": "了解二維碼錢包", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y 軸", "market.chart_style": "圖表樣式", "market.close_abbr": "收", + "market.column_name_token_age": "名稱/建立時長", "market.data_display": "數據顯示", "market.days_since_launch": "上線時間", "market.edit_preferred_intervals": "選擇偏好时间周期", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "您的觀察列表為空", "market.favorites_empty": "您的自選清單為空", "market.favorites_empty_desc": "將您喜愛的代幣添加到列表中", + "market.filter_chip_conditions_tips": "符合這些條件的熱門代幣", + "market.filter_chip_large_cap": "大市值代幣", + "market.filter_chip_mid_cap": "中市值代幣", + "market.filter_chip_top_turnover": "成交量最高", + "market.filter_chip_top_turnover_tips": "目前熱門列表按成交量排序", + "market.filters": "篩選", + "market.filters_group_metrics": "指標", + "market.filters_time_frame": "時間範圍", "market.heikin_ashi": "平均 K 線", "market.high": "高", "market.high_abbr": "高", + "market.holders_tips": "持有該代幣的地址數量。", "market.indicators": "指標", "market.intervals": "時間間隔", "market.last_price": "最新價格", "market.last_seven_days": "過去 7 天", "market.last_updated": "最後更新", "market.line": "折線圖", + "market.liquidity_tips": "資金池中可用的總流動性。", "market.low": "低下", "market.low_abbr": "低", "market.main_chart_indicators": "主圖指標", + "market.market_cap_tips": "代幣的流通市值。", "market.minus_2_percent_depth": "-2% 深度", "market.most_24h_volume": "24 小時最多交易量", "market.most_market_cap": "市值最大", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "副圖指標", "market.trending": "熱門", "market.trust_score": "信用評分", + "market.turnover_tips": "所選時間範圍內的交易額。", "market.twenty_four_hour_percentage": "24 小時%", "market.twenty_four_hour_volume": "24 小時交易量", + "market.txns_tips": "所選時間範圍內的買入與賣出交易數。", "market.volume_percentage": "交易量%", "market_preset.priority_fee_price_unit": "優先費價格單位,費用越高,通常越容易更早確認,建議參考目前網絡實時費用設定", "market_status.closed": "休市", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "手續費優惠已生效", "perps__snooze_remind_later__action": "暫不需要,下次再提醒我", "perps_alerts__title": "永續合約提醒", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "重設為預設佈局", + "perps_chart_do_not_show__action": "不顯示", + "perps_desktop_resize_panels__desc": "拖動分隔線可調整圖表和持倉區域的高度。", "perps_footer.help_us_better": "合約社区", "perps_hide_other_pairs": "隱藏其他交易對", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "佈局已重設", + "perps_layout_settings__title": "佈局設定", "perps_referral_campaign__desc": "邀請好友,一起贏取 99U。提升排名,爭取大獎。", "perps_spot_add_funds__title": "添加資金", "perps_spot_buy_token_spot__action": "買入 {token} 現貨", "perps_spot_deposit_to_usdc__desc": "將鏈上資產充值到你的帳戶", "perps_spot_deposit_to_usdc__title": "充值到 USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "交易頁 K 線圖", "perps_trigger_price_equal_current": "觸發價不能等於當前價格", "photo_library_access_denied__desc": "OneKey 需要存取照片圖庫才能儲存圖片。請前往「設定」,並啟用照片圖庫權限。", "photo_library_access_denied__title": "照片圖庫存取遭拒", @@ -4656,11 +4678,15 @@ "terms_privacy": "繼續即表示你確認已年滿 18 歲,並同意我們的使用條款私隱政策", "third_party_hw_adapter_not_registered__msg": "第三方硬體配接器未註冊。", "title_403": "403 禁止訪問", + "title_dust_sweep": "小額代幣兌換", "title_dust_sweep_completed": "零散資產已兌換", + "title_dust_swept": "小額代幣兌換完成", + "title_edit_slippage": "編輯滑點", "to_chain__title": "目標鏈", "toast.web_auth": "設置失敗", "toast_create_address_failed_message": "請先選擇一個帳戶", "toast_create_address_failed_title": "無法創建地址", + "toast_price_impact_exceeded_skipped": "價格影響超出上限,已略過", "token_manage.custom_token_address_failed": "找不到合約地址。請再試一次。", "token_no_search_results_desc": "找不到符合搜尋的結果,請重新輸入。", "token_selector.network": "網絡:", diff --git a/packages/shared/src/locale/json/zh_TW.json b/packages/shared/src/locale/json/zh_TW.json index 6d155ad6a9c5..d005186dd85d 100644 --- a/packages/shared/src/locale/json/zh_TW.json +++ b/packages/shared/src/locale/json/zh_TW.json @@ -345,6 +345,10 @@ "browser_unable_to_pay": "無法開啟付款。請從 Bitrefill 再次嘗試。", "btc_trade.btc_slow_to_confirm": "BTC 網路確認較慢 — 滑點低於 1% 容易失敗", "btc_trade.set_to_1_percent": "設為 1%", + "button_apply_slippage_all_future_orders": "所有後續訂單", + "button_apply_slippage_this_order": "本次訂單", + "button_convert_tokens_estimated_amount": "兌換 {count} 種代幣(約 {amount})", + "button_stop_conversion": "停止", "buy_and_sell": "買賣幣", "chase__action": "追單", "chase_order_confirmation__desc": "追單會將委託價格更新為當前最優價,協助訂單更快成交。", @@ -823,9 +827,9 @@ "dexmarket.stock_status_open": "交易中", "dexmarket.switch_to_trade": "如果您希望交易其他代幣,請切換至", "dexmarket.tax_rate": "稅率", - "dexmarket.token_age": "建立時間", + "dexmarket.token_age": "建立時長", "dexmarket.token_age_d": "{amount}日", - "dexmarket.token_age_h": "{amount}小時", + "dexmarket.token_age_h": "{amount}時", "dexmarket.token_age_m": "{amount}月", "dexmarket.token_age_y": "{amount}年", "dexmarket.token_amount": "代幣數量", @@ -2242,8 +2246,11 @@ "kyt_supported_assets__title": "支援監控的資產", "kyt_supported_assets_load_error__desc": "支援的資產載入失敗,請檢查網路後重試", "kyt_view_report__action": "查看報告", + "label_conversion_progress": "兌換進度", "label_converted": "已兌換", "label_market_awaiting_open": "待開盤", + "label_select_all": "全選", + "label_skipped": "已略過", "last_1_month__action": "最近 1 個月", "last_3_months__action": "最近 3 個月", "learn_more_about_qr_code_wallet": "深入了解 QR 碼錢包", @@ -2377,6 +2384,7 @@ "market.chart_settings__y_axis": "Y 軸", "market.chart_style": "圖表樣式", "market.close_abbr": "收", + "market.column_name_token_age": "名稱/建立時長", "market.data_display": "資料顯示", "market.days_since_launch": "上線時間", "market.edit_preferred_intervals": "選擇偏好时间周期", @@ -2384,18 +2392,29 @@ "market.empty_watchlist_title": "您的觀察列表是空的", "market.favorites_empty": "您的自選清單為空", "market.favorites_empty_desc": "將您喜愛的代幣新增到列表中", + "market.filter_chip_conditions_tips": "符合這些條件的熱門代幣", + "market.filter_chip_large_cap": "大市值代幣", + "market.filter_chip_mid_cap": "中市值代幣", + "market.filter_chip_top_turnover": "成交量最高", + "market.filter_chip_top_turnover_tips": "目前熱門列表依成交量排序", + "market.filters": "篩選", + "market.filters_group_metrics": "指標", + "market.filters_time_frame": "時間範圍", "market.heikin_ashi": "平均 K 線", "market.high": "高", "market.high_abbr": "高", + "market.holders_tips": "持有該代幣的地址數量。", "market.indicators": "指標", "market.intervals": "時間間隔", "market.last_price": "最新價格", "market.last_seven_days": "過去 7 天", "market.last_updated": "最後更新", "market.line": "折線圖", + "market.liquidity_tips": "資金池中可用的總流動性。", "market.low": "低", "market.low_abbr": "低", "market.main_chart_indicators": "主圖指標", + "market.market_cap_tips": "代幣的流通市值。", "market.minus_2_percent_depth": "-2% 深度", "market.most_24h_volume": "24 小時最多交易量", "market.most_market_cap": "最大市值", @@ -2423,8 +2442,10 @@ "market.sub_chart_indicators": "副圖指標", "market.trending": "熱門", "market.trust_score": "信用評分", + "market.turnover_tips": "所選時間範圍內的交易額。", "market.twenty_four_hour_percentage": "24 小時%", "market.twenty_four_hour_volume": "24 小時成交量", + "market.txns_tips": "所選時間範圍內的買入與賣出交易數。", "market.volume_percentage": "交易量%", "market_preset.priority_fee_price_unit": "優先費價格單位,費用越高,通常越容易更早確認,建議參考目前網路即時費用設定", "market_status.closed": "休市", @@ -3333,18 +3354,19 @@ "perps__fee_discount_activated__msg": "手續費優惠已生效", "perps__snooze_remind_later__action": "暫不需要,下次再提醒我", "perps_alerts__title": "永續合約提醒", - "perps_back_to_default_layout__action": "Back to default layout", - "perps_chart_do_not_show__action": "Do not show", + "perps_back_to_default_layout__action": "重設為預設版面配置", + "perps_chart_do_not_show__action": "不顯示", + "perps_desktop_resize_panels__desc": "拖動分隔線可調整圖表和持倉區域的高度。", "perps_footer.help_us_better": "合約社区", "perps_hide_other_pairs": "隱藏其他交易對", - "perps_layout_reset__msg": "Layout has been reset", - "perps_layout_settings__title": "Layout settings", + "perps_layout_reset__msg": "版面配置已重設", + "perps_layout_settings__title": "版面配置設定", "perps_referral_campaign__desc": "邀請好友,一起贏得 99U。提升排名,爭取大獎。", "perps_spot_add_funds__title": "新增資金", "perps_spot_buy_token_spot__action": "買入 {token} 現貨", "perps_spot_deposit_to_usdc__desc": "將鏈上資產充值到你的帳戶", "perps_spot_deposit_to_usdc__title": "充值到 USDC", - "perps_trading_page_chart__title": "Trading page K-line chart", + "perps_trading_page_chart__title": "交易頁 K 線圖", "perps_trigger_price_equal_current": "觸發價不能等於當前價格", "photo_library_access_denied__desc": "OneKey 需要存取照片圖庫才能儲存圖片。請前往設定並啟用照片圖庫權限。", "photo_library_access_denied__title": "照片圖庫存取遭拒", @@ -4656,11 +4678,15 @@ "terms_privacy": "繼續即表示你確認已年滿 18 歲,並同意我們的服務條款隱私權政策", "third_party_hw_adapter_not_registered__msg": "第三方硬體介接器未註冊。", "title_403": "403 禁止存取", + "title_dust_sweep": "小額代幣兌換", "title_dust_sweep_completed": "零散資產歸集完成", + "title_dust_swept": "小額代幣已兌換", + "title_edit_slippage": "編輯滑點", "to_chain__title": "目標鏈", "toast.web_auth": "設定失敗", "toast_create_address_failed_message": "請先選擇一個帳戶", "toast_create_address_failed_title": "無法建立地址", + "toast_price_impact_exceeded_skipped": "價格影響超過上限,已略過", "token_manage.custom_token_address_failed": "找不到合約地址。請再試一次。", "token_no_search_results_desc": "未找到符合您搜尋的結果,請重新輸入。", "token_selector.network": "網路:", diff --git a/packages/shared/src/logger/scopes/dex/scenes/list.ts b/packages/shared/src/logger/scopes/dex/scenes/list.ts index 4d08b44673fa..291d4fa9d591 100644 --- a/packages/shared/src/logger/scopes/dex/scenes/list.ts +++ b/packages/shared/src/logger/scopes/dex/scenes/list.ts @@ -2,6 +2,7 @@ import { BaseScene } from '../../../base/baseScene'; import { LogToLocal, LogToServer } from '../../../base/decorators'; import type { + IDexFilterChipParams, IDexListParams, IDexNetworkLoadingParams, IDexNetworkParams, @@ -47,4 +48,13 @@ export class ListScene extends BaseScene { public dexSort(params: IDexSortParams) { return params; } + + /** + * Track trending quick-filter chip interactions (P2-9 demo). + */ + @LogToServer() + @LogToLocal({ level: 'info' }) + public dexFilterChip(params: IDexFilterChipParams) { + return params; + } } diff --git a/packages/shared/src/logger/scopes/dex/types.ts b/packages/shared/src/logger/scopes/dex/types.ts index e0ed8db4e8c3..d687a30ec90d 100644 --- a/packages/shared/src/logger/scopes/dex/types.ts +++ b/packages/shared/src/logger/scopes/dex/types.ts @@ -22,6 +22,12 @@ export enum ESortWay { Volume = 'v24hUSD', MC = 'mc', Default = 'default', + Price = 'price', + Change = 'change', + Txns = 'txns', + Traders = 'traders', + Holders = 'holders', + TokenAge = 'tokenAge', } export enum ECopyFrom { @@ -126,6 +132,23 @@ export interface IDexSortParams { sortDirection?: 'asc' | 'desc'; } +export interface IDexFilterChipParams { + action: + | 'presetClick' + // Sort shortcut chip: dispatches an existing column sort, so it is tracked + // separately from the filter chips that change the result set. + | 'sortChipClick' + | 'conditionChange' + | 'conditionRemove' + | 'clearAll' + | 'popoverConfirm'; + presetId?: string; + field?: string; + // Tier value: numeric threshold or named option id. + value?: number | string; + conditionCount?: number; +} + export interface IDexCopyCAParams { copyFrom: ECopyFrom; copiedContent: string; diff --git a/packages/shared/src/utils/swrCacheUtils.ts b/packages/shared/src/utils/swrCacheUtils.ts index 678e3f0d7850..517a61a24daf 100644 --- a/packages/shared/src/utils/swrCacheUtils.ts +++ b/packages/shared/src/utils/swrCacheUtils.ts @@ -524,6 +524,7 @@ export const swrKeys = { type, category, timeFrame, + filterKey, }: { networkId: string; sortBy?: string; @@ -533,6 +534,9 @@ export const swrKeys = { type?: string; category?: string; timeFrame?: string; + // Serialized hot-token filter params. Without it a filtered response + // would be cached under the same key as the unfiltered list. + filterKey?: string; }) => { const parts = [ NS.marketHomeTokenList, @@ -548,6 +552,9 @@ export const swrKeys = { if (category) { parts.push(category); } + if (filterKey) { + parts.push(filterKey); + } return parts.join(':'); }, tokenSelectorView: ({