From 2ed7bb0c3d7acd2ef4a587961936aa9a018946a9 Mon Sep 17 00:00:00 2001
From: tommasini <46944231+tommasini@users.noreply.github.com>
Date: Fri, 6 Mar 2026 17:36:25 +0000
Subject: [PATCH 01/11] chore: increase js bundle to 53 (#27135)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Increase JS bundle 1 MB
## **Changelog**
CHANGELOG entry:
## **Related issues**
Fixes:
## **Manual testing steps**
```gherkin
Feature: my feature name
Scenario: user [verb for user action]
Given [describe expected initial app state]
When user [verb for user action]
Then [describe expected outcome]
```
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Low Risk**
> Low risk: workflow-only change that just relaxes the CI bundle-size
gate by 1 unit and doesn’t affect runtime code.
>
> **Overview**
> **CI bundle-size gating has been relaxed slightly.** The
`js-bundle-size-check` step in `.github/workflows/ci.yml` now allows an
iOS `main.jsbundle` size threshold of `53` instead of `52` when running
`./scripts/js-bundle-stats.sh`.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3c0f0a4e861f1666572af8610c74ccc441c68421. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 43478944fd2..5031bc205a6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -183,7 +183,7 @@ jobs:
NODE_OPTIONS: --max_old_space_size=12288
- name: Check bundle size
- run: ./scripts/js-bundle-stats.sh ios/main.jsbundle 52
+ run: ./scripts/js-bundle-stats.sh ios/main.jsbundle 53
- name: Upload iOS bundle
uses: actions/upload-artifact@v4
From b7aab19201a6de6233773cad31413aac887683f1 Mon Sep 17 00:00:00 2001
From: Ramon AC <36987446+racitores@users.noreply.github.com>
Date: Fri, 6 Mar 2026 18:40:18 +0100
Subject: [PATCH 02/11] test: add component view tests for Predict feature
(PredictFeed + PredictMarketDetails) (#27012)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
First component view tests for the Predict feature area (MMQA-1529).
Tests use Engine spies and real user interactions — no mocked hooks or
selectors — following the integration-test doctrine: each test models a
complete user journey, not an isolated unit behavior.
### Infrastructure added
| File | Purpose |
|---|---|
| `tests/component-view/presets/predict.ts` | State preset with
`predictTradingEnabled` remote feature flag, `PredictController` state,
`PreferencesController.privacyMode`, and `TransactionController` |
| `tests/component-view/renderers/predict.tsx` | `renderPredictFeedView`
and `renderPredictFeedViewWithRoutes`, wrapped with
`QueryClientProvider` (required by `PredictBalance` which uses
`@tanstack/react-query`) |
| `tests/component-view/renderers/predictMarketDetails.tsx` |
`renderPredictMarketDetailsView` and
`renderPredictMarketDetailsViewWithRoutes` with `initialParams` support
for route params |
| `tests/component-view/fixtures/predict.ts` | Shared
`MOCK_PREDICT_MARKET` fixture used across both test files |
| `app/components/UI/Predict/Predict.testIds.ts` | Added
`PredictSearchSelectorsIDs` (`SEARCH_BUTTON`, `CLEAR_BUTTON`,
`ERROR_STATE`) and `getPredictSearchSelector.resultCard(index)` helper;
all raw strings replaced with constants |
| `tests/component-view/mocks.ts` | Updated to support Predict engine
context |
### PredictFeed tests (12)
- Search overlay opens when the user presses the search icon
- `getMarkets` called with the debounced typed query
- Search overlay closes when the user presses Cancel
- Clear button hides after user clears the input
- No-results message includes the typed query
- **Data completeness**: result card shows title + Yes/No tokens after
`getMarkets` resolves
- Tapping a result card navigates to market details
- Back button navigates to wallet
- Balance card renders and `getBalance` is called on mount
- Add Funds triggers `trackGeoBlockTriggered` with `attemptedAction:
deposit`
- Error state shown when all `getMarkets` retries fail
- Retry press calls `getMarkets` again after an error
### PredictMarketDetails tests (5)
- `getMarket` called with `marketId` from route params on mount
- **Data completeness**: title + Yes/No bet buttons visible after
`getMarket` resolves
- Pressing a bet button triggers `trackGeoBlockTriggered` while
ineligible
- `trackMarketDetailsOpened` called after market and positions load
- Back button navigates to Predict root
### Key implementation constraints
- The main feed (`PagerView` + `FlashList`) never renders in the test
environment — it is gated by `{layoutReady && }` and
`layoutReady` stays false without native layout events. Tests focus on
the search overlay which does render.
- Market card navigation targets `Routes.PREDICT.ROOT` (nested
navigator), not `MARKET_DETAILS` directly.
- `PredictBalance` requires `QueryClientProvider`; renderer wraps with
`{ retry: false }` to surface errors immediately.
## Test plan
```bash
yarn jest -c jest.config.view.js PredictFeed.view.test PredictMarketDetails.view.test --runInBand --silent --coverage=false
```
- [x] All 17 tests pass
- [x] No ESLint errors (`yarn eslint
app/components/UI/Predict/views/**/*.view.test.tsx`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
> [!NOTE]
> **Low Risk**
> Low risk: changes are primarily test-only infrastructure plus
refactors of `testID` strings in Predict UI components/tests. Main risk
is breaking existing E2E/unit tests that rely on previous hard-coded
selector strings.
>
> **Overview**
> **Adds component view tests for Predict.** Introduces new Predict
component-view test suites for `PredictFeed` and `PredictMarketDetails`
that validate real user flows (search, navigation, balance loading,
error/retry) via `Engine.context.PredictController` spies.
>
> **Builds supporting test infrastructure and normalizes selectors.**
Adds Predict-specific component-view renderers, Redux state preset, and
a shared `MOCK_PREDICT_MARKET` fixture; extends component-view `Engine`
mocks with a stubbed `PredictController`. Updates `PredictFeed` and
multiple unit tests to replace hard-coded `testID` strings with new
constants/helpers in `Predict.testIds.ts` (feed/search/market-details
selectors, skeleton/empty-state IDs).
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8ba8bca58d99e605dc30ff31b68ac7b2e071a820. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---------
Co-authored-by: Claude Sonnet 4.6
---
app/components/UI/Predict/Predict.testIds.ts | 66 +++-
.../views/PredictFeed/PredictFeed.test.tsx | 267 +++++++++++-----
.../Predict/views/PredictFeed/PredictFeed.tsx | 61 ++--
.../PredictFeed/PredictFeed.view.test.tsx | 300 ++++++++++++++++++
.../PredictMarketDetails.test.tsx | 249 ++++++++++-----
.../PredictMarketDetails.view.test.tsx | 144 +++++++++
tests/component-view/fixtures/predict.ts | 36 +++
tests/component-view/mocks.ts | 12 +
tests/component-view/presets/predict.ts | 51 +++
tests/component-view/renderers/predict.tsx | 85 +++++
.../renderers/predictMarketDetails.tsx | 84 +++++
11 files changed, 1190 insertions(+), 165 deletions(-)
create mode 100644 app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx
create mode 100644 app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.view.test.tsx
create mode 100644 tests/component-view/fixtures/predict.ts
create mode 100644 tests/component-view/presets/predict.ts
create mode 100644 tests/component-view/renderers/predict.tsx
create mode 100644 tests/component-view/renderers/predictMarketDetails.tsx
diff --git a/app/components/UI/Predict/Predict.testIds.ts b/app/components/UI/Predict/Predict.testIds.ts
index 8fbba72d606..6d66ce1b27d 100644
--- a/app/components/UI/Predict/Predict.testIds.ts
+++ b/app/components/UI/Predict/Predict.testIds.ts
@@ -42,6 +42,40 @@ export const getPredictMarketListSelector = {
emptyState: () => 'predict-market-list-empty-state',
};
+// ========================================
+// PREDICT FEED SELECTORS
+// ========================================
+
+export const PredictFeedSelectorsIDs = {
+ TABS: 'predict-feed-tabs',
+ PAGER: 'predict-feed-pager',
+ SEARCH_ICON: 'search-icon',
+} as const;
+
+export const getPredictFeedSelector = {
+ tabPage: (key: string) => `predict-feed-tab-page-${key}`,
+ emptyState: (category: string) => `predict-empty-state-${category}`,
+ skeletonLoading: (category: string, index: number) =>
+ `skeleton-loading-${category}-${index}`,
+ skeletonFooter: (category: string, index: number) =>
+ `skeleton-footer-${category}-${index}`,
+ searchSkeleton: (index: number) => `search-skeleton-${index}`,
+ marketList: (category: string) => `predict-market-list-${category}`,
+};
+
+// PredictFeed unit test mock selectors (used by PredictFeed.test.tsx mocks)
+export const PredictFeedMockSelectorsIDs = {
+ PAGER_VIEW: 'pager-view-mock',
+ BALANCE_MOCK: 'predict-balance-mock',
+ OFFLINE_MOCK: 'predict-offline-mock',
+} as const;
+
+export const getPredictFeedMockSelector = {
+ tabKey: (key: string) => `tab-${key}`,
+ activeTab: (index: number) => `active-tab-${index}`,
+ pagerPage: (index: number) => `pager-page-${index}`,
+};
+
// ========================================
// PREDICT MARKET DETAILS SELECTORS
// ========================================
@@ -61,7 +95,7 @@ export const PredictMarketDetailsSelectorsIDs = {
POSITIONS_TAB: 'predict-market-details-positions-tab',
OUTCOMES_TAB: 'predict-market-details-outcomes-tab',
- //Tab labels
+ // Tab labels
POSITIONS_TAB_LABEL: 'predict-market-details-tab-bar-tab-0',
OUTCOMES_TAB_LABEL: 'predict-market-details-tab-bar-tab-1',
ABOUT_TAB_LABEL: 'predict-market-details-tab-bar-tab-2',
@@ -72,6 +106,22 @@ export const PredictMarketDetailsSelectorsIDs = {
OUTCOMES_TAB_CONTENT: 'outcomes-tab-content',
MARKET_DETAILS_CASH_OUT_BUTTON: 'predict-market-details-cash-out-button',
CLAIM_WINNINGS_BUTTON: 'predict-market-details-claim-winnings-button',
+
+ // Chart and content (used by component and tests)
+ DETAILS_CHART: 'predict-details-chart',
+ GAME_DETAILS_CONTENT: 'predict-game-details-content',
+
+ // Skeleton loaders
+ DETAILS_HEADER_SKELETON_BACK_BUTTON:
+ 'predict-details-header-skeleton-back-button',
+ DETAILS_CONTENT_SKELETON_LINE_1: 'predict-details-content-skeleton-line-1',
+ DETAILS_BUTTONS_SKELETON_BUTTON_1:
+ 'predict-details-buttons-skeleton-button-1',
+} as const;
+
+export const getPredictMarketDetailsSelector = {
+ tabBarTab: (index: number) => `predict-market-details-tab-bar-tab-${index}`,
+ icon: (name: string) => `icon-${name}`,
} as const;
export const PredictMarketDetailsSelectorsText = {
@@ -170,6 +220,20 @@ export const PredictActivityDetailsSelectorsIDs = {
AMOUNT_DISPLAY: 'predict-activity-details-amount',
} as const;
+// ========================================
+// PREDICT SEARCH SELECTORS
+// ========================================
+
+export const PredictSearchSelectorsIDs = {
+ SEARCH_BUTTON: 'predict-search-button',
+ CLEAR_BUTTON: 'predict-clear-button',
+ ERROR_STATE: 'predict-error-state',
+} as const;
+
+export const getPredictSearchSelector = {
+ resultCard: (index: number) => `predict-search-result-${index}`,
+};
+
// ========================================
// PREDICT BALANCE SELECTORS
// ========================================
diff --git a/app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx b/app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx
index ed8a48f829e..da0f3b9d170 100644
--- a/app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx
+++ b/app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx
@@ -1,20 +1,33 @@
import { fireEvent, render } from '@testing-library/react-native';
import React from 'react';
-import { PredictMarketListSelectorsIDs } from '../../Predict.testIds';
+import {
+ PredictMarketListSelectorsIDs,
+ PredictSearchSelectorsIDs,
+ PredictFeedSelectorsIDs,
+ PredictFeedMockSelectorsIDs,
+ getPredictMarketListSelector,
+ getPredictSearchSelector,
+ getPredictFeedSelector,
+ getPredictFeedMockSelector,
+} from '../../Predict.testIds';
import PredictFeed from './PredictFeed';
jest.mock('react-native-pager-view', () => {
const MockReact = jest.requireActual('react');
const { View } = jest.requireActual('react-native');
+ // Jest mock factory runs before module imports; require() needed for testIds
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const PredictTestIds = require('../../Predict.testIds');
return {
__esModule: true,
default: jest.fn(({ children, onPageSelected }) => (
-
+
{MockReact.Children.map(
children,
(child: React.ReactElement, index: number) =>
MockReact.cloneElement(child, {
- testID: `pager-page-${index}`,
+ testID:
+ PredictTestIds.getPredictFeedMockSelector.pagerPage(index),
onTouchEnd: () =>
onPageSelected?.({ nativeEvent: { position: index } }),
}),
@@ -26,9 +39,11 @@ jest.mock('react-native-pager-view', () => {
jest.mock('../../components/PredictBalance', () => {
const { View, Text } = jest.requireActual('react-native');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const PredictTestIds = require('../../Predict.testIds');
return {
PredictBalance: jest.fn(() => (
-
+
Balance Component
)),
@@ -95,9 +110,13 @@ jest.mock('../../components/PredictMarketSkeleton', () => {
jest.mock('../../components/PredictOffline', () => {
const { View } = jest.requireActual('react-native');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const PredictTestIds = require('../../Predict.testIds');
return {
__esModule: true,
- default: jest.fn(() => ),
+ default: jest.fn(() => (
+
+ )),
};
});
@@ -201,19 +220,25 @@ jest.mock('../../hooks/usePredictMeasurement', () => ({
jest.mock('../../../../../component-library/components-temp/Tabs', () => {
const { View, Pressable, Text } = jest.requireActual('react-native');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const PredictTestIds = require('../../Predict.testIds');
return {
TabsBar: jest.fn(({ tabs, activeIndex, onTabPress, testID }) => (
{tabs.map((tab: { key: string; label: string }, index: number) => (
onTabPress(index)}
>
{tab.label}
))}
-
+
)),
TabItem: {},
@@ -276,16 +301,22 @@ describe('PredictFeed', () => {
expect(
getByTestId(PredictMarketListSelectorsIDs.BACK_BUTTON),
).toBeOnTheScreen();
- expect(getByTestId('predict-search-button')).toBeOnTheScreen();
- expect(getByTestId('predict-balance-mock')).toBeOnTheScreen();
- expect(getByTestId('predict-feed-tabs')).toBeOnTheScreen();
- expect(getByTestId('pager-view-mock')).toBeOnTheScreen();
+ expect(
+ getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(PredictFeedMockSelectorsIDs.BALANCE_MOCK),
+ ).toBeOnTheScreen();
+ expect(getByTestId(PredictFeedSelectorsIDs.TABS)).toBeOnTheScreen();
+ expect(
+ getByTestId(PredictFeedMockSelectorsIDs.PAGER_VIEW),
+ ).toBeOnTheScreen();
});
it('hides search overlay on initial render', () => {
const { queryByTestId } = render();
- expect(queryByTestId('search-icon')).toBeNull();
+ expect(queryByTestId(PredictFeedSelectorsIDs.SEARCH_ICON)).toBeNull();
});
});
@@ -293,18 +324,20 @@ describe('PredictFeed', () => {
it('opens search overlay when search button pressed', () => {
const { getByTestId } = render();
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
- expect(getByTestId('search-icon')).toBeOnTheScreen();
+ expect(
+ getByTestId(PredictFeedSelectorsIDs.SEARCH_ICON),
+ ).toBeOnTheScreen();
});
it('closes search overlay when cancel button pressed', () => {
const { getByTestId, getByText, queryByTestId } = render();
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
fireEvent.press(getByText('Cancel'));
- expect(queryByTestId('search-icon')).toBeNull();
+ expect(queryByTestId(PredictFeedSelectorsIDs.SEARCH_ICON)).toBeNull();
});
});
@@ -312,18 +345,30 @@ describe('PredictFeed', () => {
it('renders all six category tabs', () => {
const { getByTestId } = render();
- expect(getByTestId('tab-trending')).toBeOnTheScreen();
- expect(getByTestId('tab-ending-soon')).toBeOnTheScreen();
- expect(getByTestId('tab-new')).toBeOnTheScreen();
- expect(getByTestId('tab-sports')).toBeOnTheScreen();
- expect(getByTestId('tab-crypto')).toBeOnTheScreen();
- expect(getByTestId('tab-politics')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('trending')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('ending-soon')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('new')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('sports')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('crypto')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('politics')),
+ ).toBeOnTheScreen();
});
it('does not track analytics when tab pressed', () => {
const { getByTestId } = render();
- fireEvent.press(getByTestId('tab-sports'));
+ fireEvent.press(getByTestId(getPredictFeedMockSelector.tabKey('sports')));
expect(mockSessionManager.trackTabChange).not.toHaveBeenCalled();
});
@@ -394,8 +439,12 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
- expect(getByTestId('skeleton-loading-trending-1')).toBeOnTheScreen();
- expect(getByTestId('skeleton-loading-trending-2')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedSelector.skeletonLoading('trending', 1)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedSelector.skeletonLoading('trending', 2)),
+ ).toBeOnTheScreen();
});
});
@@ -413,7 +462,9 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
- expect(getByTestId('predict-offline-mock')).toBeOnTheScreen();
+ expect(
+ getByTestId(PredictFeedMockSelectorsIDs.OFFLINE_MOCK),
+ ).toBeOnTheScreen();
});
});
@@ -431,7 +482,9 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
- expect(getByTestId('predict-empty-state-trending')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedSelector.emptyState('trending')),
+ ).toBeOnTheScreen();
});
});
@@ -439,12 +492,16 @@ describe('PredictFeed', () => {
it('displays search results when query is entered', () => {
const { getByTestId, getByPlaceholderText } = render();
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'bitcoin');
- expect(getByTestId('predict-search-result-0')).toBeOnTheScreen();
- expect(getByTestId('predict-search-result-1')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictSearchSelector.resultCard(0)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictSearchSelector.resultCard(1)),
+ ).toBeOnTheScreen();
});
it('displays skeleton loaders while search is fetching', () => {
@@ -460,11 +517,13 @@ describe('PredictFeed', () => {
const { getByTestId, getByPlaceholderText } = render();
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'bitcoin');
- expect(getByTestId('search-skeleton-1')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedSelector.searchSkeleton(1)),
+ ).toBeOnTheScreen();
});
it('clears search query when clear button is pressed', () => {
@@ -472,16 +531,20 @@ describe('PredictFeed', () => {
,
);
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'test query');
- fireEvent.press(getByTestId('clear-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.CLEAR_BUTTON));
// After clearing search, the clear button should no longer be visible
// (only shows when searchQuery.length > 0)
- expect(queryByTestId('clear-button')).not.toBeOnTheScreen();
+ expect(
+ queryByTestId(PredictSearchSelectorsIDs.CLEAR_BUTTON),
+ ).not.toBeOnTheScreen();
// Trending results visible when no search query is empty
- expect(getByTestId('predict-search-result-0')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictSearchSelector.resultCard(0)),
+ ).toBeOnTheScreen();
});
});
@@ -501,7 +564,7 @@ describe('PredictFeed', () => {
});
const { getByTestId } = render();
- const page1 = getByTestId('pager-page-1');
+ const page1 = getByTestId(getPredictFeedMockSelector.pagerPage(1));
fireEvent(page1, 'onTouchEnd');
@@ -528,7 +591,7 @@ describe('PredictFeed', () => {
const { queryByTestId } = render();
- expect(queryByTestId('pager-view-mock')).toBeNull();
+ expect(queryByTestId(PredictFeedMockSelectorsIDs.PAGER_VIEW)).toBeNull();
});
});
@@ -537,10 +600,14 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
expect(
- getByTestId('predict-market-list-trending-card-1'),
+ getByTestId(
+ getPredictMarketListSelector.marketCardByCategory('trending', 1),
+ ),
).toBeOnTheScreen();
expect(
- getByTestId('predict-market-list-trending-card-2'),
+ getByTestId(
+ getPredictMarketListSelector.marketCardByCategory('trending', 2),
+ ),
).toBeOnTheScreen();
});
});
@@ -561,7 +628,7 @@ describe('PredictFeed', () => {
,
);
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'nonexistent');
@@ -583,11 +650,13 @@ describe('PredictFeed', () => {
,
);
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'test');
- const offlineElements = getAllByTestId('predict-offline-mock');
+ const offlineElements = getAllByTestId(
+ PredictFeedMockSelectorsIDs.OFFLINE_MOCK,
+ );
expect(offlineElements.length).toBeGreaterThan(0);
});
});
@@ -609,8 +678,12 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
- expect(getByTestId('skeleton-footer-trending-1')).toBeOnTheScreen();
- expect(getByTestId('skeleton-footer-trending-2')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedSelector.skeletonFooter('trending', 1)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedSelector.skeletonFooter('trending', 2)),
+ ).toBeOnTheScreen();
});
});
@@ -649,7 +722,7 @@ describe('PredictFeed', () => {
mockUseDebouncedValue.mockReturnValue('debounced-query');
const { getByTestId, getByPlaceholderText } = render();
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'bitcoin');
@@ -672,11 +745,13 @@ describe('PredictFeed', () => {
});
const { getByTestId, getByPlaceholderText } = render();
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'bitcoin');
- expect(getByTestId('search-skeleton-1')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedSelector.searchSkeleton(1)),
+ ).toBeOnTheScreen();
});
it('displays search results after debounce completes', () => {
@@ -695,18 +770,22 @@ describe('PredictFeed', () => {
});
const { getByTestId, getByPlaceholderText } = render();
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'bitcoin');
- expect(getByTestId('predict-search-result-0')).toBeOnTheScreen();
- expect(getByTestId('predict-search-result-1')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictSearchSelector.resultCard(0)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictSearchSelector.resultCard(1)),
+ ).toBeOnTheScreen();
});
it('invokes useDebouncedValue with 200ms delay', () => {
const { getByTestId, getByPlaceholderText } = render();
- fireEvent.press(getByTestId('predict-search-button'));
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
const searchInput = getByPlaceholderText('Search prediction markets');
fireEvent.changeText(searchInput, 'test');
@@ -723,8 +802,12 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
- expect(getByTestId('tab-hot')).toBeOnTheScreen();
- expect(getByTestId('tab-trending')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('hot')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('trending')),
+ ).toBeOnTheScreen();
});
it('does not render Hot tab when flag is disabled', () => {
@@ -735,8 +818,12 @@ describe('PredictFeed', () => {
const { queryByTestId, getByTestId } = render();
- expect(queryByTestId('tab-hot')).toBeNull();
- expect(getByTestId('tab-trending')).toBeOnTheScreen();
+ expect(
+ queryByTestId(getPredictFeedMockSelector.tabKey('hot')),
+ ).toBeNull();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('trending')),
+ ).toBeOnTheScreen();
});
it('renders seven category tabs when hot tab is enabled', () => {
@@ -747,13 +834,27 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
- expect(getByTestId('tab-hot')).toBeOnTheScreen();
- expect(getByTestId('tab-trending')).toBeOnTheScreen();
- expect(getByTestId('tab-ending-soon')).toBeOnTheScreen();
- expect(getByTestId('tab-new')).toBeOnTheScreen();
- expect(getByTestId('tab-sports')).toBeOnTheScreen();
- expect(getByTestId('tab-crypto')).toBeOnTheScreen();
- expect(getByTestId('tab-politics')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('hot')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('trending')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('ending-soon')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('new')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('sports')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('crypto')),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.tabKey('politics')),
+ ).toBeOnTheScreen();
});
it('renders seven pager pages when hot tab is enabled', () => {
@@ -764,13 +865,27 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
- expect(getByTestId('pager-page-0')).toBeOnTheScreen();
- expect(getByTestId('pager-page-1')).toBeOnTheScreen();
- expect(getByTestId('pager-page-2')).toBeOnTheScreen();
- expect(getByTestId('pager-page-3')).toBeOnTheScreen();
- expect(getByTestId('pager-page-4')).toBeOnTheScreen();
- expect(getByTestId('pager-page-5')).toBeOnTheScreen();
- expect(getByTestId('pager-page-6')).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.pagerPage(0)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.pagerPage(1)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.pagerPage(2)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.pagerPage(3)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.pagerPage(4)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.pagerPage(5)),
+ ).toBeOnTheScreen();
+ expect(
+ getByTestId(getPredictFeedMockSelector.pagerPage(6)),
+ ).toBeOnTheScreen();
});
it('tracks tab change for hot tab when swiped to', () => {
@@ -793,7 +908,7 @@ describe('PredictFeed', () => {
});
const { getByTestId } = render();
- const hotTabPage = getByTestId('pager-page-0');
+ const hotTabPage = getByTestId(getPredictFeedMockSelector.pagerPage(0));
fireEvent(hotTabPage, 'onTouchEnd');
@@ -835,7 +950,9 @@ describe('PredictFeed', () => {
const { getByTestId } = render();
- expect(getByTestId('search-icon')).toBeOnTheScreen();
+ expect(
+ getByTestId(PredictFeedSelectorsIDs.SEARCH_ICON),
+ ).toBeOnTheScreen();
},
);
@@ -866,10 +983,12 @@ describe('PredictFeed', () => {
const { getByText, getByTestId, queryByTestId } = render();
- expect(getByTestId('search-icon')).toBeOnTheScreen();
+ expect(
+ getByTestId(PredictFeedSelectorsIDs.SEARCH_ICON),
+ ).toBeOnTheScreen();
fireEvent.press(getByText('Cancel'));
- expect(queryByTestId('search-icon')).toBeNull();
+ expect(queryByTestId(PredictFeedSelectorsIDs.SEARCH_ICON)).toBeNull();
});
});
});
diff --git a/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx b/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx
index 6a3db326fc7..51615f6a308 100644
--- a/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx
+++ b/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx
@@ -47,7 +47,11 @@ import {
} from '@react-navigation/native';
import {
PredictMarketListSelectorsIDs,
+ PredictSearchSelectorsIDs,
+ PredictFeedSelectorsIDs,
getPredictMarketListSelector,
+ getPredictFeedSelector,
+ getPredictSearchSelector,
} from '../../Predict.testIds';
import { usePredictMarketData } from '../../hooks/usePredictMarketData';
import { useDebouncedValue } from '../../../../hooks/useDebouncedValue';
@@ -120,7 +124,7 @@ const PredictFeedTabBar: React.FC = ({
tabs={tabItems}
activeIndex={activeIndex}
onTabPress={onTabPress}
- testID="predict-feed-tabs"
+ testID={PredictFeedSelectorsIDs.TABS}
/>
);
};
@@ -294,8 +298,12 @@ const PredictTabContent: React.FC = ({
if (!isFetchingMore) return null;
return (
-
-
+
+
);
}, [isFetchingMore, category]);
@@ -318,10 +326,18 @@ const PredictTabContent: React.FC = ({
if (!hasEverBeenActive || (isFetching && !isRefreshing && !isFetchingMore)) {
return (
-
-
-
-
+
+
+
+
);
}
@@ -337,7 +353,7 @@ const PredictTabContent: React.FC = ({
if (!marketData || marketData.length === 0) {
return (
@@ -351,7 +367,7 @@ const PredictTabContent: React.FC = ({
return (
= ({
style={tw.style('flex-1')}
initialPage={initialPage}
onPageSelected={handlePageSelected}
- testID="predict-feed-pager"
+ testID={PredictFeedSelectorsIDs.PAGER}
>
{tabs.map((tab, index) => (
= ({
),
[],
@@ -513,7 +529,7 @@ const PredictSearchOverlay: React.FC = ({
twClassName="flex-1 bg-muted rounded-lg px-3 py-2"
>
= ({
autoFocus
/>
{searchQuery.length > 0 && (
- onSearchChange('')}>
+ onSearchChange('')}
+ >
= ({
{isSearchLoading ? (
-
-
-
+
+
+
) : error ? (
@@ -705,7 +730,7 @@ const PredictFeed: React.FC = () => {
{
iconName: IconName.Search,
onPress: showSearch,
- testID: 'predict-search-button',
+ testID: PredictSearchSelectorsIDs.SEARCH_BUTTON,
},
]}
/>
diff --git a/app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx b/app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx
new file mode 100644
index 00000000000..3766759ad3a
--- /dev/null
+++ b/app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx
@@ -0,0 +1,300 @@
+/**
+ * Component view tests for PredictFeed.
+ *
+ * These are the first component view tests for the Predict area.
+ * They test user-oriented behaviour via Engine spies and real interactions —
+ * not static render checks.
+ *
+ * Run with: yarn jest -c jest.config.view.js PredictFeed.view.test --runInBand --silent --coverage=false
+ */
+import '../../../../../../tests/component-view/mocks';
+import Engine from '../../../../../../app/core/Engine';
+import {
+ renderPredictFeedView,
+ renderPredictFeedViewWithRoutes,
+} from '../../../../../../tests/component-view/renderers/predict';
+import { fireEvent, waitFor, within } from '@testing-library/react-native';
+import {
+ PredictMarketListSelectorsIDs,
+ PredictSearchSelectorsIDs,
+ PredictBalanceSelectorsIDs,
+ getPredictSearchSelector,
+} from '../../Predict.testIds';
+import Routes from '../../../../../constants/navigation/Routes';
+import { MOCK_PREDICT_MARKET } from '../../../../../../tests/component-view/fixtures/predict';
+
+const SEARCH_PLACEHOLDER = 'Search prediction markets';
+const CANCEL_TEXT = 'Cancel';
+
+describe('PredictFeed', () => {
+ describe('search interaction', () => {
+ it('opens the search overlay when the user presses the search icon', async () => {
+ const { getByTestId, findByPlaceholderText } = renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+
+ expect(await findByPlaceholderText(SEARCH_PLACEHOLDER)).toBeOnTheScreen();
+ });
+
+ it('calls PredictController.getMarkets with the typed query after the user searches', async () => {
+ const getMarketsSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarkets',
+ );
+
+ const { getByTestId, findByPlaceholderText } = renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+
+ const searchInput = await findByPlaceholderText(SEARCH_PLACEHOLDER);
+ fireEvent.changeText(searchInput, 'bitcoin');
+
+ await waitFor(
+ () => {
+ expect(getMarketsSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ q: 'bitcoin' }),
+ );
+ },
+ { timeout: 2000 },
+ );
+
+ getMarketsSpy.mockRestore();
+ });
+
+ it('closes the search overlay when the user presses Cancel', async () => {
+ const {
+ getByTestId,
+ findByText,
+ findByPlaceholderText,
+ queryByPlaceholderText,
+ } = renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+ await findByPlaceholderText(SEARCH_PLACEHOLDER);
+
+ fireEvent.press(await findByText(CANCEL_TEXT));
+
+ await waitFor(() => {
+ expect(
+ queryByPlaceholderText(SEARCH_PLACEHOLDER),
+ ).not.toBeOnTheScreen();
+ });
+ });
+
+ it('hides the clear button after the user clears the typed query', async () => {
+ const { getByTestId, findByPlaceholderText, queryByTestId } =
+ renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+
+ const searchInput = await findByPlaceholderText(SEARCH_PLACEHOLDER);
+ fireEvent.changeText(searchInput, 'ethereum');
+
+ await waitFor(() => {
+ expect(
+ getByTestId(PredictSearchSelectorsIDs.CLEAR_BUTTON),
+ ).toBeOnTheScreen();
+ });
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.CLEAR_BUTTON));
+
+ await waitFor(() => {
+ expect(
+ queryByTestId(PredictSearchSelectorsIDs.CLEAR_BUTTON),
+ ).not.toBeOnTheScreen();
+ });
+ });
+
+ it('shows a "no results" message that includes the typed query when getMarkets returns empty', async () => {
+ const { getByTestId, findByPlaceholderText, findByText } =
+ renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+
+ const searchInput = await findByPlaceholderText(SEARCH_PLACEHOLDER);
+ fireEvent.changeText(searchInput, 'xyznotfound');
+
+ expect(
+ await findByText('No results found for "xyznotfound"'),
+ ).toBeOnTheScreen();
+ });
+
+ it('shows complete market data in the search result card after getMarkets resolves', async () => {
+ // Arrange
+ const getMarketsSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarkets',
+ );
+ getMarketsSpy.mockResolvedValue([MOCK_PREDICT_MARKET]);
+ const { getByTestId, findByPlaceholderText, findByTestId } =
+ renderPredictFeedView();
+
+ // Act — user opens search and types a query
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+ const searchInput = await findByPlaceholderText(SEARCH_PLACEHOLDER);
+ fireEvent.changeText(searchInput, 'bitcoin');
+
+ // Assert — result card contains all significant market fields
+ const resultCard = await findByTestId(
+ getPredictSearchSelector.resultCard(0),
+ {},
+ { timeout: 3000 },
+ );
+ expect(
+ within(resultCard).getByText(MOCK_PREDICT_MARKET.title),
+ ).toBeOnTheScreen();
+ expect(within(resultCard).getByText(/Yes/)).toBeOnTheScreen();
+ expect(within(resultCard).getByText(/No/)).toBeOnTheScreen();
+
+ getMarketsSpy.mockRestore();
+ });
+
+ it('navigates to market details when the user taps a search result card', async () => {
+ const getMarketsSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarkets',
+ );
+ getMarketsSpy.mockResolvedValue([MOCK_PREDICT_MARKET]);
+
+ const { getByTestId, findByPlaceholderText, findByTestId } =
+ renderPredictFeedViewWithRoutes({
+ extraRoutes: [{ name: Routes.PREDICT.ROOT }],
+ });
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+
+ const searchInput = await findByPlaceholderText(SEARCH_PLACEHOLDER);
+ fireEvent.changeText(searchInput, 'bitcoin');
+
+ const resultCard = await findByTestId(
+ getPredictSearchSelector.resultCard(0),
+ {},
+ { timeout: 3000 },
+ );
+ fireEvent.press(resultCard);
+
+ expect(
+ await findByTestId(`route-${Routes.PREDICT.ROOT}`),
+ ).toBeOnTheScreen();
+
+ getMarketsSpy.mockRestore();
+ });
+ });
+
+ describe('back navigation', () => {
+ it('navigates to the wallet when the user presses back from the root feed', async () => {
+ const { getByTestId, findByTestId } = renderPredictFeedViewWithRoutes({
+ extraRoutes: [{ name: Routes.WALLET.HOME }],
+ });
+
+ await findByTestId(PredictMarketListSelectorsIDs.CONTAINER);
+
+ fireEvent.press(getByTestId(PredictMarketListSelectorsIDs.BACK_BUTTON));
+
+ expect(
+ await findByTestId(`route-${Routes.WALLET.HOME}`),
+ ).toBeOnTheScreen();
+ });
+ });
+
+ describe('balance card', () => {
+ it('calls getBalance and displays the balance card once the balance resolves', async () => {
+ const getBalanceSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getBalance',
+ );
+
+ const { findByTestId } = renderPredictFeedView();
+
+ expect(
+ await findByTestId(PredictBalanceSelectorsIDs.BALANCE_CARD),
+ ).toBeOnTheScreen();
+ expect(getBalanceSpy).toHaveBeenCalled();
+
+ getBalanceSpy.mockRestore();
+ });
+
+ it('calls trackGeoBlockTriggered when the user presses Add Funds while ineligible', async () => {
+ const trackGeoBlockSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'trackGeoBlockTriggered',
+ );
+
+ const { findByTestId, findByText } = renderPredictFeedView();
+
+ await findByTestId(PredictBalanceSelectorsIDs.BALANCE_CARD);
+ fireEvent.press(await findByText('Add funds'));
+
+ await waitFor(() => {
+ expect(trackGeoBlockSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ attemptedAction: 'deposit' }),
+ );
+ });
+
+ trackGeoBlockSpy.mockRestore();
+ });
+ });
+
+ describe('search error recovery', () => {
+ it('shows the offline error state in the search overlay when all market fetch retries fail', async () => {
+ const getMarketsSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarkets',
+ );
+ getMarketsSpy.mockRejectedValue(new Error('Network error'));
+
+ const { getByTestId, findByPlaceholderText, findByTestId } =
+ renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+ await findByPlaceholderText(SEARCH_PLACEHOLDER);
+
+ // The hook retries up to 3 times with exponential backoff (~3-5 s total).
+ // findByTestId waits until the error state appears after all retries exhaust.
+ expect(
+ await findByTestId(
+ PredictSearchSelectorsIDs.ERROR_STATE,
+ {},
+ { timeout: 10000 },
+ ),
+ ).toBeOnTheScreen();
+
+ getMarketsSpy.mockRestore();
+ });
+
+ it('calls getMarkets again when the user presses Retry after an error', async () => {
+ const getMarketsSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarkets',
+ );
+ getMarketsSpy.mockRejectedValue(new Error('Network error'));
+
+ const { getByTestId, findByPlaceholderText, findByTestId, findByText } =
+ renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+ await findByPlaceholderText(SEARCH_PLACEHOLDER);
+
+ await findByTestId(
+ PredictSearchSelectorsIDs.ERROR_STATE,
+ {},
+ { timeout: 10000 },
+ );
+
+ const callCountBeforeRetry = getMarketsSpy.mock.calls.length;
+
+ // Make subsequent calls succeed so the retry completes quickly.
+ getMarketsSpy.mockResolvedValue([]);
+
+ fireEvent.press(await findByText('Retry'));
+
+ await waitFor(() => {
+ expect(getMarketsSpy.mock.calls.length).toBeGreaterThan(
+ callCountBeforeRetry,
+ );
+ });
+
+ getMarketsSpy.mockRestore();
+ });
+ });
+});
diff --git a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx
index 8c9a3974b61..5549ef80007 100644
--- a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx
+++ b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx
@@ -9,6 +9,10 @@ import {
useRoute,
} from '@react-navigation/native';
import PredictMarketDetails from './PredictMarketDetails';
+import {
+ PredictMarketDetailsSelectorsIDs,
+ getPredictMarketDetailsSelector,
+} from '../../Predict.testIds';
import { PredictPriceHistoryInterval } from '../../types';
import type { UsePredictPriceHistoryOptions } from '../../hooks/usePredictPriceHistory';
import { strings } from '../../../../../../locales/i18n';
@@ -79,6 +83,9 @@ jest.mock('../../../../../component-library/components/Icons/Icon', () => {
const ActualIcon = jest.requireActual(
'../../../../../component-library/components/Icons/Icon',
);
+ // Jest mock factory runs before module imports; require() needed for testIds
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const PredictTestIds = require('../../Predict.testIds');
return {
...ActualIcon,
__esModule: true,
@@ -92,7 +99,15 @@ jest.mock('../../../../../component-library/components/Icons/Icon', () => {
[key: string]: unknown;
}) => {
const Icon = ActualIcon.default;
- return ;
+ return (
+
+ );
},
};
});
@@ -234,9 +249,13 @@ jest.mock('../../hooks/usePredictOrderPreview', () => ({
jest.mock('../../components/PredictDetailsChart/PredictDetailsChart', () => {
const { View, Text } = jest.requireActual('react-native');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const PredictTestIds = require('../../Predict.testIds');
return function MockPredictDetailsChart() {
return (
-
+
Chart Component
);
@@ -260,10 +279,12 @@ jest.mock('../../components/PredictMarketOutcome', () => {
jest.mock('../../components/PredictShareButton/PredictShareButton', () => {
const { View } = jest.requireActual('react-native');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const PredictTestIds = require('../../Predict.testIds');
return function MockPredictShareButton({ marketId }: { marketId?: string }) {
return (
);
@@ -272,13 +293,19 @@ jest.mock('../../components/PredictShareButton/PredictShareButton', () => {
jest.mock('../../components/PredictGameDetailsContent', () => {
const { View, Text } = jest.requireActual('react-native');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const PredictTestIds = require('../../Predict.testIds');
return function MockPredictGameDetailsContent({
market,
}: {
market: { title?: string };
}) {
return (
-
+
{market?.title || 'Game Details'}
);
@@ -669,7 +696,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest();
expect(
- screen.getByTestId('predict-market-details-screen'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.SCREEN),
).toBeOnTheScreen();
});
@@ -688,13 +715,19 @@ describe('PredictMarketDetails', () => {
// Check that skeleton loaders appear
expect(
- screen.getByTestId('predict-details-header-skeleton-back-button'),
+ screen.getByTestId(
+ PredictMarketDetailsSelectorsIDs.DETAILS_HEADER_SKELETON_BACK_BUTTON,
+ ),
).toBeOnTheScreen();
expect(
- screen.getByTestId('predict-details-content-skeleton-line-1'),
+ screen.getByTestId(
+ PredictMarketDetailsSelectorsIDs.DETAILS_CONTENT_SKELETON_LINE_1,
+ ),
).toBeOnTheScreen();
expect(
- screen.getByTestId('predict-details-buttons-skeleton-button-1'),
+ screen.getByTestId(
+ PredictMarketDetailsSelectorsIDs.DETAILS_BUTTONS_SKELETON_BUTTON_1,
+ ),
).toBeOnTheScreen();
});
@@ -703,26 +736,32 @@ describe('PredictMarketDetails', () => {
// Screen renders without a title; other sections may still show loading keys
expect(
- screen.getByTestId('predict-market-details-screen'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.SCREEN),
).toBeOnTheScreen();
});
it('renders back button with correct accessibility', () => {
setupPredictMarketDetailsTest();
- expect(screen.getByTestId('icon-ArrowLeft')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(getPredictMarketDetailsSelector.icon('ArrowLeft')),
+ ).toBeOnTheScreen();
});
it('renders share button in header when market data is loaded', () => {
setupPredictMarketDetailsTest();
- expect(screen.getByTestId('predict-share-button')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.SHARE_BUTTON),
+ ).toBeOnTheScreen();
});
it('passes market.id to share button', () => {
setupPredictMarketDetailsTest({ id: 'test-market-id' });
- const shareButton = screen.getByTestId('predict-share-button');
+ const shareButton = screen.getByTestId(
+ PredictMarketDetailsSelectorsIDs.SHARE_BUTTON,
+ );
expect(shareButton.props.accessibilityHint).toBe(
'marketId:test-market-id',
@@ -737,10 +776,12 @@ describe('PredictMarketDetails', () => {
);
expect(
- screen.queryByTestId('predict-share-button'),
+ screen.queryByTestId(PredictMarketDetailsSelectorsIDs.SHARE_BUTTON),
).not.toBeOnTheScreen();
expect(
- screen.getByTestId('predict-details-header-skeleton-back-button'),
+ screen.getByTestId(
+ PredictMarketDetailsSelectorsIDs.DETAILS_HEADER_SKELETON_BACK_BUTTON,
+ ),
).toBeOnTheScreen();
});
});
@@ -750,7 +791,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest();
const aboutTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-1',
+ getPredictMarketDetailsSelector.tabBarTab(1),
);
fireEvent.press(aboutTab);
@@ -763,7 +804,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest();
const aboutTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-1',
+ getPredictMarketDetailsSelector.tabBarTab(1),
);
fireEvent.press(aboutTab);
@@ -777,7 +818,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest();
const aboutTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-1',
+ getPredictMarketDetailsSelector.tabBarTab(1),
);
fireEvent.press(aboutTab);
@@ -791,7 +832,7 @@ describe('PredictMarketDetails', () => {
const { mockNavigate } = setupPredictMarketDetailsTest();
const aboutTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-1',
+ getPredictMarketDetailsSelector.tabBarTab(1),
);
fireEvent.press(aboutTab);
@@ -839,13 +880,17 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(singleOutcomeMarket);
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('renders multiple outcome chart for binary markets with two outcomes', () => {
setupPredictMarketDetailsTest();
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('renders multiple outcome chart for multi-outcome markets', () => {
@@ -877,7 +922,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(multiOutcomeMarket);
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('does not render chart when all outcomes are closed', () => {
@@ -910,7 +957,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(closedOutcomesMarket);
expect(
- screen.queryByTestId('predict-details-chart'),
+ screen.queryByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
).not.toBeOnTheScreen();
});
@@ -930,7 +977,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(noOpenOutcomesMarket);
expect(
- screen.queryByTestId('predict-details-chart'),
+ screen.queryByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
).not.toBeOnTheScreen();
});
@@ -956,7 +1003,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(mixedStatusMarket);
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('limits chart data to first 3 open outcomes when more are available', () => {
@@ -1044,7 +1093,9 @@ describe('PredictMarketDetails', () => {
marketIds: ['token-2', 'token-3'],
}),
);
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('removes chart when closed market lacks open outcomes', () => {
@@ -1067,7 +1118,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(emptyOutcomesMarket);
expect(
- screen.queryByTestId('predict-details-chart'),
+ screen.queryByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
).not.toBeOnTheScreen();
});
@@ -1086,7 +1137,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(noTokensMarket);
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
});
@@ -1095,10 +1148,12 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest();
expect(
- screen.getByTestId('predict-market-details-tab-bar'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.TAB_BAR),
).toBeOnTheScreen();
expect(
- screen.getByTestId('predict-market-details-scrollable-tab-view'),
+ screen.getByTestId(
+ PredictMarketDetailsSelectorsIDs.SCROLLABLE_TAB_VIEW,
+ ),
).toBeOnTheScreen();
});
@@ -1106,7 +1161,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest();
const aboutTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-1',
+ getPredictMarketDetailsSelector.tabBarTab(1),
);
fireEvent.press(aboutTab);
@@ -1185,7 +1240,9 @@ describe('PredictMarketDetails', () => {
it('handles back button press correctly', () => {
const { mockGoBack, mockCanGoBack } = setupPredictMarketDetailsTest();
- const backButton = screen.getByTestId('icon-ArrowLeft');
+ const backButton = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowLeft'),
+ );
fireEvent.press(backButton);
expect(mockCanGoBack).toHaveBeenCalled();
@@ -1196,7 +1253,9 @@ describe('PredictMarketDetails', () => {
const { mockCanGoBack, mockNavigate } = setupPredictMarketDetailsTest();
mockCanGoBack.mockReturnValue(false);
- const backButton = screen.getByTestId('icon-ArrowLeft');
+ const backButton = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowLeft'),
+ );
fireEvent.press(backButton);
expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT);
@@ -1270,7 +1329,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithoutEndDate);
const aboutTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-1',
+ getPredictMarketDetailsSelector.tabBarTab(1),
);
fireEvent.press(aboutTab);
@@ -1311,7 +1370,9 @@ describe('PredictMarketDetails', () => {
const { mockMarket } = setupPredictMarketDetailsTest();
// Find the chart component and trigger timeframe change
- const chartComponent = screen.getByTestId('predict-details-chart');
+ const chartComponent = screen.getByTestId(
+ PredictMarketDetailsSelectorsIDs.DETAILS_CHART,
+ );
expect(chartComponent).toBeOnTheScreen();
// The timeframe change is handled internally by the component
@@ -1340,7 +1401,7 @@ describe('PredictMarketDetails', () => {
// Switch to Positions tab (index 0 when positions exist)
const positionsTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-0',
+ getPredictMarketDetailsSelector.tabBarTab(0),
);
fireEvent.press(positionsTab);
@@ -1498,7 +1559,7 @@ describe('PredictMarketDetails', () => {
// Switch to Positions tab (index 0 when positions exist)
const positionsTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-0',
+ getPredictMarketDetailsSelector.tabBarTab(0),
);
fireEvent.press(positionsTab);
@@ -1533,7 +1594,7 @@ describe('PredictMarketDetails', () => {
// Switch to Positions tab (index 0 when positions exist)
const positionsTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-0',
+ getPredictMarketDetailsSelector.tabBarTab(0),
);
fireEvent.press(positionsTab);
@@ -1572,7 +1633,7 @@ describe('PredictMarketDetails', () => {
// Outcomes is the default tab when there are no positions
expect(
- screen.getByTestId('predict-market-details-outcomes-tab'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.OUTCOMES_TAB),
).toBeOnTheScreen();
});
@@ -1678,7 +1739,7 @@ describe('PredictMarketDetails', () => {
// Switch to Positions tab (index 0 when positions exist)
const positionsTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-0',
+ getPredictMarketDetailsSelector.tabBarTab(0),
);
fireEvent.press(positionsTab);
@@ -1711,7 +1772,7 @@ describe('PredictMarketDetails', () => {
// Switch to Positions tab (index 0 when positions exist)
const positionsTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-0',
+ getPredictMarketDetailsSelector.tabBarTab(0),
);
fireEvent.press(positionsTab);
@@ -1744,7 +1805,7 @@ describe('PredictMarketDetails', () => {
// Switch to Positions tab (index 0 when positions exist)
const positionsTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-0',
+ getPredictMarketDetailsSelector.tabBarTab(0),
);
fireEvent.press(positionsTab);
@@ -1758,7 +1819,7 @@ describe('PredictMarketDetails', () => {
// Component should render without errors even with invalid data
expect(
- screen.getByTestId('predict-market-details-screen'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.SCREEN),
).toBeOnTheScreen();
});
@@ -1766,7 +1827,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest({}, { params: undefined });
expect(
- screen.getByTestId('predict-market-details-screen'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.SCREEN),
).toBeOnTheScreen();
});
@@ -1982,7 +2043,7 @@ describe('PredictMarketDetails', () => {
// Verify the component renders without errors
expect(
- screen.getByTestId('predict-market-details-screen'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.SCREEN),
).toBeOnTheScreen();
});
@@ -2111,7 +2172,7 @@ describe('PredictMarketDetails', () => {
// Switch to Positions tab (index 0 when positions exist)
const positionsTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-0',
+ getPredictMarketDetailsSelector.tabBarTab(0),
);
fireEvent.press(positionsTab);
@@ -2139,7 +2200,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(singleOutcomeMarket);
// Verify chart renders for single outcome
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('handles chart color selection for multiple outcomes', () => {
@@ -2166,7 +2229,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(multiOutcomeMarket);
// Verify chart renders for multiple outcomes
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('handles outcome without tokens correctly', () => {
@@ -2192,7 +2257,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest();
// Component should handle different fidelity settings based on timeframe
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('handles empty price histories array', () => {
@@ -2202,7 +2269,9 @@ describe('PredictMarketDetails', () => {
{ priceHistory: { priceHistories: [] } },
);
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('handles errors in price history', () => {
@@ -2212,7 +2281,9 @@ describe('PredictMarketDetails', () => {
{ priceHistory: { errors: ['Network error'] } },
);
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
describe('Price history fidelity adjustments', () => {
@@ -2527,7 +2598,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(closedMarket);
expect(
- screen.getByTestId('predict-market-details-outcomes-tab'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.OUTCOMES_TAB),
).toBeOnTheScreen();
expect(screen.getByText('Yes Outcome')).toBeOnTheScreen();
expect(screen.getByText('No Outcome')).toBeOnTheScreen();
@@ -2541,7 +2612,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(closedMarket);
const aboutTab = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-1',
+ getPredictMarketDetailsSelector.tabBarTab(1),
);
fireEvent.press(aboutTab);
@@ -2577,7 +2648,7 @@ describe('PredictMarketDetails', () => {
);
const aboutTabWithPositions = screen.getByTestId(
- 'predict-market-details-tab-bar-tab-2',
+ getPredictMarketDetailsSelector.tabBarTab(2),
);
fireEvent.press(aboutTabWithPositions);
@@ -2599,7 +2670,7 @@ describe('PredictMarketDetails', () => {
screen.queryByText('predict.market_details.volume'),
).not.toBeOnTheScreen();
expect(
- screen.getByTestId('predict-market-details-outcomes-tab'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.OUTCOMES_TAB),
).toBeOnTheScreen();
});
});
@@ -2667,7 +2738,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
it('displays resolved outcomes count badge', () => {
@@ -2731,7 +2804,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- const arrowDownIcon = screen.getByTestId('icon-ArrowDown');
+ const arrowDownIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ );
const pressable = arrowDownIcon.parent?.parent;
if (pressable) {
@@ -2773,7 +2848,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- const arrowDownIcon = screen.getByTestId('icon-ArrowDown');
+ const arrowDownIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ );
const pressable = arrowDownIcon.parent?.parent;
if (pressable) {
@@ -2817,7 +2894,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- const arrowDownIcon = screen.getByTestId('icon-ArrowDown');
+ const arrowDownIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ );
const pressable = arrowDownIcon.parent?.parent;
if (pressable) {
@@ -2859,7 +2938,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- const arrowDownIcon = screen.getByTestId('icon-ArrowDown');
+ const arrowDownIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ );
const pressable = arrowDownIcon.parent?.parent;
if (pressable) {
@@ -2898,7 +2979,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- const arrowDownIcon = screen.getByTestId('icon-ArrowDown');
+ const arrowDownIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ );
const pressable = arrowDownIcon.parent?.parent;
if (pressable) {
@@ -2937,7 +3020,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- expect(screen.getByTestId('icon-ArrowDown')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(getPredictMarketDetailsSelector.icon('ArrowDown')),
+ ).toBeOnTheScreen();
});
it('displays ArrowUp icon when expanded', () => {
@@ -2969,13 +3054,17 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- const arrowDownIcon = screen.getByTestId('icon-ArrowDown');
+ const arrowDownIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ );
const pressable = arrowDownIcon.parent?.parent;
if (pressable) {
fireEvent.press(pressable);
- expect(screen.getByTestId('icon-ArrowUp')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(getPredictMarketDetailsSelector.icon('ArrowUp')),
+ ).toBeOnTheScreen();
}
});
@@ -3008,19 +3097,27 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(marketWithPartialResolution);
- const arrowDownIcon = screen.getByTestId('icon-ArrowDown');
+ const arrowDownIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ );
const pressable = arrowDownIcon.parent?.parent;
if (pressable) {
fireEvent.press(pressable);
expect(screen.getByText('Option A')).toBeOnTheScreen();
- const arrowUpIcon = screen.getByTestId('icon-ArrowUp');
+ const arrowUpIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowUp'),
+ );
const pressableAgain = arrowUpIcon.parent?.parent;
if (pressableAgain) {
fireEvent.press(pressableAgain);
expect(screen.queryByText('Option A')).not.toBeOnTheScreen();
- expect(screen.getByTestId('icon-ArrowDown')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ ),
+ ).toBeOnTheScreen();
}
}
});
@@ -3100,7 +3197,9 @@ describe('PredictMarketDetails', () => {
expect(screen.getByText('2')).toBeOnTheScreen();
- const arrowDownIcon = screen.getByTestId('icon-ArrowDown');
+ const arrowDownIcon = screen.getByTestId(
+ getPredictMarketDetailsSelector.icon('ArrowDown'),
+ );
const pressable = arrowDownIcon.parent?.parent;
if (pressable) {
@@ -3139,7 +3238,9 @@ describe('PredictMarketDetails', () => {
expect(
screen.queryByText('predict.resolved_outcomes'),
).not.toBeOnTheScreen();
- expect(screen.getByTestId('predict-details-chart')).toBeOnTheScreen();
+ expect(
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.DETAILS_CHART),
+ ).toBeOnTheScreen();
});
});
@@ -3205,7 +3306,7 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest();
expect(
- screen.getByTestId('predict-market-details-screen'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.SCREEN),
).toBeOnTheScreen();
});
});
@@ -3428,7 +3529,9 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(gameMarket);
expect(
- screen.getByTestId('predict-game-details-content'),
+ screen.getByTestId(
+ PredictMarketDetailsSelectorsIDs.GAME_DETAILS_CONTENT,
+ ),
).toBeOnTheScreen();
expect(screen.getByText('NFL: Team A vs Team B')).toBeOnTheScreen();
});
@@ -3442,10 +3545,12 @@ describe('PredictMarketDetails', () => {
setupPredictMarketDetailsTest(regularMarket);
expect(
- screen.queryByTestId('predict-game-details-content'),
+ screen.queryByTestId(
+ PredictMarketDetailsSelectorsIDs.GAME_DETAILS_CONTENT,
+ ),
).not.toBeOnTheScreen();
expect(
- screen.getByTestId('predict-market-details-screen'),
+ screen.getByTestId(PredictMarketDetailsSelectorsIDs.SCREEN),
).toBeOnTheScreen();
});
});
diff --git a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.view.test.tsx b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.view.test.tsx
new file mode 100644
index 00000000000..272da826523
--- /dev/null
+++ b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.view.test.tsx
@@ -0,0 +1,144 @@
+/**
+ * Component view tests for PredictMarketDetails.
+ *
+ * Run with: yarn jest -c jest.config.view.js PredictMarketDetails.view.test --runInBand --silent --coverage=false
+ */
+import '../../../../../../tests/component-view/mocks';
+import Engine from '../../../../../../app/core/Engine';
+import {
+ renderPredictMarketDetailsView,
+ renderPredictMarketDetailsViewWithRoutes,
+} from '../../../../../../tests/component-view/renderers/predictMarketDetails';
+import { fireEvent, waitFor, within } from '@testing-library/react-native';
+import { PredictMarketDetailsSelectorsIDs } from '../../Predict.testIds';
+import Routes from '../../../../../constants/navigation/Routes';
+import { MOCK_PREDICT_MARKET } from '../../../../../../tests/component-view/fixtures/predict';
+
+describe('PredictMarketDetails', () => {
+ describe('initial load', () => {
+ it('calls getMarket with the marketId from route params when the screen mounts', async () => {
+ const getMarketSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarket',
+ );
+ getMarketSpy.mockResolvedValue(MOCK_PREDICT_MARKET);
+
+ renderPredictMarketDetailsView({
+ initialParams: { marketId: 'market-btc-1' },
+ });
+
+ await waitFor(() => {
+ expect(getMarketSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ marketId: 'market-btc-1' }),
+ );
+ });
+
+ getMarketSpy.mockRestore();
+ });
+
+ it('shows complete market data in the details screen after getMarket resolves', async () => {
+ // Arrange
+ const getMarketSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarket',
+ );
+ getMarketSpy.mockResolvedValue(MOCK_PREDICT_MARKET);
+ const { findByTestId, findByText } = renderPredictMarketDetailsView({
+ initialParams: { marketId: 'market-btc-1' },
+ });
+
+ // Assert — all significant fields of the loaded market are visible on screen.
+ // The async resolution of getMarket is the event under test (not a render scenario).
+ const screen = await findByTestId(
+ PredictMarketDetailsSelectorsIDs.SCREEN,
+ );
+ expect(
+ within(screen).getByText(MOCK_PREDICT_MARKET.title),
+ ).toBeOnTheScreen();
+ expect(await findByText(/Yes.*¢/)).toBeOnTheScreen();
+ expect(await findByText(/No.*¢/)).toBeOnTheScreen();
+
+ getMarketSpy.mockRestore();
+ });
+
+ it('calls trackGeoBlockTriggered when the user presses a bet button while ineligible', async () => {
+ const trackGeoBlockSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'trackGeoBlockTriggered',
+ );
+ const getMarketSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarket',
+ );
+ getMarketSpy.mockResolvedValue(MOCK_PREDICT_MARKET);
+
+ const { findByText } = renderPredictMarketDetailsView({
+ initialParams: { marketId: 'market-btc-1' },
+ });
+
+ // Bet button label is "Yes • {yesPercentage}¢"; press it while ineligible
+ fireEvent.press(await findByText(/Yes.*¢/));
+
+ await waitFor(() => {
+ expect(trackGeoBlockSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ attemptedAction: 'predict_action' }),
+ );
+ });
+
+ getMarketSpy.mockRestore();
+ trackGeoBlockSpy.mockRestore();
+ });
+
+ it('calls trackMarketDetailsOpened when the market and positions finish loading', async () => {
+ const trackSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'trackMarketDetailsOpened',
+ );
+ const getMarketSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarket',
+ );
+ getMarketSpy.mockResolvedValue(MOCK_PREDICT_MARKET);
+
+ renderPredictMarketDetailsView({
+ initialParams: { marketId: 'market-btc-1' },
+ });
+
+ await waitFor(() => {
+ expect(trackSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ marketId: 'market-btc-1' }),
+ );
+ });
+
+ getMarketSpy.mockRestore();
+ trackSpy.mockRestore();
+ });
+ });
+
+ describe('back navigation', () => {
+ it('navigates to the Predict root when the user presses back from the details screen', async () => {
+ const getMarketSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'getMarket',
+ );
+ getMarketSpy.mockResolvedValue(MOCK_PREDICT_MARKET);
+
+ const { findByTestId } = renderPredictMarketDetailsViewWithRoutes({
+ initialParams: { marketId: 'market-btc-1' },
+ extraRoutes: [{ name: Routes.PREDICT.ROOT }],
+ });
+
+ await findByTestId(PredictMarketDetailsSelectorsIDs.SCREEN);
+
+ fireEvent.press(
+ await findByTestId(PredictMarketDetailsSelectorsIDs.BACK_BUTTON),
+ );
+
+ expect(
+ await findByTestId(`route-${Routes.PREDICT.ROOT}`),
+ ).toBeOnTheScreen();
+
+ getMarketSpy.mockRestore();
+ });
+ });
+});
diff --git a/tests/component-view/fixtures/predict.ts b/tests/component-view/fixtures/predict.ts
new file mode 100644
index 00000000000..972600f7f7e
--- /dev/null
+++ b/tests/component-view/fixtures/predict.ts
@@ -0,0 +1,36 @@
+import {
+ Recurrence,
+ type PredictMarket,
+} from '../../../app/components/UI/Predict/types';
+
+export const MOCK_PREDICT_MARKET: PredictMarket = {
+ id: 'market-btc-1',
+ providerId: 'polymarket',
+ slug: 'will-btc-reach-100k',
+ title: 'Will Bitcoin reach $100k?',
+ description: 'Will Bitcoin reach $100k by end of year?',
+ image: '',
+ status: 'open',
+ recurrence: Recurrence.NONE,
+ category: 'trending',
+ tags: [],
+ outcomes: [
+ {
+ id: 'outcome-yes',
+ providerId: 'polymarket',
+ marketId: 'market-btc-1',
+ title: 'Will Bitcoin reach $100k?',
+ description: '',
+ image: '',
+ status: 'open',
+ tokens: [
+ { id: 'token-yes', title: 'Yes', price: 0.65 },
+ { id: 'token-no', title: 'No', price: 0.35 },
+ ],
+ volume: 1_000_000,
+ groupItemTitle: 'Yes',
+ },
+ ],
+ liquidity: 500_000,
+ volume: 1_000_000,
+};
diff --git a/tests/component-view/mocks.ts b/tests/component-view/mocks.ts
index 75c887d9fef..8eba1c004a0 100644
--- a/tests/component-view/mocks.ts
+++ b/tests/component-view/mocks.ts
@@ -163,6 +163,18 @@ jest.mock('../../app/core/Engine', () => {
setLocation: jest.fn(),
trackUnifiedSwapBridgeEvent: jest.fn(),
},
+ PredictController: {
+ getMarkets: jest.fn().mockResolvedValue([]),
+ getMarket: jest.fn().mockResolvedValue(null),
+ getBalance: jest.fn().mockResolvedValue(0),
+ getPositions: jest.fn().mockResolvedValue([]),
+ getPrices: jest.fn().mockResolvedValue({ providerId: '', results: [] }),
+ trackFeedViewed: jest.fn(),
+ trackTabChanged: jest.fn(),
+ trackMarketDetailsOpened: jest.fn(),
+ trackGeoBlockTriggered: jest.fn(),
+ refreshEligibility: jest.fn().mockResolvedValue(undefined),
+ },
// Perps: stub so hooks (usePerpsClosePosition, usePerpsMarkets, etc.) do not throw
// getMarkets returns one market so PerpsTabView explore section renders "See all perps"
PerpsController: {
diff --git a/tests/component-view/presets/predict.ts b/tests/component-view/presets/predict.ts
new file mode 100644
index 00000000000..7b2c412734f
--- /dev/null
+++ b/tests/component-view/presets/predict.ts
@@ -0,0 +1,51 @@
+import { createStateFixture } from '../stateFixture';
+import type { DeepPartial } from '../../../app/util/test/renderWithProvider';
+import type { RootState } from '../../../app/reducers';
+
+/**
+ * Default PredictController state for component view tests.
+ * Selectors read from state.engine.backgroundState.PredictController.
+ */
+const defaultPredictControllerState = {
+ balances: {},
+ pendingDeposits: {},
+ claimablePositions: {},
+ accountMeta: {},
+ withdrawTransaction: null,
+};
+
+/**
+ * Returns a StateFixtureBuilder with minimal state for Predict views.
+ * Use .withOverrides() to set PredictController fields, feature flags, etc.
+ */
+export const initialStatePredict = () =>
+ createStateFixture()
+ .withMinimalAccounts()
+ .withMinimalMainnetNetwork()
+ .withMinimalKeyringController()
+ .withRemoteFeatureFlags({
+ predictTradingEnabled: {
+ enabled: true,
+ featureVersion: '1.0.0',
+ minimumVersion: '0.0.1',
+ },
+ })
+ .withOverrides({
+ engine: {
+ backgroundState: {
+ PredictController: defaultPredictControllerState,
+ NetworkController: {
+ selectedNetworkClientId: 'mainnet',
+ },
+ PreferencesController: {
+ privacyMode: false,
+ selectedAddress: '0x1234567890abcdef',
+ },
+ // usePredictDeposit -> useConfirmNavigation reads TransactionController
+ TransactionController: {
+ transactions: [],
+ transactionBatches: [],
+ },
+ },
+ },
+ } as unknown as DeepPartial);
diff --git a/tests/component-view/renderers/predict.tsx b/tests/component-view/renderers/predict.tsx
new file mode 100644
index 00000000000..e892e949cd3
--- /dev/null
+++ b/tests/component-view/renderers/predict.tsx
@@ -0,0 +1,85 @@
+import '../mocks';
+import React from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import type { DeepPartial } from '../../../app/util/test/renderWithProvider';
+import type { RootState } from '../../../app/reducers';
+import { renderComponentViewScreen, renderScreenWithRoutes } from '../render';
+import Routes from '../../../app/constants/navigation/Routes';
+import PredictFeed from '../../../app/components/UI/Predict/views/PredictFeed';
+import { initialStatePredict } from '../presets/predict';
+
+interface RenderPredictFeedOptions {
+ overrides?: DeepPartial;
+}
+
+/**
+ * Creates a PredictFeed component wrapped with QueryClientProvider.
+ *
+ * A fresh QueryClient (retry: false) is created per call so that query state
+ * does not leak between tests. PredictBalance uses @tanstack/react-query to
+ * fetch the balance via Engine.context.PredictController.getBalance.
+ */
+function createWrappedPredictFeed(): React.ComponentType {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+
+ return (props: Record) => (
+
+
+
+ );
+}
+
+/**
+ * Renders PredictFeed for component view tests.
+ *
+ * State is driven by Redux + preset; use overrides for per-test deltas.
+ */
+export function renderPredictFeedView(
+ options: RenderPredictFeedOptions = {},
+): ReturnType {
+ const { overrides } = options;
+
+ const builder = initialStatePredict();
+ if (overrides) {
+ builder.withOverrides(overrides);
+ }
+ const state = builder.build();
+
+ return renderComponentViewScreen(
+ createWrappedPredictFeed(),
+ { name: Routes.PREDICT.MARKET_LIST },
+ { state },
+ );
+}
+
+interface RenderPredictFeedWithRoutesOptions extends RenderPredictFeedOptions {
+ extraRoutes?: { name: string; Component?: React.ComponentType }[];
+}
+
+/**
+ * Renders PredictFeed with additional registered routes for navigation assertions.
+ *
+ * Each extra route auto-generates a probe component that renders
+ * ``, so tests can assert navigation with
+ * `findByTestId(`route-${Routes.WALLET.HOME}`)`.
+ */
+export function renderPredictFeedViewWithRoutes(
+ options: RenderPredictFeedWithRoutesOptions = {},
+): ReturnType {
+ const { overrides, extraRoutes = [] } = options;
+
+ const builder = initialStatePredict();
+ if (overrides) {
+ builder.withOverrides(overrides);
+ }
+ const state = builder.build();
+
+ return renderScreenWithRoutes(
+ createWrappedPredictFeed(),
+ { name: Routes.PREDICT.MARKET_LIST },
+ extraRoutes,
+ { state },
+ );
+}
diff --git a/tests/component-view/renderers/predictMarketDetails.tsx b/tests/component-view/renderers/predictMarketDetails.tsx
new file mode 100644
index 00000000000..1f164615fc1
--- /dev/null
+++ b/tests/component-view/renderers/predictMarketDetails.tsx
@@ -0,0 +1,84 @@
+import '../mocks';
+import React from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import type { DeepPartial } from '../../../app/util/test/renderWithProvider';
+import type { RootState } from '../../../app/reducers';
+import { renderComponentViewScreen, renderScreenWithRoutes } from '../render';
+import Routes from '../../../app/constants/navigation/Routes';
+import PredictMarketDetails from '../../../app/components/UI/Predict/views/PredictMarketDetails';
+import { initialStatePredict } from '../presets/predict';
+
+interface RenderPredictMarketDetailsOptions {
+ overrides?: DeepPartial;
+ initialParams?: Record;
+}
+
+/**
+ * Creates a PredictMarketDetails component wrapped with QueryClientProvider.
+ *
+ * A fresh QueryClient (retry: false) is created per call so that query state
+ * does not leak between tests. usePredictPositions uses @tanstack/react-query.
+ */
+function createWrappedPredictMarketDetails(): React.ComponentType {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+
+ return (props: Record) => (
+
+
+
+ );
+}
+
+/**
+ * Renders PredictMarketDetails for component view tests.
+ *
+ * Pass `initialParams` to provide route params (marketId, title, image, etc.).
+ */
+export function renderPredictMarketDetailsView(
+ options: RenderPredictMarketDetailsOptions = {},
+): ReturnType {
+ const { overrides, initialParams } = options;
+
+ const builder = initialStatePredict();
+ if (overrides) {
+ builder.withOverrides(overrides);
+ }
+ const state = builder.build();
+
+ return renderComponentViewScreen(
+ createWrappedPredictMarketDetails(),
+ { name: Routes.PREDICT.MARKET_DETAILS },
+ { state },
+ initialParams,
+ );
+}
+
+interface RenderPredictMarketDetailsWithRoutesOptions
+ extends RenderPredictMarketDetailsOptions {
+ extraRoutes?: { name: string; Component?: React.ComponentType }[];
+}
+
+/**
+ * Renders PredictMarketDetails with additional registered routes for navigation assertions.
+ */
+export function renderPredictMarketDetailsViewWithRoutes(
+ options: RenderPredictMarketDetailsWithRoutesOptions = {},
+): ReturnType {
+ const { overrides, initialParams, extraRoutes = [] } = options;
+
+ const builder = initialStatePredict();
+ if (overrides) {
+ builder.withOverrides(overrides);
+ }
+ const state = builder.build();
+
+ return renderScreenWithRoutes(
+ createWrappedPredictMarketDetails(),
+ { name: Routes.PREDICT.MARKET_DETAILS },
+ extraRoutes,
+ { state },
+ initialParams,
+ );
+}
From 88ebfedd0fd88b5045a9ac97d69d4fe46e6e3248 Mon Sep 17 00:00:00 2001
From: sahar-fehri
Date: Fri, 6 Mar 2026 18:51:35 +0100
Subject: [PATCH 03/11] fix: charting library url (#26969)
## **Description**
Fix charting library url config
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes:
## **Manual testing steps**
```gherkin
Feature: my feature name
Scenario: user [verb for user action]
Given [describe expected initial app state]
When user [verb for user action]
Then [describe expected outcome]
```
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Low Risk**
> Low-risk configuration-only change, but a missing/incorrect
`MM_CHARTING_LIBRARY_URL` secret/value could cause builds or OTA updates
to use the wrong charting assets.
>
> **Overview**
> Fixes charting library URL configuration by introducing
`MM_CHARTING_LIBRARY_URL` as a first-class env var across the build
system.
>
> The OTA push workflow (`push-eas-update.yml`) now injects
`MM_CHARTING_LIBRARY_URL` from GitHub secrets, `builds.yml` defines the
default URL, and `scripts/build.sh` exports it into the generated `.env`
for Expo update/build steps.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
18c053a8eff6ba3eadcc684bbc019fef5444b186. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.github/workflows/push-eas-update.yml | 1 +
builds.yml | 1 +
scripts/build.sh | 1 +
3 files changed, 3 insertions(+)
diff --git a/.github/workflows/push-eas-update.yml b/.github/workflows/push-eas-update.yml
index efac957c8c4..17126d1a23a 100644
--- a/.github/workflows/push-eas-update.yml
+++ b/.github/workflows/push-eas-update.yml
@@ -332,6 +332,7 @@ jobs:
QUICKNODE_POLYGON_URL: ${{ secrets.QUICKNODE_POLYGON_URL }}
QUICKNODE_BSC_URL: ${{ secrets.QUICKNODE_BSC_URL }}
QUICKNODE_SEI_URL: ${{ secrets.QUICKNODE_SEI_URL }}
+ MM_CHARTING_LIBRARY_URL: ${{ secrets.MM_CHARTING_LIBRARY_URL }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
diff --git a/builds.yml b/builds.yml
index 127d5d22a73..8c81995d392 100644
--- a/builds.yml
+++ b/builds.yml
@@ -42,6 +42,7 @@ _public_envs: &public_envs # Servers (production)
MM_PERPS_HIP3_ENABLED: 'true'
# Temporary flag to enable builds with GitHub Actions, remove it when deprecating bitrise
BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY: 'true'
+ MM_CHARTING_LIBRARY_URL: 'https://va-mmcx-terminal.s3.us-east-2.amazonaws.com/charting_library/'
# Common secrets (shared across ALL builds - same names, GitHub Environment determines values)
_secrets: &secrets # Infrastructure
diff --git a/scripts/build.sh b/scripts/build.sh
index ec121ce68f7..82a2e26a80e 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -712,6 +712,7 @@ createEnvFile() {
"QUICKNODE_OPTIMISM_URL"
"QUICKNODE_POLYGON_URL"
"QUICKNODE_HYPEREVM_URL"
+ "MM_CHARTING_LIBRARY_URL"
)
# Create .env file and export to GITHUB_ENV
From 070c029e7d0d97544923cdb41cd3828e95dfa022 Mon Sep 17 00:00:00 2001
From: Michal Szorad
Date: Fri, 6 Mar 2026 19:52:57 +0100
Subject: [PATCH 04/11] feat(perps): default pay token when no balance and Add
funds CTA on market details (#26281)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
1. **Why**: Users with no perps balance saw an unclear flow (e.g. no
preselected pay token, or Long/Short with no way to fund).
2. **What**: (a) When the user has no perps balance and the
pay-with-any-token allowlist is enabled, we preselect the allowlist
token with the highest USD balance in the order Pay row. (b) When they
have no perps balance and no such token can be preselected, we show a
single "Add funds" CTA on the market details screen instead of
Long/Short; tapping it navigates to the perps confirmation stack and
opens the deposit flow.
- **New hook** `useDefaultPayWithTokenWhenNoPerpsBalance`: returns the
allowlist token with highest balance when `availableBalance <=
PERPS_MIN_BALANCE_THRESHOLD`, otherwise `null`. Respects
`perpsPayWithAnyTokenAllowlistAssets`.
- **Constant** `PERPS_MIN_BALANCE_THRESHOLD` (0.01) in `perpsConfig.ts`
for the "no perps balance" threshold and minimum token balance for
preselection.
- **PerpsPayRow**: uses the hook; when pending config has no selected
token, either preselects that token (via `setPayToken` +
`setSelectedPaymentToken`) or sets selected payment to Perps balance
(`null`).
- **PerpsMarketDetailsView**: uses `usePerpsLiveAccount`, the new hook,
and `useConfirmNavigation`. When `showAddFundsCTA` (no position, not at
OI cap, balance < 0.01, and hook returns `null`), footer shows "Add
funds" only; `handleAddFunds` calls `navigateToConfirmation({ stack:
Routes.PERPS.ROOT })` then `depositWithConfirmation()`. Otherwise
Long/Short buttons are shown as before.
## **Changelog**
CHANGELOG entry: When users have no perps balance, the app now
preselects the allowlist token with the highest balance for payment when
available, and shows an "Add funds" button on the market details screen
when no token can be preselected.
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2569
## **Manual testing steps**
```gherkin
Feature: my feature name
Scenario: user [verb for user action]
Given [describe expected initial app state]
When user [verb for user action]
Then [describe expected outcome]
```
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Medium Risk**
> Changes the perps funding/payment-token selection and deposit
entrypoint from `PerpsMarketDetailsView`, which can affect how users
land in confirmations and which token is preselected. Logic is gated by
balance thresholds/allowlists but still touches trading UX flows and
error handling.
>
> **Overview**
> Improves the *zero/low perps balance* onboarding flow by adding
`useDefaultPayWithTokenWhenNoPerpsBalance`, which selects the
allowlisted pay-with-any-token asset with the highest fiat balance
(above `PERPS_MIN_BALANCE_THRESHOLD`) while excluding the current
provider’s native chain.
>
> `PerpsPayRow` now uses this hook to auto-preselect that token when
pending trade config has no selected token; otherwise it keeps
defaulting to Perps balance (`null`). `PerpsMarketDetailsView`
conditionally replaces Long/Short with a single **Add funds** CTA when
balance is below threshold and no default token exists; pressing it
navigates to the Perps confirmation stack and triggers
`depositWithConfirmation()`, logging any deposit errors.
>
> Adds supporting config (`PERPS_MIN_BALANCE_THRESHOLD`, provider
chain-id mapping + `getPerpsProviderChainId`), expands Perps view state
fixtures for component tests, and updates/adds unit tests covering the
new behaviors.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
13930fc3e6f3f9d1c31cb5a52c4c27ec72091747. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---------
Co-authored-by: Cursor
---
.../PerpsMarketDetailsView.test.tsx | 161 ++++++++++++-
.../PerpsMarketDetailsView.tsx | 74 +++++-
.../Views/PerpsOrderView/PerpsPayRow.test.tsx | 38 +++
.../Views/PerpsOrderView/PerpsPayRow.tsx | 36 ++-
.../UI/Perps/constants/perpsConfig.ts | 50 ++++
...aultPayWithTokenWhenNoPerpsBalance.test.ts | 216 ++++++++++++++++++
...seDefaultPayWithTokenWhenNoPerpsBalance.ts | 91 ++++++++
.../presets/perpsStatePreset.test.ts | 38 +++
.../presets/perpsStatePreset.ts | 13 ++
9 files changed, 710 insertions(+), 7 deletions(-)
create mode 100644 app/components/UI/Perps/hooks/useDefaultPayWithTokenWhenNoPerpsBalance.test.ts
create mode 100644 app/components/UI/Perps/hooks/useDefaultPayWithTokenWhenNoPerpsBalance.ts
create mode 100644 tests/component-view/presets/perpsStatePreset.test.ts
diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx
index 9e7facc3996..558dbd0a670 100644
--- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx
@@ -8,6 +8,7 @@ import {
PerpsOrderViewSelectorsIDs,
} from '../../Perps.testIds';
import { PerpsConnectionProvider } from '../../providers/PerpsConnectionProvider';
+import { useDefaultPayWithTokenWhenNoPerpsBalance } from '../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance';
import { Linking } from 'react-native';
// Mock Linking
@@ -95,6 +96,8 @@ jest.mock('../../../../../util/Logger', () => ({
const mockUsePerpsAccount = jest.fn();
const mockUsePerpsLiveAccount = jest.fn();
const mockUseHasExistingPosition = jest.fn();
+const mockNavigateToConfirmation = jest.fn();
+const mockDepositWithConfirmation = jest.fn(() => Promise.resolve());
const mockUsePerpsLiveOrders = jest.fn();
const mockUsePerpsLivePrices = jest.fn();
@@ -103,6 +106,21 @@ jest.mock('../../hooks/stream/usePerpsLiveAccount', () => ({
usePerpsLiveAccount: mockUsePerpsLiveAccount,
}));
+jest.mock('../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance', () => ({
+ useDefaultPayWithTokenWhenNoPerpsBalance: jest.fn(() => null),
+}));
+
+const mockUseDefaultPayWithTokenWhenNoPerpsBalance =
+ useDefaultPayWithTokenWhenNoPerpsBalance as jest.MockedFunction<
+ typeof useDefaultPayWithTokenWhenNoPerpsBalance
+ >;
+
+jest.mock('../../../../Views/confirmations/hooks/useConfirmNavigation', () => ({
+ useConfirmNavigation: () => ({
+ navigateToConfirmation: mockNavigateToConfirmation,
+ }),
+}));
+
// Mock usePerpsMarketFills to avoid Redux selector issues
jest.mock('../../hooks/usePerpsMarketFills', () => ({
usePerpsMarketFills: jest.fn(() => ({
@@ -425,7 +443,7 @@ jest.mock('../../hooks', () => ({
placeOrder: jest.fn(),
cancelOrder: jest.fn(),
getAccountState: jest.fn(),
- depositWithConfirmation: jest.fn(() => Promise.resolve()),
+ depositWithConfirmation: mockDepositWithConfirmation,
withdrawWithConfirmation: jest.fn(),
})),
usePerpsNetworkManagement: jest.fn(() => ({
@@ -681,6 +699,8 @@ describe('PerpsMarketDetailsView', () => {
isInitialLoading: false,
});
+ mockUseDefaultPayWithTokenWhenNoPerpsBalance.mockReturnValue(null);
+
mockUseHasExistingPosition.mockReturnValue({
hasPosition: false,
isLoading: false,
@@ -884,7 +904,13 @@ describe('PerpsMarketDetailsView', () => {
describe('Button rendering scenarios', () => {
it('shows long/short buttons when user balance is zero so user can trade', () => {
- // Override with zero balance
+ // Override with zero balance; return a default pay token so Add funds CTA is not shown
+ // (when user has allowlist token they can pay with, we show Long/Short)
+ mockUseDefaultPayWithTokenWhenNoPerpsBalance.mockReturnValue({
+ address: '0xUSDC' as const,
+ chainId: '0xa4b1' as const,
+ description: 'USDC',
+ });
mockUsePerpsAccount.mockReturnValue({
account: {
availableBalance: '0.00',
@@ -930,6 +956,137 @@ describe('PerpsMarketDetailsView', () => {
).toBeNull();
});
+ it('shows add funds CTA when user balance is below threshold and no allowlist token', () => {
+ mockUseDefaultPayWithTokenWhenNoPerpsBalance.mockReturnValue(null);
+ mockUsePerpsAccount.mockReturnValue({
+ account: {
+ availableBalance: '0.00',
+ marginUsed: '0.00',
+ unrealizedPnl: '0.00',
+ returnOnEquity: '0.00',
+ totalBalance: '0.00',
+ },
+ isInitialLoading: false,
+ });
+ mockUsePerpsLiveAccount.mockReturnValue({
+ account: {
+ availableBalance: '0',
+ marginUsed: '0',
+ unrealizedPnl: '0',
+ returnOnEquity: '0',
+ totalBalance: '0',
+ },
+ isInitialLoading: false,
+ });
+
+ const { getByTestId, queryByTestId } = renderWithProvider(
+
+
+ ,
+ { state: initialState },
+ );
+
+ expect(
+ getByTestId(PerpsMarketDetailsViewSelectorsIDs.ADD_FUNDS_BUTTON),
+ ).toBeTruthy();
+ expect(
+ queryByTestId(PerpsMarketDetailsViewSelectorsIDs.LONG_BUTTON),
+ ).toBeNull();
+ expect(
+ queryByTestId(PerpsMarketDetailsViewSelectorsIDs.SHORT_BUTTON),
+ ).toBeNull();
+ });
+
+ it('calls navigateToConfirmation and depositWithConfirmation when add funds is pressed', async () => {
+ mockUseDefaultPayWithTokenWhenNoPerpsBalance.mockReturnValue(null);
+ mockUsePerpsAccount.mockReturnValue({
+ account: {
+ availableBalance: '0.00',
+ marginUsed: '0.00',
+ unrealizedPnl: '0.00',
+ returnOnEquity: '0.00',
+ totalBalance: '0.00',
+ },
+ isInitialLoading: false,
+ });
+ mockUsePerpsLiveAccount.mockReturnValue({
+ account: {
+ availableBalance: '0',
+ marginUsed: '0',
+ unrealizedPnl: '0',
+ returnOnEquity: '0',
+ totalBalance: '0',
+ },
+ isInitialLoading: false,
+ });
+ mockNavigateToConfirmation.mockClear();
+ mockDepositWithConfirmation.mockClear();
+ mockDepositWithConfirmation.mockResolvedValue(undefined);
+
+ const { getByTestId } = renderWithProvider(
+
+
+ ,
+ { state: initialState },
+ );
+
+ const addFundsButton = getByTestId(
+ PerpsMarketDetailsViewSelectorsIDs.ADD_FUNDS_BUTTON,
+ );
+ await act(async () => {
+ fireEvent.press(addFundsButton);
+ });
+
+ expect(mockNavigateToConfirmation).toHaveBeenCalledWith({
+ stack: 'Perps',
+ });
+ expect(mockDepositWithConfirmation).toHaveBeenCalled();
+ });
+
+ it('handles depositWithConfirmation rejection without throwing', async () => {
+ mockUseDefaultPayWithTokenWhenNoPerpsBalance.mockReturnValue(null);
+ mockUsePerpsAccount.mockReturnValue({
+ account: {
+ availableBalance: '0.00',
+ marginUsed: '0.00',
+ unrealizedPnl: '0.00',
+ returnOnEquity: '0.00',
+ totalBalance: '0.00',
+ },
+ isInitialLoading: false,
+ });
+ mockUsePerpsLiveAccount.mockReturnValue({
+ account: {
+ availableBalance: '0',
+ marginUsed: '0',
+ unrealizedPnl: '0',
+ returnOnEquity: '0',
+ totalBalance: '0',
+ },
+ isInitialLoading: false,
+ });
+ mockDepositWithConfirmation.mockRejectedValueOnce(
+ new Error('Deposit failed'),
+ );
+
+ const { getByTestId } = renderWithProvider(
+
+
+ ,
+ { state: initialState },
+ );
+
+ const addFundsButton = getByTestId(
+ PerpsMarketDetailsViewSelectorsIDs.ADD_FUNDS_BUTTON,
+ );
+ await act(async () => {
+ fireEvent.press(addFundsButton);
+ });
+ await waitFor(() => {
+ expect(mockDepositWithConfirmation).toHaveBeenCalled();
+ });
+ });
+
it('renders modify/close buttons when user has balance and existing position', () => {
// Override with non-zero balance and existing position
mockUsePerpsAccount.mockReturnValue({
diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx
index 27392b8c326..0f79b08c7ea 100644
--- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx
+++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx
@@ -78,12 +78,20 @@ import TradingViewChart, {
type TradingViewChartRef,
} from '../../components/TradingViewChart';
import { PERPS_CHART_CONFIG } from '../../constants/chartConfig';
+import { PERPS_MIN_BALANCE_THRESHOLD } from '../../constants/perpsConfig';
import {
usePerpsConnection,
usePerpsNavigation,
usePositionManagement,
+ usePerpsTrading,
} from '../../hooks';
-import { usePerpsLiveOrders, usePerpsLivePrices } from '../../hooks/stream';
+import { useConfirmNavigation } from '../../../../Views/confirmations/hooks/useConfirmNavigation';
+import { useDefaultPayWithTokenWhenNoPerpsBalance } from '../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance';
+import {
+ usePerpsLiveAccount,
+ usePerpsLiveOrders,
+ usePerpsLivePrices,
+} from '../../hooks/stream';
import { usePerpsLiveCandles } from '../../hooks/stream/usePerpsLiveCandles';
import { useHasExistingPosition } from '../../hooks/useHasExistingPosition';
import { useIsPriceDeviatedAboveThreshold } from '../../hooks/useIsPriceDeviatedAboveThreshold';
@@ -389,6 +397,44 @@ const PerpsMarketDetailsView: React.FC = () => {
loadOnMount: true,
});
+ const { account, isInitialLoading: isLoadingAccount } = usePerpsLiveAccount();
+ const defaultPayTokenWhenNoPerpsBalance =
+ useDefaultPayWithTokenWhenNoPerpsBalance();
+ const { depositWithConfirmation } = usePerpsTrading();
+ const { navigateToConfirmation } = useConfirmNavigation();
+ const availableBalance = Number.parseFloat(
+ account?.availableBalance?.toString() ?? '0',
+ );
+ const showAddFundsCTA =
+ isEligible &&
+ !isLoadingPosition &&
+ !existingPosition &&
+ !isAtOICap &&
+ !isLoadingAccount &&
+ availableBalance < PERPS_MIN_BALANCE_THRESHOLD &&
+ defaultPayTokenWhenNoPerpsBalance === null;
+
+ const handleAddFunds = useCallback(async () => {
+ if (!isEligible) {
+ track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, {
+ [PERPS_EVENT_PROPERTY.SCREEN_TYPE]:
+ PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF,
+ [PERPS_EVENT_PROPERTY.SOURCE]:
+ PERPS_EVENT_VALUE.SOURCE.ADD_FUNDS_ACTION,
+ });
+ setIsEligibilityModalVisible(true);
+ return;
+ }
+ navigateToConfirmation({ stack: Routes.PERPS.ROOT });
+ try {
+ await depositWithConfirmation();
+ } catch (err) {
+ Logger.error(ensureError(err, 'PerpsMarketDetailsView.handleAddFunds'), {
+ tags: { feature: PERPS_CONSTANTS.FeatureName },
+ });
+ }
+ }, [isEligible, track, navigateToConfirmation, depositWithConfirmation]);
+
// Keep current position ref in sync for callbacks stored in route params
// This must be after useHasExistingPosition since it depends on existingPosition
useEffect(() => {
@@ -1031,6 +1077,13 @@ const PerpsMarketDetailsView: React.FC = () => {
);
}
+ const shouldShowNewPositionActions =
+ hasLongShortButtons && !existingPosition && !isAtOICap;
+ const shouldShowAddFundsCTASection =
+ shouldShowNewPositionActions && showAddFundsCTA;
+ const shouldShowLongShortButtonsOnly =
+ shouldShowNewPositionActions && !showAddFundsCTA;
+
return (
= () => {
)}
- {/* Show Long/Short buttons when no position exists */}
- {hasLongShortButtons && !existingPosition && !isAtOICap && (
+ {/* Show Add funds CTA when no perps balance and no allowlist token to preselect */}
+ {shouldShowAddFundsCTASection && (
+
+
+
+
+
+ )}
+ {/* Show Long/Short buttons when no position exists and user can trade */}
+ {shouldShowLongShortButtonsOnly && (
{buttonColorVariant === 'monochrome' ? (
diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.test.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.test.tsx
index f9ca8342d61..11b9d28a613 100644
--- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.test.tsx
@@ -21,6 +21,7 @@ import {
} from '@metamask/perps-controller';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { usePerpsSelector } from '../../hooks/usePerpsSelector';
+import { useDefaultPayWithTokenWhenNoPerpsBalance } from '../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance';
import {
ConfirmationRowComponentIDs,
TransactionPayComponentIDs,
@@ -44,6 +45,7 @@ jest.mock('../../hooks/useIsPerpsBalanceSelected', () => ({
usePerpsPayWithToken: jest.fn(),
}));
jest.mock('../../hooks/usePerpsSelector');
+jest.mock('../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance');
jest.mock('../../../../../core/Engine', () => ({
context: {
PerpsController: {
@@ -82,6 +84,10 @@ const mockUsePerpsPayWithToken = usePerpsPayWithToken as jest.MockedFunction<
const mockUsePerpsSelector = usePerpsSelector as jest.MockedFunction<
typeof usePerpsSelector
>;
+const mockUseDefaultPayWithTokenWhenNoPerpsBalance =
+ useDefaultPayWithTokenWhenNoPerpsBalance as jest.MockedFunction<
+ typeof useDefaultPayWithTokenWhenNoPerpsBalance
+ >;
const mockUseTokenWithBalance = useTokenWithBalance as jest.MockedFunction<
typeof useTokenWithBalance
>;
@@ -132,6 +138,7 @@ describe('PerpsPayRow', () => {
mockIsHardwareAccount.mockReturnValue(false);
mockUsePerpsSelector.mockReturnValue({});
mockUsePerpsPayWithToken.mockReturnValue(null);
+ mockUseDefaultPayWithTokenWhenNoPerpsBalance.mockReturnValue(null);
});
it('renders pay with label', () => {
@@ -294,6 +301,7 @@ describe('PerpsPayRow', () => {
it('calls setSelectedPaymentToken(null) when pending config has no selected token', () => {
mockUsePerpsSelector.mockReturnValue({});
mockUsePerpsPayWithToken.mockReturnValue(null);
+ mockUseDefaultPayWithTokenWhenNoPerpsBalance.mockReturnValue(null);
renderWithProvider();
@@ -302,6 +310,36 @@ describe('PerpsPayRow', () => {
).toHaveBeenCalledWith(null);
});
+ it('preselects allowlist token with highest balance when no perps balance and pending has no token', () => {
+ const setPayTokenMock = jest.fn();
+ const defaultToken = {
+ address: '0xUSDC' as const,
+ chainId: '0xa4b1' as const,
+ description: 'USDC',
+ };
+ mockUsePerpsSelector.mockReturnValue({});
+ mockUsePerpsPayWithToken.mockReturnValue(null);
+ mockUseDefaultPayWithTokenWhenNoPerpsBalance.mockReturnValue(defaultToken);
+ mockUseTransactionPayToken.mockReturnValue({
+ payToken: null,
+ setPayToken: setPayTokenMock,
+ } as unknown as ReturnType);
+
+ renderWithProvider();
+
+ expect(setPayTokenMock).toHaveBeenCalledWith({
+ address: defaultToken.address,
+ chainId: defaultToken.chainId,
+ });
+ expect(
+ Engine.context.PerpsController?.setSelectedPaymentToken,
+ ).toHaveBeenCalledWith({
+ description: defaultToken.description,
+ address: defaultToken.address,
+ chainId: defaultToken.chainId,
+ });
+ });
+
describe('pending config sync (apply once per load)', () => {
it('does not overwrite pay token when user switches token after pending config was applied', () => {
const setPayTokenMock = jest.fn();
diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.tsx
index bf352d10b57..a4b8208a5f1 100644
--- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.tsx
+++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.tsx
@@ -51,6 +51,7 @@ import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking';
import { Hex } from '@metamask/utils';
import { MetaMetricsEvents } from '../../../../../core/Analytics/MetaMetrics.events';
import { usePerpsSelector } from '../../hooks/usePerpsSelector';
+import { useDefaultPayWithTokenWhenNoPerpsBalance } from '../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance';
import Engine from '../../../../../core/Engine';
const tokenIconStyles = StyleSheet.create({
@@ -116,6 +117,8 @@ export const PerpsPayRow = ({
selectPendingTradeConfiguration(state, initialAsset),
);
const selectedPaymentToken = usePerpsPayWithToken();
+ const defaultPayTokenWhenNoPerpsBalance =
+ useDefaultPayWithTokenWhenNoPerpsBalance();
const pendingConfigSelectedPaymentToken = pendingConfig?.selectedPaymentToken;
@@ -131,12 +134,41 @@ export const PerpsPayRow = ({
appliedPendingTokenRef.current = undefined;
}
+ // When pending config has no selected token: either set Perps balance (null)
+ // or preselect the allowlist token with highest USD balance when user has no perps balance.
useEffect(() => {
- if (pendingConfigSelectedPaymentToken != null) return;
+ if (
+ pendingConfigSelectedPaymentToken != null ||
+ appliedPendingTokenRef.current != null
+ )
+ return;
+
+ const defaultToken = defaultPayTokenWhenNoPerpsBalance;
+ if (defaultToken != null) {
+ setPayToken({
+ address: defaultToken.address as Hex,
+ chainId: defaultToken.chainId as Hex,
+ });
+ Engine.context.PerpsController?.setSelectedPaymentToken?.({
+ description: defaultToken.description,
+ address: defaultToken.address as Hex,
+ chainId: defaultToken.chainId as Hex,
+ });
+ appliedPendingTokenRef.current = {
+ address: defaultToken.address,
+ chainId: defaultToken.chainId,
+ };
+ return;
+ }
+
if (appliedPendingTokenRef.current === null) return;
appliedPendingTokenRef.current = null;
Engine.context.PerpsController?.setSelectedPaymentToken?.(null);
- }, [pendingConfigSelectedPaymentToken]);
+ }, [
+ pendingConfigSelectedPaymentToken,
+ defaultPayTokenWhenNoPerpsBalance,
+ setPayToken,
+ ]);
useEffect(() => {
if (!pendingConfigSelectedPaymentToken || !selectedPaymentToken) return;
diff --git a/app/components/UI/Perps/constants/perpsConfig.ts b/app/components/UI/Perps/constants/perpsConfig.ts
index 280281947db..142186148b0 100644
--- a/app/components/UI/Perps/constants/perpsConfig.ts
+++ b/app/components/UI/Perps/constants/perpsConfig.ts
@@ -17,6 +17,20 @@ export const PERPS_BALANCE_PLACEHOLDER_ADDRESS =
/** Chain id used for the "Perps balance" payment option. */
export { ARBITRUM_MAINNET_CHAIN_ID_HEX as PERPS_BALANCE_CHAIN_ID } from '@metamask/perps-controller/constants/hyperLiquidConfig';
+import {
+ HYPERLIQUID_MAINNET_CHAIN_ID,
+ HYPERLIQUID_TESTNET_CHAIN_ID,
+} from '@metamask/perps-controller/constants/hyperLiquidConfig';
+
+export { HYPERLIQUID_MAINNET_CHAIN_ID, HYPERLIQUID_TESTNET_CHAIN_ID };
+
+/**
+ * Minimum perps balance (USD) threshold for default pay token logic.
+ * When available perps balance is above this, we do not preselect a pay token.
+ * When below, we may preselect the allowlist token with highest balance.
+ * Also used as the minimum token balance (USD) to consider for preselection.
+ */
+export const PERPS_MIN_BALANCE_THRESHOLD = 0.01;
/**
* Minimum number of aggregators (exchanges) a token must be listed on
@@ -257,3 +271,39 @@ export const PROVIDER_CONFIG = {
/** Force MYX to testnet only (mainnet credentials not yet available) */
MYX_TESTNET_ONLY: false,
} as const;
+
+/** Network mode for perps (testnet vs mainnet). */
+export type PerpsNetwork = 'mainnet' | 'testnet';
+
+/**
+ * Chain IDs for each perps provider by network.
+ * Identifies the provider's native chain (where "Perps balance" lives) so callers
+ * can exclude it from pay-with-any-token allowlist or filter tokens.
+ * Add entries when integrating new providers (e.g. MYX).
+ */
+export const PERPS_PROVIDER_CHAIN_IDS: Record<
+ string,
+ Partial>
+> = {
+ hyperliquid: {
+ mainnet: HYPERLIQUID_MAINNET_CHAIN_ID,
+ testnet: HYPERLIQUID_TESTNET_CHAIN_ID,
+ },
+ // myx: add mainnet/testnet chain IDs when MYX integration provides them
+};
+
+/**
+ * Returns the chain ID for the given perps provider and network.
+ * Used to exclude the provider's native chain from pay-with-any-token options.
+ *
+ * @param provider - Perps provider (e.g. 'hyperliquid'). Use concrete provider;
+ * for 'aggregated' mode callers should pass PROVIDER_CONFIG.DefaultProvider.
+ * @param network - 'mainnet' or 'testnet'
+ * @returns Chain ID hex string, or undefined if provider has no chain configured
+ */
+export function getPerpsProviderChainId(
+ provider: string,
+ network: PerpsNetwork,
+): string | undefined {
+ return PERPS_PROVIDER_CHAIN_IDS[provider]?.[network];
+}
diff --git a/app/components/UI/Perps/hooks/useDefaultPayWithTokenWhenNoPerpsBalance.test.ts b/app/components/UI/Perps/hooks/useDefaultPayWithTokenWhenNoPerpsBalance.test.ts
new file mode 100644
index 00000000000..45c966c9130
--- /dev/null
+++ b/app/components/UI/Perps/hooks/useDefaultPayWithTokenWhenNoPerpsBalance.test.ts
@@ -0,0 +1,216 @@
+import { renderHookWithProvider } from '../../../../util/test/renderWithProvider';
+import { useDefaultPayWithTokenWhenNoPerpsBalance } from './useDefaultPayWithTokenWhenNoPerpsBalance';
+import type { PerpsToken } from '@metamask/perps-controller';
+
+jest.mock('./usePerpsPaymentTokens', () => ({
+ usePerpsPaymentTokens: jest.fn(() => []),
+}));
+
+const mockUsePerpsPaymentTokens = jest.requireMock<
+ typeof import('./usePerpsPaymentTokens')
+>('./usePerpsPaymentTokens').usePerpsPaymentTokens as jest.MockedFunction<
+ (typeof import('./usePerpsPaymentTokens'))['usePerpsPaymentTokens']
+>;
+
+function getState(
+ overrides: {
+ perpsAccount?: { availableBalance: string } | null;
+ allowlistAssets?: string[];
+ isTestnet?: boolean;
+ activeProvider?: 'hyperliquid' | 'myx' | 'aggregated';
+ } = {},
+) {
+ const {
+ perpsAccount = { availableBalance: '0' },
+ allowlistAssets = [],
+ isTestnet = false,
+ activeProvider,
+ } = overrides;
+ return {
+ engine: {
+ backgroundState: {
+ PerpsController: {
+ accountState: perpsAccount,
+ isTestnet,
+ ...(activeProvider !== undefined && { activeProvider }),
+ },
+ RemoteFeatureFlagController: {
+ remoteFeatureFlags: {
+ perpsPayWithAnyTokenAllowlistAssets: allowlistAssets,
+ },
+ },
+ },
+ },
+ };
+}
+
+function runHook(state: ReturnType) {
+ return renderHookWithProvider(
+ () => useDefaultPayWithTokenWhenNoPerpsBalance(),
+ {
+ state,
+ },
+ );
+}
+
+describe('useDefaultPayWithTokenWhenNoPerpsBalance', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUsePerpsPaymentTokens.mockReturnValue([]);
+ });
+
+ it('returns null when available perps balance is above threshold', () => {
+ mockUsePerpsPaymentTokens.mockReturnValue([
+ {
+ address: '0xusdc',
+ chainId: '0xa4b1',
+ symbol: 'USDC',
+ balanceFiat: '$500',
+ decimals: 6,
+ },
+ ] as PerpsToken[]);
+
+ const { result } = runHook(
+ getState({
+ perpsAccount: { availableBalance: '100' },
+ allowlistAssets: ['0xa4b1.0xusdc'],
+ }),
+ );
+
+ expect(result.current).toBeNull();
+ });
+
+ it('returns null when allowlist is empty', () => {
+ const { result } = runHook(getState({ allowlistAssets: [] }));
+
+ expect(result.current).toBeNull();
+ });
+
+ it('returns null when no payment tokens match allowlist', () => {
+ mockUsePerpsPaymentTokens.mockReturnValue([
+ {
+ address: '0xusdc',
+ chainId: '0xa4b1',
+ symbol: 'USDC',
+ balanceFiat: '$500',
+ decimals: 6,
+ },
+ ] as PerpsToken[]);
+
+ const { result } = runHook(getState({ allowlistAssets: ['0x1.0xother'] }));
+
+ expect(result.current).toBeNull();
+ });
+
+ it('returns null when top allowlist token balance is below threshold', () => {
+ mockUsePerpsPaymentTokens.mockReturnValue([
+ {
+ address: '0xusdc',
+ chainId: '0xa4b1',
+ symbol: 'USDC',
+ balanceFiat: 'US$0.005',
+ decimals: 6,
+ },
+ ] as PerpsToken[]);
+
+ const { result } = runHook(
+ getState({ allowlistAssets: ['0xa4b1.0xusdc'] }),
+ );
+
+ expect(result.current).toBeNull();
+ });
+
+ it('returns top allowlist token by fiat balance when perps balance is below threshold', () => {
+ mockUsePerpsPaymentTokens.mockReturnValue([
+ {
+ address: '0xweth',
+ chainId: '0x1',
+ symbol: 'WETH',
+ balanceFiat: 'US$100',
+ decimals: 18,
+ },
+ {
+ address: '0xusdc',
+ chainId: '0xa4b1',
+ symbol: 'USDC',
+ balanceFiat: 'US$500',
+ decimals: 6,
+ },
+ ] as PerpsToken[]);
+
+ const { result } = runHook(
+ getState({
+ allowlistAssets: ['0xa4b1.0xusdc', '0x1.0xweth'],
+ }),
+ );
+
+ expect(result.current).not.toBeNull();
+ expect(result.current?.address).toBe('0xusdc');
+ expect(result.current?.chainId).toBe('0xa4b1');
+ expect(result.current?.description).toBe('USDC');
+ });
+
+ it('treats null perps account as zero balance and returns default token when allowlist has balance', () => {
+ mockUsePerpsPaymentTokens.mockReturnValue([
+ {
+ address: '0xusdc',
+ chainId: '0xa4b1',
+ symbol: 'USDC',
+ balanceFiat: 'US$500',
+ decimals: 6,
+ },
+ ] as PerpsToken[]);
+
+ const { result } = runHook(
+ getState({
+ perpsAccount: null,
+ allowlistAssets: ['0xa4b1.0xusdc'],
+ }),
+ );
+
+ expect(result.current).not.toBeNull();
+ expect(result.current?.description).toBe('USDC');
+ });
+
+ it('excludes current perps provider chain tokens from allowlist result', () => {
+ mockUsePerpsPaymentTokens.mockReturnValue([
+ {
+ address: '0xhlusdc',
+ chainId: '0x3e7',
+ symbol: 'USDC',
+ balanceFiat: 'US$500',
+ decimals: 6,
+ },
+ ] as PerpsToken[]);
+
+ const { result } = runHook(
+ getState({
+ allowlistAssets: ['0x3e7.0xhlusdc'],
+ activeProvider: 'hyperliquid',
+ }),
+ );
+
+ expect(result.current).toBeNull();
+ });
+
+ it('excludes default provider chain when activeProvider is aggregated', () => {
+ mockUsePerpsPaymentTokens.mockReturnValue([
+ {
+ address: '0xhlusdc',
+ chainId: '0x3e7',
+ symbol: 'USDC',
+ balanceFiat: 'US$500',
+ decimals: 6,
+ },
+ ] as PerpsToken[]);
+
+ const { result } = runHook(
+ getState({
+ allowlistAssets: ['0x3e7.0xhlusdc'],
+ activeProvider: 'aggregated',
+ }),
+ );
+
+ expect(result.current).toBeNull();
+ });
+});
diff --git a/app/components/UI/Perps/hooks/useDefaultPayWithTokenWhenNoPerpsBalance.ts b/app/components/UI/Perps/hooks/useDefaultPayWithTokenWhenNoPerpsBalance.ts
new file mode 100644
index 00000000000..5cc8346e582
--- /dev/null
+++ b/app/components/UI/Perps/hooks/useDefaultPayWithTokenWhenNoPerpsBalance.ts
@@ -0,0 +1,91 @@
+import type { PerpsSelectedPaymentToken } from '@metamask/perps-controller';
+import type { Hex } from '@metamask/utils';
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import {
+ getPerpsProviderChainId,
+ PERPS_MIN_BALANCE_THRESHOLD,
+ PROVIDER_CONFIG,
+} from '../constants/perpsConfig';
+import { selectPerpsPayWithAnyTokenAllowlistAssets } from '../selectors/featureFlags';
+import {
+ selectPerpsAccountState,
+ selectPerpsProvider,
+} from '../selectors/perpsController';
+import { usePerpsNetwork } from './index';
+import { usePerpsPaymentTokens } from './usePerpsPaymentTokens';
+
+/**
+ * When the user has no perps balance and the pay-with-any-token allowlist is enabled,
+ * returns the allowlist token with the highest balance to use as the default payment method.
+ * Otherwise returns null (caller should default to "Perps balance").
+ */
+export function useDefaultPayWithTokenWhenNoPerpsBalance(): PerpsSelectedPaymentToken | null {
+ const perpsAccount = useSelector(selectPerpsAccountState);
+ const allowlistAssets = useSelector(
+ selectPerpsPayWithAnyTokenAllowlistAssets,
+ );
+ const activeProvider = useSelector(selectPerpsProvider);
+ const currentNetwork = usePerpsNetwork();
+ const paymentTokens = usePerpsPaymentTokens();
+
+ return useMemo(() => {
+ const availableBalance = Number.parseFloat(
+ perpsAccount?.availableBalance?.toString() ?? '0',
+ );
+
+ if (availableBalance > PERPS_MIN_BALANCE_THRESHOLD) {
+ return null;
+ }
+ if (!allowlistAssets?.length) {
+ return null;
+ }
+
+ const allowSet = new Set(allowlistAssets);
+ const effectiveProvider =
+ activeProvider === 'aggregated' || activeProvider === undefined
+ ? PROVIDER_CONFIG.DefaultProvider
+ : activeProvider;
+ const perpsProviderChainId = getPerpsProviderChainId(
+ effectiveProvider,
+ currentNetwork,
+ );
+
+ const allowlistTokens = paymentTokens.filter((token) => {
+ if (
+ perpsProviderChainId !== undefined &&
+ token.chainId === perpsProviderChainId
+ )
+ return false;
+ const key = `${token.chainId}.${(token.address ?? '').toLowerCase()}`;
+ return allowSet.has(key);
+ });
+
+ if (allowlistTokens.length === 0) return null;
+
+ const tokensWithBalance = allowlistTokens
+ .map((token) => ({
+ ...token,
+ balanceFiat: Number.parseFloat(
+ token.balanceFiat?.replace('US$', '').trim() || '0',
+ ),
+ }))
+ .sort((a, b) => b.balanceFiat - a.balanceFiat);
+
+ const top = tokensWithBalance[0];
+
+ if (top.balanceFiat < PERPS_MIN_BALANCE_THRESHOLD) return null;
+
+ return {
+ address: top.address as Hex,
+ chainId: top.chainId as Hex,
+ description: top.symbol,
+ };
+ }, [
+ perpsAccount?.availableBalance,
+ allowlistAssets,
+ activeProvider,
+ currentNetwork,
+ paymentTokens,
+ ]);
+}
diff --git a/tests/component-view/presets/perpsStatePreset.test.ts b/tests/component-view/presets/perpsStatePreset.test.ts
new file mode 100644
index 00000000000..f32d642934a
--- /dev/null
+++ b/tests/component-view/presets/perpsStatePreset.test.ts
@@ -0,0 +1,38 @@
+import { initialStatePerps } from './perpsStatePreset';
+
+describe('initialStatePerps', () => {
+ it('returns a builder with build method', () => {
+ const builder = initialStatePerps();
+
+ expect(typeof builder.build).toBe('function');
+ expect(typeof builder.withOverrides).toBe('function');
+ });
+
+ it('builds state with PerpsController and perps feature flag enabled', () => {
+ const state = initialStatePerps().build();
+
+ const perps = state?.engine?.backgroundState?.PerpsController as
+ | { isEligible?: boolean }
+ | undefined;
+ expect(perps).toBeDefined();
+ expect(perps?.isEligible).toBe(true);
+ });
+
+ it('builds state with NetworkController and PreferencesController overrides', () => {
+ const state = initialStatePerps().build();
+
+ const network = state?.engine?.backgroundState?.NetworkController as
+ | {
+ providerConfig?: { chainId: string };
+ selectedNetworkClientId?: string;
+ }
+ | undefined;
+ expect(network?.providerConfig?.chainId).toBe('0x1');
+ expect(network?.selectedNetworkClientId).toBe('mainnet');
+
+ const prefs = state?.engine?.backgroundState?.PreferencesController as
+ | { selectedAddress?: string }
+ | undefined;
+ expect(prefs?.selectedAddress).toBe('0x1234567890abcdef');
+ });
+});
diff --git a/tests/component-view/presets/perpsStatePreset.ts b/tests/component-view/presets/perpsStatePreset.ts
index d56e4313070..7ebba29a5b7 100644
--- a/tests/component-view/presets/perpsStatePreset.ts
+++ b/tests/component-view/presets/perpsStatePreset.ts
@@ -29,6 +29,11 @@ const defaultPerpsControllerState = {
export const initialStatePerps = () =>
createStateFixture()
.withMinimalAccounts()
+ .withMinimalKeyringController()
+ .withMinimalTokenRates()
+ .withMinimalMultichainBalances()
+ .withMinimalMultichainAssets()
+ .withMinimalMultichainAssetsRates()
.withMinimalMainnetNetwork()
.withMinimalMultichainNetwork(true)
.withRemoteFeatureFlags({
@@ -48,12 +53,20 @@ export const initialStatePerps = () =>
},
PreferencesController: {
selectedAddress: '0x1234567890abcdef',
+ // useTokensWithBalance -> sortAssets expects tokenSortConfig.key
+ tokenSortConfig: {
+ key: 'tokenFiatAmount',
+ order: 'dsc',
+ sortCallback: 'stringNumeric',
+ },
},
// PerpsMarketBalanceActions -> usePerpsHomeActions -> useConfirmNavigation reads TransactionController
TransactionController: {
transactions: [],
transactionBatches: [],
},
+ // usePerpsPaymentTokens -> useTokensWithBalance reads TokenBalancesController
+ TokenBalancesController: { tokenBalances: {} },
// HeroCardView -> useReferralDetails/useSeasonStatus -> selectRewardsSubscriptionId reads RewardsController
RewardsController: {
activeAccount: null,
From 546b2fa22ebc2676cd1f1c0b3bcd0b4802a49071 Mon Sep 17 00:00:00 2001
From: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com>
Date: Fri, 6 Mar 2026 15:24:37 -0330
Subject: [PATCH 05/11] chore: New Crowdin translations by Github Action
cp-7.68.0 (#26477)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
> [!NOTE]
> **Low Risk**
> Low risk localization-only changes, but a few keys change
structure/names (e.g., `seed_phrase_warning_explanation` array→string,
`homepage.sections.more_predictions`→`view_more`) which could cause
missing/incorrect text if the app expects the old format.
>
> **Overview**
> Updates German, Greek, Spanish, and French locale JSONs with
new/expanded strings for several UI areas, including perps order
details, AI market insights (new feedback copy), predictions fee
breakdown, ramps order details/notifications, mUSD conversion
quick-convert copy, card PIN/cashback screens, rewards bonus-code
messaging, trending *stocks*, and hardware wallet connection/error
flows.
>
> Also adjusts some existing keys/labels (including a couple key renames
and a `seed_phrase_warning_explanation` value type change) to match
updated UI text requirements.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
66f4a23faa727b6e6122f76cd504dbbe87efaab6. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
Co-authored-by: metamaskbot
---
locales/languages/de.json | 409 +++++++++++++++++++++++++++++++-------
locales/languages/el.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/es.json | 401 +++++++++++++++++++++++++++++++------
locales/languages/fr.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/hi.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/id.json | 407 ++++++++++++++++++++++++++++++-------
locales/languages/ja.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/ko.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/pt.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/ru.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/tl.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/tr.json | 407 ++++++++++++++++++++++++++++++-------
locales/languages/vi.json | 405 ++++++++++++++++++++++++++++++-------
locales/languages/zh.json | 405 ++++++++++++++++++++++++++++++-------
14 files changed, 4734 insertions(+), 940 deletions(-)
diff --git a/locales/languages/de.json b/locales/languages/de.json
index 3c8e5ce22c2..d28cf392262 100644
--- a/locales/languages/de.json
+++ b/locales/languages/de.json
@@ -103,6 +103,10 @@
"message": "Nicht genügend {{ticker}}, um die Gebühren zu decken. Verwenden Sie ein Token in einem anderen Netzwerk oder fügen Sie weitere {{ticker}} hinzu, um fortzufahren.",
"title": "Unzureichende Gelder"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Diese Zahlungsroute ist derzeit nicht verfügbar. Versuchen Sie, den Betrag, das Netzwerk oder das Token zu ändern, und wir finden die beste Option.",
"title": "Keine Angebote"
@@ -203,7 +207,7 @@
"connector": "bei"
},
"autocomplete": {
- "placeholder": "Search by site or address",
+ "placeholder": "Suche nach Website oder Adresse",
"recents": "Aktuelle",
"favorites": "Favoriten",
"sites": "Websites",
@@ -1180,6 +1184,7 @@
"title": "Neue Order",
"leverage": "Hebel",
"limit_price": "Limitpreis",
+ "market_price": "Marktpreis",
"enter_price": "Preis eingeben",
"trigger_price": "Auslösungspreis",
"liquidation_price": "Liquidationspreis",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Gelder sind für den Handel verfügbar",
"close_order_still_active": "Schließung der Order noch aktiv",
"order_submitted": "Order übermittelt",
+ "submitting_your_trade": "Handel übermitteln",
"order_filled": "Order ausgeführt",
"order_placed": "Order erteilt",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "Order-Details",
"cancel_order": "Order stornieren",
"date": "Datum",
+ "trigger_condition": "Auslösebedingung",
+ "price": "Preis",
"fee": "Gebühr",
"limit_buy": "Long-Position limitieren",
"limit_price": "Limitpreis",
"limit_sell": "Short-Position limitieren",
"market_buy": "Markt-Long-Position",
"market_sell": "Markt-Short-Position",
+ "market": "Markt",
"open": "Eröffnen",
"size": "Größe",
+ "original_size": "Originalgröße",
+ "order_value": "Orderwert",
+ "reduce_only": "Nur reduzieren",
+ "yes": "Ja",
+ "no": "Nein",
+ "price_above": "Preis über {{price}}",
+ "price_below": "Preis unter {{price}}",
+ "take_profit": "Take-Profit",
+ "stop_loss": "Stop-Loss",
"status": "Status",
"view_explorer": "Im Explorer anzeigen"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Markteinblicke",
- "updated_ago": "{{time}} aktualisiert",
- "disclaimer": "KI-gestützte Erkenntnisse. Keine Finanzberatung.",
- "whats_driving_price": "Was treibt den Preis an?",
- "what_people_saying": "Was die Leute sagen",
+ "a_closer_look": "Ein genauerer Blick",
+ "whats_being_said": "Was gesagt wird",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Trade",
"sources_count": "+{{count}} Quellen",
- "sources_title": "Quellen"
+ "sources_title": "News sources",
+ "feedback_submitted": "Feedback eingereicht",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Feedback",
+ "description": "Helfen Sie uns, unsere KI-generierten Markteinblicke zu erweitern.",
+ "not_relevant": "Nicht relevant",
+ "not_accurate": "Nicht genau",
+ "hard_to_understand": "Schwer zu verstehen",
+ "harmful_or_offensive": "Schädlich oder beleidigend",
+ "something_else": "Etwas anderes",
+ "additional_feedback_label": "Zusätzliches Feedback (optional)",
+ "additional_feedback_placeholder": "Wie können wir uns verbessern?",
+ "characters_remaining": "{{count}} verbleibende Zeichen",
+ "submit": "Absenden"
+ }
},
"predict": {
"title": "MetaMask Predictions",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Verfügbar in etwa 1 Minute",
"withdraw_completed": "Auszahlung abgeschlossen",
"withdraw_completed_subtitle": "{{amount}} USDC wurden Ihre Wallet hinzugefügt",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} wurde Ihrer Wallet hinzugefügt",
"error_title": "Hoppla! Etwas ist schiefgelaufen ...",
"error_description": "Fortfahren mit Auszahlung fehlgeschlagen",
"try_again": "Erneut versuchen"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Beachten Sie bitte, dass es sich bei diesem Wert um einen Schätzwert handelt, der erst bei Abschluss der Transaktion endgültig festgelegt wird. Es kann bis zu 1 Stunde dauern, bis die Punkte in Ihrem Belohnungsguthaben bestätigt werden.",
"points_error": "Wir können momentan keine Punkte laden",
"points_error_content": "Sie erhalten für diese Transaktion weiterhin Punkte. Wir werden Sie benachrichtigen, sobald sie Ihrem Konto gutgeschrieben wurden. Sie können auch in etwa einer Stunde Ihre Registerkarte „Belohnungen“ überprüfen.",
- "slippage": "Slippage"
+ "slippage": "Slippage",
+ "price_details": "Preisdetails",
+ "prediction_order": "Prognose-Order",
+ "prediction_order_description": "~{{count}} Kontrakte zu je {{price}}. Der endgültige Betrag kann je nach Verfügbarkeit im Orderbuch variieren (bis zu {{slippage}} %).",
+ "metamask_fee_description": "Servicegebühr für die Bearbeitung dieser Prognose",
+ "exchange_fee": "Umrechnungsgebühr",
+ "exchange_fee_description": "An die Börse oder den Markt entrichtete Gebühr",
+ "total_incl_fees": "inkl. Gebühren",
+ "close": "Schließen"
},
"error": {
"title": "Verbindung zu Prognosen nicht möglich",
@@ -2399,7 +2440,7 @@
"add_tokens": "Token importieren",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Möchten Sie diese Token importieren?",
"tokens_detected_in_account": "{{tokenCount}} neue {{tokensLabel}} in diesem Konto gefunden",
"token_toast": {
"tokens_imported_title": "Importierte Tokens",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Token-Dezimalzahlen können nicht leer sein.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "Wir konnten keine Token mit diesem Namen finden.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Nach einem beliebigen Token suchen und importieren",
"select_token": "Token auswählen",
"address_must_be_smart_contract": "Privatadresse erkannt. Geben Sie die Token-Contract-Adresse ein.",
"billion_abbreviation": "Mrd.",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "RPC-URL hinzufügen",
"add_block_explorer_url": "Block-Explorer-URL hinzufügen",
"networks_desc": "Benutzerdefinierte RPC-Netzwerke hinzufügen und verwalten",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Aktivierte Netzwerke",
+ "networks_test_networks": "Testnetzwerke",
+ "networks_additional": "Zusätzliche Netzwerke",
"networks_search_placeholder": "Netzwerk suchen",
"networks_no_results": "Keine Netzwerke gefunden",
"network_name_label": "Netzwerkname",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "RPC-Netzwerke",
"network_add_network": "Netzwerk hinzufügen",
"add_chain_title": "Ein Netzwerk hinzufügen",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Suche nach Name, Chain-ID oder Währung",
+ "add_chain_loading": "Netzwerke werden geladen…",
+ "add_chain_error": "Netzwerke konnten nicht geladen werden. Bitte versuchen Sie es erneut.",
"add_chain_retry": "Erneut versuchen",
- "add_chain_added": "Added",
+ "add_chain_added": "Hinzugefügt",
"add_chain_or": "oder",
"add_chain_custom_link": "Benutzerdefiniertes Netzwerk hinzufügen",
"network_add_custom_network": "Benutzerdefiniertes Netzwerk hinzufügen",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Testnetzwerk hinzufügen",
"network_add": "Hinzufügen",
"network_save": "Speichern",
"remove_network_title": "Möchten Sie dieses Netzwerk entfernen?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Speichern Sie diese an einem sicheren und geheimen Ort.",
"private_key_warning": "Dies ist der private Key für das aktuell ausgewählte Konto: {{accountName}}. Geben Sie diesen Key niemals weiter. Jeder, der über Ihren privaten Key verfügt, kann Ihr Konto vollständig kontrollieren, einschließlich der Überweisung von Geldern.",
- "seed_phrase_warning_explanation": [
- "Stellen Sie sicher, dass niemand auf Ihren Bildschirm schaut.",
- "Der MetaMask Support wird Sie nie danach fragen."
- ],
+ "seed_phrase_warning_explanation": "Stellen Sie sicher, dass niemand auf Ihren Bildschirm schaut. Der MetaMask-Support wird Sie nie danach fragen.",
"private_key_warning_explanation": "Geben Sie diesen Key niemals weiter. Jeder, der Ihren privaten Key hat, kann Ihr Konto vollständig kontrollieren, einschließlich der Überweisung Ihrer Gelder.",
+ "reveal_srp_description": "Mit Ihrer geheimen Wiederherstellungsphrase haben Sie uneingeschränkten Zugriff auf Ihre Wallet. Geben Sie diese nicht an Dritte weiter.",
"reveal_credential_modal": [
"Ihr {{credentialName}} bietet ",
"vollständigen Zugriff auf Ihr Konto sowie Ihre Gelder.\n\nTeilen Sie dies mit niemandem.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "Geheime Wiederherstellungsphrase",
"private_key_text": "Privater Schlüssel",
"got_it": "Verstanden",
- "learn_more": "Mehr erfahren"
+ "learn_more": "Mehr erfahren",
+ "copied_to_clipboard": "In die Zwischenablage kopiert"
},
"screenshot_deterrent": {
"title": "Sicherheitsbenachrichtigung",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Versuchen, zu beschleunigen?",
"speedup_tx_message": "Das Absenden dieses Versuches garantiert nicht, dass die Transaktion beschleunigt wird. Wenn der Beschleunigungsversuch erfolgreich ist, wird Ihnen die obige Transaktionsgebühr in Rechnung gestellt.",
"nevermind": "Macht nichts",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Gas-Gebühr bearbeiten",
"edit_priority": "Priorität bearbeiten",
"gas_cancel_fee": "Gas-Stornogebühr",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Bei dieser Genehmigung fällt eine Transaktionsgebühr an.",
"view_details": "Detail ansehen",
"view_transaction_details": "Transaktionsdetails anzeigen",
- "view_data": "View data",
+ "view_data": "Daten anzeigen",
"transaction_details": "Transaktionsdetails",
"site_url": "Seiten-URL",
"permission_request": "Berechtigungsanfrage",
@@ -3817,7 +3861,7 @@
"no_tabs_desc": "Öffnen Sie einen neuen Tab, um im dezentralisieren Web zu surfen.",
"got_it": "Verstanden",
"max_tabs_title": "Maximale Anzahl an Tabs erreicht",
- "max_tabs_desc": "We currently only support 20 open tabs at once. Please close existing tabs before adding new ones.",
+ "max_tabs_desc": "Derzeit können nur 20 Tabs gleichzeitig geöffnet werden. Bitte schließen Sie vorhandene Tabs, bevor Sie neue hinzufügen.",
"failed_to_resolve_ens_name": "Wir konnten diesen ENS-Namen nicht lösen",
"remove_bookmark_title": "Favoriten entfernen",
"remove_bookmark_msg": "Möchten Sie diese Seite wirklich aus Ihren Favoriten entfernen?",
@@ -3843,7 +3887,7 @@
"right_button": "Wallet schützen"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Favoriten hinzufügen",
"title_label": "Name",
"url_label": "URL",
"add_button": "Hinzufügen",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Mit Touch ID entsperren?",
"enable_faceid": "Mit Face ID entsperren?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "Mit Fingerabdruck entsperren?",
+ "enable_biometrics": "Mit Biometrisch entsperren?",
"enable_device_passcode_ios": "Mit Geräte-Passcode entsperren?",
"enable_device_passcode_android": "Mit Geräte-PIN entsperren?"
},
@@ -4724,7 +4768,7 @@
"public_address": "Öffentliche Adresse",
"public_address_qr_code": "Öffentliche Adresse",
"coming_soon": "In Kürze ...",
- "request_payment": "Request payment",
+ "request_payment": "Zahlung anfragen",
"copy": "Kopieren",
"scan_address": "Adresse scannen, um Zahlung zu erhalten",
"copy_address": "Adresse kopieren"
@@ -4738,8 +4782,8 @@
"switch_network": "Bitte wechseln Sie auf Mainnet oder Sepolia.",
"card_title": "Schaltfläche MetaMask-Karte immer anzeigen",
"card_desc": "MetaMask-Karte ist nur für Einwohner ausgewählter Länder verfügbar.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "DaimoPay-Demo-Umgebung verwenden",
+ "daimo_demo_desc": "Wechseln Sie für Kartenzahlungen zwischen der DaimoPay-Demo- und Produktionsumgebung."
},
"walletconnect_sessions": {
"no_active_sessions": "Sie haben keine aktiven Sitzungen",
@@ -4752,10 +4796,10 @@
"close_current_session": "Schließen Sie die aktuelle Sitzung, bevor Sie eine neue beginnen."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Zahlungsanfrage",
+ "title_complete": "Zahlung abgeschlossen",
"confirm": "Bezahlen",
- "cancel": "Decline",
+ "cancel": "Ablehnen",
"is_requesting_you_to_pay": "bittet Sie um eine Zahlung",
"total": "Insgesamt:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Es stehen keine Zahlungsmethoden zur Verfügung.",
"error_fetching_quotes": "Etwas ist schiefgelaufen! Bitte versuchen Sie es erneut.",
"no_quotes_available": "Keine Anbieter verfügbar.",
+ "quote_unavailable": "Angebot nicht verfügbar.",
"providers": "Anbieter",
+ "quotes_displayed_for": "Angebote angezeigt für {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Zuvor verwendet",
+ "best_rate": "Bester Wechselkurs",
+ "most_reliable": "Am zuverlässigsten",
"continue": "Fortfahren",
"powered_by_provider": "Unterstützt von {{provider}}",
"purchased_currency": "{{currency}} gekauft",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Fehler beim Abmelden"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Nicht verfügbar",
+ "description": "{{token}} ist bei {{provider}} in Ihrer Region nicht verfügbar.",
+ "change_token": "Token ändern",
+ "change_provider": "Anbieter wechseln"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Anbieter auswählen"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Verstanden",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Anbieter wechseln"
},
"fiat_on_ramp_aggregator": {
"buy": "kaufen",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "{{currency}}-Einzahlung"
},
+ "ramps_order_details": {
+ "title": "Order-Details",
+ "status": "Status",
+ "processing": "Verarbeitung",
+ "complete": "Abgeschlossen",
+ "failed": "Fehlgeschlagen",
+ "cancelled": "Abgebrochen",
+ "view_on_provider": "Auf {{provider}} anzeigen",
+ "order_id": "Auftrags-ID",
+ "date_and_time": "Datum und Uhrzeit",
+ "fees": "Gebühren",
+ "total": "Insgesamt",
+ "card_processing_info": "Kartenkäufe dauern in der Regel nur wenige Minuten",
+ "processing_info_modal_description": "Kartenkäufe dauern in der Regel nur wenige Minuten. Bei Fragen können Sie sich gerne an den Support wenden.",
+ "go_to_provider_support": "Zur Supportseite des {{provider}}",
+ "error_title": "Hoppla! Etwas ist schiefgelaufen ...",
+ "error_message": "Order-Details können nicht geladen werden. Bitte versuchen Sie es erneut.",
+ "try_again": "Erneut versuchen",
+ "close": "Schließen"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Bearbeitung Ihres Kaufs von {{cryptocurrency}}",
+ "purchase_pending_description": "Dies sollte nur ein paar Minuten dauern ...",
+ "purchase_completed_title": "Ihr Kauf von {{amount}} {{cryptocurrency}} war erfolgreich!",
+ "purchase_completed_description": "Ihr {{cryptocurrency}} ist jetzt verfügbar",
+ "purchase_failed_title": "Kauf von {{cryptocurrency}} fehlgeschlagen",
+ "purchase_failed_description": "Bitte versuchen Sie es in Kürze erneut",
+ "purchase_cancelled_title": "Ihr Kauf wurde storniert",
+ "purchase_cancelled_description": "Ihr Kauf von {{cryptocurrency}} wurde storniert",
+ "track": "Verfolgen"
+ }
+ },
"swaps": {
"title": "Swap",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Halten Sie Ihre MetaMask-App und Ihr Betriebssystem immer auf dem neuesten Stand, um zu vermeiden, dass dies erneut geschieht."
},
"srp_security_quiz": {
+ "question_step": "Frage {{step}} von {{total}}",
"title": "Sicherheitsquiz",
"introduction": "Zur Enthüllung Ihrer geheimen Wiederherstellungsphrase müssen Sie zwei Fragen beantworten.",
"get_started": "Erste Schritte",
"learn_more": "Erfahren Sie mehr",
"try_again": "Erneut versuchen",
"continue": "Fortfahren",
+ "correct": "Richtig!",
+ "incorrect": "Falsch!",
"of": "von",
"question_one": {
"question": "Wenn Sie Ihre geheime Wiederherstellungsphrase verlieren, kann MetaMask ...",
@@ -5823,11 +5914,11 @@
"error_description": "Installation von {{snap}} fehlgeschlagen."
},
"earn": {
- "claimable_bonus_tooltip": "Ein jährlicher Bonus, den Sie täglich aus Ihrer Wallet abholen können.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Verdienen Sie einen Bonus von {{percentage}} %",
"claimable_bonus": "Anspruchsberechtigter Bonus",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Bonus einfordern",
+ "claim_bonus_subtitle": "Der Bonus wird auf {{networkName}} ausgezahlt.",
"empty_state_cta": {
"heading": "Verleihen Sie {{tokenSymbol}} und verdienen Sie",
"body": "Verleihen Sie Ihre {{tokenSymbol}} mit {{protocol}} und verdienen Sie",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Wir sind wegen Wartungsarbeiten nicht erreichbar. Wir sind bald wieder online!"
},
- "supply": "Supply",
+ "supply": "Vorrat",
"deposit": "Einzahlung",
"approve": "Genehmigen",
"approval": "Genehmigung",
@@ -5887,7 +5978,7 @@
"network": "Netzwerk",
"health_factor": "Gesundheitsfaktor",
"liquidation_risk": "Liquidationsrisiko",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Unzureichende Pool-Liquidität",
"available_to_withdraw": "zur Auszahlung verfügbar",
"unknown": "unbekannt",
"how_it_works": "Wie es funktioniert",
@@ -5906,7 +5997,7 @@
"lending": "Positionsverlauf",
"staking": "Auszahlungsverlauf"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Zulagen zurücksetzen",
"tron": {
"fee": "Gebühr"
},
@@ -5914,32 +6005,60 @@
"ok": "OK",
"continue": "Fortfahren",
"convert_and_get_percentage_bonus": "Konvertieren und {{percentage}} % erhalten",
+ "your_musd": "Ihre mUSD",
+ "balance_breakdown_title": "Ihr mUSD-Guthaben nach Netzwerk",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "Zu mUSD konvertieren",
"get_a_percentage_musd_bonus": "Erhalten Sie {{percentage}} % mUSD-Bonus",
"convert": "Konvertieren",
+ "fetching_quote": "Angebot einholen...",
+ "you_convert": "Sie konvertieren",
+ "network_fee": "Netzwerkgebühr",
+ "earning": "Verdienen",
+ "quick_convert_description": "Wählen Sie einen Token zur Konvertierung Ihres gesamten Guthabens oder klicken Sie auf das Bearbeitungssymbol, um einen eigenen Betrag einzugeben.",
+ "no_tokens_to_convert": "Sie haben keine Token, die in mUSD konvertiert werden können.",
"toasts": {
"converting": "Konvertierung von {{token}} → mUSD",
"eta": "~{{time}}",
- "delivered": "Ihr mUSD ist hier!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "mUSD-Konvertierung fehlgeschlagen"
},
"education": {
"heading": "ERHALTEN SIE {{percentage}} % AUF\nSTABILECOINS",
- "description": "Konvertieren Sie Ihre Stablecoins in mUSD, MetaMasks in US-Dollar gedeckten Stablecoin, und erhalten Sie einen Bonus von bis zu {{percentage}} %.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Es gelten die Nutzungsbedingungen.",
"primary_button": "Erste Schritte",
"secondary_button": "Nicht jetzt"
},
"buy_musd": "mUSD kaufen",
"get_musd": "mUSD erhalten",
- "bonus_title": "Erhalten Sie {{percentage}} % auf Ihre Stablecoins",
- "bonus_description": "Konvertieren Sie Ihre Stablecoins in mUSD und sichern Sie sich einen Bonus von bis zu {{percentage}} %.",
- "powered_by_relay": "Unterstützt von Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Unterstützt von Relay",
+ "max": "Max.",
+ "quick_convert_button": "Konvertieren",
+ "learn_more": "Mehr erfahren",
+ "tooltip_title": "Verdienen Sie Rendite mit mUSD",
+ "tooltip_content": "Konvertieren Sie Ihre USDC, USDT oder DAI gegen mUSD, den an den US-Dollar gekoppelten Stablecoin von MetaMask. Verdienen Sie {{apy}} Rendite für jeden von Ihnen gehaltenen US-Dollar.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Konvertierung fehlgeschlagen. Versuchen Sie es erneut.",
+ "confirmation": {
+ "title": "Konvertieren max"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Rate"
},
"bonus_claim": {
"toasts": {
"claiming": "Ihr mUSD-Bonus wird bearbeitet",
"delivered": "Ihr mUSD-Bonus ist hier!",
- "failed": "Bonus claim failed"
+ "failed": "Einfordern des Bonus fehlgeschlagen"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Die Punkte werden Ihrem Konto automatisch gutgeschrieben.",
"tooltip_not_opted_in_footer": "Melden Sie sich für Belohnungen an, um Ihre Punkte zu erhalten.",
"tooltip_close": "Schließen"
- }
+ },
+ "your_stablecoins": "Ihre Stablecoins"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Transaktionsgebühren",
"metamask_fee": "MetaMask-Gebühr",
"network_fee": "Netzwerk-Gebühr",
- "bridge_fee": "Gebühr für Bridge-Anbieter"
+ "bridge_fee": "Gebühr für Bridge-Anbieter",
+ "provider_fee": "Anbietergebühr"
},
"title": {
"signature": "Signaturanfrage",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Wir swappen Ihre Token gegen USDC.e auf Polygon, dem von Predictions verwendeten Netzwerk. Swap-Anbieter erheben möglicherweise eine Gebühr, MetaMask jedoch nicht."
},
"predict_withdraw": {
- "transaction_fee": "MetaMask swappt den Token für Sie in den gewünschten Token um. Beim Swap in MUSD fallen keine MetaMask-Gebühren an."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "Die Gebühren für die Umstellung von mUSD umfassen Netzwerkkosten und können Anbietergebühren beinhalten."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Gebühren"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Preiseinfluss",
"price_impact_info_description": "Der Preiseinfluss spiegelt wider, wie Ihre Swap-Order den Marktpreis des Assets beeinflusst. Er hängt von der Trade-Größe und der verfügbaren Liquidität im Pool ab. MetaMask hat keinerlei Kontrolle über den Preiseinfluss.",
"price_impact_info_gasless_description": "Die Preisauswirkung spiegelt wider, wie Ihre Swap-Order den Marktpreis des Assets beeinflusst. Falls Sie nicht über genügend Gelder für Gas halten, wird ein Teil Ihres Quelltokens automatisch zur Deckung der Gebühren verwendet, was die Preisauswirkung erhöht. MetaMask hat keinerlei Kontrolle über die Preisauswirkung.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Stornieren",
"slippage_info_title": "Slippage",
"slippage_info_description": "Die prozentuale Preisänderung, die Sie zuzulassen bereit sind, bevor Ihre Transaktion storniert wird.",
"blockaid_error_title": "Diese Transaktion wird rückgängig gemacht",
@@ -6470,13 +6596,14 @@
},
"submit": "Absenden",
"default_slippage_description": "Ihre Transaktion wird nicht durchgeführt, wenn sich der Preis um mehr als den Slippage-Prozentsatz ändert.",
- "cancel": "Stornieren",
"confirm": "Bestätigen",
"exceeding_upper_slippage_warning": "Hohe Slippage, was zu einem ungünstigen Swap führen kann",
"exceeding_lower_slippage_warning": "Niedrige Slippage, was zu einem ungünstigen Swap führen kann",
"exceeding_lower_slippage_error": "Geben Sie einen Wert ein, der größer als {{value}}% ist.",
"exceeding_upper_slippage_error": "Sie können keinen Wert eingeben, der größer als {{value}} % ist.",
- "custom": "Benutzerdefiniert"
+ "custom": "Benutzerdefiniert",
+ "invalid_recipient_address": "Ungültige Adresse",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Neue Angebote sind verfügbar",
@@ -6773,12 +6900,16 @@
"title": "Passwort eingeben",
"description": "Geben Sie Ihr Wallet-Passwort ein, um die Kartendetails anzuzeigen.",
"description_unfreeze": "Geben Sie Ihr Wallet-Passwort ein, um mit Ihrer Karte weiter bezahlen zu können.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Passwort",
"confirm": "Bestätigen",
"cancel": "Stornieren",
"error_empty": "Bitte geben Sie Ihr Passwort ein",
"error_incorrect": "Falsches Passwort. Bitte versuchen Sie es erneut."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Wählen Sie Ihre Karte",
"upgrade_title": "Upgrade auf Metall",
@@ -7094,6 +7225,9 @@
"view_card_details": "Kartendetails anzeigen",
"hide_card_details": "Kartendetails verbergen",
"view_card_details_description": "Kartennummer, Ablaufdatum und Karteprüfwert",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Limit verwalten",
"manage_spending_limit_description_restricted": "Begrenzte Ausgaben sind an",
"manage_spending_limit_description_full": "Vollzugriff ist an",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Geschäftsbedingungen",
"order_metal_card": "Metallkarte",
"order_metal_card_description": "Bestellen Sie jetzt Ihre physische Metallkarte",
+ "cashback": "Cashback",
+ "cashback_description": "Verdienen Sie 1 % Rabatt auf alle Ausgaben",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Karte sperren",
"unfreeze_card": "Karte entsperren",
"freeze_card_description": "Alle Ausgaben mit Ihrer Karte pausieren",
@@ -7141,6 +7278,19 @@
"retry": "Erneut versuchen",
"on_linea": "auf Linea"
},
+ "cashback_screen": {
+ "title": "Cashback",
+ "available_cashback": "Verfügbares Cashback",
+ "network_fee": "Netzwerkgebühr",
+ "expected_to_receive": "Erwartet zu empfangen",
+ "withdraw": "Auszahlen",
+ "withdraw_unavailable": "Auszahlung nicht verfügbar",
+ "withdrawal_initiated": "Auszahlung wurde eingeleitet",
+ "withdrawal_success": "Auszahlung erfolgreich abgeschlossen",
+ "withdrawal_failed": "Auszahlung fehlgeschlagen. Bitte versuchen Sie es erneut.",
+ "no_cashback": "Kein Cashback verfügbar",
+ "loading_error": "Cashback konnte nicht geladen werden. Bitte versuchen Sie es erneut."
+ },
"change_asset": {
"title": "Token und Netzwerk ändern",
"full_spending_access": "Vollständiger Zugriff auf die Ausgaben",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Zahlungsmethode auswählen",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Token empfangen auswählen",
+ "no_gas": "Kein nativer Saldo für Gas"
},
"connection_removed_modal": {
"title": "Verbindungen entfernt",
@@ -7315,7 +7465,10 @@
"service_not_available": "Der Dienst ist derzeit nicht verfügbar. Bitte versuchen Sie es in Kürze erneut.",
"invalid_referral_code": "Ungültiger Empfehlungscode. Bitte überprüfen Sie ihn und versuchen Sie es erneut.",
"already_referred": "Sie wurden bereits von einem anderen Nutzer empfohlen.",
- "cannot_use_own_referral_code": "Sie können nicht Ihren eigenen Empfehlungscode verwenden."
+ "cannot_use_own_referral_code": "Sie können nicht Ihren eigenen Empfehlungscode verwenden.",
+ "invalid_bonus_code": "Ungültiger Bonuscode",
+ "already_redeemed": "Sie haben diesen Bonuscode bereits eingelöst",
+ "reached_maximum": "Dieser Bonuscode hat seine maximale Anzahl an Nutzungen erreicht"
},
"claim_reward_error": {
"title": "Einforderung der Belohnung fehlgeschlagen"
@@ -7372,8 +7525,10 @@
"predict": "Prognose",
"musd_deposit": "mUSD-Einzahlung",
"apply_referral_bonus": "Empfehlungscode-Bonus",
+ "bonus_code": "Bonuscode",
"uncategorized_event": "Nicht kategorisiertes Ereignis"
},
+ "code": "Code",
"date": "Datum",
"account": "Konto",
"bonus": "Bonus",
@@ -7473,7 +7628,16 @@
"show_less": "Weniger anzeigen",
"linking_progress": "Konten werden hinzufügen ... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} angemeldet",
- "add_all_accounts": "Alle Konten hinzufügen"
+ "add_all_accounts": "Alle Konten hinzufügen",
+ "environment_selector": "Umgebung",
+ "environment_cancel": "Stornieren",
+ "environment_default": "Standard",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Empfehlungscode",
@@ -7483,6 +7647,10 @@
"invalid_code": "Ungültiger Empfehlungscode",
"apply_button": "Empfehlungscode anwenden"
},
+ "bonus_code": {
+ "input_placeholder": "Bonuscode eingeben",
+ "apply_success": "Bonuscode angewandt!"
+ },
"optout": {
"title": "Rewards-Fortschritt löschen",
"description": "Dadurch werden Ihre Konten aus dem Rewards-Programm entfernt und Ihre Punkte sowie Ihr Fortschritt gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Bridge-Gebühr",
+ "provider_fee": "Anbietergebühr",
"network_fee": "Netzwerk-Gebühr",
"paid_with": "Bezahlt mit",
+ "receive_token": "Token empfangen",
"retry_button": "Erneut versuchen",
"total": "Insgesamt",
"account": "Konto",
- "received_total": "Received total"
+ "received_total": "Insgesamt erhalten"
},
"summary_title": {
"bridge_approval": "{{approveSymbol}} genehmigen",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Verbindung nicht gefunden",
"description": "Bitte stellen Sie über die App eine neue Verbindung her, um fortzufahren."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Noch beim Verbindungsaufbau zu {{networkName}} ...",
"unable_to_connect_network": "Verbindungsherstellung mit {{networkName}} nicht möglich.",
"update_rpc": "RPC aktualisieren",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Zu MetaMask Standard-RPC wechseln",
"check_network_connectivity": "Überprüfen Sie Ihre Netzwerkverbindung.",
"check_network_connectivity_or": "Überprüfen Sie Ihre Netzwerkverbindung oder",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Auf Standardeinstellung von MetaMask aktualisiert"
},
"trending": {
"title": "Entdecken",
"trending_tokens": "Trendige Tokens",
+ "stocks": "Aktien",
"price_change": "Preisänderung",
"all_networks": "Alle Netzwerke",
"24h": "24 Stunden",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Erneut laden",
"primary_action_acknowledge": "Verstanden"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "QR-Hardware-Wallet",
+ "hardware_wallet": "Hardware-Wallet"
+ },
+ "common": {
+ "cancel": "Stornieren",
+ "continue": "Fortfahren"
+ },
+ "connecting": {
+ "title": "Verbinden Sie Ihr {{device}}",
+ "searching": "Suche nach {{device}} ...",
+ "tips_header": "Um fortzufahren, stellen Sie Folgendes sicher:",
+ "tip_unlock": "Ihr {{device}} ist freigeschaltet",
+ "tip_open_app": "Die Ethereum-App ist geöffnet",
+ "tip_enable_bluetooth": "Bluetooth ist eingeschaltet",
+ "tip_dnd_off": "Nicht stören ist ausgeschaltet",
+ "tip_bluetooth_permission": "Standort- und Bluetooth-Berechtigung werden erteilt",
+ "tip_bluetooth_permission_v12": "Berechtigung für Geräte in der Nähe wird erteilt",
+ "tip_stay_close": "Ihr Gerät bleibt in der Nähe Ihres Telefons"
+ },
+ "awaiting_app": {
+ "title": "{{app}} öffnen",
+ "message": "Bitte öffnen Sie die {{app}}-App auf Ihrem {{device}}",
+ "current_app": "Derzeit geöffnet: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Auf {{device}} bestätigen",
+ "title_message": "Auf {{device}} anmelden",
+ "message": "Überprüfen und bestätigen Sie auf Ihrem {{device}}"
+ },
+ "success": {
+ "title": "{{device}} verbunden"
+ },
+ "errors": {
+ "device_locked": "Entsperren Sie es und versuchen Sie es erneut, um fortzufahren",
+ "app_not_open": "Bitte öffnen Sie die Ethereum-App auf Ihrem Gerät",
+ "device_disconnected": "Die Verbindung zu Ihrem {{device}} wurde unterbrochen. Bitte verbinden Sie sich wieder und versuchen Sie es erneut",
+ "device_not_found": "Ihr {{device}} konnte nicht gefunden werden. Überprüfen Sie bitte die Verbindung",
+ "device_not_ready": "Ihr Gerät ist nicht bereit. Bitte überprüfen Sie es und versuchen Sie es erneut",
+ "blind_signing": "Die Blindsignatur ist deaktiviert. Bitte aktivieren Sie sie in den Einstellungen Ihres Geräts",
+ "connection_closed": "Die Verbindung wurde unterbrochen. Bitte versuchen Sie es erneut",
+ "connection_timeout": "Zeitüberschreitung der Verbindung. Bitte versuchen Sie es erneut",
+ "user_cancelled": "Die Aktion wurde auf Ihrem Gerät abgebrochen",
+ "pending_confirmation": "Es gibt eine ausstehende Aktion auf Ihrem Gerät. Bitte schließen oder brechen Sie diese ab",
+ "bluetooth_permission_denied": "Zur Verbindung mit Ihrem Gerät ist eine Bluetooth-Berechtigung erforderlich",
+ "location_permission_denied": "Um nach Geräten zu scannen, ist eine Standortberechtigung erforderlich",
+ "nearby_permission_denied": "Berechtigung für Geräte in der Nähe ist erforderlich",
+ "bluetooth_off": "Bitte schalten Sie Bluetooth ein, um eine Verbindung mit Ihrem Gerät herzustellen",
+ "bluetooth_scan_failed": "Scannen nach Geräten fehlgeschlagen. Bitte versuchen Sie es erneut",
+ "bluetooth_connection_failed": "Aktivieren Sie Bluetooth auf Ihrem Gerät, um fortzufahren",
+ "not_supported": "Dieser Vorgang wird nicht unterstützt",
+ "unknown_error": "Stellen Sie sicher, dass Ihr {{device}} mit der geheimen Wiederherstellungsphrase oder Passphrase für dieses Konto eingerichtet ist"
+ },
+ "error": {
+ "title": "Hoppla! Etwas ist schiefgelaufen ...",
+ "default_title": "Ein Fehler ist aufgetreten",
+ "continue": "Fortfahren",
+ "retry": "Erneut versuchen",
+ "view_settings": "Einstellungen anzeigen",
+ "device_locked_title": "{{device}} gesperrt",
+ "device_disconnected_title": "{{device}} getrennt",
+ "device_not_found_title": "{{device}} nicht gefunden",
+ "app_not_open": "Ethereum-App nicht geöffnet",
+ "blind_signing_disabled": "Blindsignatur ist deaktiviert",
+ "connection_timeout": "Gerät reagiert nicht",
+ "connection_closed": "Verbindung unterbrochen",
+ "user_cancelled": "Aktion abgebrochen",
+ "pending_confirmation": "Bestätigung ausstehend",
+ "bluetooth_required": "Bluetooth erforderlich",
+ "bluetooth_permission_denied": "Bluetooth-Berechtigung erforderlich",
+ "location_permission_denied": "Standortberechtigung erforderlich",
+ "nearby_devices_permission_denied": "Berechtigung für Geräte in der Nähe ist erforderlich",
+ "scan_failed": "Scanvorgang fehlgeschlagen",
+ "something_went_wrong": "Hoppla! Etwas ist schiefgelaufen ..."
+ },
+ "device_selection": {
+ "title": "{{device}} auswählen",
+ "scanning": "Scannen nach Geräten ...",
+ "no_devices_found": "Es wurden keine Geräte gefunden",
+ "no_devices_hint": "Vergewissern Sie sich, dass Ihr {{device}} entsperrt und Bluetooth aktiviert ist",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Verbinden",
+ "rescan": "Erneut scannen",
+ "unknown_device": "Unbekanntes Gerät",
+ "signal_strength": "Signal: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Token",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFTs",
"import_nfts": "NFTs importieren",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Fügen Sie ganz einfach Ihre Sammlerstücke hinzu",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Kein TP/SL"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "{{section}} kann nicht geladen werden",
+ "retry": "Erneut versuchen"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/el.json b/locales/languages/el.json
index 392dd89622b..92fda1f7048 100644
--- a/locales/languages/el.json
+++ b/locales/languages/el.json
@@ -103,6 +103,10 @@
"message": "Δεν υπάρχουν αρκετά {{ticker}} για την κάλυψη των τελών. Χρησιμοποιήστε token σε άλλο δίκτυο ή προσθέστε περισσότερα {{ticker}} για να συνεχίσετε.",
"title": "Ανεπαρκές ποσό"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Αυτή η διαδρομή πληρωμής δεν είναι διαθέσιμη αυτή τη στιγμή. Δοκιμάστε να αλλάξετε το ποσό, το δίκτυο ή το token και θα βρούμε την καλύτερη επιλογή.",
"title": "Δεν υπάρχουν προσφορές"
@@ -1180,6 +1184,7 @@
"title": "Νέα εντολή",
"leverage": "Μόχλευση",
"limit_price": "Τιμή ορίου",
+ "market_price": "Τιμή αγοράς",
"enter_price": "Εισάγετε τιμή",
"trigger_price": "Τιμή ενεργοποίησης",
"liquidation_price": "Τιμή ρευστοποίησης",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Τα κεφάλαια είναι διαθέσιμα για συναλλαγές",
"close_order_still_active": "Η εντολή κλεισίματος είναι ακόμα ενεργή",
"order_submitted": "Η εντολή υποβλήθηκε",
+ "submitting_your_trade": "Υποβολή της συναλλαγής σας",
"order_filled": "Η εντολή εκτελέστηκε",
"order_placed": "Η εντολή καταχωρήθηκε",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "Λεπτομέρειες εντολής",
"cancel_order": "Ακύρωση εντολής",
"date": "Ημερομηνία",
+ "trigger_condition": "Συνθήκη ενεργοποίησης",
+ "price": "Τιμή",
"fee": "Τέλη",
"limit_buy": "Εντολή Αγοράς με Όριο",
"limit_price": "Τιμή ορίου",
"limit_sell": "Εντολή Πώλησης με Όριο",
"market_buy": "Εντολή Αγοράς στην Τιμή Αγοράς",
"market_sell": "Εντολή Πώλησης στην Τιμή Αγοράς",
+ "market": "Τιμή αγοράς",
"open": "Άνοιγμα",
"size": "Μέγεθος",
+ "original_size": "Αρχικό μέγεθος",
+ "order_value": "Αξία εντολής",
+ "reduce_only": "Μόνο μείωση",
+ "yes": "Ναι",
+ "no": "Όχι",
+ "price_above": "Τιμή πάνω από {{price}}",
+ "price_below": "Τιμή κάτω από {{price}}",
+ "take_profit": "Λήψη κερδών",
+ "stop_loss": "Περιορισμός ζημιών",
"status": "Κατάσταση",
"view_explorer": "Προβολή στο Explorer"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Πληροφορίες αγοράς",
- "updated_ago": "Ενημερώθηκε {{time}}",
- "disclaimer": "Πληροφορίες ΤΝ. Όχι οικονομικές συμβουλές.",
- "whats_driving_price": "Τι καθορίζει την τιμή;",
- "what_people_saying": "Τι λένε οι άνθρωποι",
+ "a_closer_look": "Μια πιο προσεκτική ματιά",
+ "whats_being_said": "Τι συζητιέται",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Συναλλαγές",
"sources_count": "+{{count}} πηγές",
- "sources_title": "Πηγές ρευστοποίησης"
+ "sources_title": "News sources",
+ "feedback_submitted": "Το σχόλιό σας υποβλήθηκε",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Ανατροφοδότηση",
+ "description": "Βοηθήστε μας να βελτιώσουμε τις αναλύσεις αγοράς που δημιουργεί η τεχνητή νοημοσύνη.",
+ "not_relevant": "Δεν είναι σχετικό",
+ "not_accurate": "Δεν είναι ακριβές",
+ "hard_to_understand": "Δύσκολο στην κατανόηση",
+ "harmful_or_offensive": "Επιβλαβές ή προσβλητικό",
+ "something_else": "Κάτι άλλο",
+ "additional_feedback_label": "Επιπλέον σχόλια (προαιρετικά)",
+ "additional_feedback_placeholder": "Πώς μπορούμε να βελτιωθούμε;",
+ "characters_remaining": "Απομένουν {{count}} χαρακτήρες",
+ "submit": "Υποβολή"
+ }
},
"predict": {
"title": "Προβλέψεις στο MetaMask",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Διαθέσιμο σε περίπου 1 λεπτό",
"withdraw_completed": "Η ανάληψη ολοκληρώθηκε",
"withdraw_completed_subtitle": "{{amount}} USDC μεταφέρθηκαν στο πορτοφόλι σας",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} μεταφέρθηκαν στο πορτοφόλι σας",
"error_title": "Κάτι πήγε στραβά",
"error_description": "Δεν ήταν δυνατή η συνέχιση της ανάληψης",
"try_again": "Προσπαθήστε ξανά"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Λάβετε υπόψη ότι αυτή η τιμή είναι κατ' εκτίμηση και θα οριστικοποιηθεί με την ολοκλήρωση της συναλλαγής. Η επιβεβαίωση των πόντων στο υπόλοιπο ανταμοιβών σας μπορεί να διαρκέσει έως και 1 ώρα.",
"points_error": "Δεν είναι δυνατή η φόρτωση πόντων αυτή τη στιγμή",
"points_error_content": "Θα εξακολουθήσετε να κερδίζετε τους πόντους που αντιστοιχούν σε αυτήν τη συναλλαγή. Θα σας ειδοποιήσουμε μόλις προστεθούν στον λογαριασμό σας. Μπορείτε επίσης να ελέγξετε την καρτέλα ανταμοιβών σας σε περίπου μία ώρα.",
- "slippage": "Απόκλιση"
+ "slippage": "Απόκλιση",
+ "price_details": "Λεπτομέρειες τιμής",
+ "prediction_order": "Εντολή πρόβλεψης",
+ "prediction_order_description": "~{{count}} συμβόλαια στα {{price}} το καθένα. Το τελικό ποσό μπορεί να διαφέρει λόγω διαθεσιμότητας στο βιβλίο εντολών (έως και {{slippage}}%).",
+ "metamask_fee_description": "Τέλη υπηρεσίας για την επεξεργασία αυτής της πρόβλεψης",
+ "exchange_fee": "Τέλη ανταλλαγής",
+ "exchange_fee_description": "Τέλη που καταβάλλονται στην πλατφόρμα συναλλαγών ή στην αγορά",
+ "total_incl_fees": "συμπερ. τέλη",
+ "close": "Κλείσιμο"
},
"error": {
"title": "Δεν ήταν δυνατή η σύνδεση με την πλατφόρμα προβλέψεων",
@@ -2399,7 +2440,7 @@
"add_tokens": "Εισαγωγή tokens",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Θέλετε να εισαγάγετε αυτά τα tokens;",
"tokens_detected_in_account": "{{tokenCount}} νέο {{tokensLabel}} βρέθηκε σε αυτόν τον λογαριασμό",
"token_toast": {
"tokens_imported_title": "Εισαγόμενα tokens",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Τα δεκαδικά ψηφία του token δεν μπορεί να είναι κενά.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "Δεν μπορέσαμε να βρούμε κανένα token με αυτό το όνομα.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Αναζητήστε οποιοδήποτε token και εισαγάγετέ το",
"select_token": "Επιλέξτε token",
"address_must_be_smart_contract": "Εντοπίστηκε προσωπική διεύθυνση. Εισαγάγετε τη διεύθυνση συμβολαίου του token.",
"billion_abbreviation": "Δ",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "Προσθήκη διεύθυνσης URL του RPC",
"add_block_explorer_url": "Προσθήκη διεύθυνσης URL στο block explorer",
"networks_desc": "Προσθέστε και επεξεργαστείτε προσαρμοσμένα δίκτυα RPC",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Ενεργοποιημένα δίκτυα",
+ "networks_test_networks": "Δοκιμαστικά δίκτυα",
+ "networks_additional": "Πρόσθετα δίκτυα",
"networks_search_placeholder": "Αναζήτηση δικτύων",
"networks_no_results": "Δεν βρέθηκαν δίκτυα",
"network_name_label": "Όνομα δικτύου",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "Δίκτυα RPC",
"network_add_network": "Προσθήκη δικτύου",
"add_chain_title": "Προσθήκη δικτύου",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Αναζήτηση με βάση το όνομα, το αναγνωριστικό δικτύου ή το νόμισμα",
+ "add_chain_loading": "Φόρτωση δικτύων…",
+ "add_chain_error": "Δεν ήταν δυνατή η φόρτωση των δικτύων. Παρακαλούμε προσπαθήστε ξανά.",
"add_chain_retry": "Επανάληψη",
- "add_chain_added": "Added",
+ "add_chain_added": "Προστέθηκε",
"add_chain_or": "ή",
"add_chain_custom_link": "Προσθήκη προσαρμοσμένου δικτύου",
"network_add_custom_network": "Προσθήκη προσαρμοσμένου δικτύου",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Προσθήκη δοκιμαστικού δικτύου",
"network_add": "Προσθήκη",
"network_save": "Αποθήκευση",
"remove_network_title": "Θέλετε να αφαιρέσετε αυτό το δίκτυο;",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Αποθηκεύστε την κάπου μυστικά και με ασφάλεια.",
"private_key_warning": "Αυτό είναι το ιδιωτικό κλειδί για τον τρέχοντα επιλεγμένο λογαριασμό: {{accountName}}. Μην αποκαλύψετε ποτέ αυτό το κλειδί. Όποιος έχει το ιδιωτικό σας κλειδί μπορεί να έχει τον πλήρη έλεγχο του λογαριασμού σας, όπως και τη μεταφορά των χρημάτων σας.",
- "seed_phrase_warning_explanation": [
- "Σιγουρευτείτε ότι δεν κοιτάζει κανείς την οθόνη σας.",
- "Η Υποστήριξη του MetaMask δεν θα σας το ζητήσει ποτέ."
- ],
+ "seed_phrase_warning_explanation": "Βεβαιωθείτε ότι κανείς δεν κοιτάζει την οθόνη σας. Η υποστήριξη του MetaMask δεν θα σας ζητήσει ποτέ κάτι τέτοιο.",
"private_key_warning_explanation": "Μην αποκαλύψετε ποτέ αυτό το κλειδί. Όποιος έχει το ιδιωτικό σας κλειδί μπορεί να ελέγξει πλήρως τον λογαριασμό σας, όπως και τη μεταφορά των χρημάτων σας.",
+ "reveal_srp_description": "Η Μυστική Φράση Ανάκτησής σας δίνει πλήρη πρόσβαση στο πορτοφόλι σας. Μην την αναφέρετε σε κανέναν.",
"reveal_credential_modal": [
"Το {{credentialName}} σάς παρέχει ",
"πλήρη πρόσβαση στον λογαριασμό σας και τα χρήματά σας.\n\nΜην το μοιράζεστε με κανέναν.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "Μυστική Φράση Ανάκτησής σας",
"private_key_text": "Ιδιωτικό κλειδί",
"got_it": "Κατανοητό",
- "learn_more": "Μάθετε περισσότερα"
+ "learn_more": "Μάθετε περισσότερα",
+ "copied_to_clipboard": "Αντιγραφή στο πρόχειρο"
},
"screenshot_deterrent": {
"title": "Ειδοποίηση ασφαλείας",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Προσπάθεια για επιτάχυνση;",
"speedup_tx_message": "Η υποβολή αυτής της προσπάθειας δεν εγγυάται ότι η αρχική σας συναλλαγή θα επιταχυνθεί. Εάν η προσπάθεια επιτάχυνσης είναι επιτυχής, θα χρεωθείτε με το παραπάνω τέλος συναλλαγής.",
"nevermind": "Άκυρο",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Επεξεργασία τέλους συναλλαγής",
"edit_priority": "Επεξεργασία προτεραιότητας",
"gas_cancel_fee": "Τέλος ακύρωσης συναλλαγής",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Ένα τέλος συναλλαγής σχετίζεται με αυτήν την άδεια.",
"view_details": "Προβολή λεπτομερειών",
"view_transaction_details": "Προβολή λεπτομερειών συναλλαγής",
- "view_data": "View data",
+ "view_data": "Προβολή δεδομένων",
"transaction_details": "Λεπτομέρειες συναλλαγής",
"site_url": "URL ιστότοπου",
"permission_request": "Αίτημα άδειας",
@@ -3843,7 +3887,7 @@
"right_button": "Προστατέψτε το πορτοφόλι"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Προσθήκη στα αγαπημένα",
"title_label": "Όνομα",
"url_label": "Διεύθυνση URL",
"add_button": "Προσθήκη",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Ξεκλείδωμα με Touch ID;",
"enable_faceid": "Ξεκλείδωμα με Face ID;",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "Ξεκλείδωμα με δακτυλικό αποτύπωμα;",
+ "enable_biometrics": "Ξεκλείδωμα με βιομετρικά στοιχεία;",
"enable_device_passcode_ios": "Ξεκλείδωμα με κωδικό συσκευής;",
"enable_device_passcode_android": "Ξεκλείδωμα με PIN συσκευής;"
},
@@ -4724,7 +4768,7 @@
"public_address": "Δημόσια διεύθυνση",
"public_address_qr_code": "Δημόσια διεύθυνση",
"coming_soon": "Προσεχώς...",
- "request_payment": "Request payment",
+ "request_payment": "Αίτημα πληρωμής",
"copy": "Αντιγραφή",
"scan_address": "Σαρώστε τη διεύθυνση για να λάβετε την πληρωμή",
"copy_address": "Αντιγραφή διεύθυνσης"
@@ -4738,8 +4782,8 @@
"switch_network": "Παρακαλούμε μεταβείτε στο mainnet ή το sepolia",
"card_title": "Να εμφανίζεται πάντα το κουμπί MetaMask Card",
"card_desc": "Η MetaMask Card είναι διαθέσιμη μόνο για κατοίκους επιλεγμένων χωρών.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "Χρήση του δοκιμαστικού περιβάλλοντος της DaimoPay",
+ "daimo_demo_desc": "Εναλλαγή μεταξύ των περιβαλλόντων δοκιμής και παραγωγής του DaimoPay για πληρωμές με κάρτα."
},
"walletconnect_sessions": {
"no_active_sessions": "Δεν έχετε ενεργές συνεδρίες",
@@ -4752,10 +4796,10 @@
"close_current_session": "Κλείστε την τρέχουσα συνεδρία πριν ξεκινήσετε μια νέα."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Αίτημα πληρωμής",
+ "title_complete": "Η πληρωμή ολοκληρώθηκε",
"confirm": "Πληρωμή",
- "cancel": "Decline",
+ "cancel": "Απόρριψη",
"is_requesting_you_to_pay": "σας ζητά να πληρώσετε",
"total": "Σύνολο:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Δεν υπάρχουν διαθέσιμες μέθοδοι πληρωμής.",
"error_fetching_quotes": "Κάτι πήγε στραβά. Παρακαλούμε προσπαθήστε ξανά.",
"no_quotes_available": "Δεν υπάρχουν διαθέσιμοι πάροχοι.",
+ "quote_unavailable": "Δεν υπάρχει διαθέσιμη προσφορά.",
"providers": "Πάροχοι",
+ "quotes_displayed_for": "Οι προσφορές που εμφανίζονται για το {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Χρησιμοποιήθηκε στο παρελθόν",
+ "best_rate": "Καλύτερη τιμή",
+ "most_reliable": "Πιο αξιόπιστο",
"continue": "Συνεχίστε",
"powered_by_provider": "Με την υποστήριξη του {{provider}}",
"purchased_currency": "Αγοράστηκαν {{currency}}",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Σφάλμα κατά την αποσύνδεση"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Μη διαθέσιμο",
+ "description": "Το {{token}} δεν είναι διαθέσιμο μέσω του {{provider}} στην περιοχή σας.",
+ "change_token": "Αλλαγή token",
+ "change_provider": "Αλλαγή παρόχου"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Επιλέξτε πάροχο"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Κατανοητό",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Αλλαγή παρόχου"
},
"fiat_on_ramp_aggregator": {
"buy": "αγορά",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "Κατάθεση σε {{currency}}"
},
+ "ramps_order_details": {
+ "title": "Λεπτομέρειες εντολής",
+ "status": "Κατάσταση",
+ "processing": "Επεξεργασία",
+ "complete": "Ολοκληρώθηκε",
+ "failed": "Απέτυχε",
+ "cancelled": "Ακυρώθηκε",
+ "view_on_provider": "Προβολή στο {{provider}}",
+ "order_id": "Αριθμός Εντολής",
+ "date_and_time": "Ημερομηνία και ώρα",
+ "fees": "Τέλη",
+ "total": "Σύνολο",
+ "card_processing_info": "Οι αγορές με κάρτα συνήθως διαρκούν λίγα λεπτά",
+ "processing_info_modal_description": "Οι αγορές με κάρτα ολοκληρώνονται συνήθως μέσα σε λίγα λεπτά. Μπορείτε να επικοινωνήσετε με την υποστήριξη αν έχετε απορίες.",
+ "go_to_provider_support": "Μεταβείτε στη σελίδα υποστήριξης του {{provider}}",
+ "error_title": "Κάτι πήγε στραβά",
+ "error_message": "Δεν είναι δυνατή η φόρτωση των λεπτομερειών της εντολής. Παρακαλούμε δοκιμάστε ξανά.",
+ "try_again": "Προσπαθήστε ξανά",
+ "close": "Κλείσιμο"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Επεξεργασία της αγοράς σας των {{cryptocurrency}}",
+ "purchase_pending_description": "Αναμένεται να ολοκληρωθεί σε λίγα λεπτά...",
+ "purchase_completed_title": "Η αγορά {{amount}} {{cryptocurrency}} ήταν επιτυχής!",
+ "purchase_completed_description": "Τα {{cryptocurrency}} είναι τώρα διαθέσιμα",
+ "purchase_failed_title": "Η αγορά του {{cryptocurrency}} απέτυχε",
+ "purchase_failed_description": "Παρακαλούμε δοκιμάστε ξανά σε λίγο",
+ "purchase_cancelled_title": "Η αγορά σας ακυρώθηκε",
+ "purchase_cancelled_description": "Η αγορά του {{cryptocurrency}} ακυρώθηκε",
+ "track": "Παρακολούθηση"
+ }
+ },
"swaps": {
"title": "Ανταλλαγή",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Για να μη συμβεί αυτό ξανά, φροντίστε να διατηρείτε πάντα την εφαρμογή MetaMask και το λειτουργικό σας σύστημα ενημερωμένα στην τελευταία έκδοση."
},
"srp_security_quiz": {
+ "question_step": "Ερώτηση {{step}} από {{total}}",
"title": "Ερώτηση ασφαλείας",
"introduction": "Για να αποκαλύψετε τη Μυστική Φράση Ανάκτησης, πρέπει να απαντήσετε σωστά σε δύο ερωτήσεις",
"get_started": "Ξεκινήστε",
"learn_more": "Μάθετε περισσότερα",
"try_again": "Προσπαθήστε ξανά",
"continue": "Συνεχίστε",
+ "correct": "Σωστό!",
+ "incorrect": "Λάθος!",
"of": "από",
"question_one": {
"question": "Αν χάσετε τη Μυστική Φράση Ανάκτησης, το MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "Η εγκατάσταση του {{snap}} απέτυχε."
},
"earn": {
- "claimable_bonus_tooltip": "Ένα ετήσιο μπόνους που μπορείτε να διεκδικήσετε καθημερινά από το πορτοφόλι σας.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Κερδίστε ένα μπόνους {{percentage}}%",
"claimable_bonus": "Μπόνους προς εξαργύρωση",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Εξαργύρωση του μπόνους",
+ "claim_bonus_subtitle": "Το μπόνους θα καταβληθεί στο δίκτυο {{networkName}}.",
"empty_state_cta": {
"heading": "Δανείστε {{tokenSymbol}} και κερδίστε",
"body": "Δανείστε τα {{tokenSymbol}} μέσω του {{protocol}} και κερδίστε",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Είμαστε εκτός λειτουργίας για συντήρηση. Θα επανέλθουμε σύντομα!"
},
- "supply": "Supply",
+ "supply": "Διαθέσιμη ποσότητα",
"deposit": "Κατάθεση",
"approve": "Εγκρίνετε",
"approval": "Έγκριση",
@@ -5887,7 +5978,7 @@
"network": "Δίκτυο",
"health_factor": "Δείκτης ασφάλειας",
"liquidation_risk": "Κίνδυνος ρευστοποίησης",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Ανεπαρκής ρευστότητα στη δεξαμένη (liquidity pool)",
"available_to_withdraw": "διαθέσιμα για ανάληψη",
"unknown": "άγνωστο",
"how_it_works": "Πώς λειτουργεί",
@@ -5906,7 +5997,7 @@
"lending": "Ιστορικό θέσεων",
"staking": "Ιστορικό πληρωμών"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Επαναφορά έγκρισης",
"tron": {
"fee": "Τέλη"
},
@@ -5914,32 +6005,60 @@
"ok": "Εντάξει",
"continue": "Συνεχίστε",
"convert_and_get_percentage_bonus": "Μετατρέψτε και κερδίστε {{percentage}}%",
+ "your_musd": "Τα mUSD σας",
+ "balance_breakdown_title": "Το υπόλοιπό σας σε mUSD ανά δίκτυο",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "Μετατροπή σε mUSD",
"get_a_percentage_musd_bonus": "Κερδίστε {{percentage}}% μπόνους σε mUSD",
"convert": "Μετατροπή",
+ "fetching_quote": "Λήψη προσφοράς...",
+ "you_convert": "Μετατρέπετε",
+ "network_fee": "Τέλη δικτύου",
+ "earning": "Κέρδος",
+ "quick_convert_description": "Επιλέξτε ένα token για να μετατρέψετε όλο το υπόλοιπό σας ή πατήστε το εικονίδιο επεξεργασίας για να πληκτρολογήσετε ένα προσαρμοσμένο ποσό.",
+ "no_tokens_to_convert": "Δεν έχετε tokens που μπορούν να μετατραπούν σε mUSD.",
"toasts": {
"converting": "Μετατροπή {{token}} → mUSD",
"eta": "~{{time}}",
- "delivered": "Τα mUSD σας είναι εδώ!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "Η μετατροπή σε mUSD απέτυχε"
},
"education": {
"heading": "ΚΕΡΔΙΣΤΕ {{percentage}}% ΣΤΑ\nSTABLECOINS",
- "description": "Μετατρέψτε τα stablecoins σε mUSD, το stablecoin του MetaMask που υποστηρίζεται από το αμερικανικό δολάριο, και λάβετε μπόνους έως και {{percentage}}%.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Ισχύουν όροι.",
"primary_button": "Ξεκινήστε",
"secondary_button": "Όχι τώρα"
},
"buy_musd": "Αγοράστε mUSD",
"get_musd": "Αποκτήστε mUSD",
- "bonus_title": "Λάβετε {{percentage}}% σε stablecoins",
- "bonus_description": "Μετατρέψτε τα stablecoins σε mUSD και λάβετε μπόνους έως και {{percentage}}%.",
- "powered_by_relay": "Υποστηρίζεται από το Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Υποστηρίζεται από το Relay",
+ "max": "Μεγ",
+ "quick_convert_button": "Μετατροπή",
+ "learn_more": "Μάθετε περισσότερα",
+ "tooltip_title": "Κερδίστε απόδοση με τα mUSD",
+ "tooltip_content": "Μετατρέψτε USDC, USDT ή DAI σε mUSD, το stablecoin του MetaMask που υποστηρίζεται από το δολάριο. Κερδίστε απόδοση {{apy}} για κάθε δολάριο που διατηρείτε.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Η μετατροπή απέτυχε. Προσπαθήστε ξανά.",
+ "confirmation": {
+ "title": "Μετατροπή μέγιστου ποσού"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Χρέωση"
},
"bonus_claim": {
"toasts": {
"claiming": "Γίνεται επεξεργασία του μπόνους σας σε mUSD",
"delivered": "Το μπόνους σας σε mUSD είναι εδώ!",
- "failed": "Bonus claim failed"
+ "failed": "Αποτυχία εξαργύρωσης του μπόνους"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Οι πόντοι θα προστεθούν αυτόματα στον λογαριασμό σας.",
"tooltip_not_opted_in_footer": "Εγγραφείτε στις ανταμοιβές για να λάβετε τους πόντους σας.",
"tooltip_close": "Κλείσιμο"
- }
+ },
+ "your_stablecoins": "Τα stablecoins σας"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Τέλος συναλλαγής",
"metamask_fee": "Χρεώσεις στο MetaMask",
"network_fee": "Τέλη δικτύου",
- "bridge_fee": "Τέλος παρόχου για την μεταφορά"
+ "bridge_fee": "Τέλος παρόχου για την μεταφορά",
+ "provider_fee": "Τέλη παρόχου"
},
"title": {
"signature": "Αίτημα υπογραφής",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Θα ανταλλάξουμε τα tokens σας με USDC.e στο δίκτυο Polygon, που χρησιμοποιείται για τις Προβλέψεις. Οι πάροχοι ανταλλαγών ενδέχεται να χρεώσουν προμήθεια, αλλά το MetaMask δεν χρεώνει."
},
"predict_withdraw": {
- "transaction_fee": "Το MetaMask θα κάνει την αλλαγή στο επιθυμητό συμβολικό token για εσάς. Δεν ισχύει χρέωση MetaMask όταν κάνετε αλλαγή σε MUSD."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "Τα τέλη μετατροπής σε mUSD περιλαμβάνουν τις χρεώσεις δικτύου και ενδέχεται να περιλαμβάνουν χρεώσεις παρόχου."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Τέλη"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Αντίκτυπος στην τιμή",
"price_impact_info_description": "Η επίδραση στη τιμή (price impact) δείχνει πόσο επηρεάζει η εντολή ανταλλαγής σας την τιμή του περιουσιακού στοιχείου στην αγορά. Εξαρτάται από το μέγεθος της συναλλαγής και τη διαθέσιμη ρευστότητα στη δεξαμενή (pool). Το MetaMask δεν επηρεάζει ούτε ελέγχει την επίδραση στη τιμή.",
"price_impact_info_gasless_description": "Η επίδραση στην τιμή αντικατοπτρίζει το πώς η εντολή ανταλλαγής σας επηρεάζει την τιμή αγοράς του περιουσιακού στοιχείου. Αν δεν διαθέτετε αρκετά κεφάλαια για τα τέλη συναλλαγής, μέρος του αρχικού σας token κατανέμεται αυτόματα για την κάλυψη των χρεώσεων, γεγονός που αυξάνει την επίδραση στην τιμή. Το MetaMask δεν επηρεάζει ούτε ελέγχει την επίδραση στην τιμή.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Άκυρο",
"slippage_info_title": "Απόκλιση",
"slippage_info_description": "Το ποσοστό μεταβολής στην τιμή που είστε διατεθειμένοι να αποδεχθείτε πριν ακυρωθεί η συναλλαγή.",
"blockaid_error_title": "Αυτή η συναλλαγή θα ακυρωθεί",
@@ -6470,13 +6596,14 @@
},
"submit": "Υποβολή",
"default_slippage_description": "Η συναλλαγή σας δεν θα ολοκληρωθεί εάν η τιμή αλλάξει περισσότερο από το ποσοστό απόκλισης.",
- "cancel": "Άκυρο",
"confirm": "Επιβεβαίωση",
"exceeding_upper_slippage_warning": "Υψηλή απόκλιση— αυτό μπορεί να οδηγήσει σε μη ευνοϊκή ανταλλαγή",
"exceeding_lower_slippage_warning": "Χαμηλή απόκλιση— αυτό μπορεί να οδηγήσει σε μη ευνοϊκή ανταλλαγή",
"exceeding_lower_slippage_error": "Εισαγάγετε μια τιμή μεγαλύτερη από {{value}}%",
"exceeding_upper_slippage_error": "Δεν μπορείτε να εισαγάγετε τιμή μεγαλύτερη από {{value}}%",
- "custom": "Προσαρμογή"
+ "custom": "Προσαρμογή",
+ "invalid_recipient_address": "Μη έγκυρη διεύθυνση",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Υπάρχουν διαθέσιμες νέες προσφορές",
@@ -6773,12 +6900,16 @@
"title": "Πληκτρολογήστε τον κωδικό πρόσβασης",
"description": "Πληκτρολογήστε τον κωδικό του πορτοφολιού σας για να δείτε τα στοιχεία της κάρτας.",
"description_unfreeze": "Εισαγάγετε τον κωδικό πρόσβασης του πορτοφολιού σας για να συνεχίσετε τις δαπάνες με την κάρτα σας.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Κωδικός πρόσβασης",
"confirm": "Επιβεβαίωση",
"cancel": "Ακύρωση",
"error_empty": "Πληκτρολογήστε τον κωδικό σας",
"error_incorrect": "Λανθασμένος κωδικός. Παρακαλούμε δοκιμάστε ξανά."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Επιλέξτε την κάρτα σας",
"upgrade_title": "Αναβάθμιση σε Metal",
@@ -7094,6 +7225,9 @@
"view_card_details": "Προβολή στοιχείων κάρτας",
"hide_card_details": "Απόκρυψη στοιχείων κάρτας",
"view_card_details_description": "Αριθμός κάρτας, ημερομηνία λήξης και CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Διαχείριση ορίου",
"manage_spending_limit_description_restricted": "Ο περιορισμός συναλλαγών είναι ενεργός",
"manage_spending_limit_description_full": "Η πλήρης πρόσβαση είναι ενεργή",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Όροι και προϋποθέσεις",
"order_metal_card": "Metal Card",
"order_metal_card_description": "Παραγγείλετε τώρα τη φυσική Metal Card σας",
+ "cashback": "Επιστροφή χρημάτων",
+ "cashback_description": "Κερδίστε 1% επιστροφή σε κάθε συναλλαγή",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Πάγωμα κάρτας",
"unfreeze_card": "Ξεπάγωμα κάρτας",
"freeze_card_description": "Παύση όλων των δαπανών στην κάρτα σας",
@@ -7141,6 +7278,19 @@
"retry": "Προσπαθήστε ξανά",
"on_linea": "στο δίκτυο Linea"
},
+ "cashback_screen": {
+ "title": "Επιστροφή χρημάτων",
+ "available_cashback": "Διαθέσιμη επιστροφή χρημάτων",
+ "network_fee": "Τέλη δικτύου",
+ "expected_to_receive": "Αναμένεται να λάβετε",
+ "withdraw": "Ανάληψη",
+ "withdraw_unavailable": "Η ανάληψη δεν είναι διαθέσιμη",
+ "withdrawal_initiated": "Η διαδικασία ανάληψης ξεκίνησε",
+ "withdrawal_success": "Η ανάληψη ολοκληρώθηκε με επιτυχία",
+ "withdrawal_failed": "Η ανάληψη απέτυχε. Προσπαθήστε ξανά.",
+ "no_cashback": "Δεν υπάρχει διαθέσιμη επιστροφή χρημάτων",
+ "loading_error": "Αποτυχία φόρτωσης της επιστροφής χρημάτων. Προσπαθήστε ξανά."
+ },
"change_asset": {
"title": "Αλλαγή token και δικτύου",
"full_spending_access": "Πλήρης πρόσβαση για δαπάνες",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Επιλέξτε μέθοδο πληρωμής",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Επιλέξτε το token που θα λάβετε",
+ "no_gas": "Δεν υπάρχει διαθέσιμο υπόλοιπο για τα τέλη συναλλαγών"
},
"connection_removed_modal": {
"title": "Οι συνδέσεις αφαιρέθηκαν",
@@ -7315,7 +7465,10 @@
"service_not_available": "Η υπηρεσία δεν είναι διαθέσιμη αυτήν τη στιγμή. Παρακαλούμε προσπαθήστε ξανά σε λίγο.",
"invalid_referral_code": "Μη έγκυρος κωδικός παραπομπής. Ελέγξτε τον και προσπαθήστε ξανά.",
"already_referred": "Έχετε ήδη παραπεμφθεί από άλλον χρήστη.",
- "cannot_use_own_referral_code": "Δεν μπορείτε να χρησιμοποιήσετε τον δικό σας κωδικό παραπομπής."
+ "cannot_use_own_referral_code": "Δεν μπορείτε να χρησιμοποιήσετε τον δικό σας κωδικό παραπομπής.",
+ "invalid_bonus_code": "Μη έγκυρος κωδικός μπόνους",
+ "already_redeemed": "Έχετε ήδη εξαργυρώσει αυτόν τον κωδικό μπόνους",
+ "reached_maximum": "Αυτός ο κωδικός μπόνους έχει φτάσει το μέγιστο όριο χρήσεων"
},
"claim_reward_error": {
"title": "Η ενεργοποίηση της ανταμοιβής απέτυχε"
@@ -7372,8 +7525,10 @@
"predict": "Πρόβλεψη",
"musd_deposit": "Κατάθεση σε mUSD",
"apply_referral_bonus": "Μπόνους κωδικού παραπομπής",
+ "bonus_code": "Κωδικός μπόνους",
"uncategorized_event": "Μη κατηγοριοποιημένο συμβάν"
},
+ "code": "Κωδικός",
"date": "Ημερομηνία",
"account": "Λογαριασμός",
"bonus": "Μπόνους",
@@ -7473,7 +7628,16 @@
"show_less": "Εμφάνιση λιγότερων",
"linking_progress": "Προσθήκη λογαριασμών… ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} εγγεγραμμένοι",
- "add_all_accounts": "Προσθήκη όλων των λογαριασμών"
+ "add_all_accounts": "Προσθήκη όλων των λογαριασμών",
+ "environment_selector": "Περιβάλλον",
+ "environment_cancel": "Άκυρο",
+ "environment_default": "Προεπιλογή",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Κωδικός παραπομπής",
@@ -7483,6 +7647,10 @@
"invalid_code": "Μη έγκυρος κωδικός παραπομπής",
"apply_button": "Εφαρμογή κωδικού παραπομπής"
},
+ "bonus_code": {
+ "input_placeholder": "Πληκτρολογήστε τον κωδικό μπόνους",
+ "apply_success": "Ο κωδικός μπόνους εφαρμόστηκε!"
+ },
"optout": {
"title": "Διαγραφή προόδου Ανταμοιβών",
"description": "Αυτή η ενέργεια θα αφαιρέσει τους λογαριασμούς σας από το πρόγραμμα Ανταμοιβών και θα διαγράψει τους πόντους και την πρόοδό σας. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Τέλη διασύνδεσης",
+ "provider_fee": "Τέλη παρόχου",
"network_fee": "Τέλη δικτύου",
"paid_with": "Πληρώθηκε με",
+ "receive_token": "Λήψη token",
"retry_button": "Προσπαθήστε ξανά",
"total": "Σύνολο",
"account": "Λογαριασμός",
- "received_total": "Received total"
+ "received_total": "Σύνολο που ελήφθη"
},
"summary_title": {
"bridge_approval": "Έγκριση {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Δεν βρέθηκε σύνδεση",
"description": "Παρακαλούμε δημιουργήστε μια νέα σύνδεση από την εφαρμογή για να συνεχίσετε."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Ακόμη γίνεται σύνδεση με το {{networkName}}...",
"unable_to_connect_network": "Δεν είναι δυνατή η σύνδεση με το {{networkName}}.",
"update_rpc": "Ενημέρωση RPC",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Μετάβαση στο προεπιλεγμένο RPC του MetaMask",
"check_network_connectivity": "Ελέγξτε τη σύνδεση δικτύου σας.",
"check_network_connectivity_or": "Ελέγξτε τη σύνδεση δικτύου σας ή",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Ενημερώθηκε στο προεπιλεγμένο MetaMask"
},
"trending": {
"title": "Εξερεύνηση",
"trending_tokens": "Δημοφιλή token",
+ "stocks": "Μετοχές",
"price_change": "Αλλαγή τιμής",
"all_networks": "Όλα τα δίκτυα",
"24h": "24ω",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Επαναφόρτωση",
"primary_action_acknowledge": "Κατανοητό"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "Πορτοφόλι υλικού με QR",
+ "hardware_wallet": "Πορτοφόλι υλικού"
+ },
+ "common": {
+ "cancel": "Άκυρο",
+ "continue": "Συνεχίστε"
+ },
+ "connecting": {
+ "title": "Συνδέστε τη συσκευή σας {{device}}",
+ "searching": "Αναζήτηση για το {{device}}...",
+ "tips_header": "Για να συνεχίσετε, βεβαιωθείτε ότι:",
+ "tip_unlock": "Η συσκευή σας {{device}} είναι ξεκλείδωτη",
+ "tip_open_app": "Η εφαρμογή Ethereum είναι ανοιχτή",
+ "tip_enable_bluetooth": "Το Bluetooth είναι ενεργοποιημένο",
+ "tip_dnd_off": "Η λειτουργία Μην Ενοχλείτε είναι απενεργοποιημένη",
+ "tip_bluetooth_permission": "Η άδεια τοποθεσίας και Bluetooth έχει δοθεί",
+ "tip_bluetooth_permission_v12": "Η άδεια για κοντινές συσκευές έχει δοθεί",
+ "tip_stay_close": "Η συσκευή σας πρέπει να βρίσκεται κοντά στο τηλέφωνό σας"
+ },
+ "awaiting_app": {
+ "title": "Ανοίξτε την εφαρμογή {{app}}",
+ "message": "Ανοίξτε την εφαρμογή {{app}} στη συσκευή {{device}}",
+ "current_app": "Ενεργή εφαρμογή: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Επιβεβαιώστε στη συσκευή {{device}}",
+ "title_message": "Είσοδος στη συσκευή {{device}}",
+ "message": "Ελέγξτε και επιβεβαιώστε στη συσκευή {{device}}"
+ },
+ "success": {
+ "title": "Το {{device}} συνδέθηκε"
+ },
+ "errors": {
+ "device_locked": "Ξεκλειδώστε την και προσπαθήστε ξανά για να συνεχίσετε",
+ "app_not_open": "Ανοίξτε την εφαρμογή Ethereum στη συσκευή σας",
+ "device_disconnected": "Η συσκευή {{device}} αποσυνδέθηκε. Παρακαλώ συνδέστε την ξανά και προσπαθήστε πάλι",
+ "device_not_found": "Δεν ήταν δυνατή η εύρεση της συσκευής {{device}}. Βεβαιωθείτε ότι είναι συνδεδεμένη",
+ "device_not_ready": "Η συσκευή σας δεν είναι έτοιμη. Ελέγξτε την και δοκιμάστε ξανά",
+ "blind_signing": "Η τυφλή υπογραφή είναι απενεργοποιημένη. Ενεργοποιήστε την από τις ρυθμίσεις της συσκευής σας",
+ "connection_closed": "Η σύνδεση χάθηκε. Προσπαθήστε ξανά",
+ "connection_timeout": "Η σύνδεση έληξε. Προσπαθήστε ξανά",
+ "user_cancelled": "Η ενέργεια ακυρώθηκε στη συσκευή σας",
+ "pending_confirmation": "Υπάρχει μια εκκρεμής ενέργεια στη συσκευή σας. Παρακαλούμε ολοκληρώστε την ή ακυρώστε την",
+ "bluetooth_permission_denied": "Απαιτείται άδεια πρόσβασης στο Bluetooth για να συνδεθεί η συσκευή σας",
+ "location_permission_denied": "Απαιτείται άδεια τοποθεσίας για την ανίχνευση συσκευών",
+ "nearby_permission_denied": "Απαιτείται άδεια πρόσβασης σε κοντινές συσκευές",
+ "bluetooth_off": "Ενεργοποιήστε το Bluetooth για να συνδεθεί με τη συσκευή σας",
+ "bluetooth_scan_failed": "Δεν ήταν δυνατή η σάρωση συσκευών. Προσπαθήστε ξανά",
+ "bluetooth_connection_failed": "Ενεργοποιήστε το Bluetooth στη συσκευή σας για να συνεχίσετε",
+ "not_supported": "Αυτή η λειτουργία δεν υποστηρίζεται",
+ "unknown_error": "Βεβαιωθείτε ότι το {{device}} σας έχει ρυθμιστεί με τη Μυστική Φράση Ανάκτησης ή τη φράση πρόσβασης για αυτόν τον λογαριασμό"
+ },
+ "error": {
+ "title": "Κάτι πήγε στραβά",
+ "default_title": "Προέκυψε ένα σφάλμα",
+ "continue": "Συνεχίστε",
+ "retry": "Επανάληψη",
+ "view_settings": "Προβολή ρυθμίσεων",
+ "device_locked_title": "Το {{device}} είναι κλειδωμένο",
+ "device_disconnected_title": "Το {{device}} αποσυνδέθηκε",
+ "device_not_found_title": "Το {{device}} δεν βρέθηκε",
+ "app_not_open": "Η εφαρμογή Ethereum δεν άνοιξε",
+ "blind_signing_disabled": "Η τυφλή υπογραφή είναι απενεργοποιημένη",
+ "connection_timeout": "Η συσκευή δεν ανταποκρίνεται",
+ "connection_closed": "Η σύνδεση χάθηκε",
+ "user_cancelled": "Η ενέργεια ακυρώθηκε",
+ "pending_confirmation": "Σε αναμονή επιβεβαίωσης",
+ "bluetooth_required": "Απαιτείται το Bluetooth",
+ "bluetooth_permission_denied": "Απαιτείται άδεια πρόσβασης για το Bluetooth",
+ "location_permission_denied": "Απαιτείται άδεια τοποθεσίας",
+ "nearby_devices_permission_denied": "Απαιτείται άδεια πρόσβασης για κοντινές συσκευές",
+ "scan_failed": "Η σάρωση απέτυχε",
+ "something_went_wrong": "Κάτι πήγε στραβά"
+ },
+ "device_selection": {
+ "title": "Επιλέξτε το {{device}}",
+ "scanning": "Σάρωση για συσκευές...",
+ "no_devices_found": "Δεν βρέθηκαν συσκευές",
+ "no_devices_hint": "Βεβαιωθείτε ότι το {{device}} είναι ξεκλείδωτο και ότι το Bluetooth είναι ενεργοποιημένο",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Σύνδεση",
+ "rescan": "Σάρωση ξανά",
+ "unknown_device": "Άγνωστη συσκευή",
+ "signal_strength": "Σήμα: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Token",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT",
"import_nfts": "Εισαγωγή NFT",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Προσθέστε εύκολα τα συλλεκτικά σας αντικείμενα",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Δεν έχουν οριστεί ΛΚ/ΠΖ"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "Δεν είναι δυνατή η φόρτωση του {{section}}",
+ "retry": "Επανάληψη"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/es.json b/locales/languages/es.json
index 5518f346eaf..98a8601b1cf 100644
--- a/locales/languages/es.json
+++ b/locales/languages/es.json
@@ -103,6 +103,10 @@
"message": "No hay suficientes {{ticker}} para cubrir las tarifas. Usa un token en otra red o añade más {{ticker}} para continuar.",
"title": "Fondos insuficientes"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Esta ruta de pago no está disponible en este momento. Intenta cambiar el monto, la red o el token y encontraremos la mejor opción.",
"title": "Sin cotizaciones"
@@ -1180,6 +1184,7 @@
"title": "Nueva orden",
"leverage": "Apalancamiento",
"limit_price": "Precio límite",
+ "market_price": "Precio de mercado",
"enter_price": "Ingresar precio",
"trigger_price": "Precio de activación",
"liquidation_price": "Precio de liquidación",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Los fondos están disponibles para operar",
"close_order_still_active": "Cerrar orden aún activa",
"order_submitted": "Orden enviada",
+ "submitting_your_trade": "Enviando tu operación",
"order_filled": "Orden ejecutada",
"order_placed": "Orden colocada",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "Detalles de la orden",
"cancel_order": "Cancelar orden",
"date": "Fecha",
+ "trigger_condition": "Condición de activación",
+ "price": "Precio",
"fee": "Tarifa",
"limit_buy": "Límite de posición larga",
"limit_price": "Precio límite",
"limit_sell": "Límite de posición corta",
"market_buy": "Posición larga de mercado",
"market_sell": "Posición corta de mercado",
+ "market": "Mercado",
"open": "Abierto",
"size": "Tamaño",
+ "original_size": "Tamaño original",
+ "order_value": "Valor de la orden",
+ "reduce_only": "Reducir solamente",
+ "yes": "Sí",
+ "no": "No",
+ "price_above": "Precio por encima de {{price}}",
+ "price_below": "Precio por debajo de {{price}}",
+ "take_profit": "Toma de ganancias",
+ "stop_loss": "Límite de pérdidas",
"status": "Estado",
"view_explorer": "Ver en Explorer"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Perspectivas del mercado",
- "updated_ago": "Actualizado {{time}}",
- "disclaimer": "Perspectivas de IA. No es asesoramiento financiero.",
- "whats_driving_price": "¿Qué determina el precio?",
- "what_people_saying": "Qué dice la gente",
+ "a_closer_look": "Un vistazo más de cerca",
+ "whats_being_said": "Qué se dice",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Operar",
"sources_count": "+{{count}} fuentes",
- "sources_title": "De liquiDez"
+ "sources_title": "News sources",
+ "feedback_submitted": "Comentarios enviados",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Comentarios",
+ "description": "Ayuda a mejorar nuestra información de mercado generada por IA.",
+ "not_relevant": "No es relevante",
+ "not_accurate": "No es precisa",
+ "hard_to_understand": "Difícil de entender",
+ "harmful_or_offensive": "Perjudicial u ofensiva",
+ "something_else": "Otra cosa",
+ "additional_feedback_label": "Comentarios adicionales (opcional)",
+ "additional_feedback_placeholder": "¿Cómo podemos mejorar?",
+ "characters_remaining": "{{count}} caracteres restantes",
+ "submit": "Enviar"
+ }
},
"predict": {
"title": "Predicciones de MetaMask",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Disponible en aproximadamente 1 minuto",
"withdraw_completed": "Retiro finalizado",
"withdraw_completed_subtitle": "Se han transferido {{amount}} USDC a tu billetera",
+ "withdraw_any_token_completed_subtitle": "Se transfirieron {{amount}} {{token}} a tu billetera",
"error_title": "Algo salió mal",
"error_description": "No se pudo proceder con el retiro",
"try_again": "Inténtalo de nuevo"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Ten en cuenta que este valor es una estimación y se finalizará una vez que se complete la transacción. Los puntos pueden tardar hasta 1 hora en confirmarse en tu saldo de Recompensas.",
"points_error": "No podemos cargar puntos en este momento",
"points_error_content": "Aún ganarás puntos por esta transacción. Te notificaremos una vez que se hayan agregado a tu cuenta. También puedes consultar la pestaña de recompensas en aproximadamente una hora.",
- "slippage": "Deslizamiento"
+ "slippage": "Deslizamiento",
+ "price_details": "Detalles del precio",
+ "prediction_order": "Orden de predicción",
+ "prediction_order_description": "~{{count}} contratos a {{price}} cada uno. El monto final puede variar en función de la disponibilidad del libro de órdenes (hasta un {{slippage}} %).",
+ "metamask_fee_description": "Tarifa de servicio por procesar esta predicción",
+ "exchange_fee": "Tarifa de cambio",
+ "exchange_fee_description": "Tarifa pagada al cambio o al mercado",
+ "total_incl_fees": "tarifas incl.",
+ "close": "Cerrar"
},
"error": {
"title": "No se puede conectar a las predicciones",
@@ -2399,7 +2440,7 @@
"add_tokens": "Agregar activo",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "¿Deseas importar estos tokens?",
"tokens_detected_in_account": "{{tokenCount}} nuevos {{tokensLabel}} se han encontrado en esta cuenta",
"token_toast": {
"tokens_imported_title": "Tokens importados",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Los decimales del token no pueden estar vacíos.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "No se encontró ningún token con ese nombre.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Busca cualquier token e impórtalo",
"select_token": "Selecciona un token",
"address_must_be_smart_contract": "Se detectó una dirección personal. Escriba la dirección de contrato del token.",
"billion_abbreviation": "B",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "Agregar URL de RPC",
"add_block_explorer_url": "Añadir URL del explorador de bloques",
"networks_desc": "Agregar y editar redes RPC personalizadas",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Redes habilitadas",
+ "networks_test_networks": "Redes de prueba",
+ "networks_additional": "Redes adicionales",
"networks_search_placeholder": "Buscar redes",
"networks_no_results": "No se encontraron redes",
"network_name_label": "Nombre de la red",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "Redes RPC",
"network_add_network": "Añadir red",
"add_chain_title": "Agregar una red",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Buscar por nombre, ID de cadena o moneda",
+ "add_chain_loading": "Cargando redes…",
+ "add_chain_error": "Error al cargar las redes. Inténtalo de nuevo.",
"add_chain_retry": "Reintentar",
- "add_chain_added": "Added",
+ "add_chain_added": "Agregada",
"add_chain_or": "o",
"add_chain_custom_link": "Agregar una red personalizada",
"network_add_custom_network": "Agregar una red personalizada",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Agregar una red de prueba",
"network_add": "Agregar",
"network_save": "Guardar",
"remove_network_title": "¿Quiere quitar esta red?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Guárdela en un lugar seguro y secreto.",
"private_key_warning": "Esta es la clave privada para la cuenta seleccionada actual: {{accountName}}. No revele esta clave. Cualquier persona que tenga su clave privada podrá controlar completamente su cuenta, incluida la transferencia de sus fondos.",
- "seed_phrase_warning_explanation": [
- "Asegúrese de que nadie esté viendo su pantalla.",
- "El soporte técnico de MetaMask nunca se lo solicitará."
- ],
+ "seed_phrase_warning_explanation": "Asegúrate de que nadie esté mirando tu pantalla. El soporte de MetaMask nunca te pedirá esto.",
"private_key_warning_explanation": "No revele esta clave. Cualquier persona que tenga su clave privada podrá controlar completamente su cuenta, incluida la transferencia de sus fondos.",
+ "reveal_srp_description": "Tu frase secreta de recuperación da acceso completo a tu billetera. No la compartas con nadie.",
"reveal_credential_modal": [
"Su {{credentialName}} le proporciona ",
"acceso completo a su monedero y fondos. \n\nNo la comparta con nadie.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "Frase secreta de recuperación",
"private_key_text": "Clave privada",
"got_it": "Entendido",
- "learn_more": "Más información"
+ "learn_more": "Más información",
+ "copied_to_clipboard": "Copiado al portapapeles"
},
"screenshot_deterrent": {
"title": "Alerta de seguridad",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "¿Intentar acelerar?",
"speedup_tx_message": "Enviar este intento no garantiza que se acelerará la transacción original. Si el intento de aceleración se completa correctamente, se le cobrará la tarifa de transacción anterior.",
"nevermind": "No importa",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Editar tarifa de gas",
"edit_priority": "Editar prioridad",
"gas_cancel_fee": "Tarifa de cancelación de gas",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Este permiso tiene asociada una tarifa de transacción.",
"view_details": "Ver detalles",
"view_transaction_details": "Ver detalles de la transacción",
- "view_data": "View data",
+ "view_data": "Ver datos",
"transaction_details": "Detalles de la transacción",
"site_url": "Dirección URL del sitio",
"permission_request": "Solicitud de permiso",
@@ -3843,7 +3887,7 @@
"right_button": "Proteger monedero"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Agregar a favoritos",
"title_label": "Nombre",
"url_label": "URL",
"add_button": "Agregar",
@@ -4724,7 +4768,7 @@
"public_address": "Dirección pública",
"public_address_qr_code": "Dirección pública",
"coming_soon": "Próximamente…",
- "request_payment": "Request payment",
+ "request_payment": "Solicitar pago",
"copy": "Copiar",
"scan_address": "Escanear dirección para recibir pago",
"copy_address": "Copiar dirección"
@@ -4738,8 +4782,8 @@
"switch_network": "Cambie a la red principal o a Sepolia",
"card_title": "Mostrar siempre el botón de la tarjeta MetaMask",
"card_desc": "La tarjeta MetaMask solo está disponible para residentes de países seleccionados.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "Usar el entorno de demostración de DaimoPay",
+ "daimo_demo_desc": "Alterna entre los entornos de demostración y producción de DaimoPay para pagos con tarjeta."
},
"walletconnect_sessions": {
"no_active_sessions": "No tiene sesiones activas",
@@ -4752,10 +4796,10 @@
"close_current_session": "Cierre la sesión actual antes de iniciar una nueva."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Solicitud de pago",
+ "title_complete": "Pago finalizado",
"confirm": "Pagar",
- "cancel": "Decline",
+ "cancel": "Rechazar",
"is_requesting_you_to_pay": "le solicita que pague",
"total": "Total:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "No hay métodos de pago disponibles.",
"error_fetching_quotes": "Se produjo un error. Inténtalo de nuevo.",
"no_quotes_available": "No hay proveedores disponibles.",
+ "quote_unavailable": "Cotización no disponible.",
"providers": "Proveedores",
+ "quotes_displayed_for": "Cotizaciones mostradas para {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Usado anteriormente",
+ "best_rate": "Mejor tarifa",
+ "most_reliable": "Más confiable",
"continue": "Continuar",
"powered_by_provider": "Desarrollado por {{provider}}",
"purchased_currency": "{{currency}} comprado",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Error al cerrar sesión"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "No disponible",
+ "description": "{{token}} no está disponible con {{provider}} en tu región.",
+ "change_token": "Cambiar token",
+ "change_provider": "Cambiar de proveedor"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Elegir un proveedor"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Entendido",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Cambiar de proveedor"
},
"fiat_on_ramp_aggregator": {
"buy": "comprar",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "Depósito de {{currency}}"
},
+ "ramps_order_details": {
+ "title": "Detalles de la orden",
+ "status": "Estado",
+ "processing": "Procesando",
+ "complete": "Completado",
+ "failed": "Fallido",
+ "cancelled": "Cancelado",
+ "view_on_provider": "Ver en {{provider}}",
+ "order_id": "Id. de orden",
+ "date_and_time": "Fecha y hora",
+ "fees": "Tarifas",
+ "total": "Total",
+ "card_processing_info": "Las compras con tarjeta suelen tardar unos minutos",
+ "processing_info_modal_description": "Las compras con tarjeta suelen tardar unos minutos. Si tienes alguna pregunta, puedes comunicarte con el equipo de soporte.",
+ "go_to_provider_support": "Ir a la página de soporte de {{provider}}",
+ "error_title": "Algo salió mal",
+ "error_message": "No se pueden cargar los detalles de la orden. Inténtalo de nuevo.",
+ "try_again": "Inténtalo de nuevo",
+ "close": "Cerrar"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Procesando su compra de {{cryptocurrency}}",
+ "purchase_pending_description": "Esto solo tardará unos minutos...",
+ "purchase_completed_title": "Tu compra de {{amount}} {{cryptocurrency}} se realizó correctamente!",
+ "purchase_completed_description": "Su {{cryptocurrency}} ahora está disponible",
+ "purchase_failed_title": "Compra de {{cryptocurrency}} fallida",
+ "purchase_failed_description": "Inténtalo de nuevo en unos momentos",
+ "purchase_cancelled_title": "Su compra fue cancelada",
+ "purchase_cancelled_description": "Se canceló tu compra de {{cryptocurrency}}",
+ "track": "Hacer seguimiento"
+ }
+ },
"swaps": {
"title": "Canjear",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Para evitar que esto vuelva a ocurrir, asegúrese de mantener siempre actualizados a la última versión la aplicación de MetaMask y el sistema operativo."
},
"srp_security_quiz": {
+ "question_step": "Pregunta {{step}} de {{total}}",
"title": "Cuestionario de seguridad",
"introduction": "Para revelar tu frase secreta de recuperación, debes responder correctamente dos preguntas",
"get_started": "Comenzar",
"learn_more": "Conozca más",
"try_again": "Inténtalo de nuevo",
"continue": "Continuar",
+ "correct": "¡Correcto!",
+ "incorrect": "¡Incorrecto!",
"of": "de",
"question_one": {
"question": "Si extravía su frase secreta de recuperación, MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "Instalación fallida de {{snap}}."
},
"earn": {
- "claimable_bonus_tooltip": "Un bono anual que podrás reclamar diariamente desde tu billetera.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Gana un bono del {{percentage}} %",
"claimable_bonus": "Bonificación reclamable",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Reclamar bono",
+ "claim_bonus_subtitle": "El bono se pagará en {{networkName}}.",
"empty_state_cta": {
"heading": "Presta {{tokenSymbol}} y gana",
"body": "Presta tu {{tokenSymbol}} con {{protocol}} y gana",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Estamos en mantenimiento. ¡Pronto volveremos a estar en línea!"
},
- "supply": "Supply",
+ "supply": "Suministro",
"deposit": "Depósito",
"approve": "Aprobar",
"approval": "Aprobación",
@@ -5887,7 +5978,7 @@
"network": "Red",
"health_factor": "Factor de salud",
"liquidation_risk": "Riesgo de liquidación",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Liquidez insuficiente del fondo",
"available_to_withdraw": "disponible para retirar",
"unknown": "desconocida",
"how_it_works": "Cómo funciona",
@@ -5906,7 +5997,7 @@
"lending": "Historial de posiciones",
"staking": "Historial de pagos"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Restablecimiento de asignación",
"tron": {
"fee": "Tarifa"
},
@@ -5914,32 +6005,60 @@
"ok": "OK",
"continue": "Continuar",
"convert_and_get_percentage_bonus": "Convierte y obtén un {{percentage}} %",
+ "your_musd": "Tus mUSD",
+ "balance_breakdown_title": "Tu saldo de mUSD por red",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "Convertir a mUSD",
"get_a_percentage_musd_bonus": "Obtén un bono del {{percentage}} % en mUSD",
"convert": "Convertir",
+ "fetching_quote": "Obteniendo cotización...",
+ "you_convert": "Conviertes",
+ "network_fee": "Tarifa de red",
+ "earning": "Ganancia",
+ "quick_convert_description": "Selecciona un token para convertir tu saldo completo o toca el ícono de edición para ingresar un monto personalizado.",
+ "no_tokens_to_convert": "No tienes ningún token que se pueda convertir a mUSD.",
"toasts": {
"converting": "Convirtiendo {{token}} → mUSD",
"eta": "~{{time}}",
- "delivered": "¡Tus mUSD ya están aquí!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "Conversión de mUSD fallida"
},
"education": {
"heading": "OBTÉN {{percentage}} % EN\nMONEDAS ESTABLES",
- "description": "Convierte tus monedas estables a mUSD, la moneda estable de MetaMask respaldada por el dólar estadounidense, y recibe un bono de hasta el {{percentage}} %.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Se aplican términos y condiciones.",
"primary_button": "Comenzar",
"secondary_button": "Ahora no"
},
"buy_musd": "Comprar mUSD",
"get_musd": "Obtener USDC",
- "bonus_title": "Obtén un {{percentage}} % en tus monedas estables",
- "bonus_description": "Convierte tus monedas estables a mUSD y recibe un bono de hasta el {{percentage}} %.",
- "powered_by_relay": "Desarrollado por Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Desarrollado por Relay",
+ "max": "Máx.",
+ "quick_convert_button": "Convertir",
+ "learn_more": "Conozca más",
+ "tooltip_title": "Gana rendimiento con mUSD",
+ "tooltip_content": "Convierte tus USDC, USDT o DAI en mUSD, la moneda estable de MetaMask respaldada por dólares. Gana un rendimiento del {{apy}} por cada dólar que mantengas.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Conversión fallida. Inténtalo de nuevo.",
+ "confirmation": {
+ "title": "Convertir máx."
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Tasa"
},
"bonus_claim": {
"toasts": {
"claiming": "Tu bono en mUSD está en proceso",
"delivered": "¡Tu bono en mUSD ya está disponible!",
- "failed": "Bonus claim failed"
+ "failed": "Reclamación de bono fallida"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Los puntos se agregarán automáticamente a tu cuenta.",
"tooltip_not_opted_in_footer": "Regístrate en las recompensas para recibir tus puntos.",
"tooltip_close": "Cerrar"
- }
+ },
+ "your_stablecoins": "Tus monedas estables"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Tarifas de transacción",
"metamask_fee": "Tarifa de MetaMask",
"network_fee": "Tarifa de red",
- "bridge_fee": "Tarifa del proveedor del puente"
+ "bridge_fee": "Tarifa del proveedor del puente",
+ "provider_fee": "Tarifa del proveedor"
},
"title": {
"signature": "Solicitud de firma",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Canjearemos tus tokens por USDC en Polygon, la red que usa Predicciones. Los proveedores de canje pueden cobrar una comisión, pero MetaMask no."
},
"predict_withdraw": {
- "transaction_fee": "MetaMask se cambiará al token que desees. No se aplica ninguna comisión de MetaMask al cambiar a MUSD."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "Las tarifas de conversión de mUSD incluyen los costos de red y pueden incluir las tarifas del proveedor."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Tarifas"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Impacto sobre el precio",
"price_impact_info_description": "El impacto en el precio refleja cómo tu orden de canje afecta el precio de mercado del activo. Depende del tamaño de la operación y de la liquidez disponible en el fondo. MetaMask no influye ni controla el impacto en el precio.",
"price_impact_info_gasless_description": "El impacto en el precio refleja cómo tu orden de canje afecta el precio de mercado del activo. Si no tienes suficientes fondos para gas, parte de tu token de origen se asigna automáticamente para cubrir las tarifas, lo que aumenta el impacto en el precio. MetaMask no influye ni controla el impacto en el precio.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Cancelar",
"slippage_info_title": "Deslizamiento",
"slippage_info_description": "El % de cambio en el precio que estás dispuesto a permitir antes de que se cancele tu transacción.",
"blockaid_error_title": "Esta transacción se revertirá",
@@ -6470,13 +6596,14 @@
},
"submit": "Enviar",
"default_slippage_description": "Tu transacción no se realizará si el precio cambia más que el porcentaje de deslizamiento.",
- "cancel": "Cancelar",
"confirm": "Confirmar",
"exceeding_upper_slippage_warning": "Alto deslizamiento, lo que puede resultar en un canje desfavorable",
"exceeding_lower_slippage_warning": "Bajo deslizamiento, lo que puede resultar en un canje desfavorable",
"exceeding_lower_slippage_error": "Introduce un valor mayor al {{value}} %",
"exceeding_upper_slippage_error": "No puedes introducir un valor mayor al {{value}} %",
- "custom": "Personalizar"
+ "custom": "Personalizar",
+ "invalid_recipient_address": "Dirección no válida",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Hay nuevas cotizaciones disponibles",
@@ -6773,12 +6900,16 @@
"title": "Ingresar contraseña",
"description": "Ingresa la contraseña de tu billetera para ver los detalles de la tarjeta.",
"description_unfreeze": "Ingresa la contraseña de tu billetera para reanudar el gasto con tu tarjeta.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Contraseña",
"confirm": "Confirmar",
"cancel": "Cancelar",
"error_empty": "Ingresa tu contraseña",
"error_incorrect": "Contraseña incorrecta. Inténtalo de nuevo."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Elige tu tarjeta",
"upgrade_title": "Actualiza a Metal",
@@ -7094,6 +7225,9 @@
"view_card_details": "Ver detalles de la tarjeta",
"hide_card_details": "Ocultar detalles de la tarjeta",
"view_card_details_description": "Número de tarjeta, fecha de vencimiento y CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Administrar límite",
"manage_spending_limit_description_restricted": "Hay un gasto limitado",
"manage_spending_limit_description_full": "El acceso completo está activado",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Términos y condiciones",
"order_metal_card": "Tarjeta Metal",
"order_metal_card_description": "Solicita ya tu tarjeta Metal física",
+ "cashback": "Cashback",
+ "cashback_description": "Obtén 1 % de cashback en todas tus compras",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Tarjeta suspendida",
"unfreeze_card": "Reactivar la tarjeta",
"freeze_card_description": "Pausa todos los gastos de tu tarjeta",
@@ -7141,6 +7278,19 @@
"retry": "Inténtalo de nuevo",
"on_linea": "en Linea"
},
+ "cashback_screen": {
+ "title": "Cashback",
+ "available_cashback": "Cashback disponible",
+ "network_fee": "Tarifa de red",
+ "expected_to_receive": "Se espera recibir",
+ "withdraw": "Retirar",
+ "withdraw_unavailable": "Retiro no disponible",
+ "withdrawal_initiated": "Se ha iniciado el retiro",
+ "withdrawal_success": "Retiro finalizado correctamente",
+ "withdrawal_failed": "Retiro fallido. Inténtalo de nuevo.",
+ "no_cashback": "No hay cashback disponible",
+ "loading_error": "Error al cargar el cashback. Inténtalo de nuevo."
+ },
"change_asset": {
"title": "Cambiar token y red",
"full_spending_access": "Acceso total al gasto",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Seleccionar método de pago",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Seleccionar el token a recibir",
+ "no_gas": "No hay saldo nativo para gas"
},
"connection_removed_modal": {
"title": "Conexiones eliminadas",
@@ -7315,7 +7465,10 @@
"service_not_available": "El servicio no está disponible en este momento. Inténtalo de nuevo en breve.",
"invalid_referral_code": "Código de recomendación no válido. Compruébalo e inténtalo de nuevo.",
"already_referred": "Ya otro usuario te ha recomendado.",
- "cannot_use_own_referral_code": "No puedes usar tu propio código de recomendación."
+ "cannot_use_own_referral_code": "No puedes usar tu propio código de recomendación.",
+ "invalid_bonus_code": "Código de bono no válido",
+ "already_redeemed": "Ya canjeaste este código de bono",
+ "reached_maximum": "Este código de bono alcanzó su número máximo de usos"
},
"claim_reward_error": {
"title": "No se pudo reclamar la recompensa"
@@ -7372,8 +7525,10 @@
"predict": "Predicción",
"musd_deposit": "Depósito en mUSD",
"apply_referral_bonus": "Bono por código de recomendación",
+ "bonus_code": "Código de bono",
"uncategorized_event": "Evento sin categoría"
},
+ "code": "Código",
"date": "Fecha",
"account": "Cuenta",
"bonus": "Bonificación",
@@ -7473,7 +7628,16 @@
"show_less": "Mostrar menos",
"linking_progress": "Agregando cuentas... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} inscritos",
- "add_all_accounts": "Agregar todas las cuentas"
+ "add_all_accounts": "Agregar todas las cuentas",
+ "environment_selector": "Entorno",
+ "environment_cancel": "Cancelar",
+ "environment_default": "Por defecto",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Código de recomendación",
@@ -7483,6 +7647,10 @@
"invalid_code": "Código de recomendación no válido",
"apply_button": "Aplicar código de recomendación"
},
+ "bonus_code": {
+ "input_placeholder": "Ingresa el código de bono",
+ "apply_success": "¡Código de bono aplicado!"
+ },
"optout": {
"title": "Eliminar del programa de Recompensas",
"description": "Esta acción eliminará tus cuentas del programa de Recompensas y borrará tus puntos y progreso. No podrás deshacer esta acción.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Tarifa de puente",
+ "provider_fee": "Tarifa del proveedor",
"network_fee": "Tarifa de red",
"paid_with": "Pagado con",
+ "receive_token": "Token a recibir",
"retry_button": "Inténtalo de nuevo",
"total": "Total",
"account": "Cuenta",
- "received_total": "Received total"
+ "received_total": "Total recibido"
},
"summary_title": {
"bridge_approval": "Aprobar {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Conexión no encontrada",
"description": "Establece una nueva conexión desde la aplicación para continuar."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Todavía conectándose a {{networkName}}...",
"unable_to_connect_network": "No se puede conectar a {{networkName}}.",
"update_rpc": "Actualizar RPC",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Cambiar al RPC por defecto de MetaMask",
"check_network_connectivity": "Comprueba tu conectividad de red.",
"check_network_connectivity_or": "Comprueba tu conectividad de red o",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Se actualizó a la configuración por defecto de MetaMask"
},
"trending": {
"title": "Explorar",
"trending_tokens": "Tokens de tendencia",
+ "stocks": "Acciones",
"price_change": "Cambio de precio",
"all_networks": "Todas las redes",
"24h": "24h",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Recargar",
"primary_action_acknowledge": "Entendido"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "Billetera física con QR",
+ "hardware_wallet": "Billetera física"
+ },
+ "common": {
+ "cancel": "Cancelar",
+ "continue": "Continuar"
+ },
+ "connecting": {
+ "title": "Conecta tu {{device}}",
+ "searching": "Buscando {{device}}...",
+ "tips_header": "Para continuar, asegúrate de que:",
+ "tip_unlock": "Tu {{device}} esté desbloqueado",
+ "tip_open_app": "La aplicación de Ethereum esté abierta",
+ "tip_enable_bluetooth": "El Bluetooth esté activado",
+ "tip_dnd_off": "El modo \"No molestar\" esté desactivado",
+ "tip_bluetooth_permission": "Se hayan concedido permisos de ubicación y Bluetooth",
+ "tip_bluetooth_permission_v12": "Se haya concedido permiso para dispositivos cercanos",
+ "tip_stay_close": "Tu dispositivo se mantenga cerca de tu teléfono"
+ },
+ "awaiting_app": {
+ "title": "Abrir {{app}}",
+ "message": "Abre la aplicación {{app}} en tu {{device}}",
+ "current_app": "Abierta actualmente: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Confirma en {{device}}",
+ "title_message": "Firma en {{device}}",
+ "message": "Revisa y confirma en tu {{device}}"
+ },
+ "success": {
+ "title": "{{device}} conectado"
+ },
+ "errors": {
+ "device_locked": "Desbloquéalo e inténtalo de nuevo para continuar",
+ "app_not_open": "Abre la aplicación Ethereum en tu dispositivo",
+ "device_disconnected": "Tu {{device}} se desconectó. Vuelve a conectarlo e inténtalo de nuevo",
+ "device_not_found": "No se pudo encontrar tu {{device}}. Asegúrate de que esté conectado",
+ "device_not_ready": "Tu dispositivo no está listo. Revísalo e inténtalo de nuevo",
+ "blind_signing": "La firma ciega está desactivada. Actívala en la configuración de tu dispositivo",
+ "connection_closed": "Se perdió la conexión. Inténtalo de nuevo",
+ "connection_timeout": "Se agotó el tiempo de espera de la conexión. Inténtalo de nuevo",
+ "user_cancelled": "La acción se canceló en tu dispositivo",
+ "pending_confirmation": "Hay una acción pendiente en tu dispositivo. Complétala o cancélala",
+ "bluetooth_permission_denied": "Se requiere permiso de Bluetooth para conectar tu dispositivo",
+ "location_permission_denied": "Se requiere permiso de ubicación para buscar dispositivos",
+ "nearby_permission_denied": "Se requiere permiso para dispositivos cercanos",
+ "bluetooth_off": "Activa el Bluetooth para conectar tu dispositivo",
+ "bluetooth_scan_failed": "Error al buscar dispositivos. Inténtalo de nuevo",
+ "bluetooth_connection_failed": "Activa el Bluetooth en tu dispositivo para continuar",
+ "not_supported": "No se admite esta operación",
+ "unknown_error": "Asegúrate de que tu {{device}} esté configurado con la frase secreta de recuperación o la frase de contraseña de esta cuenta"
+ },
+ "error": {
+ "title": "Algo salió mal",
+ "default_title": "Ocurrió un error",
+ "continue": "Continuar",
+ "retry": "Reintentar",
+ "view_settings": "Ver configuración",
+ "device_locked_title": "{{device}} bloqueado",
+ "device_disconnected_title": "{{device}} desconectado",
+ "device_not_found_title": "{{device}} no encontrado",
+ "app_not_open": "La aplicación Ethereum no está abierta",
+ "blind_signing_disabled": "La firma ciega está desactivada",
+ "connection_timeout": "El dispositivo no responde",
+ "connection_closed": "Conexión perdida",
+ "user_cancelled": "Acción cancelada",
+ "pending_confirmation": "Confirmación pendiente",
+ "bluetooth_required": "Se requiere Bluetooth",
+ "bluetooth_permission_denied": "Se requiere permiso de Bluetooth",
+ "location_permission_denied": "Se requiere permiso de ubicación",
+ "nearby_devices_permission_denied": "Se requiere permiso para dispositivos cercanos",
+ "scan_failed": "Error al buscar",
+ "something_went_wrong": "Algo salió mal"
+ },
+ "device_selection": {
+ "title": "Seleccionar {{device}}",
+ "scanning": "Buscando dispositivos...",
+ "no_devices_found": "No se encontraron dispositivos",
+ "no_devices_hint": "Asegúrate de que tu {{device}} esté desbloqueado y el Bluetooth esté activado",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Conectar",
+ "rescan": "Volver a buscar",
+ "unknown_device": "Dispositivo desconocido",
+ "signal_strength": "Señal: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Tokens",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT",
"import_nfts": "Importar NFT",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Agrega fácilmente tus coleccionables",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Sin TG/LP"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "No se pudo cargar {{section}}",
+ "retry": "Reintentar"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/fr.json b/locales/languages/fr.json
index 3ca4ebed0fc..d805b5f683f 100644
--- a/locales/languages/fr.json
+++ b/locales/languages/fr.json
@@ -103,6 +103,10 @@
"message": "Pas assez de {{ticker}} pour couvrir les frais. Utilisez un jeton sur un autre réseau ou ajoutez plus de {{ticker}} pour continuer.",
"title": "Fonds insuffisants"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Cette voie de paiement n’est pas disponible pour le moment. Essayez de modifier le montant, le réseau ou le jeton, et nous trouverons la meilleure option.",
"title": "Aucune cotation"
@@ -1180,6 +1184,7 @@
"title": "Nouvel ordre",
"leverage": "Effet de levier",
"limit_price": "Prix limite",
+ "market_price": "Prix du marché",
"enter_price": "Saisir le prix",
"trigger_price": "Prix déclencheur",
"liquidation_price": "Prix de liquidation",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Fonds disponibles pour le trading",
"close_order_still_active": "Ordre de fermeture toujours actif",
"order_submitted": "Ordre envoyé",
+ "submitting_your_trade": "Soumettre votre ordre",
"order_filled": "Ordre exécuté",
"order_placed": "Ordre passé",
"order_placement_subtitle": "{{direction}} à {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "Détails de l’ordre de bourse",
"cancel_order": "Annuler l’ordre",
"date": "Date",
+ "trigger_condition": "Condition d’exécution",
+ "price": "Prix",
"fee": "Frais",
"limit_buy": "Ordre d’achat à cours limité",
"limit_price": "Prix limite",
"limit_sell": "Ordre de vente à cours limité",
"market_buy": "Ordre d’achat au marché",
"market_sell": "Ordre de vente au marché",
+ "market": "Marché",
"open": "Ouvert",
"size": "Taille",
+ "original_size": "Taille originale",
+ "order_value": "Valeur de l’ordre",
+ "reduce_only": "Réduire uniquement",
+ "yes": "Oui",
+ "no": "Non",
+ "price_above": "Prix supérieur à {{price}}",
+ "price_below": "Prix inférieur à {{price}}",
+ "take_profit": "Take profit",
+ "stop_loss": "Stop loss",
"status": "État",
"view_explorer": "Afficher sur l’explorateur"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Aperçu du marché",
- "updated_ago": "Mises à jour à {{time}}",
- "disclaimer": "Les perçus du marché générés par l’IA ne constituent pas des conseils financiers.",
- "whats_driving_price": "Qu’est-ce qui influence le prix ?",
- "what_people_saying": "Ce que les gens en disent",
+ "a_closer_look": "Regarder de plus près",
+ "whats_being_said": "Ce qu’on en dit",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Trader",
"sources_count": "+{{count}} sources",
- "sources_title": "Sources de liquidités"
+ "sources_title": "News sources",
+ "feedback_submitted": "Commentaire soumis",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Commentaires",
+ "description": "Aidez-nous à améliorer nos analyses de marché générées par l’IA.",
+ "not_relevant": "Pas pertinent",
+ "not_accurate": "Inexact",
+ "hard_to_understand": "Difficile à comprendre",
+ "harmful_or_offensive": "Nuisible ou offensant",
+ "something_else": "Autre chose",
+ "additional_feedback_label": "Commentaires supplémentaires (facultatif)",
+ "additional_feedback_placeholder": "Quelles améliorations suggérez-vous ?",
+ "characters_remaining": "{{count}} caractères restants",
+ "submit": "Soumettre"
+ }
},
"predict": {
"title": "Prédictions MetaMask",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Disponible dans environ 1 minute",
"withdraw_completed": "Retrait effectué",
"withdraw_completed_subtitle": "{{amount}} USDC transféré vers votre portefeuille",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} transféré(s) vers votre portefeuille",
"error_title": "Quelque chose a mal tourné",
"error_description": "Échec du retrait",
"try_again": "Réessayez"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Gardez à l’esprit que cette valeur est une estimation et que la valeur finale sera déterminée une fois la transaction terminée. La confirmation des points dans votre solde de récompenses peut prendre jusqu’à 1 heure.",
"points_error": "Nous ne pouvons pas charger les points pour le moment",
"points_error_content": "Vous gagnerez tout de même des points pour cette transaction. Nous vous informerons dès qu’ils auront été ajoutés à votre compte. Vous pouvez également consulter votre onglet « Récompenses » dans environ une heure.",
- "slippage": "Slippage/effet de glissement"
+ "slippage": "Slippage/effet de glissement",
+ "price_details": "Détails du prix",
+ "prediction_order": "Ordre prédictif",
+ "prediction_order_description": "~{{count}} contrats au prix de {{price}} par contrat. Le montant final peut varier en fonction de la liquidité disponible sur le carnet d’ordres (jusqu’à {{slippage}} %).",
+ "metamask_fee_description": "Frais de service pour le traitement de cette prédiction",
+ "exchange_fee": "Frais de change",
+ "exchange_fee_description": "Frais payés à la bourse ou au marché",
+ "total_incl_fees": "frais inclus",
+ "close": "Fermer"
},
"error": {
"title": "Impossible de se connecter aux marchés de prédiction",
@@ -2399,7 +2440,7 @@
"add_tokens": "Importer des jetons",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Voulez-vous importer ces jetons ?",
"tokens_detected_in_account": "{{tokenCount}} nouveaux {{tokensLabel}} trouvés dans ce compte",
"token_toast": {
"tokens_imported_title": "Jetons importés",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Le nombre de décimales du jeton ne peut être vide.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "Nous n’avons pas trouvé de jetons portant ce nom.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Recherchez n’importe quel jeton et importez-le",
"select_token": "Sélectionnez un jeton",
"address_must_be_smart_contract": "Adresse personnelle détectée. Saisissez l’adresse du contrat de jeton.",
"billion_abbreviation": "Mrd",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "Ajouter l’URL de l’appel de procédure à distance (RPC)",
"add_block_explorer_url": "Ajouter l’URL de l’explorateur de blocs",
"networks_desc": "Ajouter et modifier des réseaux RPC personnalisés",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Réseaux activés",
+ "networks_test_networks": "Réseaux de test",
+ "networks_additional": "Réseaux supplémentaires",
"networks_search_placeholder": "Rechercher un réseau",
"networks_no_results": "Aucun réseau trouvé",
"network_name_label": "Nom du réseau",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "Réseaux RPC",
"network_add_network": "Ajouter un réseau",
"add_chain_title": "Ajouter un réseau",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Recherchez par nom, ID de chaîne ou devise",
+ "add_chain_loading": "Chargement des réseaux…",
+ "add_chain_error": "Échec du chargement des réseaux. Veuillez réessayer.",
"add_chain_retry": "Réessayer",
- "add_chain_added": "Added",
+ "add_chain_added": "Ajoutée",
"add_chain_or": "ou",
"add_chain_custom_link": "Ajouter un réseau personnalisé",
"network_add_custom_network": "Ajouter un réseau personnalisé",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Ajouter un réseau de test",
"network_add": "Ajouter",
"network_save": "Enregistrer",
"remove_network_title": "Voulez-vous supprimer ce réseau ?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Conservez-la dans un endroit sûr et secret.",
"private_key_warning": "Il s’agit de la clé privée du compte actuellement sélectionné : {{accountName}}. Ne divulguez jamais cette clé. Quiconque est en sa possession peut exercer un contrôle total sur votre compte, y compris le transfert de vos fonds.",
- "seed_phrase_warning_explanation": [
- "Assurez-vous que personne ne regarde votre écran.",
- "Le service d’assistance MetaMask ne vous la demandera jamais."
- ],
+ "seed_phrase_warning_explanation": "Assurez-vous que personne ne regarde votre écran. Le service d’assistance de MetaMask ne vous demandera jamais de fournir ce type d’information.",
"private_key_warning_explanation": "Ne divulguez jamais cette clé. Quiconque est en sa possession peut exercer un contrôle total sur votre compte, y compris le transfert de vos fonds.",
+ "reveal_srp_description": "Votre phrase secrète de récupération donne un accès complet à votre portefeuille. Ne la partagez avec personne.",
"reveal_credential_modal": [
"Votre {{credentialName}} donne ",
"un accès complet à votre compte et vos fonds. \n\nNe la partagez avec personne.",
@@ -3385,7 +3424,8 @@
"srp_text": "Phrase secrète de récupération",
"private_key_text": "Clé privée",
"got_it": "J’ai compris",
- "learn_more": "En savoir plus"
+ "learn_more": "En savoir plus",
+ "copied_to_clipboard": "Copié dans le presse-papiers"
},
"screenshot_deterrent": {
"title": "Alerte de sécurité",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Essayer d’accélérer ?",
"speedup_tx_message": "Cette tentative ne garantit aucunement l’accélération de votre transaction initiale. Si la tentative d’accélération réussit, les frais de transaction ci-dessus vous seront facturés.",
"nevermind": "Peu importe",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Modifier les frais de gaz",
"edit_priority": "Modifier la priorité",
"gas_cancel_fee": "Frais de gaz d’annulation",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Des frais de transaction sont associés à cette autorisation.",
"view_details": "Voir les détails",
"view_transaction_details": "Afficher les détails de la transaction",
- "view_data": "View data",
+ "view_data": "Afficher les données",
"transaction_details": "Détails de la transaction",
"site_url": "URL du site",
"permission_request": "Demande d’autorisation",
@@ -3843,7 +3887,7 @@
"right_button": "Protéger le portefeuille"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Ajouter aux favoris",
"title_label": "Nom",
"url_label": "URL",
"add_button": "Ajouter",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Déverrouiller avec Touch ID ?",
"enable_faceid": "Déverrouiller avec Face ID ?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "Déverrouiller à l’aide de votre empreinte digitale ?",
+ "enable_biometrics": "Déverrouiller grâce à la reconnaissance biométrique ?",
"enable_device_passcode_ios": "Déverrouiller avec le code secret de l’appareil ?",
"enable_device_passcode_android": "Déverrouiller avec le code PIN de l’appareil ?"
},
@@ -4724,7 +4768,7 @@
"public_address": "Adresse publique",
"public_address_qr_code": "Adresse publique",
"coming_soon": "Bientôt disponible…",
- "request_payment": "Request payment",
+ "request_payment": "Demander un paiement",
"copy": "Copiez",
"scan_address": "Scanner l’adresse pour recevoir le paiement",
"copy_address": "Copier l’adresse"
@@ -4738,8 +4782,8 @@
"switch_network": "Veuillez passer à Mainnet ou à Sepolia",
"card_title": "Toujours afficher le bouton MetaMask Card",
"card_desc": "MetaMask Card est uniquement disponible pour les résidents de certains pays.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "Utiliser l’environnement de démonstration de DaimoPay",
+ "daimo_demo_desc": "Basculez entre les environnements de démonstration et de production de DaimoPay pour les paiements par carte."
},
"walletconnect_sessions": {
"no_active_sessions": "Vous n’avez aucune session active",
@@ -4752,10 +4796,10 @@
"close_current_session": "Fermez la session en cours avant d’en commencer une nouvelle."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Demande de paiement",
+ "title_complete": "Paiement effectué",
"confirm": "Payer",
- "cancel": "Decline",
+ "cancel": "Refuser",
"is_requesting_you_to_pay": "vous demande de payer",
"total": "Total:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Aucun moyen de paiement n’est disponible.",
"error_fetching_quotes": "Un problème est survenu, veuillez réessayer.",
"no_quotes_available": "Aucun fournisseur disponible.",
+ "quote_unavailable": "Cotation non disponible.",
"providers": "Fournisseurs",
+ "quotes_displayed_for": "Cotations affichées pour {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Utilisé précédemment",
+ "best_rate": "Meilleur taux",
+ "most_reliable": "Le plus fiable",
"continue": "Continuer",
"powered_by_provider": "Propulsé par {{provider}}",
"purchased_currency": "{{currency}} acheté",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Erreur lors de la déconnexion"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Non disponible",
+ "description": "{{token}} n’est pas pris en charge par {{provider}} dans votre région.",
+ "change_token": "Changer de jeton",
+ "change_provider": "Changer de fournisseur"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Choisir un fournisseur"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "J’ai compris",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Changer de fournisseur"
},
"fiat_on_ramp_aggregator": {
"buy": "acheter",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "Dépôt en {{currency}}"
},
+ "ramps_order_details": {
+ "title": "Détails de l’ordre",
+ "status": "État",
+ "processing": "Traitement en cours",
+ "complete": "Terminé",
+ "failed": "Échec",
+ "cancelled": "Annulé",
+ "view_on_provider": "Afficher sur {{provider}}",
+ "order_id": "Numéro de la commande",
+ "date_and_time": "Date et heure",
+ "fees": "Frais",
+ "total": "Total",
+ "card_processing_info": "Vous pouvez finaliser les achats par carte de crédit/débit en quelques minutes",
+ "processing_info_modal_description": "Les achats par carte prennent généralement quelques minutes. Vous pouvez contacter le service d’assistance si vous avez des questions.",
+ "go_to_provider_support": "Accéder à la page d’assistance de {{provider}}",
+ "error_title": "Quelque chose a mal tourné",
+ "error_message": "Impossible de charger les détails de l’ordre. Veuillez réessayer.",
+ "try_again": "Réessayez",
+ "close": "Fermer"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Traitement de votre ordre d’achat de {{cryptocurrency}}",
+ "purchase_pending_description": "Cela ne devrait prendre que quelques minutes…",
+ "purchase_completed_title": "L’achat de {{amount}} {{cryptocurrency}} a été effectué avec succès !",
+ "purchase_completed_description": "Vos {{cryptocurrency}} sont maintenant disponibles",
+ "purchase_failed_title": "L’achat de {{cryptocurrency}} a échoué",
+ "purchase_failed_description": "Veuillez réessayer dans quelques instants",
+ "purchase_cancelled_title": "Votre achat a été annulé",
+ "purchase_cancelled_description": "Votre achat de {{cryptocurrency}} a été annulé",
+ "track": "Suivre"
+ }
+ },
"swaps": {
"title": "Échanger",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Pour éviter que cela ne se reproduise, veillez à ce que votre application MetaMask et votre système d'exploitation soient toujours mis à jour."
},
"srp_security_quiz": {
+ "question_step": "Question {{step}} sur {{total}}",
"title": "Quiz sur la sécurité",
"introduction": "Pour révéler votre Phrase secrète de récupération, vous devez répondre correctement à deux questions",
"get_started": "Commencer",
"learn_more": "En savoir plus",
"try_again": "Réessayez",
"continue": "Continuer",
+ "correct": "Correct !",
+ "incorrect": "Incorrect !",
"of": "de",
"question_one": {
"question": "Si vous perdez votre Phrase secrète de récupération, MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "L’installation de {{snap}} a échoué."
},
"earn": {
- "claimable_bonus_tooltip": "Un bonus annuel que vous pouvez réclamer quotidiennement depuis votre portefeuille.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Obtenez un bonus de {{percentage}} %",
"claimable_bonus": "Bonus réclamable",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Réclamer le bonus",
+ "claim_bonus_subtitle": "Le bonus sera versé sur {{networkName}}.",
"empty_state_cta": {
"heading": "Prêtez des {{tokenSymbol}} et gagnez",
"body": "Prêtez vos {{tokenSymbol}} avec {{protocol}} et touchez un revenu",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Maintenance en cours. Le service sera bientôt rétabli !"
},
- "supply": "Supply",
+ "supply": "Offre",
"deposit": "Dépôt",
"approve": "Approuver",
"approval": "Approbation",
@@ -5887,7 +5978,7 @@
"network": "Réseau",
"health_factor": "Facteur de santé financière",
"liquidation_risk": "Risque de liquidation",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Pool de liquidités insuffisant",
"available_to_withdraw": "disponible pour le retrait",
"unknown": "inconnu",
"how_it_works": "Comment ça marche ",
@@ -5906,7 +5997,7 @@
"lending": "Historique des positions",
"staking": "Historique des paiements"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Réinitialisation de la provision",
"tron": {
"fee": "Frais"
},
@@ -5914,32 +6005,60 @@
"ok": "OK",
"continue": "Continuer",
"convert_and_get_percentage_bonus": "Convertissez et obtenez {{percentage}} %",
+ "your_musd": "Vos mUSD",
+ "balance_breakdown_title": "Vos soldes mUSD par réseau",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "Convertir en mUSD",
"get_a_percentage_musd_bonus": "Obtenir {{percentage}} % de bonus en mUSD",
"convert": "Convertir",
+ "fetching_quote": "Récupération de la cotation...",
+ "you_convert": "Vous convertissez",
+ "network_fee": "Frais de réseau",
+ "earning": "Revenus",
+ "quick_convert_description": "Sélectionnez un jeton pour convertir la totalité de votre solde ou appuyez sur l’icône de modification pour saisir un montant personnalisé.",
+ "no_tokens_to_convert": "Vous ne disposez d’aucun jeton pouvant être converti en mUSD.",
"toasts": {
"converting": "Conversion de {{token}} → mUSD",
"eta": "Environ {{time}}",
- "delivered": "Votre mUSD est arrivé !",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "Échec de la conversion en mUSD"
},
"education": {
"heading": "OBTENEZ {{percentage}} % SUR LES\nSTABLECOINS",
- "description": "Convertissez vos stablecoins en mUSD, la stablecoin adossée au dollar américain de MetaMask, et recevez jusqu’à {{percentage}} % de bonus.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Des conditions s’appliquent.",
"primary_button": "Commencer",
"secondary_button": "Pas maintenant"
},
"buy_musd": "Acheter des mUSD",
"get_musd": "Obtenir des mUSD",
- "bonus_title": "Obtenez {{percentage}} % sur vos stablecoins",
- "bonus_description": "Convertissez vos stablecoins en mUSD et recevez jusqu’à {{percentage}} % de bonus.",
- "powered_by_relay": "Propulsé par Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Propulsé par Relay",
+ "max": "Maximum",
+ "quick_convert_button": "Convertir",
+ "learn_more": "En savoir plus",
+ "tooltip_title": "Obtenez un rendement en détenant des mUSD",
+ "tooltip_content": "Convertissez vos USDC, USDT ou DAI en mUSD, la stablecoin adossée au dollar de MetaMask. Obtenez un rendement de {{apy}} sur chaque mUSD que vous détenez.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "La conversion a échoué. Veuillez réessayer.",
+ "confirmation": {
+ "title": "Convertir le maximum"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Taux"
},
"bonus_claim": {
"toasts": {
"claiming": "Votre bonus mUSD est en cours de traitement",
"delivered": "Votre bonus mUSD est arrivé !",
- "failed": "Bonus claim failed"
+ "failed": "Échec de la réclamation du bonus"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Les points seront automatiquement ajoutés à votre compte.",
"tooltip_not_opted_in_footer": "Inscrivez-vous au programme de fidélité pour recevoir vos points.",
"tooltip_close": "Fermer"
- }
+ },
+ "your_stablecoins": "Vos stablecoins"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Frais de transaction",
"metamask_fee": "Commission MetaMask",
"network_fee": "Frais de réseau",
- "bridge_fee": "Frais du fournisseur de passerelles"
+ "bridge_fee": "Frais du fournisseur de passerelles",
+ "provider_fee": "Commission du fournisseur"
},
"title": {
"signature": "Demande de signature",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Nous échangerons vos jetons contre des USDC.e sur Polygon, le réseau utilisé par « Prédictions ». Les fournisseurs de services d’échange peuvent facturer des frais, mais MetaMask ne le fera pas."
},
"predict_withdraw": {
- "transaction_fee": "MetaMask échangera vos jetons pour vous. Aucuns frais MetaMask ne s’appliquent lorsque vous effectuez un échange contre des MUSD."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "Les frais de conversion en mUSD comprennent les coûts de réseau et peuvent inclure les frais du fournisseur."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Frais"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Impact sur les prix",
"price_impact_info_description": "L’impact sur le prix reflète la manière dont votre ordre de swap affecte le prix du marché de l’actif. Il dépend du volume de la transaction et de la liquidité disponible dans le pool. MetaMask n’influence ni ne contrôle l’impact sur le prix.",
"price_impact_info_gasless_description": "L’impact sur le prix reflète la manière dont votre ordre de swap affecte le prix de l’actif sur le marché. Si vous ne disposez pas de fonds suffisants pour payer les frais de gaz, une partie de votre jeton source est automatiquement allouée pour couvrir ces frais, ce qui augmente l’impact sur le prix. MetaMask n’influence ni ne contrôle l’impact sur le prix.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Annuler",
"slippage_info_title": "Slippage/effet de glissement",
"slippage_info_description": "Le pourcentage de variation de prix que vous êtes prêt à accepter avant que votre transaction ne soit annulée.",
"blockaid_error_title": "Cette transaction sera annulée",
@@ -6470,13 +6596,14 @@
},
"submit": "Soumettre",
"default_slippage_description": "Votre transaction n’aboutira pas si la variation de prix dépasse le pourcentage de slippage.",
- "cancel": "Annuler",
"confirm": "Confirmer",
"exceeding_upper_slippage_warning": "Slippage élevé, cela peut entraîner un swap défavorable",
"exceeding_lower_slippage_warning": "Slippage faible, cela peut entraîner un swap défavorable",
"exceeding_lower_slippage_error": "Saisissez une valeur supérieure à {{value}} %",
"exceeding_upper_slippage_error": "Vous ne pouvez pas saisir une valeur supérieure à {{value}} %",
- "custom": "Personnalisé"
+ "custom": "Personnalisé",
+ "invalid_recipient_address": "Adresse non valide",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "De nouvelles cotations sont disponibles",
@@ -6773,12 +6900,16 @@
"title": "Saisissez votre mot de passe",
"description": "Saisissez le mot de passe de votre portefeuille pour afficher les détails de la carte.",
"description_unfreeze": "Saisissez le mot de passe de votre portefeuille pour réactiver les paiements avec votre carte.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Mot de passe",
"confirm": "Confirmer",
"cancel": "Annuler",
"error_empty": "Veuillez saisir votre mot de passe",
"error_incorrect": "Mot de passe incorrect. Veuillez réessayer."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Choisissez votre carte",
"upgrade_title": "Passer à Metal",
@@ -7094,6 +7225,9 @@
"view_card_details": "Afficher les détails de la carte",
"hide_card_details": "Masquer les détails de la carte",
"view_card_details_description": "Numéro, date d’expiration et CVV de la carte",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Gérer la limite",
"manage_spending_limit_description_restricted": "La limite de dépenses est activée",
"manage_spending_limit_description_full": "L’accès complet est activé",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Conditions générales",
"order_metal_card": "Carte Metal",
"order_metal_card_description": "Commandez votre Carte Metal physique dès maintenant",
+ "cashback": "Cashback",
+ "cashback_description": "Gagner 1 % sur toutes vos dépenses",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Bloquer la carte",
"unfreeze_card": "Débloquer la carte",
"freeze_card_description": "Suspendre tous les paiements avec votre carte",
@@ -7141,6 +7278,19 @@
"retry": "Réessayez",
"on_linea": "sur Linea"
},
+ "cashback_screen": {
+ "title": "Cashback",
+ "available_cashback": "Cashback disponible",
+ "network_fee": "Frais de réseau",
+ "expected_to_receive": "Montant estimé à recevoir",
+ "withdraw": "Retirer",
+ "withdraw_unavailable": "Retrait non disponible",
+ "withdrawal_initiated": "Le retrait a été initié",
+ "withdrawal_success": "Retrait effectué avec succès",
+ "withdrawal_failed": "Le retrait a échoué. Veuillez réessayer.",
+ "no_cashback": "Aucun cashback disponible",
+ "loading_error": "Échec du chargement du cashback. Veuillez réessayer."
+ },
"change_asset": {
"title": "Modifier le jeton et le réseau",
"full_spending_access": "Possibilité de dépenser sans restriction",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Sélectionnez le mode de paiement",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Sélectionner le jeton à recevoir",
+ "no_gas": "Pas de solde natif pour le gaz"
},
"connection_removed_modal": {
"title": "Connexions supprimées",
@@ -7315,7 +7465,10 @@
"service_not_available": "Le service n’est pas disponible pour le moment. Veuillez réessayer dans quelques instants.",
"invalid_referral_code": "Code de parrainage non valide. Veuillez vérifier et réessayer.",
"already_referred": "Vous avez déjà été parrainé par un autre utilisateur.",
- "cannot_use_own_referral_code": "Vous ne pouvez pas utiliser votre propre code de parrainage."
+ "cannot_use_own_referral_code": "Vous ne pouvez pas utiliser votre propre code de parrainage.",
+ "invalid_bonus_code": "Code bonus non valide",
+ "already_redeemed": "Vous avez déjà utilisé ce code bonus",
+ "reached_maximum": "Ce code bonus a été utilisé le nombre maximum de fois"
},
"claim_reward_error": {
"title": "Impossible de réclamer la récompense"
@@ -7372,8 +7525,10 @@
"predict": "Prédiction",
"musd_deposit": "Dépôt en mUSD",
"apply_referral_bonus": "Bonus code de parrainage",
+ "bonus_code": "Code bonus",
"uncategorized_event": "Événement non classé"
},
+ "code": "Code",
"date": "Date",
"account": "Compte",
"bonus": "Bonus",
@@ -7473,7 +7628,16 @@
"show_less": "Afficher moins",
"linking_progress": "Ajout des comptes… ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} inscrit(s)",
- "add_all_accounts": "Ajouter tous les comptes"
+ "add_all_accounts": "Ajouter tous les comptes",
+ "environment_selector": "Environnement",
+ "environment_cancel": "Annuler",
+ "environment_default": "Par défaut",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Code de parrainage",
@@ -7483,6 +7647,10 @@
"invalid_code": "Code de parrainage non valide",
"apply_button": "Appliquer le code de parrainage"
},
+ "bonus_code": {
+ "input_placeholder": "Saisir le code bonus",
+ "apply_success": "Code bonus appliqué !"
+ },
"optout": {
"title": "Supprimer la progression Rewards",
"description": "Cette action retirera vos comptes du programme Rewards et effacera vos points ainsi que votre progression. Cette action est irréversible.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Frais de passerelle",
+ "provider_fee": "Commission du fournisseur",
"network_fee": "Frais de réseau",
"paid_with": "Payé avec",
+ "receive_token": "Recevoir le jeton",
"retry_button": "Réessayez",
"total": "Total",
"account": "Compte",
- "received_total": "Received total"
+ "received_total": "Total reçu"
},
"summary_title": {
"bridge_approval": "Approuver {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Connexion introuvable",
"description": "Veuillez établir une nouvelle connexion depuis l’application pour continuer."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Connexion en cours à {{networkName}}…",
"unable_to_connect_network": "Impossible de se connecter à {{networkName}}.",
"update_rpc": "Mettre à jour le RPC",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Passer au RPC par défaut de MetaMask",
"check_network_connectivity": "Vérifiez votre connexion réseau.",
"check_network_connectivity_or": "Vérifiez votre connexion réseau ou",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Configuration par défaut de MetaMask appliquée"
},
"trending": {
"title": "Explorer",
"trending_tokens": "Jetons tendance",
+ "stocks": "Actions",
"price_change": "Variation du prix",
"all_networks": "Tous les réseaux",
"24h": "24 h",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Recharger",
"primary_action_acknowledge": "J’ai compris"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "Portefeuille matériel QR",
+ "hardware_wallet": "Portefeuille matériel"
+ },
+ "common": {
+ "cancel": "Annuler",
+ "continue": "Continuer"
+ },
+ "connecting": {
+ "title": "Connectez votre {{device}}",
+ "searching": "Recherche de {{device}}…",
+ "tips_header": "Pour continuer, assurez-vous que :",
+ "tip_unlock": "Votre {{device}} est déverrouillé",
+ "tip_open_app": "L’application Ethereum est ouverte",
+ "tip_enable_bluetooth": "Le Bluetooth est activé",
+ "tip_dnd_off": "Le mode « Ne pas déranger » est désactivé",
+ "tip_bluetooth_permission": "Les autorisations de localisation et de connexion Bluetooth sont accordées",
+ "tip_bluetooth_permission_v12": "L’autorisation d’accès aux appareils à proximité est accordée",
+ "tip_stay_close": "Votre appareil reste à proximité de votre téléphone"
+ },
+ "awaiting_app": {
+ "title": "Ouvrir {{app}}",
+ "message": "Veuillez ouvrir l’application {{app}} sur votre {{device}}",
+ "current_app": "Application actuellement ouverte : {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Confirmer sur {{device}}",
+ "title_message": "Se connecter sur {{device}}",
+ "message": "Vérifiez et confirmez sur votre {{device}}"
+ },
+ "success": {
+ "title": "{{device}} connecté"
+ },
+ "errors": {
+ "device_locked": "Déverrouillez-le et réessayez pour continuer",
+ "app_not_open": "Veuillez ouvrir l’application Ethereum sur votre appareil",
+ "device_disconnected": "Votre {{device}} a été déconnecté. Veuillez le reconnecter et réessayer",
+ "device_not_found": "Impossible de trouver votre {{device}}. Veuillez vous assurer qu’il est connecté",
+ "device_not_ready": "Votre appareil n’est pas prêt. Veuillez le vérifier et réessayer",
+ "blind_signing": "La signature aveugle est désactivée. Veuillez l’activer dans les paramètres de votre appareil",
+ "connection_closed": "La connexion a été interrompue. Veuillez réessayer",
+ "connection_timeout": "Le délai de connexion a expiré. Veuillez réessayer",
+ "user_cancelled": "L’action a été annulée sur votre appareil",
+ "pending_confirmation": "Une action est en attente sur votre appareil. Veuillez la finaliser ou l’annuler",
+ "bluetooth_permission_denied": "L'autorisation de connexion Bluetooth est requise pour établir une connexion avec votre appareil",
+ "location_permission_denied": "L’autorisation de localisation est requise pour rechercher des appareils",
+ "nearby_permission_denied": "L’autorisation d’accès aux appareils à proximité est requise",
+ "bluetooth_off": "Veuillez activer le Bluetooth pour établir une connexion avec votre appareil",
+ "bluetooth_scan_failed": "Échec de la recherche d’appareils. Veuillez réessayer",
+ "bluetooth_connection_failed": "Activez le Bluetooth sur votre appareil pour continuer",
+ "not_supported": "Cette opération n’est pas prise en charge",
+ "unknown_error": "Assurez-vous que votre {{device}} est configuré avec la phrase de récupération secrète ou la phrase secrète de ce compte"
+ },
+ "error": {
+ "title": "Quelque chose a mal tourné",
+ "default_title": "Une erreur est survenue",
+ "continue": "Continuer",
+ "retry": "Réessayer",
+ "view_settings": "Afficher les paramètres",
+ "device_locked_title": "{{device}} verrouillé",
+ "device_disconnected_title": "{{device}} déconnecté",
+ "device_not_found_title": "{{device}} introuvable",
+ "app_not_open": "L’application Ethereum n’est pas ouverte",
+ "blind_signing_disabled": "La signature aveugle est désactivée",
+ "connection_timeout": "L’appareil ne répond pas",
+ "connection_closed": "Connexion interrompue",
+ "user_cancelled": "Action annulée",
+ "pending_confirmation": "En attente de confirmation",
+ "bluetooth_required": "Bluetooth requis",
+ "bluetooth_permission_denied": "Autorisation de connexion Bluetooth requise",
+ "location_permission_denied": "Autorisation de localisation requise",
+ "nearby_devices_permission_denied": "L’autorisation d’accès aux appareils à proximité est requise",
+ "scan_failed": "La recherche a échoué",
+ "something_went_wrong": "Quelque chose a mal tourné"
+ },
+ "device_selection": {
+ "title": "Sélectionnez {{device}}",
+ "scanning": "Recherche d’appareils…",
+ "no_devices_found": "Aucun appareil trouvé",
+ "no_devices_hint": "Assurez-vous que votre {{device}} est déverrouillé et que le Bluetooth est activé",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Connecter",
+ "rescan": "Rechercher à nouveau",
+ "unknown_device": "Appareil inconnu",
+ "signal_strength": "Signal : {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Jetons",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT",
"import_nfts": "Importer des NFT",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Ajoutez facilement vos objets de collection",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Aucun TP/SL"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "Impossible de charger {{section}}",
+ "retry": "Réessayer"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/hi.json b/locales/languages/hi.json
index cc345f5967a..f5b3c5c71a1 100644
--- a/locales/languages/hi.json
+++ b/locales/languages/hi.json
@@ -103,6 +103,10 @@
"message": "फीस कवर करने के लिए ज़्यादा {{ticker}} मौजूद नहीं हैं। जारी रखने के लिए किसी दूसरे नेटवर्क पर टोकन इस्तेमाल करें या और {{ticker}} जोड़ें।",
"title": "अपर्याप्त फंड"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "यह भुगतान रूट अभी उपलब्ध नहीं है। राशि, नेटवर्क या टोकन बदलने का प्रयास करें और हम सबसे अच्छा विकल्प ढूँढ लेंगे।",
"title": "कोई कोटेशन मौजूद नहीं है"
@@ -1180,6 +1184,7 @@
"title": "नया ऑर्डर",
"leverage": "लीवरेज",
"limit_price": "लिमिट प्राइस",
+ "market_price": "मार्केट प्राइस",
"enter_price": "एंटर प्राइस",
"trigger_price": "ट्रिगर प्राइस",
"liquidation_price": "लिक्विडेशन प्राइस",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "फंड्स ट्रेड करने के लिए उपलब्ध हैं",
"close_order_still_active": "क्लोज़ ऑर्डर अभी भी सक्रिय है",
"order_submitted": "ऑर्डर सबमिट किया गया",
+ "submitting_your_trade": "आपका ट्रेड सबमिट कर रहे हैं",
"order_filled": "ऑर्डर पूरा हो गया",
"order_placed": "ऑर्डर लगाया गया",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "ऑर्डर का विवरण",
"cancel_order": "ऑर्डर कैंसिल करें",
"date": "तिथि",
+ "trigger_condition": "ट्रिगर का शर्त",
+ "price": "प्राइस",
"fee": "फीस",
"limit_buy": "लिमिट लॉन्ग",
"limit_price": "लिमिट प्राइस",
"limit_sell": "लिमिट शॉर्ट",
"market_buy": "मार्केट लॉन्ग",
"market_sell": "मार्केट शॉर्ट",
+ "market": "मार्केट",
"open": "ओपन",
"size": "आकार",
+ "original_size": "मूल आकार",
+ "order_value": "ऑर्डर वैल्यू",
+ "reduce_only": "केवल घटाएं",
+ "yes": "हां",
+ "no": "नहीं",
+ "price_above": "{{price}} से ऊपर प्राइस",
+ "price_below": "{{price}} से नीचे प्राइस",
+ "take_profit": "टेक प्रॉफ़िट",
+ "stop_loss": "स्टॉप लॉस",
"status": "स्टेटस",
"view_explorer": "एक्सप्लोरर पर देखें"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "मार्केट इनसाइट्स",
- "updated_ago": "{{time}} अपडेट किया गया",
- "disclaimer": "AI इनसाइट्स। फाइनेंशियल सलाह नहीं।",
- "whats_driving_price": "कीमत किस वजह से बढ़ रही है?",
- "what_people_saying": "लोग क्या कह रहे हैं",
+ "a_closer_look": "एक करीबी निगाह",
+ "whats_being_said": "क्या कहा जा रहा है",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "ट्रेड करें",
"sources_count": "+{{count}} सोर्स",
- "sources_title": "सोर्स"
+ "sources_title": "News sources",
+ "feedback_submitted": "फीडबैक सबमिट किया गया",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "फीडबैक",
+ "description": "हमारे AI-जनित मार्केट इनसाइट्स को बेहतर बनाने में सहायता करें।",
+ "not_relevant": "संबंधित नहीं",
+ "not_accurate": "सही नहीं",
+ "hard_to_understand": "समझने में मुश्किल",
+ "harmful_or_offensive": "हानिकारक या आपत्तिजनक",
+ "something_else": "कुछ और",
+ "additional_feedback_label": "अतिरिक्त फीडबैक (वैकल्पिक)",
+ "additional_feedback_placeholder": "हम कैसे सुधार कर सकते हैं?",
+ "characters_remaining": "{{count}} कैरेक्टर्स शेष हैं",
+ "submit": "सबमिट करें"
+ }
},
"predict": {
"title": "MetaMask प्रिडिक्शन्स",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "लगभग 1 मिनट में उपलब्ध होगा",
"withdraw_completed": "विदड्रॉवल पूरा हुआ",
"withdraw_completed_subtitle": "{{amount}} USDC आपके वॉलेट में भेज दिया गया",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} आपके वॉलेट में भेजा गया",
"error_title": "कुछ गलत हो गया",
"error_description": "निकालना जारी नहीं हो पाया",
"try_again": "फिर से प्रयास करें"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "ध्यान रखें कि यह मूल्य केवल एक अनुमान है और ट्रांसेक्शन पूरा होने के बाद अंतिम रूप दिया जाएगा। आपके रिवॉर्ड्स बैलेंस में पॉइंट्स कन्फर्म होने में 1 घंटे तक का समय लग सकता है।",
"points_error": "हम अभी पॉइंट्स लोड नहीं कर सकते",
"points_error_content": "आप इस ट्रांसेक्शन के लिए अभी भी पॉइंट्स अर्जित करेंगे। जब ये आपके अकाउंट में जोड़ दिए जाएंगे, तब हम आपको सूचित करेंगे। आप लगभग एक घंटे में अपने रिवॉर्ड्स टैब में भी जांच सकते हैं।",
- "slippage": "स्लिपेज (slippage)"
+ "slippage": "स्लिपेज (slippage)",
+ "price_details": "प्राइस विवरण",
+ "prediction_order": "प्रेडिक्शन ऑर्डर",
+ "prediction_order_description": "~{{count}} कॉन्ट्रैक्ट्स प्रत्येक {{price}} पर। अंतिम राशि ऑर्डर बुक की उपलब्धता के आधार पर बदल सकती है (अधिकतम {{slippage}}%)।",
+ "metamask_fee_description": "इस प्रेडिक्शन को प्रोसेस करने के लिए सेवा शुल्क",
+ "exchange_fee": "एक्सचेंज शुल्क",
+ "exchange_fee_description": "एक्सचेंज या मार्केट को दिया गया शुल्क",
+ "total_incl_fees": "शुल्क सहित",
+ "close": "बंद करें"
},
"error": {
"title": "प्रीडिक्शंस से कनेक्ट नहीं हो पाया",
@@ -2399,7 +2440,7 @@
"add_tokens": "टोकन इम्पोर्ट करें",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "क्या आप इन टोकन को इंपोर्ट करना चाहते हैं?",
"tokens_detected_in_account": "इस खाते में {{tokenCount}} नया {{tokensLabel}} मिला",
"token_toast": {
"tokens_imported_title": "इंपोर्ट किए गए टोकन",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "टोकन का दशमलव खाली नहीं रह सकता।",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "हम उस नाम के साथ कोई टोकन नहीं ढ़ूंढ़ सके।",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "किसी भी टोकन को ढूंढें और इंपोर्ट करें",
"select_token": "टोकन चुनें",
"address_must_be_smart_contract": "व्यक्तिगत पता का पहचान हुई। टोकन अनुबंध पता दर्ज करें।",
"billion_abbreviation": "B",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "RPC URL जोड़ें",
"add_block_explorer_url": "ब्लॉक एक्सप्लोरर URL जोड़ें",
"networks_desc": "कस्टम RPC नेटवर्क को जोड़ें और संपादित करें",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "चालू किए गए नेटवर्क्स",
+ "networks_test_networks": "टेस्ट नेटवर्क्स",
+ "networks_additional": "अतिरिक्त नेटवर्क्स",
"networks_search_placeholder": "नेटवर्क ढूंढें",
"networks_no_results": "कोई नेटवर्क नहीं मिला",
"network_name_label": "नेटवर्क का नाम",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "RPC नेटवर्क",
"network_add_network": "नेटवर्क जोड़ें",
"add_chain_title": "एक नेटवर्क जोड़ें",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "नाम, चैन ID, या करेंसी से ढूंढें",
+ "add_chain_loading": "नेटवर्क लोड हो रहे हैं…",
+ "add_chain_error": "नेटवर्क लोड करना नहीं हो पाया। कृपया फिर से प्रयास करें।",
"add_chain_retry": "फिर से प्रयास करें",
- "add_chain_added": "Added",
+ "add_chain_added": "जोड़ा गया",
"add_chain_or": "या",
"add_chain_custom_link": "एक कस्टम नेटवर्क जोड़ें",
"network_add_custom_network": "एक कस्टम नेटवर्क जोड़ें",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "एक टेस्ट नेटवर्क जोड़ें",
"network_add": "जोड़ें",
"network_save": "सहेजें",
"remove_network_title": "क्या आप इस नेटवर्क को हटाना चाहते हैं?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "इसे कहीं सुरक्षित और गुप्त जगह पर सहेजें।",
"private_key_warning": "वर्तमान चयनित अकाउंट के लिए यह गोपनीय की है: {{accountName}}। इस की को कभी उजागर ना करें आपकी गोपनीय की के साथ कोई भी आपके अकाउंट पर पूरा नियंत्रण रख सकता है, जिसमें आपके फंड का स्थानांतरण भी शामिल है।। ",
- "seed_phrase_warning_explanation": [
- "यह सुनिश्चित करें कि कोई भी आपकी स्क्रीन ना देख रहा हो।",
- "MetaMask सपोर्ट कभी इसका अनुरोध नहीं करेगा।"
- ],
+ "seed_phrase_warning_explanation": "सुनिश्चित करें कि कोई आपकी स्क्रीन को न देख रहा हो। MetaMask सपोर्ट कभी भी इसके लिए नहीं कहेगा।",
"private_key_warning_explanation": "इस की को कभी उजागर ना करें। आपकी निजी की के साथ कोई भी आपके अकाउंट पर पूरा नियंत्रण रख सकता है, जिसमें आपके किसी फंड का स्थानांतरण शामिल है।",
+ "reveal_srp_description": "आपका सीक्रेट रिकवरी फ्रेज़ आपके वॉलेट का पूरा एक्सेस देता है। इसे किसी के साथ साझा न करें।",
"reveal_credential_modal": [
"आपके {{credentialName}} ",
"आपके खाते तथा धनराशि तक पूर्ण पहुंच प्रदान करते हैं।\n\nइसे किसी के साथ साझा न करें।",
@@ -3385,7 +3424,8 @@
"srp_text": "सीक्रेट रिकवरी फ्रेज़ है",
"private_key_text": "प्राइवेट की (key)",
"got_it": "समझ गए",
- "learn_more": "ज़्यादा जानें"
+ "learn_more": "ज़्यादा जानें",
+ "copied_to_clipboard": "क्लिपबोर्ड पर कॉपी किया गया"
},
"screenshot_deterrent": {
"title": "सुरक्षा चेतावनी",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "गति बढ़ाने का प्रयास?",
"speedup_tx_message": "इस प्रयास को सबमिट करने से इस बात की गारंटी नहीं मिलती कि आपका मूल लेन-देन तेजी से होगा। यदि गति बढ़ाने का प्रयास सफल रहा, तो आपसे उपरोक्त लेन-देन शुल्क लिया जाएगा।",
"nevermind": "कोई बात नहीं",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "गैस शुल्क संपादित करें",
"edit_priority": "प्राथमिकता को संपादित करें",
"gas_cancel_fee": "गैस कैंसिल करने का शुल्क",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "लेन-देन का शुल्क इस अनुमति से संबंधित है।",
"view_details": "विवरण देखें",
"view_transaction_details": "लेन-देन विवरण देखें",
- "view_data": "View data",
+ "view_data": "डेटा देखें",
"transaction_details": "ट्रांसेक्शन संबंधी विवरण",
"site_url": "साइट URL",
"permission_request": "अनुमति के लिए अनुरोध",
@@ -3843,7 +3887,7 @@
"right_button": "वॉलेट को सुरक्षित रखें"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "पसंदीदा जोड़ें",
"title_label": "नाम",
"url_label": "URL",
"add_button": "जोड़ें",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "टच ID के साथ अनलॉक करें?",
"enable_faceid": "फेस ID के साथ अनलॉक करें?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "फिंगरप्रिंट के साथ अनलॉक करें?",
+ "enable_biometrics": "बायोमेट्रिक्स के साथ अनलॉक करें?",
"enable_device_passcode_ios": "डिवाइस के पासकोड के साथ अनलॉक करें?",
"enable_device_passcode_android": "डिवाइस के PIN के साथ अनलॉक करें?"
},
@@ -4724,7 +4768,7 @@
"public_address": "पब्लिक एड्रेस",
"public_address_qr_code": "पब्लिक एड्रेस",
"coming_soon": "जल्द आ रहा है...",
- "request_payment": "Request payment",
+ "request_payment": "भुगतान का अनुरोध करें",
"copy": "कॉपी करें",
"scan_address": "भुगतान प्राप्त करने के लिए पता को स्कैन करें",
"copy_address": "एड्रेस कॉपी करें"
@@ -4738,8 +4782,8 @@
"switch_network": "कृपया Mainnet या sepolia पर बदलें",
"card_title": "हमेशा MetaMask कार्ड बटन दिखाएं",
"card_desc": "MetaMask कार्ड केवल कुछ चुने हुए देशों के निवासियों के लिए उपलब्ध है।",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "DaimoPay का डेमो वातावरण उपयोग करें",
+ "daimo_demo_desc": "कार्ड भुगतान के लिए DaimoPay के डेमो और प्रोडक्शन वातावरण के बीच बदलें।"
},
"walletconnect_sessions": {
"no_active_sessions": "आपके पास सक्रिय सत्र नहीं हैं",
@@ -4752,10 +4796,10 @@
"close_current_session": "नया सत्र शुरू करने से पहले वर्तमान सत्र को बंद कर दें।"
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "भुगतान अनुरोध",
+ "title_complete": "भुगतान पूरा हुआ",
"confirm": "भुगतान करें",
- "cancel": "Decline",
+ "cancel": "अस्वीकार करें",
"is_requesting_you_to_pay": "आपसे भुगतान करने का अनुरोध कर रहा है",
"total": "कुल:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "पेमेंट का कोई तरीका उपलब्ध नहीं है।",
"error_fetching_quotes": "कुछ गलत हो गया। कृपया फिर से कोशिश करें।",
"no_quotes_available": "कोई प्रोवाइडर उपलब्ध नहीं है।",
+ "quote_unavailable": "कोटेशन उपलब्ध नहीं है।",
"providers": "प्रोवाइडर्स",
+ "quotes_displayed_for": "{{paymentMethodName}} के लिए कोटेशन दिखाए गए हैं।",
+ "other_options": "Other options",
+ "previously_used": "पहले इस्तेमाल किया गया",
+ "best_rate": "सबसे अच्छी रेट",
+ "most_reliable": "सबसे विश्वसनीय",
"continue": "जारी रखें",
"powered_by_provider": "{{provider}} द्वारा संचालित",
"purchased_currency": "{{currency}} खरीदा गया",
@@ -4890,14 +4940,19 @@
"logged_out_error": "लॉग आउट करते समय गड़बड़ी"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "उपलब्ध नहीं है",
+ "description": "{{token}} आपके क्षेत्र में {{provider}} के साथ उपलब्ध नहीं है।",
+ "change_token": "टोकन बदलें",
+ "change_provider": "प्रदाता बदलें"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "एक प्रदाता चुनें"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "समझ गए",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "प्रदाता बदलें"
},
"fiat_on_ramp_aggregator": {
"buy": "खरीदें",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "{{currency}} डिपॉज़िट करें"
},
+ "ramps_order_details": {
+ "title": "ऑर्डर का विवरण",
+ "status": "स्टेटस",
+ "processing": "प्रोसेस हो रहा है",
+ "complete": "पूरा",
+ "failed": "नहीं हो पाया",
+ "cancelled": "कैंसिल किया गया",
+ "view_on_provider": "{{provider}} पर देखें",
+ "order_id": "ऑर्डर ID",
+ "date_and_time": "दिनांक और समय",
+ "fees": "फीस",
+ "total": "कुल",
+ "card_processing_info": "कार्ड से की गई खरीदारी में आमतौर पर कुछ मिनट लगते हैं",
+ "processing_info_modal_description": "कार्ड से खरीदारी में आमतौर पर कुछ मिनट लगते हैं। यदि आपके पास सवाल हैं तो आप सपोर्ट से कॉन्टेक्ट कर सकते हैं।",
+ "go_to_provider_support": "{{provider}} सपोर्ट पेज पर जाएँ",
+ "error_title": "कुछ गलत हो गया",
+ "error_message": "ऑर्डर विवरण लोड नहीं हो पाया। कृपया फिर से प्रयास करें।",
+ "try_again": "फिर से प्रयास करें",
+ "close": "बंद करें"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "{{cryptocurrency}} की आपकी खरीद प्रोसेस की जा रही है",
+ "purchase_pending_description": "इसमें केवल कुछ मिनटों का समय लगना चाहिए...",
+ "purchase_completed_title": "{{amount}} {{cryptocurrency}} की आपकी खरीद सफल रही!",
+ "purchase_completed_description": "आपकी {{cryptocurrency}} अब उपलब्ध है",
+ "purchase_failed_title": "{{cryptocurrency}} की खरीद नहीं हो पायी",
+ "purchase_failed_description": "कृपया कुछ समय बाद फिर से प्रयास करें",
+ "purchase_cancelled_title": "आपकी खरीद कैंसिल कर दी गई थी",
+ "purchase_cancelled_description": "आपकी {{cryptocurrency}} की खरीद कैंसिल कर दी गई है",
+ "track": "ट्रैक करें"
+ }
+ },
"swaps": {
"title": "स्वैप करें",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "इसे फिर से होने से रोकने के लिए, सुनिश्चित करें कि आपका MetaMask ऐप और OS हमेशा नवीनतम संस्करण में अपडेट रहें।"
},
"srp_security_quiz": {
+ "question_step": "{{total}} में से सवाल {{step}}",
"title": "सुरक्षा प्रश्नोत्तरी",
"introduction": "अपना सीक्रेट रिकवरी फ्रेज़ देखने के लिए, आपको दो प्रश्नों का सही उत्तर देना होगा",
"get_started": "शुरू करें",
"learn_more": "ज़्यादा जानें",
"try_again": "फिर से कोशिश करें",
"continue": "जारी रखें",
+ "correct": "सही!",
+ "incorrect": "गलत!",
"of": "का",
"question_one": {
"question": "यदि आप अपना सीक्रेट रिकवरी फ्रेज़ खो देते हैं, तो MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "{{snap}} का इंस्टॉलेशन नहीं हो पाया।"
},
"earn": {
- "claimable_bonus_tooltip": "एक सालाना बोनस जिसे आप अपने वॉलेट से रोज़ाना क्लेम कर सकते हैं।",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "{{percentage}}% बोनस कमाएं",
"claimable_bonus": "क्लेम करने योग्य बोनस",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "बोनस क्लेम करें",
+ "claim_bonus_subtitle": "बोनस {{networkName}} पर दिया जाएगा।",
"empty_state_cta": {
"heading": "{{tokenSymbol}} उधार दें और कमाएं",
"body": "{{protocol}} के साथ अपना {{tokenSymbol}} उधार दें और सालाना",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "हम रखरखाव की वजह से उपलब्ध नहीं हैं। हम जल्द ही ऑनलाइन वापस आएँगे!"
},
- "supply": "Supply",
+ "supply": "सप्लाई करें",
"deposit": "डिपॉज़िट करें",
"approve": "एप्रूव करें",
"approval": "एप्रूवल",
@@ -5887,7 +5978,7 @@
"network": "नेटवर्क",
"health_factor": "स्वास्थ्य कारक",
"liquidation_risk": "लिक्विडेशन जोखिम",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "पूल की लिक्विडिटी अपर्याप्त है",
"available_to_withdraw": "निकालने के लिए उपलब्ध है",
"unknown": "अज्ञात",
"how_it_works": "ये कैसे काम करता है",
@@ -5906,7 +5997,7 @@
"lending": "पोजीशन का इतिहास",
"staking": "भुगतान इतिहास"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "भत्ता रीसेट करें",
"tron": {
"fee": "फीस"
},
@@ -5914,32 +6005,60 @@
"ok": "ठीक है",
"continue": "जारी रखें",
"convert_and_get_percentage_bonus": "कन्वर्ट करें और {{percentage}}% पाएं",
+ "your_musd": "आपका mUSD",
+ "balance_breakdown_title": "आपका mUSD बैलेंस नेटवर्क के अनुसार",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "mUSD में कन्वर्ट करें",
"get_a_percentage_musd_bonus": "{{percentage}}% mUSD बोनस पाएं",
"convert": "कन्वर्ट करें",
+ "fetching_quote": "कोटेशन लाया जा रहा है...",
+ "you_convert": "आप बदलते हैं",
+ "network_fee": "नेटवर्क फीस",
+ "earning": "कमाना",
+ "quick_convert_description": "अपने पूरे बैलेंस को बदलने के लिए एक टोकन चुनें या कस्टम राशि डालने के लिए एडिट आइकन पर टैप करें।",
+ "no_tokens_to_convert": "आपके पास कोई भी टोकन नहीं है जिसे mUSD में बदला जा सके।",
"toasts": {
"converting": "{{token}} → mUSD में कन्वर्ट किया जा रहा है",
"eta": "~{{time}}",
- "delivered": "आपका mUSD यहाँ है!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "mUSD कन्वर्शन नहीं हो पाया"
},
"education": {
"heading": "स्टेबलकॉइन्स पर {{percentage}}% पाएं",
- "description": "अपने स्टेबलकॉइन को mUSD में बदलें, जो MetaMask का US डॉलर-बैक्ड स्टेबलकॉइन है, और {{percentage}}% तक का बोनस प्राप्त करें।",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "नियम लागू।",
"primary_button": "शुरू करें",
"secondary_button": "अभी नहीं"
},
"buy_musd": "mUSD खरीदें",
"get_musd": "mUSD प्राप्त करें",
- "bonus_title": "अपने स्टेबलकॉइंस पर {{percentage}}% पाएं",
- "bonus_description": "अपने स्टेबलकॉइंस को mUSD में कन्वर्ट करें और {{percentage}}% तक का बोनस प्राप्त करें।",
- "powered_by_relay": "Relay द्वारा संचालित"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Relay द्वारा संचालित",
+ "max": "मैक्स",
+ "quick_convert_button": "कन्वर्ट करें",
+ "learn_more": "ज़्यादा जानें",
+ "tooltip_title": "mUSD के साथ यील्ड कमाएं",
+ "tooltip_content": "अपने USDC, USDT, या DAI को mUSD में बदलें, MetaMask का डॉलर-समर्थित स्टेबलकॉइन। आपके द्वारा रखे गए हर डॉलर पर {{apy}} यील्ड कमाएं।",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "बदलाव नहीं हो पाया। कृपया दोबारा प्रयास करें।",
+ "confirmation": {
+ "title": "अधिकतम बदलें"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "रेट"
},
"bonus_claim": {
"toasts": {
"claiming": "आपका mUSD बोनस प्रोसेस हो रहा है",
"delivered": "आपका mUSD बोनस यहाँ है!",
- "failed": "Bonus claim failed"
+ "failed": "बोनस क्लेम नहीं हो पाया"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "पॉइंट्स अपने आप आपके अकाउंट में जुड़ जाएंगे।",
"tooltip_not_opted_in_footer": "अपने पॉइंट्स प्राप्त करने के लिए रिवॉर्ड्स के लिए ऑप्ट-इन करें।",
"tooltip_close": "बंद करें"
- }
+ },
+ "your_stablecoins": "आपके स्टेबलकॉइन"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "ट्रांसेक्शन फीस",
"metamask_fee": "MetaMask फीस",
"network_fee": "नेटवर्क फीस",
- "bridge_fee": "प्रदाता फीस ब्रिज करें"
+ "bridge_fee": "प्रदाता फीस ब्रिज करें",
+ "provider_fee": "प्रदाता फीस"
},
"title": {
"signature": "हस्ताक्षर का अनुरोध",
@@ -6260,10 +6381,10 @@
"transaction_fee": "हम आपके टोकन को Polygon पर USDC.e में स्वैप करेंगे, जो प्रिडिक्शन्स द्वारा इस्तेमाल होने वाला नेटवर्क है। स्वैप प्रदाता फीस ले सकते हैं, लेकिन MetaMask कोई फीस नहीं लेगा।"
},
"predict_withdraw": {
- "transaction_fee": "MetaMask आपके लिए आपके पसंदीदा टोकन में स्वैप करेगा। जब आप MUSD में स्वैप करते हैं तो कोई MetaMask फीस लागू नहीं होती है।"
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "mUSD कन्वर्शन फ़ीस में नेटवर्क कॉस्ट और प्रोवाइडर फ़ीस शामिल हो सकती है।"
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "फीस"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "कीमत का प्रभाव",
"price_impact_info_description": "प्राइस इम्पैक्ट यह दर्शाता है कि आपका स्वैप ऑर्डर एसेट की मार्केट प्राइस को कैसे प्रभावित करता है। यह ट्रेड के आकार और पूल में उपलब्ध लिक्विडिटी पर निर्भर करता है। MetaMask प्राइस इम्पैक्ट को प्रभावित या नियंत्रित नहीं करता। ",
"price_impact_info_gasless_description": "प्राइस इम्पैक्ट यह दर्शाता है कि आपका स्वैप ऑर्डर एसेट की मार्केट प्राइस को कैसे प्रभावित करता है। अगर आपके पास गैस के लिए पर्याप्त फंड नहीं हैं, तो आपके स्रोत टोकन का एक हिस्सा ऑटोमैटिकली फीस को कवर करने के लिए इस्तेमाल किया जाएगा, जिससे प्राइस इम्पैक्ट बढ़ जाता है। MetaMask प्राइस इम्पैक्ट को प्रभावित या नियंत्रित नहीं करता।",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "कैंसिल करें",
"slippage_info_title": "स्लिपेज (slippage)",
"slippage_info_description": "ट्रांसेक्शन कैंसिल होने से पहले आप जो प्राइस में % परिवर्तन स्वीकार करने के लिए तैयार हैं।",
"blockaid_error_title": "इस ट्रांसेक्शन को वापस किया जाएगा",
@@ -6470,13 +6596,14 @@
},
"submit": "सबमिट करें",
"default_slippage_description": "अगर प्राइस स्लिपेज परसेंट से ज़्यादा बदल जाता है, तो आपका ट्रांसेक्शन पूरा नहीं होगा।",
- "cancel": "कैंसिल करें",
"confirm": "कन्फर्म करें",
"exceeding_upper_slippage_warning": "ज़्यादा स्लिपेज (slippage), इससे स्वैप बिगड़ सकता है",
"exceeding_lower_slippage_warning": "कम स्लिपेज (slippage), इससे स्वैप बिगड़ सकता है",
"exceeding_lower_slippage_error": "{{value}}% से ज़्यादा वैल्यू डालें",
"exceeding_upper_slippage_error": "आप {{value}}% से ज़्यादा वैल्यू नहीं डाल सकते",
- "custom": "कस्टम"
+ "custom": "कस्टम",
+ "invalid_recipient_address": "एड्रेस ग़लत है",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "नये कोटेशन उपलब्ध हैं",
@@ -6773,12 +6900,16 @@
"title": "पासवर्ड दर्ज करें",
"description": "कार्ड विवरण देखने के लिए अपना वॉलेट पासवर्ड दर्ज करें।",
"description_unfreeze": "अपने कार्ड पर खर्च फिर से शुरू करने के लिए अपना वॉलेट पासवर्ड डालें।",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "पासवर्ड",
"confirm": "कन्फर्म करें",
"cancel": "कैंसिल करें",
"error_empty": "कृपया अपना पासवर्ड डालें",
"error_incorrect": "गलत पासवर्ड। कृपया फिर से प्रयास करें।"
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "अपना कार्ड चुनें",
"upgrade_title": "मेटल में अपग्रेड करें",
@@ -7094,6 +7225,9 @@
"view_card_details": "कार्ड विवरण देखें",
"hide_card_details": "कार्ड विवरण छिपाएँ",
"view_card_details_description": "कार्ड नंबर, समाप्ति तिथि और CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "लिमिट प्रबंधित करें",
"manage_spending_limit_description_restricted": "सीमित खर्च की सेटिंग चालू है",
"manage_spending_limit_description_full": "फुल एक्सेस चालू है",
@@ -7104,6 +7238,9 @@
"card_tos_title": "नियम और शर्त",
"order_metal_card": "मेटल कार्ड",
"order_metal_card_description": "अपना भौतिक मेटल कार्ड अभी ऑर्डर करें",
+ "cashback": "कैशबैक",
+ "cashback_description": "सारी खर्च पर 1% वापस कमाएं",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "कार्ड फ़्रीज़ करें",
"unfreeze_card": "कार्ड अनफ़्रीज़ करें",
"freeze_card_description": "अपने कार्ड पर सभी खर्च रोकें",
@@ -7141,6 +7278,19 @@
"retry": "फिर से प्रयास करें",
"on_linea": "Linea पर"
},
+ "cashback_screen": {
+ "title": "कैशबैक",
+ "available_cashback": "उपलब्ध कैशबैक",
+ "network_fee": "नेटवर्क फीस",
+ "expected_to_receive": "प्राप्त होने की उम्मीद",
+ "withdraw": "निकालें",
+ "withdraw_unavailable": "विदड्रॉवल उपलब्ध नहीं है",
+ "withdrawal_initiated": "विदड्रॉवल शुरू कर दिया गया है",
+ "withdrawal_success": "विदड्रॉवल सफलतापूर्वक पूरा हुआ",
+ "withdrawal_failed": "विदड्रॉवल नहीं हो पाया। कृपया फिर से प्रयास करें।",
+ "no_cashback": "कोई कैशबैक उपलब्ध नहीं है",
+ "loading_error": "कैशबैक लोड करना नहीं हो पाया। कृपया फिर से प्रयास करें।"
+ },
"change_asset": {
"title": "टोकन और नेटवर्क बदलें",
"full_spending_access": "पूर्ण खर्च की एक्सेस",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "भुगतान विधि चुनें",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "प्राप्त करने के लिए टोकन चुनें",
+ "no_gas": "गैस के लिए कोई नेटिव बैलेंस नहीं है"
},
"connection_removed_modal": {
"title": "कनेक्शन हटा दिए गए",
@@ -7315,7 +7465,10 @@
"service_not_available": "सेवा इस समय उपलब्ध नहीं है। कृपया थोड़ी देर में फिर से प्रयास करें।",
"invalid_referral_code": "रेफरल कोड ग़लत है। कृपया जांचें और फिर से प्रयास करें।",
"already_referred": "आप पहले ही किसी अन्य उपयोगकर्ता द्वारा रेफर किए जा चुके हैं।",
- "cannot_use_own_referral_code": "आप अपना स्वयं का रेफरल कोड उपयोग नहीं कर सकते।"
+ "cannot_use_own_referral_code": "आप अपना स्वयं का रेफरल कोड उपयोग नहीं कर सकते।",
+ "invalid_bonus_code": "बोनस कोड ग़लत है",
+ "already_redeemed": "आपने यह बोनस कोड पहले ही रिडीम कर लिया है",
+ "reached_maximum": "इस बोनस कोड का अधिकतम उपयोग संख्या पूरी हो गई है"
},
"claim_reward_error": {
"title": "रिवॉर्ड क्लेम नहीं हो पाया"
@@ -7372,8 +7525,10 @@
"predict": "प्रेडिक्शन",
"musd_deposit": "mUSD डिपॉज़िट",
"apply_referral_bonus": "रेफरल कोड बोनस",
+ "bonus_code": "बोनस कोड",
"uncategorized_event": "अवर्गीकृत इवेंट"
},
+ "code": "कोड",
"date": "तिथि",
"account": "अकाउंट",
"bonus": "बोनस",
@@ -7473,7 +7628,16 @@
"show_less": "कम दिखाएं",
"linking_progress": "एकाउंट्स जोड़ा जा रहा है... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} नामांकन किया गया",
- "add_all_accounts": "सभी एकाउंट जोड़ें"
+ "add_all_accounts": "सभी एकाउंट जोड़ें",
+ "environment_selector": "वातावरण",
+ "environment_cancel": "कैंसिल करें",
+ "environment_default": "डिफॉल्ट",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "रेफरल कोड",
@@ -7483,6 +7647,10 @@
"invalid_code": "रेफरल कोड ग़लत है",
"apply_button": "रेफरल कोड लागू करें"
},
+ "bonus_code": {
+ "input_placeholder": "बोनस कोड दर्ज करें",
+ "apply_success": "बोनस कोड लागू किया गया!"
+ },
"optout": {
"title": "रिवॉर्ड्स प्रगति हटाएँ",
"description": "यह क्रिया आपके खाते को रिवॉर्ड्स प्रोग्राम से हटा देगी और आपके पॉइंट्स व प्रगति को मिटा देगी। यह कार्रवाई वापस नहीं की जा सकती।",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "ब्रिज फीस",
+ "provider_fee": "प्रदाता फीस",
"network_fee": "नेटवर्क फीस",
"paid_with": "के साथ भुगतान किया गया",
+ "receive_token": "टोकन प्राप्त करें",
"retry_button": "फिर से प्रयास करें",
"total": "कुल",
"account": "अकाउंट",
- "received_total": "Received total"
+ "received_total": "कुल प्राप्त हुआ"
},
"summary_title": {
"bridge_approval": "{{approveSymbol}} एप्रूव करें",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "कनेक्शन नहीं मिला",
"description": "जारी रखने के लिए कृपया ऐप से नया कनेक्शन बनाएं।"
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "अब भी {{networkName}} से कनेक्ट किया जा रहा है...",
"unable_to_connect_network": "{{networkName}} से कनेक्ट करने में असमर्थ।",
"update_rpc": "RPC अपडेट करें",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "MetaMask डिफ़ॉल्ट RPC पर स्विच करें",
"check_network_connectivity": "अपनी नेटवर्क कनेक्टिविटी जाँचें।",
"check_network_connectivity_or": "अपनी नेटवर्क कनेक्टिविटी जाँचें या",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "MetaMask डिफ़ॉल्ट में अपडेट किया गया"
},
"trending": {
"title": "एक्सप्लोर करें",
"trending_tokens": "ट्रेंडिंग टोकन",
+ "stocks": "स्टॉक्स",
"price_change": "प्राइस में बदलाव",
"all_networks": "सभी नेटवर्क",
"24h": "24 घंटे",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "फिर से लोड करें",
"primary_action_acknowledge": "समझ गए"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "QR हार्डवेयर वॉलेट",
+ "hardware_wallet": "हार्डवेयर वॉलेट"
+ },
+ "common": {
+ "cancel": "कैंसिल करें",
+ "continue": "जारी रखें"
+ },
+ "connecting": {
+ "title": "अपना {{device}} कनेक्ट करें",
+ "searching": "{{device}} खोज रहे हैं…",
+ "tips_header": "आगे बढ़ने के लिए, सुनिश्चित करें:",
+ "tip_unlock": "आपका {{device}} अनलॉक है",
+ "tip_open_app": "Ethereum ऐप खुला है",
+ "tip_enable_bluetooth": "ब्लूटूथ चालू है",
+ "tip_dnd_off": "डू नॉट डिस्टर्ब बंद है",
+ "tip_bluetooth_permission": "लोकेशन और ब्लूटूथ की अनुमति दी गई है",
+ "tip_bluetooth_permission_v12": "नज़दीकी डिवाइस की अनुमति दी गई है",
+ "tip_stay_close": "आपका डिवाइस आपके फोन के पास रहता है"
+ },
+ "awaiting_app": {
+ "title": "{{app}} खोलें",
+ "message": "कृपया अपने {{device}} पर {{app}} ऐप खोलें",
+ "current_app": "वर्तमान में खुला: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "{{device}} पर कन्फर्म करें",
+ "title_message": "{{device}} पर साइन करें",
+ "message": "अपने {{device}} पर समीक्षा करें और कन्फर्म करें"
+ },
+ "success": {
+ "title": "{{device}} कनेक्ट किया गया"
+ },
+ "errors": {
+ "device_locked": "इसे अनलॉक करें और जारी रखने के लिए फिर से प्रयास करें",
+ "app_not_open": "कृपया अपने डिवाइस पर Ethereum ऐप खोलें",
+ "device_disconnected": "आपका {{device}} डिस्कनेक्ट हो गया। कृपया फिर से कनेक्ट करें और दोबारा प्रयास करें",
+ "device_not_found": "आपका {{device}} नहीं मिला। कृपया सुनिश्चित करें कि यह कनेक्टेड है",
+ "device_not_ready": "आपका डिवाइस तैयार नहीं है। कृपया इसे चेक करें और फिर से प्रयास करें",
+ "blind_signing": "ब्लाइंड साइनिंग बंद है। कृपया इसे अपने डिवाइस सेटिंग्स में चालू करें",
+ "connection_closed": "कनेक्शन चला गया। कृपया दोबारा प्रयास करें",
+ "connection_timeout": "कनेक्शन टाइम आउट हो गया। कृपया दोबारा प्रयास करें",
+ "user_cancelled": "यह क्रिया आपके डिवाइस पर कैंसिल कर दी गई",
+ "pending_confirmation": "आपके डिवाइस पर एक क्रिया अभी पूरी नहीं हुई है। कृपया इसे पूरा करें या कैंसिल करें",
+ "bluetooth_permission_denied": "आपके डिवाइस से कनेक्ट होने के लिए ब्लूटूथ की अनुमति आवश्यक है",
+ "location_permission_denied": "डिवाइस स्कैन करने के लिए लोकेशन की अनुमति आवश्यक है",
+ "nearby_permission_denied": "नज़दीकी डिवाइस की अनुमति आवश्यक है",
+ "bluetooth_off": "कृपया अपने डिवाइस से कनेक्ट करने के लिए ब्लूटूथ चालू करें",
+ "bluetooth_scan_failed": "डिवाइस स्कैन नहीं हो पाया। कृपया दोबारा प्रयास करें",
+ "bluetooth_connection_failed": "जारी रखने के लिए अपने डिवाइस पर ब्लूटूथ चालू करें",
+ "not_supported": "यह ऑपरेशन सपोर्टेड नहीं है",
+ "unknown_error": "सुनिश्चित करें कि आपका {{device}} इस अकाउंट के लिए सीक्रेट रिकवरी फ्रेज़ या पासफ़्रेज़ के साथ सेटअप किया गया है"
+ },
+ "error": {
+ "title": "कुछ गलत हो गया",
+ "default_title": "एक गड़बड़ी हुई",
+ "continue": "जारी रखें",
+ "retry": "फिर से प्रयास करें",
+ "view_settings": "सेटिंग्स देखें",
+ "device_locked_title": "{{device}} लॉक किया गया",
+ "device_disconnected_title": "{{device}} डिसकनेक्ट किया गया",
+ "device_not_found_title": "{{device}} नहीं पाया गया",
+ "app_not_open": "Ethereum ऐप खुला नहीं है",
+ "blind_signing_disabled": "ब्लाइंड साइनिंग बंद है",
+ "connection_timeout": "डिवाइस प्रतिक्रिया नहीं दे रहा है",
+ "connection_closed": "कनेक्शन चला गया",
+ "user_cancelled": "क्रिया कैंसिल हो गई",
+ "pending_confirmation": "कन्फर्मेशन अभी पूरा नहीं हुआ है",
+ "bluetooth_required": "ब्लूटूथ आवश्यक है",
+ "bluetooth_permission_denied": "ब्लूटूथ की अनुमति आवश्यक है",
+ "location_permission_denied": "लोकेशन की अनुमति आवश्यक है",
+ "nearby_devices_permission_denied": "नज़दीकी डिवाइस की अनुमति आवश्यक है",
+ "scan_failed": "स्कैन नहीं हो पाया",
+ "something_went_wrong": "कुछ गलत हो गया"
+ },
+ "device_selection": {
+ "title": "{{device}} चुनें",
+ "scanning": "डिवाइस के लिए स्कैन किया जा रहा है...",
+ "no_devices_found": "कोई डिवाइस नहीं पाया गया",
+ "no_devices_hint": "सुनिश्चित करें कि आपका {{device}} अनलॉक है और ब्लूटूथ चालू है",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "कनेक्ट करें",
+ "rescan": "फिर से स्कैन करें",
+ "unknown_device": "अज्ञात डिवाइस",
+ "signal_strength": "सिग्नल: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "टोकन",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFTs",
"import_nfts": "NFTs इंपोर्ट करें",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "अपने कलेक्टिबल्स आसानी से जोड़ें",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "कोई TP/SL नहीं"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "{{section}} लोड करने में असमर्थ",
+ "retry": "फिर से प्रयास करें"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/id.json b/locales/languages/id.json
index c825298f98d..e561569176c 100644
--- a/locales/languages/id.json
+++ b/locales/languages/id.json
@@ -103,6 +103,10 @@
"message": "Token {{ticker}} tidak cukup untuk menutupi biaya. Gunakan token di jaringan lain atau tambahkan token {{ticker}} untuk melanjutkan.",
"title": "Dana tidak cukup"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Rute pembayaran ini tidak tersedia untuk saat ini. Coba ubah jumlah, jaringan, atau token, dan kami akan mencari opsi terbaik.",
"title": "Tidak ada kuotasi"
@@ -1180,6 +1184,7 @@
"title": "Order baru",
"leverage": "Leverage",
"limit_price": "Harga limit",
+ "market_price": "Harga pasar",
"enter_price": "Harga masuk",
"trigger_price": "Harga pemicu",
"liquidation_price": "Harga likuidasi",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Dana tersedia untuk diperdagangkan",
"close_order_still_active": "Tutup order masih aktif",
"order_submitted": "Order dikirim",
+ "submitting_your_trade": "Mengirimkan perdagangan Anda",
"order_filled": "Order terpenuhi",
"order_placed": "Order dibuat",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1338,16 +1344,28 @@
},
"order_details": {
"title": "Detail Order",
- "cancel_order": "Batalkan Order",
+ "cancel_order": "Batalkan order",
"date": "Tanggal",
+ "trigger_condition": "Kondisi pemicu",
+ "price": "Harga",
"fee": "Biaya",
"limit_buy": "Batas Long",
"limit_price": "Harga Limit",
"limit_sell": "Batas Short",
"market_buy": "Pasar Long",
"market_sell": "Pasar Short",
+ "market": "Pasar",
"open": "Buka",
"size": "Ukuran",
+ "original_size": "Ukuran asli",
+ "order_value": "Nilai order",
+ "reduce_only": "Kurangi saja",
+ "yes": "Ya",
+ "no": "Tidak",
+ "price_above": "Harga di atas {{price}}",
+ "price_below": "Harga di bawah {{price}}",
+ "take_profit": "Take profit",
+ "stop_loss": "Stop loss",
"status": "Status",
"view_explorer": "Lihat di Explorer"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Wawasan pasar",
- "updated_ago": "Diperbarui {{time}}",
- "disclaimer": "Wawasan AI. Bukan nasihat keuangan.",
- "whats_driving_price": "Apa yang mendorong kenaikan harga?",
- "what_people_saying": "Apa yang dikatakan orang-orang",
+ "a_closer_look": "Melihat lebih dekat",
+ "whats_being_said": "Yang sedang dibicarakan",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Berdagang",
"sources_count": "+{{count}} sumber",
- "sources_title": "Teratas"
+ "sources_title": "News sources",
+ "feedback_submitted": "Umpan balik telah dikirim",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Umpan balik",
+ "description": "Bantu tingkatkan wawasan pasar yang dihasilkan AI.",
+ "not_relevant": "Tidak relevan",
+ "not_accurate": "Tidak akurat",
+ "hard_to_understand": "Sulit dipahami",
+ "harmful_or_offensive": "Berbahaya atau menyinggung",
+ "something_else": "Sesuatu yang lain",
+ "additional_feedback_label": "Umpan balik tambahan (opsional)",
+ "additional_feedback_placeholder": "Apa yang dapat kami tingkatkan?",
+ "characters_remaining": "{{count}} karakter tersisa",
+ "submit": "Kirim"
+ }
},
"predict": {
"title": "MetaMask Predictions",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Tersedia dalam waktu sekitar 1 menit",
"withdraw_completed": "Penarikan selesai",
"withdraw_completed_subtitle": "{{amount}} USDC dipindahkan ke dompet Anda",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} dipindahkan ke dompet Anda",
"error_title": "Terjadi kesalahan",
"error_description": "Gagal melanjutkan penarikan",
"try_again": "Coba lagi"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Ingatlah bahwa nilai ini merupakan estimasi dan akan difinalisasi setelah transaksi selesai. Poin dapat membutuhkan waktu hingga 1 jam untuk dikonfirmasi di saldo Reward Anda.",
"points_error": "Kami tidak dapat memuat poin saat ini",
"points_error_content": "Anda tetap akan memperoleh poin untuk transaksi ini. Kami akan memberi tahu Anda setelah poin ditambahkan ke akun. Anda juga dapat memeriksa tab reward dalam waktu satu jam.",
- "slippage": "Selip"
+ "slippage": "Selip",
+ "price_details": "Detail harga",
+ "prediction_order": "Order prediksi",
+ "prediction_order_description": "~{{count}} kontrak dengan harga {{price}} per kontrak. Jumlah akhir dapat bervariasi berdasarkan ketersediaan daftar order (hingga {{slippage}}%).",
+ "metamask_fee_description": "Biaya layanan untuk memproses prediksi ini",
+ "exchange_fee": "Biaya penukaran",
+ "exchange_fee_description": "Biaya yang dibayarkan ke bursa atau pasar",
+ "total_incl_fees": "termasuk biaya",
+ "close": "Tutup"
},
"error": {
"title": "Tidak dapat terhubung ke prediksi",
@@ -2399,7 +2440,7 @@
"add_tokens": "Impor token",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Ingin mengimpor token-token ini?",
"tokens_detected_in_account": "{{tokenCount}} {{tokensLabel}} baru ditemukan di akun ini",
"token_toast": {
"tokens_imported_title": "Token yang diimpor",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Desimal token wajib diisi.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "Kami tidak dapat menemukan token dengan nama tersebut.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Cari token apa pun dan impor token tersebut",
"select_token": "Pilih token",
"address_must_be_smart_contract": "Alamat pribadi terdeteksi. Masukkan alamat kontrak token.",
"billion_abbreviation": "M",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "Tambahkan URL RPC",
"add_block_explorer_url": "Tambahkan URL block explorer",
"networks_desc": "Tambahkan dan edit jaringan RPC khusus",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Jaringan yang Diaktifkan",
+ "networks_test_networks": "Jaringan Uji",
+ "networks_additional": "Jaringan Tambahan",
"networks_search_placeholder": "Cari jaringan",
"networks_no_results": "Jaringan tidak ditemukan",
"network_name_label": "Nama jaringan",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "Jaringan RPC",
"network_add_network": "Tambahkan jaringan",
"add_chain_title": "Tambahkan jaringan",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Cari berdasarkan nama, ID chain, atau mata uang",
+ "add_chain_loading": "Sedang memuat jaringan…",
+ "add_chain_error": "Gagal memuat jaringan. Coba lagi.",
"add_chain_retry": "Coba lagi",
- "add_chain_added": "Added",
+ "add_chain_added": "Ditambahkan",
"add_chain_or": "atau",
"add_chain_custom_link": "Tambahkan jaringan khusus",
"network_add_custom_network": "Tambahkan jaringan khusus",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Tambahkan jaringan uji",
"network_add": "Tambahkan",
"network_save": "Simpan",
"remove_network_title": "Apakah Anda ingin menghapus jaringan ini?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Simpan di tempat yang aman dan rahasia.",
"private_key_warning": "Ini merupakan kunci pribadi untuk akun yang dipilih saat ini: {{accountName}}. Jangan pernah mengungkapkan kunci ini. Siapa pun yang memiliki kunci pribadi Anda dapat mengontrol akun Anda sepenuhnya, termasuk mentransfer semua dana Anda.",
- "seed_phrase_warning_explanation": [
- "Pastikan tidak ada yang melihat layar Anda.",
- "Dukungan MetaMask tidak akan pernah memintanya."
- ],
+ "seed_phrase_warning_explanation": "Pastikan tidak ada orang yang melihat layar Anda. Dukungan MetaMask tidak akan pernah meminta hal ini.",
"private_key_warning_explanation": "Jangan pernah mengungkapkan kunci ini. Siapa pun yang memiliki kunci pribadi Anda dapat mengontrol akun Anda sepenuhnya, termasuk mentransfer semua dana Anda.",
+ "reveal_srp_description": "Frasa Pemulihan Rahasia memberikan akses penuh ke dompet Anda. Jangan membagikannya kepada siapa pun.",
"reveal_credential_modal": [
"{{credentialName}} memberikan ",
"akses penuh ke akun dan dana Anda.\n\nJangan membagikannya kepada siapa pun.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "Frasa Pemulihan Rahasia",
"private_key_text": "Kunci pribadi",
"got_it": "Mengerti",
- "learn_more": "Selengkapnya"
+ "learn_more": "Selengkapnya",
+ "copied_to_clipboard": "Disalin ke papan klip"
},
"screenshot_deterrent": {
"title": "Peringatan keamanan",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Mencoba mempercepat?",
"speedup_tx_message": "Mengirimkan upaya ini tidak menjamin percepatan transaksi awal Anda. Jika upaya percepatan berhasil, Anda akan dikenakan biaya transaksi di atas.",
"nevermind": "Abaikan",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Edit biaya gas",
"edit_priority": "Edit prioritas",
"gas_cancel_fee": "Biaya pembatalan gas",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Biaya transaksi dikaitkan dengan izin ini.",
"view_details": "Lihat detail",
"view_transaction_details": "Lihat detail transaksi",
- "view_data": "View data",
+ "view_data": "Lihat data",
"transaction_details": "Detail transaksi",
"site_url": "URL Situs",
"permission_request": "Permintaan izin",
@@ -3843,7 +3887,7 @@
"right_button": "Lindungi dompet"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Tambahkan ke favorit",
"title_label": "Nama",
"url_label": "URL",
"add_button": "Tambahkan",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Buka dengan ID Sentuh?",
"enable_faceid": "Buka dengan ID Wajah?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "Buka dengan sidik jari?",
+ "enable_biometrics": "Buka dengan biometrik?",
"enable_device_passcode_ios": "Buka dengan kode sandi perangkat?",
"enable_device_passcode_android": "Buka dengan PIN perangkat?"
},
@@ -4724,7 +4768,7 @@
"public_address": "Alamat publik",
"public_address_qr_code": "Alamat publik",
"coming_soon": "Segera hadir...",
- "request_payment": "Request payment",
+ "request_payment": "Permintaan pembayaran",
"copy": "Salin",
"scan_address": "Pindai alamat untuk menerima pembayaran",
"copy_address": "Salin alamat"
@@ -4738,8 +4782,8 @@
"switch_network": "Harap beralih ke mainnet atau sepolia",
"card_title": "Selalu tampilkan tombol Kartu MetaMask",
"card_desc": "Kartu MetaMask hanya tersedia untuk penduduk negara tertentu.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "Gunakan lingkungan demo DaimoPay",
+ "daimo_demo_desc": "Beralih antara lingkungan demo dan produksi DaimoPay untuk pembayaran kartu."
},
"walletconnect_sessions": {
"no_active_sessions": "Anda tidak memiliki sesi aktif",
@@ -4752,10 +4796,10 @@
"close_current_session": "Tutup sesi saat ini sebelum memulai yang baru."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Permintaan pembayaran",
+ "title_complete": "Pembayaran selesai",
"confirm": "Bayar",
- "cancel": "Decline",
+ "cancel": "Tolak",
"is_requesting_you_to_pay": "meminta Anda untuk membayar",
"total": "Total:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Tidak ada metode pembayaran yang tersedia.",
"error_fetching_quotes": "Terjadi kesalahan. Coba lagi.",
"no_quotes_available": "Penyedia tidak tersedia.",
+ "quote_unavailable": "Kuotasi tidak tersedia.",
"providers": "Penyedia",
+ "quotes_displayed_for": "Kuotasi yang ditampilkan untuk {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Digunakan sebelumnya",
+ "best_rate": "Tarif terbaik",
+ "most_reliable": "Paling dapat diandalkan",
"continue": "Lanjutkan",
"powered_by_provider": "Didukung oleh {{provider}}",
"purchased_currency": "Membeli {{currency}}",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Kesalahan saat keluar"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Tidak tersedia",
+ "description": "{{token}} tidak tersedia dengan {{provider}} di wilayah Anda.",
+ "change_token": "Ubah token",
+ "change_provider": "Ubah penyedia"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Pilih penyedia"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Mengerti",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Ubah penyedia"
},
"fiat_on_ramp_aggregator": {
"buy": "beli",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "Deposit {{currency}}"
},
+ "ramps_order_details": {
+ "title": "Detail order",
+ "status": "Status",
+ "processing": "Memproses",
+ "complete": "Selesaikan",
+ "failed": "Gagal",
+ "cancelled": "Dibatalkan",
+ "view_on_provider": "Lihat di {{provider}}",
+ "order_id": "ID Order",
+ "date_and_time": "Tanggal dan waktu",
+ "fees": "Biaya",
+ "total": "Total",
+ "card_processing_info": "Pembelian kartu umumnya memerlukan waktu beberapa menit",
+ "processing_info_modal_description": "Pembelian menggunakan kartu umumnya hanya membutuhkan beberapa menit. Anda dapat menghubungi dukungan pelanggan jika ada pertanyaan.",
+ "go_to_provider_support": "Kunjungi halaman dukungan {{provider}}",
+ "error_title": "Terjadi kesalahan",
+ "error_message": "Tidak dapat memuat detail order. Coba lagi.",
+ "try_again": "Coba lagi",
+ "close": "Tutup"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Memproses pembelian {{cryptocurrency}}",
+ "purchase_pending_description": "Hanya perlu beberapa menit...",
+ "purchase_completed_title": "Pembelian senilai {{amount}} {{cryptocurrency}} berhasil!",
+ "purchase_completed_description": "{{cryptocurrency}} kini telah tersedia",
+ "purchase_failed_title": "Pembelian {{cryptocurrency}} gagal",
+ "purchase_failed_description": "Cobalah beberapa saat lagi",
+ "purchase_cancelled_title": "Pembelian dibatalkan",
+ "purchase_cancelled_description": "Pembelian {{cryptocurrency}} telah dibatalkan",
+ "track": "Lacak"
+ }
+ },
"swaps": {
"title": "Swap",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Agar hal ini tidak terjadi lagi, pastikan untuk selalu memperbarui aplikasi dan OS MetaMask ke versi terbaru."
},
"srp_security_quiz": {
+ "question_step": "Pertanyaan {{step}} dari {{total}}",
"title": "Kuis keamanan",
"introduction": "Untuk mengungkapkan Frasa Pemulihan Rahasia, Anda perlu menjawab dua pertanyaan dengan benar",
"get_started": "Mulai",
"learn_more": "Pelajari selengkapnya",
"try_again": "Coba lagi",
"continue": "Lanjutkan",
+ "correct": "Benar!",
+ "incorrect": "Salah!",
"of": "dari",
"question_one": {
"question": "Jika Anda kehilangan Frasa Pemulihan Rahasia, MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "Instalasi {{snap}} gagal."
},
"earn": {
- "claimable_bonus_tooltip": "Bonus tahunan yang dapat diklaim setiap hari dari dompet Anda.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Dapatkan bonus sebesar {{percentage}}%",
"claimable_bonus": "Bonus yang dapat diklaim",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Klaim bonus",
+ "claim_bonus_subtitle": "Bonus akan dibayarkan melalui {{networkName}}.",
"empty_state_cta": {
"heading": "Pinjamkan {{tokenSymbol}} dan hasilkan",
"body": "Pinjamkan {{tokenSymbol}} Anda dengan {{protocol}} dan dapatkan",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Kami sedang dalam perbaikan dan akan segera kembali online!"
},
- "supply": "Supply",
+ "supply": "Suplai",
"deposit": "Deposit",
"approve": "Setujui",
"approval": "Persetujuan",
@@ -5887,7 +5978,7 @@
"network": "Jaringan",
"health_factor": "Faktor kesehatan",
"liquidation_risk": "Risiko likuidasi",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Likuiditas pool tidak cukup",
"available_to_withdraw": "tersedia untuk ditarik",
"unknown": "tidak dikenal",
"how_it_works": "Cara kerjanya",
@@ -5906,7 +5997,7 @@
"lending": "Riwayat posisi",
"staking": "Riwayat pembayaran"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Reset tunjangan",
"tron": {
"fee": "Biaya"
},
@@ -5914,32 +6005,60 @@
"ok": "Oke",
"continue": "Lanjutkan",
"convert_and_get_percentage_bonus": "Konversi dan dapatkan {{percentage}}%",
+ "your_musd": "mUSD Anda",
+ "balance_breakdown_title": "Saldo mUSD Anda berdasarkan jaringan",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "Konversikan ke mUSD",
"get_a_percentage_musd_bonus": "Dapatkan bonus {{percentage}}% mUSD",
"convert": "Konversikan",
+ "fetching_quote": "Mengambil kuotasi...",
+ "you_convert": "Anda mengonversi",
+ "network_fee": "Biaya jaringan",
+ "earning": "Penghasilan",
+ "quick_convert_description": "Pilih token untuk mengonversi seluruh saldo Anda atau ketuk ikon edit untuk memasukkan jumlah khusus.",
+ "no_tokens_to_convert": "Anda tidak memiliki token yang dapat dikonversi ke mUSD.",
"toasts": {
"converting": "Mengonversi {{token}} → mUSD",
"eta": "~{{time}}",
- "delivered": "mUSD telah tiba!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "Konversi mUSD gagal"
},
"education": {
"heading": "DAPATKAN {{percentage}}% PADA\nSTABLECOIN",
- "description": "Konversikan stablecoin ke mUSD, stablecoin berbasis dolar AS milik MetaMask, dan dapatkan bonus hingga {{percentage}}%.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Syarat berlaku.",
"primary_button": "Mulai",
"secondary_button": "Tidak sekarang"
},
"buy_musd": "Beli mUSD",
"get_musd": "Dapatkan mUSD",
- "bonus_title": "Dapatkan {{percentage}}% pada stablecoin",
- "bonus_description": "Konversikan stablecoin ke mUSD dan dapatkan bonus hingga {{percentage}}%.",
- "powered_by_relay": "Didukung oleh Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Didukung oleh Relay",
+ "max": "Maks",
+ "quick_convert_button": "Konversikan",
+ "learn_more": "Selengkapnya",
+ "tooltip_title": "Dapatkan keuntungan dengan mUSD",
+ "tooltip_content": "Konversikan USDC, USDT, atau DAI dengan mUSD, stablecoin berbasis dolar milik MetaMask. Dapatkan keuntungan {{apy}} untuk setiap dolar yang Anda miliki.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Konversi gagal. Coba lagi.",
+ "confirmation": {
+ "title": "Konversi maksimal"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Tingkat"
},
"bonus_claim": {
"toasts": {
"claiming": "Bonus mUSD sedang diproses",
"delivered": "Bonus mUSD telah tiba!",
- "failed": "Bonus claim failed"
+ "failed": "Klaim bonus gagal"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Poin akan ditambahkan secara otomatis ke akun Anda.",
"tooltip_not_opted_in_footer": "Ikuti program reward untuk mendapatkan poin Anda.",
"tooltip_close": "Tutup"
- }
+ },
+ "your_stablecoins": "Stablecoin Anda"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Biaya transaksi",
"metamask_fee": "Biaya MetaMask",
"network_fee": "Biaya jaringan",
- "bridge_fee": "Biaya penyedia bridge"
+ "bridge_fee": "Biaya penyedia bridge",
+ "provider_fee": "Biaya penyedia"
},
"title": {
"signature": "Permintaan tanda tangan",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Kami akan menukar token Anda dengan USDC.e di Polygon, jaringan yang digunakan oleh Predictions. Penyedia swap mungkin akan mengenakan biaya, tetapi MetaMask tidak."
},
"predict_withdraw": {
- "transaction_fee": "MetaMask akan menukar token Anda ke token yang Anda inginkan. MetaMask tidak mengenakan biaya saat Anda menukar ke MUSD."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "Biaya konversi mUSD mencakup biaya jaringan dan mungkin termasuk biaya penyedia layanan."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Biaya"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Dampak harga",
"price_impact_info_description": "Dampak harga mencerminkan bagaimana order swap Anda memengaruhi harga pasar aset. Hal ini bergantung pada ukuran perdagangan dan likuiditas yang tersedia di pool. MetaMask tidak memengaruhi atau mengontrol dampak harga.",
"price_impact_info_gasless_description": "Dampak harga mencerminkan bagaimana perintah swap Anda memengaruhi harga pasar aset. Jika Anda tidak memiliki cukup dana untuk gas, sebagian token sumber akan dialokasikan secara otomatis untuk menutupi biaya, yang meningkatkan dampak harga. MetaMask tidak memengaruhi atau mengendalikan dampak harga.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Batal",
"slippage_info_title": "Selip",
"slippage_info_description": "% perubahan harga yang Anda bersedia izinkan sebelum transaksi dibatalkan.",
"blockaid_error_title": "Transaksi ini akan dikembalikan",
@@ -6470,13 +6596,14 @@
},
"submit": "Kirim",
"default_slippage_description": "Transaksi Anda tidak akan berhasil jika harga berubah lebih dari persentase selip.",
- "cancel": "Batal",
"confirm": "Konfirmasikan",
"exceeding_upper_slippage_warning": "Selip tinggi, ini dapat mengakibatkan swap yang tidak menguntungkan",
"exceeding_lower_slippage_warning": "Selip rendah, ini dapat mengakibatkan swap yang tidak menguntungkan",
"exceeding_lower_slippage_error": "Masukkan nilai yang lebih besar dari {{value}}%",
"exceeding_upper_slippage_error": "Anda tidak dapat memasukkan nilai yang lebih besar dari {{value}}%",
- "custom": "Kustom"
+ "custom": "Kustom",
+ "invalid_recipient_address": "Alamat tidak valid",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Kuotasi baru tersedia",
@@ -6773,12 +6900,16 @@
"title": "Masukkan kata sandi",
"description": "Masukkan kata sandi dompet untuk melihat detail kartu.",
"description_unfreeze": "Masukkan kata sandi dompet Anda untuk melanjutkan penggunaan kartu.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Kata sandi",
"confirm": "Konfirmasikan",
"cancel": "Batalkan",
"error_empty": "Masukkan kata sandi",
"error_incorrect": "Kata sandi salah. Coba lagi."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Pilih kartu",
"upgrade_title": "Upgrade ke Logam",
@@ -7094,6 +7225,9 @@
"view_card_details": "Lihat detail kartu",
"hide_card_details": "Sembunyikan detail kartu",
"view_card_details_description": "Nomor kartu, tanggal kedaluwarsa, dan CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Kelola batasan",
"manage_spending_limit_description_restricted": "Penggunaan terbatas aktif",
"manage_spending_limit_description_full": "Akses penuh aktif",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Syarat dan Ketentuan",
"order_metal_card": "Kartu Logam",
"order_metal_card_description": "Pesan Kartu Logam fisik sekarang",
+ "cashback": "Cashback",
+ "cashback_description": "Dapatkan pengembalian 1% untuk semua penggunaan",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Blokir kartu",
"unfreeze_card": "Buka blokir kartu",
"freeze_card_description": "Jeda semua penggunaan kartu",
@@ -7141,6 +7278,19 @@
"retry": "Coba lagi",
"on_linea": "di Linea"
},
+ "cashback_screen": {
+ "title": "Cashback",
+ "available_cashback": "Cashback yang tersedia",
+ "network_fee": "Biaya jaringan",
+ "expected_to_receive": "Diharapkan menerima",
+ "withdraw": "Tarik",
+ "withdraw_unavailable": "Penarikan tidak tersedia",
+ "withdrawal_initiated": "Penarikan telah dimulai",
+ "withdrawal_success": "Penarikan berhasil diselesaikan",
+ "withdrawal_failed": "Penarikan gagal. Coba lagi.",
+ "no_cashback": "Cashback tidak tersedia",
+ "loading_error": "Gagal memuat cashback. Coba lagi."
+ },
"change_asset": {
"title": "Ubah token dan jaringan",
"full_spending_access": "Akses penggunaan penuh",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Pilih metode pembayaran",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Pilih terima token",
+ "no_gas": "Tidak ada saldo asli untuk gas"
},
"connection_removed_modal": {
"title": "Koneksi dihapus",
@@ -7315,7 +7465,10 @@
"service_not_available": "Layanan tidak tersedia untuk saat ini. Coba lagi nanti.",
"invalid_referral_code": "Kode referensi tidak valid. Periksa dan coba lagi.",
"already_referred": "Anda telah dirujuk oleh pengguna lain.",
- "cannot_use_own_referral_code": "Anda tidak dapat menggunakan kode referensi milik Anda sendiri."
+ "cannot_use_own_referral_code": "Anda tidak dapat menggunakan kode referensi milik Anda sendiri.",
+ "invalid_bonus_code": "Kode bonus tidak valid",
+ "already_redeemed": "Anda telah menukarkan kode bonus ini",
+ "reached_maximum": "Kode bonus ini telah mencapai jumlah penggunaan maksimum"
},
"claim_reward_error": {
"title": "Gagal mengklaim reward"
@@ -7372,8 +7525,10 @@
"predict": "Prediksi",
"musd_deposit": "Deposit mUSD",
"apply_referral_bonus": "Bonus kode referensi",
+ "bonus_code": "Kode bonus",
"uncategorized_event": "Acara yang tidak dikategorikan"
},
+ "code": "Kode",
"date": "Tanggal",
"account": "Akun",
"bonus": "Bonus",
@@ -7473,7 +7628,16 @@
"show_less": "Ciutkan",
"linking_progress": "Menambahkan akun... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} terdaftar",
- "add_all_accounts": "Tambahkan semua akun"
+ "add_all_accounts": "Tambahkan semua akun",
+ "environment_selector": "Lingkungan",
+ "environment_cancel": "Batal",
+ "environment_default": "Default",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Kode Referensi",
@@ -7483,6 +7647,10 @@
"invalid_code": "Kode referensi tidak valid",
"apply_button": "Gunakan kode referensi"
},
+ "bonus_code": {
+ "input_placeholder": "Masukkan kode bonus",
+ "apply_success": "Kode bonus telah diterapkan!"
+ },
"optout": {
"title": "Hapus progres Rewards",
"description": "Tindakan ini akan menghapus akun Anda dari program Rewards serta menghapus semua poin dan progres Anda. Tindakan ini tidak dapat dibatalkan.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Biaya bridge",
+ "provider_fee": "Biaya penyedia",
"network_fee": "Biaya jaringan",
"paid_with": "Dibayar dengan",
+ "receive_token": "Terima token",
"retry_button": "Coba lagi",
"total": "Total",
"account": "Akun",
- "received_total": "Received total"
+ "received_total": "Total yang diterima"
},
"summary_title": {
"bridge_approval": "Setujui {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Koneksi Tidak Ditemukan",
"description": "Buat koneksi baru dari aplikasi untuk melanjutkan."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Masih terhubung ke {{networkName}}...",
"unable_to_connect_network": "Tidak dapat terhubung ke {{networkName}}.",
"update_rpc": "Perbarui RPC",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Beralih ke RPC default MetaMask",
"check_network_connectivity": "Periksa konektivitas jaringan Anda.",
"check_network_connectivity_or": "Periksa konektivitas jaringan Anda atau",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Diperbarui ke default MetaMask"
},
"trending": {
"title": "Jelajahi",
"trending_tokens": "Token yang tren",
+ "stocks": "Saham",
"price_change": "Perubahan harga",
"all_networks": "Semua jaringan",
"24h": "24 jam",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Muat ulang",
"primary_action_acknowledge": "Mengerti"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "Dompet Perangkat Keras QR",
+ "hardware_wallet": "Dompet Perangkat Keras"
+ },
+ "common": {
+ "cancel": "Batal",
+ "continue": "Lanjutkan"
+ },
+ "connecting": {
+ "title": "Hubungkan {{device}} Anda",
+ "searching": "Mencari {{device}}...",
+ "tips_header": "Untuk melanjutkan, pastikan:",
+ "tip_unlock": "{{device}} Anda tidak terkunci",
+ "tip_open_app": "Aplikasi Ethereum sudah dibuka",
+ "tip_enable_bluetooth": "Bluetooth diaktifkan",
+ "tip_dnd_off": "Jangan Ganggu dinonaktifkan",
+ "tip_bluetooth_permission": "Izin lokasi dan Bluetooth telah diberikan",
+ "tip_bluetooth_permission_v12": "Izin perangkat terdekat telah diberikan",
+ "tip_stay_close": "Perangkat Anda tetap berada di dekat ponsel Anda"
+ },
+ "awaiting_app": {
+ "title": "Buka {{app}}",
+ "message": "Buka aplikasi {{app}} di {{device}} Anda",
+ "current_app": "Saat ini dibuka: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Konfirmasikan melalui {{device}}",
+ "title_message": "Tandatangani melalui {{device}}",
+ "message": "Tinjau dan konfirmasikan melalui {{device}} Anda"
+ },
+ "success": {
+ "title": "{{device}} terhubung"
+ },
+ "errors": {
+ "device_locked": "Buka dan coba lagi untuk melanjutkan",
+ "app_not_open": "Buka aplikasi Ethereum di perangkat Anda",
+ "device_disconnected": "Koneksi {{device}} Anda telah terputus. Hubungkan kembali dan coba lagi",
+ "device_not_found": "{{device}} tidak dapat ditemukan. Pastikan perangkat Anda terhubung",
+ "device_not_ready": "Perangkat Anda belum siap. Periksa dan coba lagi",
+ "blind_signing": "Penandatanganan buta dinonaktifkan. Aktifkan di pengaturan perangkat Anda",
+ "connection_closed": "Koneksi terputus. Coba lagi",
+ "connection_timeout": "Waktu koneksi telah habis. Coba lagi",
+ "user_cancelled": "Tindakan dibatalkan di perangkat Anda",
+ "pending_confirmation": "Terdapat tindakan yang tertunda di perangkat Anda. Selesaikan atau batalkan tindakan tersebut",
+ "bluetooth_permission_denied": "Izin Bluetooth diperlukan untuk terhubung ke perangkat Anda",
+ "location_permission_denied": "Izin lokasi diperlukan untuk memindai perangkat",
+ "nearby_permission_denied": "Izin perangkat terdekat diperlukan",
+ "bluetooth_off": "Aktifkan Bluetooth untuk terhubung ke perangkat Anda",
+ "bluetooth_scan_failed": "Gagal memindai perangkat. Coba lagi",
+ "bluetooth_connection_failed": "Aktifkan Bluetooth di perangkat Anda untuk melanjutkan",
+ "not_supported": "Operasi ini tidak didukung",
+ "unknown_error": "Pastikan {{device}} Anda telah diatur dengan Frasa Pemulihan Rahasia atau passphrase untuk akun ini"
+ },
+ "error": {
+ "title": "Terjadi kesalahan",
+ "default_title": "Terjadi kesalahan",
+ "continue": "Lanjutkan",
+ "retry": "Coba lagi",
+ "view_settings": "Lihat Pengaturan",
+ "device_locked_title": "{{device}} terkunci",
+ "device_disconnected_title": "Koneksi {{device}} terputus",
+ "device_not_found_title": "{{device}} tidak ditemukan",
+ "app_not_open": "Aplikasi Ethereum Tidak Terbuka",
+ "blind_signing_disabled": "Penandatanganan Buta Dinonaktifkan",
+ "connection_timeout": "Perangkat Tidak Merespons",
+ "connection_closed": "Koneksi Terputus",
+ "user_cancelled": "Tindakan Dibatalkan",
+ "pending_confirmation": "Menunggu Konfirmasi",
+ "bluetooth_required": "Bluetooth diperlukan",
+ "bluetooth_permission_denied": "Izin Bluetooth Diperlukan",
+ "location_permission_denied": "Izin Lokasi Diperlukan",
+ "nearby_devices_permission_denied": "Izin Perangkat Terdekat Diperlukan",
+ "scan_failed": "Pemindaian Gagal",
+ "something_went_wrong": "Terjadi kesalahan"
+ },
+ "device_selection": {
+ "title": "Pilih {{device}}",
+ "scanning": "Memindai perangkat...",
+ "no_devices_found": "Perangkat tidak ditemukan",
+ "no_devices_hint": "Pastikan {{device}} Anda tidak terkunci dan Bluetooth diaktifkan",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Hubungkan",
+ "rescan": "Pindai Lagi",
+ "unknown_device": "Perangkat Tidak Dikenal",
+ "signal_strength": "Sinyal: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Token",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT",
"import_nfts": "Impor NFT",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Tambahkan koleksi Anda dengan mudah",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Tidak ada TP/SL"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "Tidak dapat memuat {{section}}",
+ "retry": "Coba lagi"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/ja.json b/locales/languages/ja.json
index 4bb346917ee..646b257d951 100644
--- a/locales/languages/ja.json
+++ b/locales/languages/ja.json
@@ -103,6 +103,10 @@
"message": "手数料の支払いに十分な{{ticker}}がありません。続行するには、別のネットワークのトークンを使用するか、{{ticker}}をさらに追加してください。",
"title": "資金不足"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "この決済ルートは現在使用できません。金額、ネットワーク、またはトークンを変更してみてください。最善のオプションを探します。",
"title": "クォートがありません"
@@ -1180,6 +1184,7 @@
"title": "新規注文",
"leverage": "レバレッジ",
"limit_price": "リミット価格",
+ "market_price": "市場価格",
"enter_price": "価格を入力してください",
"trigger_price": "トリガー価格",
"liquidation_price": "清算価格",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "資金を取引に利用できます",
"close_order_still_active": "クローズ注文は引き続き有効です",
"order_submitted": "注文が送信されました",
+ "submitting_your_trade": "取引を送信中",
"order_filled": "注文が約定しました",
"order_placed": "注文が発注されました",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "注文詳細",
"cancel_order": "注文取消",
"date": "日付",
+ "trigger_condition": "トリガー条件",
+ "price": "価格",
"fee": "手数料",
"limit_buy": "リミットロング",
"limit_price": "リミット価格",
"limit_sell": "リミットショート",
"market_buy": "成行ロング",
"market_sell": "成行ショート",
+ "market": "マーケット",
"open": "未約定",
"size": "サイズ",
+ "original_size": "元のサイズ",
+ "order_value": "注文額",
+ "reduce_only": "リデュースオンリー",
+ "yes": "はい",
+ "no": "いいえ",
+ "price_above": "価格が{{price}}を超えた場合",
+ "price_below": "価格が{{price}}を下回った場合",
+ "take_profit": "利益確定",
+ "stop_loss": "ストップロス",
"status": "ステータス",
"view_explorer": "エクスプローラーで表示"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "市場分析情報",
- "updated_ago": "{{time}}に更新",
- "disclaimer": "AI分析情報であり、金融に関するアドバイスではありません。",
- "whats_driving_price": "価格はなぜ変動するのですか?",
- "what_people_saying": "人々のコメント",
+ "a_closer_look": "詳細",
+ "whats_being_said": "市場の声",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "取引",
"sources_count": "他{{count}}件のソース",
- "sources_title": "ソース"
+ "sources_title": "News sources",
+ "feedback_submitted": "フィードバックが送信されました",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "フィードバック",
+ "description": "AIが生成した市場分析情報の改善にご協力ください。",
+ "not_relevant": "関連性が低い",
+ "not_accurate": "正確でない",
+ "hard_to_understand": "わかりにくい",
+ "harmful_or_offensive": "有害または不快",
+ "something_else": "その他",
+ "additional_feedback_label": "その他のご意見 (任意)",
+ "additional_feedback_placeholder": "どのような改善が必要ですか?",
+ "characters_remaining": "残り{{count}}文字",
+ "submit": "送信"
+ }
},
"predict": {
"title": "MetaMask 予測",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "約1分後にご利用可能となります",
"withdraw_completed": "出金完了",
"withdraw_completed_subtitle": "{{amount}} USDCがウォレットに移動されました",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}}がウォレットに移動されました",
"error_title": "問題が発生しました",
"error_description": "出金に失敗しました",
"try_again": "再試行してください"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "この金額は見積もりであり、トランザクション完了後に確定されることをご留意ください。ポイントがリワード残高で確定されるまでに最長1時間かかる場合があります。",
"points_error": "現在、ポイントを読み込めません",
"points_error_content": "このトランザクションで引き続きポイントを獲得できます。ポイントがアカウントに追加され次第、お知らせいたします。約1時間後に「リワード」タブにてご確認いただくことも可能です。",
- "slippage": "スリッページ"
+ "slippage": "スリッページ",
+ "price_details": "価格の詳細",
+ "prediction_order": "予測の注文",
+ "prediction_order_description": "約{{count}}枚のコントラクト (1枚あたり{{price}})。最終的な金額は、オーダーブックの状況により変動する可能性があります (最大{{slippage}}%)。",
+ "metamask_fee_description": "この予測を処理するためのサービス手数料",
+ "exchange_fee": "取引所手数料",
+ "exchange_fee_description": "取引所または市場に支払われる手数料",
+ "total_incl_fees": "手数料込",
+ "close": "閉じる"
},
"error": {
"title": "予想に接続できません",
@@ -2399,7 +2440,7 @@
"add_tokens": "トークンをインポート",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "これらのトークンをインポートしますか?",
"tokens_detected_in_account": "このアカウントに{{tokenCount}}件の新しい{{tokensLabel}}が見つかりました",
"token_toast": {
"tokens_imported_title": "トークンをインポートしました",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "トークンの小数点以下は必須入力項目です。",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "その名前のトークンは見つかりませんでした。",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "お好きなトークンを検索して、インポートしてください",
"select_token": "トークンを選択",
"address_must_be_smart_contract": "パーソナルアドレスが検出されました。トークンのコントラクトアドレスを入力してください。",
"billion_abbreviation": "B",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "RPC URLを追加",
"add_block_explorer_url": "ブロックエクスプローラーURLを追加",
"networks_desc": "カスタムRPCネットワークの追加と編集",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "有効なネットワーク",
+ "networks_test_networks": "テストネットワーク",
+ "networks_additional": "他のネットワーク",
"networks_search_placeholder": "ネットワークを検索",
"networks_no_results": "ネットワークが見つかりません",
"network_name_label": "ネットワーク名",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "RPCネットワーク",
"network_add_network": "ネットワークを追加",
"add_chain_title": "ネットワークを追加",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "名前、チェーンID、通貨で検索できます",
+ "add_chain_loading": "ネットワークを読み込み中…",
+ "add_chain_error": "ネットワークを読み込めませんでした。もう一度お試しください。",
"add_chain_retry": "再試行",
- "add_chain_added": "Added",
+ "add_chain_added": "追加されました",
"add_chain_or": "または",
"add_chain_custom_link": "カスタムネットワークを追加",
"network_add_custom_network": "カスタムネットワークを追加",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "テストネットワークを追加",
"network_add": "追加",
"network_save": "保存",
"remove_network_title": "このネットワークを削除しますか?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "安全な秘密の場所に保管してください。",
"private_key_warning": "これは現在選択されたアカウント {{accountName}} の秘密鍵です。この鍵は絶対に開示しないでください。資金の送金を含め、秘密鍵を持っていれば誰でもアカウントを完全にコントロールできます。",
- "seed_phrase_warning_explanation": [
- "誰にも画面を見られていないことを確認してください。",
- "MetaMaskサポートがこれを求めることはありません。"
- ],
+ "seed_phrase_warning_explanation": "誰にも画面を見られていないことを確認してください。MetaMaskサポートがフレーズをお尋ねすることはありません。",
"private_key_warning_explanation": "この鍵は絶対に開示しないでください。資金の送金を含め、秘密鍵を持っていれば誰でもアカウントを完全にコントロールできます。",
+ "reveal_srp_description": "シークレットリカバリーフレーズがあれば、ウォレットに自由にアクセスできます。このフレーズは誰にも教えないでください。",
"reveal_credential_modal": [
"{{credentialName}}があれば、",
"アカウントと資金に完全にアクセスできます。\n\n誰にも教えないでください。",
@@ -3385,7 +3424,8 @@
"srp_text": "シークレットリカバリーフレーズです。",
"private_key_text": "秘密鍵",
"got_it": "了解",
- "learn_more": "詳細"
+ "learn_more": "詳細",
+ "copied_to_clipboard": "クリップボードにコピーしました"
},
"screenshot_deterrent": {
"title": "安全性に関するアラート",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "スピードアップを試みますか?",
"speedup_tx_message": "このリクエストを実行しても、元のトランザクションが速くなる保証はありません。スピードアップできた場合、上記のトランザクション手数料が発生します。",
"nevermind": "キャンセル",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "ガス代を編集",
"edit_priority": "優先度を編集",
"gas_cancel_fee": "キャンセルのガス代",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "この許可に関連するトランザクション手数料。",
"view_details": "詳細を表示",
"view_transaction_details": "トランザクションの詳細を表示",
- "view_data": "View data",
+ "view_data": "データを表示",
"transaction_details": "トランザクションの詳細",
"site_url": "サイトURL",
"permission_request": "許可のリクエスト",
@@ -3843,7 +3887,7 @@
"right_button": "ウォレットを保護"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "お気に入りに追加",
"title_label": "名前",
"url_label": "URL",
"add_button": "追加",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Touch IDでロックを解除しますか?",
"enable_faceid": "Face IDでロックを解除しますか?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "指紋でロックを解除しますか?",
+ "enable_biometrics": "生体認証でロックを解除しますか?",
"enable_device_passcode_ios": "デバイスのパスコードでロックを解除しますか?",
"enable_device_passcode_android": "デバイスの暗証番号でロックを解除しますか?"
},
@@ -4724,7 +4768,7 @@
"public_address": "パブリックアドレス",
"public_address_qr_code": "パブリックアドレス",
"coming_soon": "近日追加予定...",
- "request_payment": "Request payment",
+ "request_payment": "支払いをリクエスト",
"copy": "コピー",
"scan_address": "支払いを受け取るにはアドレスをスキャンしてください",
"copy_address": "アドレスをコピー"
@@ -4738,8 +4782,8 @@
"switch_network": "メインネットまたはSepoliaに切り替えてください",
"card_title": "MetaMaskカードボタンを常に表示する",
"card_desc": "MetaMaskカードは一部の国にお住まいの方のみご利用いただけます.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "DaimoPayのデモ環境を使用する",
+ "daimo_demo_desc": "カード決済のためのDaimoPayのデモ環境と本番環境を切り替えます。"
},
"walletconnect_sessions": {
"no_active_sessions": "アクティブなセッションがありません",
@@ -4752,10 +4796,10 @@
"close_current_session": "新しいセッションを開始する前に、現在のセッションを閉じてください。"
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "支払いのリクエスト",
+ "title_complete": "支払い完了",
"confirm": "支払う",
- "cancel": "Decline",
+ "cancel": "拒否",
"is_requesting_you_to_pay": "が支払いを求めています",
"total": "合計:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "使用可能な支払方法がありません。",
"error_fetching_quotes": "問題が発生しました。もう一度お試しください。",
"no_quotes_available": "利用可能なプロバイダーがありません。",
+ "quote_unavailable": "クォートが利用できません。",
"providers": "プロバイダー",
+ "quotes_displayed_for": "{{paymentMethodName}}用のクォートが表示されています。",
+ "other_options": "Other options",
+ "previously_used": "以前使用",
+ "best_rate": "最も有利なレート",
+ "most_reliable": "信頼性が最も高い",
"continue": "続行",
"powered_by_provider": "Powered by {{provider}}",
"purchased_currency": "{{currency}}を購入しました",
@@ -4890,14 +4940,19 @@
"logged_out_error": "ログアウト中にエラーが発生しました"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "利用できません",
+ "description": "お住まいの地域の{{provider}}では{{token}}を利用できません。",
+ "change_token": "トークンを変更",
+ "change_provider": "プロバイダーを変更"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "プロバイダーの選択"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "了解",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "プロバイダーを変更"
},
"fiat_on_ramp_aggregator": {
"buy": "購入",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "{{currency}}のデポジット"
},
+ "ramps_order_details": {
+ "title": "注文詳細",
+ "status": "ステータス",
+ "processing": "処理中",
+ "complete": "完了",
+ "failed": "失敗",
+ "cancelled": "キャンセル済み",
+ "view_on_provider": "{{provider}}で表示",
+ "order_id": "注文ID",
+ "date_and_time": "日付と時刻",
+ "fees": "手数料",
+ "total": "合計",
+ "card_processing_info": "カードでの購入には通常数分かかります",
+ "processing_info_modal_description": "カードでのご購入には通常数分かかります。ご質問がある場合は、サポートにお問い合わせください。",
+ "go_to_provider_support": "{{provider}}のサポートページに移動",
+ "error_title": "問題が発生しました",
+ "error_message": "注文情報を読み込めません。もう一度お試しください。",
+ "try_again": "もう一度お試しください",
+ "close": "閉じる"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "{{cryptocurrency}}の購入を処理中です",
+ "purchase_pending_description": "この処理はほんの数分で完了する予定です...",
+ "purchase_completed_title": "{{amount}}{{cryptocurrency}}の購入が完了しました",
+ "purchase_completed_description": "{{cryptocurrency}}が利用可能になりました",
+ "purchase_failed_title": "{{cryptocurrency}}の購入に失敗しました",
+ "purchase_failed_description": "直ちに再試行してださい",
+ "purchase_cancelled_title": "購入がキャンセルされました",
+ "purchase_cancelled_description": "{{cryptocurrency}}の購入がキャンセルされました",
+ "track": "追跡"
+ }
+ },
"swaps": {
"title": "スワップ",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "再びこのような事態が起こらないように、常にMetaMaskアプリとOSを最新バージョンにアップデートしてください。"
},
"srp_security_quiz": {
+ "question_step": "質問 {{step}} / {{total}}",
"title": "セキュリティの質問",
"introduction": "シークレットリカバリーフレーズを表示するには、2つの質問に正しく答える必要があります",
"get_started": "開始",
"learn_more": "詳細",
"try_again": "再試行してください",
"continue": "続行",
+ "correct": "正解です!",
+ "incorrect": "不正解です!",
"of": "-",
"question_one": {
"question": "シークレットリカバリーフレーズをなくした場合、MetaMaskは...",
@@ -5823,11 +5914,11 @@
"error_description": "{{snap}}のインストールに失敗しました。"
},
"earn": {
- "claimable_bonus_tooltip": "ウォレットから毎日請求できる年次ボーナス。",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "{{percentage}}%のボーナスを獲得",
"claimable_bonus": "獲得できるボーナス",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "ボーナスを請求する",
+ "claim_bonus_subtitle": "ボーナスは{{networkName}}上で支払われます。",
"empty_state_cta": {
"heading": "{{tokenSymbol}}を貸して収益化",
"body": "{{protocol}}で{{tokenSymbol}}を貸し付けて、",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "メンテナンス中のためご利用いただけません。再開まで今しばらくお待ちください。"
},
- "supply": "Supply",
+ "supply": "供給量",
"deposit": "入金する",
"approve": "承認",
"approval": "承認",
@@ -5887,7 +5978,7 @@
"network": "ネットワーク",
"health_factor": "健全性指標",
"liquidation_risk": "清算リスク",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "プールの流動性不足",
"available_to_withdraw": "引き出し可能額",
"unknown": "不明",
"how_it_works": "使い方",
@@ -5906,7 +5997,7 @@
"lending": "ポジション履歴",
"staking": "払出履歴"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "許容額のリセット",
"tron": {
"fee": "手数料"
},
@@ -5914,32 +6005,60 @@
"ok": "OK",
"continue": "続行",
"convert_and_get_percentage_bonus": "変換して{{percentage}}%をゲット",
+ "your_musd": "保有中のmUSD",
+ "balance_breakdown_title": "ネットワークごとのmUSD残高",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "mUSDに変換",
"get_a_percentage_musd_bonus": "{{percentage}}%のmUSDボーナスを獲得",
"convert": "変換",
+ "fetching_quote": "クォートを取得中...",
+ "you_convert": "変換するトークン",
+ "network_fee": "ネットワーク手数料",
+ "earning": "収益",
+ "quick_convert_description": "トークンを選択して全残高を変換するか、編集アイコンをタップしてご自身で金額を入力してください。",
+ "no_tokens_to_convert": "mUSDに変換可能なトークンがありません。",
"toasts": {
"converting": "{{token}} → mUSDに変換中",
"eta": "~{{time}}",
- "delivered": "mUSDへの変換が完了しました!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "mUSDへの変換に失敗しました"
},
"education": {
"heading": "ステーブルコインで\n{{percentage}}%をゲット",
- "description": "お持ちのステーブルコインを、MetaMaskのドルを裏付けとするステーブルコインmUSDに換えて、最大{{percentage}}%のボーナスを受け取りましょう。",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "諸条件が適用されます。",
"primary_button": "開始",
"secondary_button": "後で"
},
"buy_musd": "mUSDを購入",
"get_musd": "mUSDを入手",
- "bonus_title": "ステーブルコインで{{percentage}}%をゲット",
- "bonus_description": "お持ちのステーブルコインをmUSDに換えて、最大{{percentage}}%のボーナスを受け取りましょう。",
- "powered_by_relay": "Powered by Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Powered by Relay",
+ "max": "最大額",
+ "quick_convert_button": "変換",
+ "learn_more": "詳細",
+ "tooltip_title": "mUSDで収益を得ましょう",
+ "tooltip_content": "USDC、USDT、またはDAIを、MetaMaskのドルを裏付けとするステーブルコイン、mUSDに変換しましょう。保有額1ドルあたり{{apy}}の利回りが得られます。",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "変換に失敗しました。もう一度お試しください。",
+ "confirmation": {
+ "title": "最大額を変換"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "レート"
},
"bonus_claim": {
"toasts": {
"claiming": "mUSDのボーナスを処理しています",
"delivered": "mUSDのボーナスを獲得しました!",
- "failed": "Bonus claim failed"
+ "failed": "ボーナスの請求に失敗しました"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "ポイントは自動的にアカウントに追加されます。",
"tooltip_not_opted_in_footer": "リワードに参加してポイントを受け取りましょう。",
"tooltip_close": "閉じる"
- }
+ },
+ "your_stablecoins": "保有中のステーブルコイン"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "トランザクション手数料",
"metamask_fee": "MetaMaskの手数料",
"network_fee": "ネットワーク手数料",
- "bridge_fee": "ブリッジプロバイダー手数料"
+ "bridge_fee": "ブリッジプロバイダー手数料",
+ "provider_fee": "プロバイダーの手数料"
},
"title": {
"signature": "署名要求",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Polygon (予測で使用されるネットワーク) 上でトークンをUSDCにスワップします。スワッププロバイダーは手数料を請求する場合がありますが、MetaMaskは無料です。"
},
"predict_withdraw": {
- "transaction_fee": "MetaMaskはユーザーに代わり、希望のトークンをスワップします。MUSDへのスワップにはMetaMaskの手数料がかかりません。"
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "mUSD換算手数料にはネットワーク費用が含まれ、プロバイダー手数料も含まれる場合があります。"
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "手数料"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "プライスインパクト",
"price_impact_info_description": "プライスインパクトは、スワップ注文がその資産の市場価格にどのように影響するかを反映します。取引のサイズとプールの利用可能な流動性によって異なります。MetaMaskがプライスインパクトに影響を与えたりコントロールしたりすることはありません。",
"price_impact_info_gasless_description": "プライスインパクトは、スワップ注文がその資産の市場価格にどのように影響するかを反映します。ガス代の支払いに十分な資金を保有していない場合、交換前のトークンの一部が自動的に手数料の支払いに充当され、プライスインパクトが増大します。MetaMaskがプライスインパクトに影響を与えたりコントロールしたりすることはありません。",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "キャンセル",
"slippage_info_title": "スリッページ",
"slippage_info_description": "トランザクションが取り消される前に許容する価格の変動率(%)。",
"blockaid_error_title": "このトランザクションは取り消されます",
@@ -6470,13 +6596,14 @@
},
"submit": "送信",
"default_slippage_description": "価格がスリッページの割合を超えて変動した場合、トランザクションは処理されません。",
- "cancel": "キャンセル",
"confirm": "確定",
"exceeding_upper_slippage_warning": "高スリッページ、これにより不利なスワップになる可能性があります",
"exceeding_lower_slippage_warning": "低スリッページ、これにより不利なスワップになる可能性があります",
"exceeding_lower_slippage_error": "{{value}}%より大きい値を入力してください",
"exceeding_upper_slippage_error": "{{value}}%より大きい値を入力することはできません",
- "custom": "カスタム"
+ "custom": "カスタム",
+ "invalid_recipient_address": "無効なアドレス",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "新しい価格が利用可能です",
@@ -6773,12 +6900,16 @@
"title": "パスワードを入力してください",
"description": "カード情報を表示するには、ウォレットのパスワードを入力してください。",
"description_unfreeze": "カードでの支払いを再開するには、ウォレットのパスワードを入力してください。",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "パスワード",
"confirm": "確定",
"cancel": "キャンセル",
"error_empty": "パスワードを入力してください",
"error_incorrect": "パスワードが正しくありません。もう一度お試しください。"
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "カードの選択",
"upgrade_title": "メタルにアップグレード",
@@ -7094,6 +7225,9 @@
"view_card_details": "カード情報を表示",
"hide_card_details": "カード情報を非表示",
"view_card_details_description": "カード番号、有効期限、CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "利用限度額の管理",
"manage_spending_limit_description_restricted": "利用制限がオンになっています",
"manage_spending_limit_description_full": "フルアクセスがオンになっています",
@@ -7104,6 +7238,9 @@
"card_tos_title": "利用規約",
"order_metal_card": "メタルカード",
"order_metal_card_description": "実物のメタルカードを今すぐ注文",
+ "cashback": "キャッシュバック",
+ "cashback_description": "常に支出額の1%をキャッシュバック",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "カードを凍結する",
"unfreeze_card": "カードの凍結を解除する",
"freeze_card_description": "カードでのすべての支払いを一時停止する",
@@ -7141,6 +7278,19 @@
"retry": "再試行してください",
"on_linea": "Lineaで"
},
+ "cashback_screen": {
+ "title": "キャッシュバック",
+ "available_cashback": "利用可能なキャッシュバック",
+ "network_fee": "ネットワーク手数料",
+ "expected_to_receive": "受取予定額",
+ "withdraw": "出金",
+ "withdraw_unavailable": "出金できません",
+ "withdrawal_initiated": "出金が開始されました",
+ "withdrawal_success": "出金が完了しました",
+ "withdrawal_failed": "出金に失敗しました。もう一度お試しください。",
+ "no_cashback": "利用可能なキャッシュバックはありません",
+ "loading_error": "キャッシュバックを読み込めませんでした。もう一度お試しください。"
+ },
"change_asset": {
"title": "トークンとネットワークの変更",
"full_spending_access": "全額使用アクセス",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "支払方法を選択",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "トークンの受取を選択してください",
+ "no_gas": "ガスの支払いに使えるネイティブ残高がありません"
},
"connection_removed_modal": {
"title": "接続が削除されました",
@@ -7315,7 +7465,10 @@
"service_not_available": "現在サービスをご利用いただけません。しばらくしてからもう一度お試しください。",
"invalid_referral_code": "紹介コードが無効です。ご確認の上、もう一度お試しください。",
"already_referred": "お客様はすでに別のユーザーにより招待されています。",
- "cannot_use_own_referral_code": "ご自身の紹介コードを利用することはできません。"
+ "cannot_use_own_referral_code": "ご自身の紹介コードを利用することはできません。",
+ "invalid_bonus_code": "無効なボーナスコード",
+ "already_redeemed": "このボーナスコードはすでに使用されています",
+ "reached_maximum": "このボーナスコードは使用回数の上限に達しました"
},
"claim_reward_error": {
"title": "リワードの請求に失敗しました"
@@ -7372,8 +7525,10 @@
"predict": "予測",
"musd_deposit": "mUSDデポジット",
"apply_referral_bonus": "紹介コードボーナス",
+ "bonus_code": "ボーナスコード",
"uncategorized_event": "未分類のイベント"
},
+ "code": "コード",
"date": "日付",
"account": "アカウント",
"bonus": "ボーナス",
@@ -7473,7 +7628,16 @@
"show_less": "表示を戻す",
"linking_progress": "アカウントを追加中… ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}}件を登録済み",
- "add_all_accounts": "すべてのアカウントを追加"
+ "add_all_accounts": "すべてのアカウントを追加",
+ "environment_selector": "環境",
+ "environment_cancel": "キャンセル",
+ "environment_default": "デフォルト",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "紹介コード",
@@ -7483,6 +7647,10 @@
"invalid_code": "無効な紹介コード",
"apply_button": "紹介コードを適用"
},
+ "bonus_code": {
+ "input_placeholder": "ボーナスコードを入力してください",
+ "apply_success": "ボーナスコードが適用されました!"
+ },
"optout": {
"title": "リワードの進行状況を削除",
"description": "この操作を行うと、アカウントがリワードプログラムから削除され、すべてのポイントと進行状況が失われます。この操作は元に戻せません。",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "ブリッジ手数料",
+ "provider_fee": "プロバイダーの手数料",
"network_fee": "ネットワーク手数料",
"paid_with": "支払方法:",
+ "receive_token": "トークンの受取",
"retry_button": "再試行してください",
"total": "合計",
"account": "アカウント",
- "received_total": "Received total"
+ "received_total": "受取額合計"
},
"summary_title": {
"bridge_approval": "{{approveSymbol}}を承認",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "接続が見つかりません",
"description": "続行するには、アプリから新しい接続を確立させてください。"
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "{{networkName}}に引き続き接続中です...",
"unable_to_connect_network": "{{networkName}}に接続できません。",
"update_rpc": "RPCを更新",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "MetaMaskのデフォルトRPCに切り替え",
"check_network_connectivity": "ネットワーク接続を確認してください。",
"check_network_connectivity_or": "ネットワーク接続を確認するか、",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "MetaMaskのデフォルトに更新されました"
},
"trending": {
"title": "閲覧",
"trending_tokens": "流行のトークン",
+ "stocks": "株式",
"price_change": "価格変動",
"all_networks": "すべてのネットワーク",
"24h": "24時間",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "再読み込み",
"primary_action_acknowledge": "了解"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "QRハードウェアウォレット",
+ "hardware_wallet": "ハードウェアウォレット"
+ },
+ "common": {
+ "cancel": "キャンセル",
+ "continue": "続行"
+ },
+ "connecting": {
+ "title": "{{device}}の接続",
+ "searching": "{{device}}を検索中...",
+ "tips_header": "続行するには、以下の点をご確認ください。",
+ "tip_unlock": "{{device}}のロックが解除されている",
+ "tip_open_app": "イーサリアムアプリが開いている",
+ "tip_enable_bluetooth": "Bluetoothがオンになっている",
+ "tip_dnd_off": "サイレントモードがオフになっている",
+ "tip_bluetooth_permission": "位置情報とBluetoothへのアクセス許可が付与されている",
+ "tip_bluetooth_permission_v12": "付近のデバイスへのアクセス許可が付与されている",
+ "tip_stay_close": "デバイスがスマートフォンの近くにある"
+ },
+ "awaiting_app": {
+ "title": "{{app}}を開きます",
+ "message": "{{device}}で{{app}}アプリを開いてください",
+ "current_app": "現在開いているアプリ: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "{{device}}で確定します",
+ "title_message": "{{device}}でサインオンします",
+ "message": "{{device}}で確認の上、確定してください"
+ },
+ "success": {
+ "title": "{{device}}が接続されました"
+ },
+ "errors": {
+ "device_locked": "続行するには、ロックを解除してもう一度お試しください",
+ "app_not_open": "デバイスでイーサリアムアプリを開いてください。",
+ "device_disconnected": "{{device}}の接続が解除されました。再接続して、もう一度お試しください。",
+ "device_not_found": "{{device}}が見つかりませんでした。接続状態を確認してください。",
+ "device_not_ready": "デバイスの準備ができていません。確認の上、もう一度お試しください。",
+ "blind_signing": "ブラインド署名が無効になっています。デバイスの設定で有効にしてください。",
+ "connection_closed": "接続が失われました。もう一度お試しください。",
+ "connection_timeout": "接続がタイムアウトしました。もう一度お試しください。",
+ "user_cancelled": "デバイス上で操作がキャンセルされました",
+ "pending_confirmation": "デバイス上で未完了の操作があります。操作を完了するか、キャンセルしてください。",
+ "bluetooth_permission_denied": "デバイスに接続するには、Bluetoothへのアクセス許可が必要です",
+ "location_permission_denied": "デバイスを検索するには、位置情報へのアクセス許可が必要です",
+ "nearby_permission_denied": "付近のデバイスへのアクセス許可が必要です",
+ "bluetooth_off": "デバイスに接続するには、Bluetoothをオンにしてください",
+ "bluetooth_scan_failed": "デバイスを検索できませんでした。もう一度お試しください。",
+ "bluetooth_connection_failed": "続行するには、デバイスでBluetoothを有効にしてください",
+ "not_supported": "この操作はサポートされていません。",
+ "unknown_error": "{{device}}がこのアカウント用のシークレットリカバリーフレーズまたはパスフレーズを使ってセットアップされていることを確認してください。"
+ },
+ "error": {
+ "title": "問題が発生しました",
+ "default_title": "エラーが発生しました",
+ "continue": "続行",
+ "retry": "再試行",
+ "view_settings": "設定を表示",
+ "device_locked_title": "{{device}}がロックされました",
+ "device_disconnected_title": "{{device}}の接続が解除されました",
+ "device_not_found_title": "{{device}}が見つかりません",
+ "app_not_open": "イーサリアムアプリが開いていません",
+ "blind_signing_disabled": "ブラインド署名が無効になっています",
+ "connection_timeout": "デバイスが応答しません",
+ "connection_closed": "接続が失われました",
+ "user_cancelled": "操作がキャンセルされました",
+ "pending_confirmation": "確認待ち",
+ "bluetooth_required": "Bluetoothが必要です",
+ "bluetooth_permission_denied": "Bluetoothへのアクセス許可が必要です",
+ "location_permission_denied": "位置情報へのアクセス許可が必要です",
+ "nearby_devices_permission_denied": "付近のデバイスへのアクセス許可が必要です",
+ "scan_failed": "検索に失敗しました",
+ "something_went_wrong": "問題が発生しました"
+ },
+ "device_selection": {
+ "title": "{{device}}の選択",
+ "scanning": "デバイスを検索中...",
+ "no_devices_found": "デバイスが見つかりませんでした",
+ "no_devices_hint": "{{device}}のロックが解除され、Bluetoothが有効になっていることを確認してください",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "接続",
+ "rescan": "もう一度検索",
+ "unknown_device": "不明なデバイス",
+ "signal_strength": "信号: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "トークン",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT",
"import_nfts": "NFTをインポート",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "コレクティブルを簡単に追加できます",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "TP/SLはありません"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "{{section}}を読み込めませんでした",
+ "retry": "再試行"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/ko.json b/locales/languages/ko.json
index e96fb45ec82..6e7082bc899 100644
--- a/locales/languages/ko.json
+++ b/locales/languages/ko.json
@@ -103,6 +103,10 @@
"message": "{{ticker}} 잔액이 부족해 수수료를 결제할 수 없습니다. 다른 네트워크의 토큰을 사용하거나 {{ticker}} 추가 후 계속 진행하세요.",
"title": "자금 부족"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "현재, 이 결제 경로를 이용할 수 없습니다. 금액이나 네트워크, 토큰을 변경해 보세요. 최적의 옵션을 찾아드리겠습니다.",
"title": "견적 없음"
@@ -1180,6 +1184,7 @@
"title": "신규 주문",
"leverage": "레버리지",
"limit_price": "지정가",
+ "market_price": "시장 가격",
"enter_price": "가격 입력",
"trigger_price": "트리거 가격",
"liquidation_price": "청산 가격",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "자금을 거래에 사용할 수 있습니다",
"close_order_still_active": "종료 주문이 여전히 유효합니다",
"order_submitted": "주문 제출됨",
+ "submitting_your_trade": "거래 제출 중",
"order_filled": "주문 체결 완료",
"order_placed": "주문 접수 완료",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "주문 세부 정보",
"cancel_order": "주문 취소",
"date": "날짜",
+ "trigger_condition": "트리거 조건",
+ "price": "가격",
"fee": "수수료",
"limit_buy": "롱 지정가",
"limit_price": "지정가",
"limit_sell": "숏 지정가",
"market_buy": "롱 시장가",
"market_sell": "숏 시장가",
+ "market": "시장",
"open": "미체결",
"size": "규모",
+ "original_size": "원주문 규모",
+ "order_value": "주문 금액",
+ "reduce_only": "청산 전용",
+ "yes": "예",
+ "no": "아니요",
+ "price_above": "{{price}} 이상의 가격",
+ "price_below": "{{price}} 이하의 가격",
+ "take_profit": "익절",
+ "stop_loss": "손절",
"status": "상태",
"view_explorer": "익스플로러에서 보기"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "시장 인사이트",
- "updated_ago": "{{time}} 업데이트됨",
- "disclaimer": "AI 인사이트. 투자 조언이 아닙니다.",
- "whats_driving_price": "가격 변동 원인은 무엇인가요?",
- "what_people_saying": "커뮤니티 의견",
+ "a_closer_look": "자세히 보기",
+ "whats_being_said": "시장 반응",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "거래하기",
"sources_count": "출처 {{count}}개 이상",
- "sources_title": "출처에서 얻으세요"
+ "sources_title": "News sources",
+ "feedback_submitted": "피드백 제출됨",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "피드백",
+ "description": "AI 기반 시장 인사이트를 개선할 수 있도록 도와주세요.",
+ "not_relevant": "관련 없음",
+ "not_accurate": "정확하지 않음",
+ "hard_to_understand": "이해하기 어려움",
+ "harmful_or_offensive": "유해하거나 모욕적임",
+ "something_else": "기타",
+ "additional_feedback_label": "추가 피드백(선택 사항)",
+ "additional_feedback_placeholder": "개선을 위한 의견을 남겨 주세요.",
+ "characters_remaining": "{{count}}자 남음",
+ "submit": "제출"
+ }
},
"predict": {
"title": "MetaMask 예측",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "약 1분 후 사용 가능",
"withdraw_completed": "출금 완료",
"withdraw_completed_subtitle": "{{amount}} USDC가 지갑으로 전송되었습니다",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}}이(가) 지갑으로 이동했습니다",
"error_title": "문제가 발생했습니다",
"error_description": "출금에 실패했습니다",
"try_again": "다시 시도"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "이 값은 추정치이며, 거래가 완료되면 최종적으로 확정됩니다. 포인트는 보상 잔액에 반영되기까지 최대 1시간이 걸릴 수 있습니다.",
"points_error": "지금 포인트를 불러올 수 없습니다",
"points_error_content": "이 트랜잭션에 대해서도 포인트가 적립됩니다. 포인트가 계정에 추가되면 알려드립니다. 약 한 시간이 지나면 보상 탭에서도 확인할 수 있습니다.",
- "slippage": "슬리피지"
+ "slippage": "슬리피지",
+ "price_details": "가격 정보",
+ "prediction_order": "예측 주문",
+ "prediction_order_description": "각각 {{price}}에 약 {{count}}개 계약입니다. 최종 금액은 오더북 상황에 따라 달라질 수 있습니다(최대 {{slippage}}%까지 변동 가능).",
+ "metamask_fee_description": "이 예측을 처리하기 위한 서비스 수수료",
+ "exchange_fee": "거래소 수수료",
+ "exchange_fee_description": "거래소 또는 시장에 지불한 수수료",
+ "total_incl_fees": "수수료 포함",
+ "close": "닫기"
},
"error": {
"title": "예측에 연결할 수 없습니다",
@@ -2399,7 +2440,7 @@
"add_tokens": "토큰 가져오기",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "이 토큰을 가져올까요?",
"tokens_detected_in_account": "계정에서 {{tokenCount}}개의 신규 {{tokensLabel}} 발견됨",
"token_toast": {
"tokens_imported_title": "가져온 토큰",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "토큰 십진수는 비워둘 수 없습니다.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "해당 이름의 토큰을 찾을 수 없습니다.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "토큰을 검색하여 가져오기",
"select_token": "토큰 선택",
"address_must_be_smart_contract": "개인 주소가 탐지되었습니다. 토큰 거래 주소를 입력하세요.",
"billion_abbreviation": "B",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "RPC URL 추가",
"add_block_explorer_url": "블록 탐색기 URL 추가",
"networks_desc": "맞춤 RPC 네트워크 추가 및 수정",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "활성화된 네트워크",
+ "networks_test_networks": "테스트 네트워크",
+ "networks_additional": "추가 네트워크",
"networks_search_placeholder": "네트워크 검색",
"networks_no_results": "네트워크를 찾을 수 없습니다",
"network_name_label": "네트워크 이름",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "RPC 네트워크",
"network_add_network": "네트워크 추가",
"add_chain_title": "네트워크 추가",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "이름별, 체인 ID별, 통화별 검색",
+ "add_chain_loading": "네트워크를 불러오는 중…",
+ "add_chain_error": "네트워크를 불러오지 못했습니다. 다시 시도하세요.",
"add_chain_retry": "다시 시도",
- "add_chain_added": "Added",
+ "add_chain_added": "추가함",
"add_chain_or": "또는",
"add_chain_custom_link": "사용자 지정 네트워크 추가",
"network_add_custom_network": "사용자 지정 네트워크 추가",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "테스트 네트워크 추가",
"network_add": "추가",
"network_save": "저장",
"remove_network_title": "해당 네트워크를 제거하시겠습니까?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "그러니 이를 안전하게 다른 사람이 볼 수 없게 보관해 놓으시기 바랍니다.",
"private_key_warning": "이것은 현재 선택된 계정용 개인 키 {{accountName}} 입니다. 이 키를 절대 다른 사람에게 공개하지 마세요. 개인 키를 가지고 있는 사람은 누구든 해당 계정을 완전히 통제할 수 있습니다. 계정의 자금을 다른 곳에 송금할 수도 있습니다.",
- "seed_phrase_warning_explanation": [
- "본인의 화면을 아무도 보지 못하게 하세요.",
- "MetaMask 지원팀은 절대 개인 키를 알려달라고 요청하지 않습니다."
- ],
+ "seed_phrase_warning_explanation": "다른 사람에게 화면을 보여주지 마세요. MetaMask 지원팀은 해당 정보를 절대 요구하지 않습니다.",
"private_key_warning_explanation": "이 키를 절대 다른 사람에게 공개하지 마세요. 개인 키를 가지고 있는 사람은 누구든 해당 계정을 완전히 통제할 수 있습니다. 계정의 자금을 다른 곳에 송금할 수도 있습니다.",
+ "reveal_srp_description": "비밀복구구문을 알면 지갑을 완전히 제어할 수 있습니다. 절대로 타인과 공유하지 마세요.",
"reveal_credential_modal": [
"{{credentialName}} 사용자 이름으로 ",
"계정과 자금에 완전히 액세스할 수 있습니다.\n\n이 정보를 아무에게도 알려주지 마세요.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "비밀복구구문입니다",
"private_key_text": "개인 키",
"got_it": "확인",
- "learn_more": "더 보기"
+ "learn_more": "더 보기",
+ "copied_to_clipboard": "클립보드에 복사되었습니다"
},
"screenshot_deterrent": {
"title": "안전성 알림",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "속도 향상을 원하시나요?",
"speedup_tx_message": "이 시도를 제출한다고 해서 기존의 거래의 속도가 향상될 것이라는 보장은 없습니다. 속도 향상 시도가 성공적으로 진행되면 위에 공지된 거래 수수료가 부과됩니다.",
"nevermind": "취소",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "가스요금 수정",
"edit_priority": "우선 순위 편집",
"gas_cancel_fee": "가스 취소 수수료",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "거래 수수료는 이 권한과 연관되어 있습니다.",
"view_details": "세부 사항 확인",
"view_transaction_details": "트랜잭션 세부 정보 보기",
- "view_data": "View data",
+ "view_data": "데이터 보기",
"transaction_details": "트랜잭션 세부 사항",
"site_url": "사이트 URL",
"permission_request": "권한 요청",
@@ -3843,7 +3887,7 @@
"right_button": "지갑 보호하기"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "즐겨찾기 추가",
"title_label": "이름",
"url_label": "URL",
"add_button": "추가",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "터치 ID로 잠금 해제하시겠습니까?",
"enable_faceid": "페이스 ID로 잠금 해제하시겠습니까?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "지문으로 잠금 해제하시겠습니까?",
+ "enable_biometrics": "바이오메트릭으로 잠금 해제하시겠습니까?",
"enable_device_passcode_ios": "기기 비밀번호로 잠금 해제하시겠습니까?",
"enable_device_passcode_android": "기기 PIN으로 잠금 해제하시겠습니까?"
},
@@ -4724,7 +4768,7 @@
"public_address": "공개 주소",
"public_address_qr_code": "공개 주소",
"coming_soon": "곧 추가 예정...",
- "request_payment": "Request payment",
+ "request_payment": "결제 요청",
"copy": "복사",
"scan_address": "결제 받은 주소를 스캔하세요",
"copy_address": "주소 복사"
@@ -4738,8 +4782,8 @@
"switch_network": "메인넷이나 세폴리아로 전환하세요",
"card_title": "MetaMask 카드 버튼 항상 표시",
"card_desc": "MetaMask 카드는 일부 국가의 거주자만 사용할 수 있습니다.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "DaimoPay 데모 환경 사용",
+ "daimo_demo_desc": "카드 결제를 위해 DaimoPay 데모와 실제 서비스 환경 간을 전환할 수 있습니다."
},
"walletconnect_sessions": {
"no_active_sessions": "현재 활성화된 세션이 없습니다",
@@ -4752,10 +4796,10 @@
"close_current_session": "새 세션을 시작하기 전에 현재 세션을 닫아야 합니다."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "결제 요청",
+ "title_complete": "결제 완료",
"confirm": "결제",
- "cancel": "Decline",
+ "cancel": "거절",
"is_requesting_you_to_pay": "이(가) 결제를 요청했습니다",
"total": "총액:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "사용 가능한 결제 방법이 없습니다.",
"error_fetching_quotes": "오류가 발생했습니다. 다시 시도해 주세요.",
"no_quotes_available": "공급자가 없습니다.",
+ "quote_unavailable": "견적을 확인할 수 없습니다.",
"providers": "공급자",
+ "quotes_displayed_for": "{{paymentMethodName}}에 대한 견적이 표시됨",
+ "other_options": "Other options",
+ "previously_used": "사용한 적 있음",
+ "best_rate": "최적 환율",
+ "most_reliable": "가장 신뢰할 수 있음",
"continue": "계속",
"powered_by_provider": "제공자: {{provider}}",
"purchased_currency": "구매한 {{currency}}",
@@ -4890,14 +4940,19 @@
"logged_out_error": "로그아웃 오류"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "이용할 수 없음",
+ "description": "현재 지역의 경우 {{provider}}에서 {{token}}을(를) 지원하지 않습니다.",
+ "change_token": "토큰 변경",
+ "change_provider": "공급자를 변경하세요"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "공급업체 변경"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "컨펌",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "공급자를 변경하세요"
},
"fiat_on_ramp_aggregator": {
"buy": "매수",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "{{currency}} 예치"
},
+ "ramps_order_details": {
+ "title": "주문 세부 정보",
+ "status": "상태",
+ "processing": "처리 중",
+ "complete": "완료",
+ "failed": "실패",
+ "cancelled": "취소됨",
+ "view_on_provider": "{{provider}}에서 보기",
+ "order_id": "주문 ID",
+ "date_and_time": "날짜 및 시간",
+ "fees": "수수료",
+ "total": "총액",
+ "card_processing_info": "카드 결제는 일반적으로 몇 분 내로 처리됩니다",
+ "processing_info_modal_description": "카드 결제는 보통 몇 분 정도 소요됩니다. 문의 사항이 있으면 지원팀에 연락해 주세요.",
+ "go_to_provider_support": "{{provider}} 지원 페이지로 이동",
+ "error_title": "문제가 발생했습니다",
+ "error_message": "주문 정보를 불러올 수 없습니다. 다시 시도하세요.",
+ "try_again": "다시 시도",
+ "close": "닫기"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "{{cryptocurrency}} 매수 처리 중",
+ "purchase_pending_description": "몇 분 정도 소요됩니다...",
+ "purchase_completed_title": "{{amount}} {{cryptocurrency}} 매수에 성공했습니다!",
+ "purchase_completed_description": "{{cryptocurrency}}을(를) 사용할 수 있습니다",
+ "purchase_failed_title": "{{cryptocurrency}} 구매 실패",
+ "purchase_failed_description": "잠시 후 다시 시도하세요",
+ "purchase_cancelled_title": "매수 주문이 취소되었습니다",
+ "purchase_cancelled_description": "{{cryptocurrency}} 구매가 취소되었습니다",
+ "track": "조회"
+ }
+ },
"swaps": {
"title": "스왑",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "이런 일이 다시 발생하지 않도록 MetaMask 앱과 OS를 항상 최신 버전으로 업데이트하세요."
},
"srp_security_quiz": {
+ "question_step": "총 {{total}}개 중 {{step}}번째 질문",
"title": "보안 퀴즈",
"introduction": "비밀복구구문을 찾으려면 두 가지 질문에 올바르게 답해야 합니다",
"get_started": "지금 시작하세요",
"learn_more": "더 보기",
"try_again": "다시 시도",
"continue": "계속",
+ "correct": "맞습니다!",
+ "incorrect": "틀렸습니다!",
"of": "따른",
"question_one": {
"question": "비밀복구구문을 분실하시면 MetaMask가...",
@@ -5823,11 +5914,11 @@
"error_description": "{{snap}} 설치에 실패했습니다."
},
"earn": {
- "claimable_bonus_tooltip": "지갑에서 매일 청구할 수 있는 연간 보너스.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "{{percentage}}% 보너스 받기",
"claimable_bonus": "청구 가능한 보너스",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "보너스 수령",
+ "claim_bonus_subtitle": "보너스는 {{networkName}}에서 지급됩니다.",
"empty_state_cta": {
"heading": "{{tokenSymbol}} 토큰을 빌려주고 수익을 올리세요",
"body": "{{protocol}}에서 {{tokenSymbol}}을 예치하고 연간 이자를",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "현재 점검 중입니다. 빠른 시일 내에 서비스를 재개하겠습니다!"
},
- "supply": "Supply",
+ "supply": "공급",
"deposit": "예치",
"approve": "승인",
"approval": "승인",
@@ -5887,7 +5978,7 @@
"network": "네트워크",
"health_factor": "청산 위험 지수",
"liquidation_risk": "청산 위험",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "풀 유동성 부족",
"available_to_withdraw": "출금 가능",
"unknown": "알 수 없음",
"how_it_works": "작동 원리",
@@ -5906,7 +5997,7 @@
"lending": "포지션 내역",
"staking": "지급 내역"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "허용량 초기화",
"tron": {
"fee": "수수료"
},
@@ -5914,32 +6005,60 @@
"ok": "확인",
"continue": "계속",
"convert_and_get_percentage_bonus": "전환 후 {{percentage}}% 받기",
+ "your_musd": "내 mUSD",
+ "balance_breakdown_title": "네트워크별 mUSD 잔액",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "mUSD로 전환",
"get_a_percentage_musd_bonus": "{{percentage}}%의 mUSD 보너스 혜택",
"convert": "전환",
+ "fetching_quote": "견적 가져오기...",
+ "you_convert": "전환 전:",
+ "network_fee": "네트워크 수수료",
+ "earning": "수익 창출",
+ "quick_convert_description": "전체 잔액을 전환할 토큰을 선택하거나 편집 아이콘을 탭하여 사용자 지정 금액을 입력하세요.",
+ "no_tokens_to_convert": "mUSD로 전환할 수 있는 토큰이 없습니다.",
"toasts": {
"converting": "{{token}} → mUSD로 전환 중",
"eta": "~{{time}}",
- "delivered": "mUSD 전환 완료!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "mUSD 전환 실패"
},
"education": {
"heading": "스테이블 코인\n{{percentage}}% 혜택",
- "description": "스테이블코인을 MetaMask의 미화 달러 연동 스테이블코인인 mUSD로 전환하고 최대 {{percentage}}%의 보너스를 받으세요.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "약관이 적용됩니다.",
"primary_button": "시작하기",
"secondary_button": "나중에"
},
"buy_musd": "mUSD 구매",
"get_musd": "mUSD 받기",
- "bonus_title": "스테이블코인으로 {{percentage}}% 받기",
- "bonus_description": "스테이블코인을 mUSD로 전환하고 최대 {{percentage}}%의 보너스를 받으세요.",
- "powered_by_relay": "제공자: Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "제공자: Relay",
+ "max": "최대",
+ "quick_convert_button": "전환",
+ "learn_more": "더 보기",
+ "tooltip_title": "mUSD로 수익 얻기",
+ "tooltip_content": "USDC, USDT, DAI를 MetaMask의 달러 연동 스테이블코인인 mUSD로 전환하세요. 보유 금액 전체에 대해 {{apy}} 수익을 얻을 수 있습니다.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "전환에 실패했습니다. 다시 시도하세요.",
+ "confirmation": {
+ "title": "최대 금액 전환"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "비율"
},
"bonus_claim": {
"toasts": {
"claiming": "mUSD 보너스를 처리 중입니다",
"delivered": "mUSD 보너스가 지급되었습니다!",
- "failed": "Bonus claim failed"
+ "failed": "보너스 수령 실패"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "포인트는 계정에 자동으로 추가됩니다.",
"tooltip_not_opted_in_footer": "보상 프로그램에 가입하면 포인트를 받을 수 있습니다.",
"tooltip_close": "닫기"
- }
+ },
+ "your_stablecoins": "사용자의 스테이블코인"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "트랜잭션 수수료",
"metamask_fee": "MetaMask 수수료",
"network_fee": "네트워크 수수료",
- "bridge_fee": "브릿지 제공자 수수료"
+ "bridge_fee": "브릿지 제공자 수수료",
+ "provider_fee": "공급자 수수료"
},
"title": {
"signature": "서명 요청",
@@ -6260,10 +6381,10 @@
"transaction_fee": "예측에서 사용하는 네트워크인 Polygon에서 토큰을 USDC.e로 스왑합니다. 스왑 제공업체가 수수료를 부과할 수 있지만 MetaMask에서 부과하는 수수료는 없습니다."
},
"predict_withdraw": {
- "transaction_fee": "MetaMask가 원하는 토큰으로 대신 스왑해 드립니다. MUSD로 스왑할 경우 MetaMask수수료는 부과되지 않습니다."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "mUSD 전환 수수료에는 네트워크 비용이 포함되며, 공급자 수수료가 추가될 수 있습니다."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "수수료"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "가격 영향",
"price_impact_info_description": "가격 영향은 회원님의 스왑 주문이 자산의 시장 가격에 미치는 영향을 나타냅니다. 이는 거래 규모와 풀의 가용 유동성에 따라 달라집니다. MetaMask는 가격 영향에 관여하지 않으며 이를 통제하지도 않습니다.",
"price_impact_info_gasless_description": "가격 영향은 사용자의 스왑 주문이 자산의 시장 가격에 미치는 영향을 의미합니다. 가스비를 지불할 충분한 자금이 없는 경우, 스왑할 토큰 일부가 자동으로 수수료로 사용되므로 가격 영향이 커질 수 있습니다. MetaMask는 가격 영향에 관여하지 않으며 이를 통제하지도 않습니다.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "취소",
"slippage_info_title": "슬리피지",
"slippage_info_description": "사용자가 허용하는 가격 변동률(%)로, 가격이 이보다 크게 변동하면 트랜잭션이 취소됩니다.",
"blockaid_error_title": "이 트랜잭션은 취소됩니다",
@@ -6470,13 +6596,14 @@
},
"submit": "제출",
"default_slippage_description": "가격이 슬리피지 비율보다 더 크게 변동하면 트랜잭션이 처리되지 않습니다.",
- "cancel": "취소",
"confirm": "컨펌",
"exceeding_upper_slippage_warning": "슬리피지가 높아 스왑이 불리한 조건으로 이루어질 수 있습니다",
"exceeding_lower_slippage_warning": "슬리피지가 높아 스왑이 불리한 조건으로 이루어질 수 있습니다",
"exceeding_lower_slippage_error": "{{value}}%보다 큰 값을 입력하세요",
"exceeding_upper_slippage_error": "{{value}}%를 초과하는 값은 입력할 수 없습니다",
- "custom": "맞춤형"
+ "custom": "맞춤형",
+ "invalid_recipient_address": "잘못된 주소",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "새로운 견적이 있습니다",
@@ -6773,12 +6900,16 @@
"title": "비밀번호 입력",
"description": "카드 상세 정보를 보려면 지갑 비밀번호를 입력하세요.",
"description_unfreeze": "카드 사용을 재개하려면 지갑 비밀번호를 입력하세요.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "비밀번호",
"confirm": "컨펌",
"cancel": "취소",
"error_empty": "비밀번호를 입력하세요",
"error_incorrect": "잘못된 비밀번호입니다. 다시 시도해 주세요."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "카드 선택",
"upgrade_title": "메탈 카드로 업그레이드",
@@ -7094,6 +7225,9 @@
"view_card_details": "카드 상세 정보 보기",
"hide_card_details": "카드 상세 정보 숨기기",
"view_card_details_description": "카드 번호, 만료일 및 CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "한도 관리",
"manage_spending_limit_description_restricted": "사용 한도가 설정되어 있습니다",
"manage_spending_limit_description_full": "제한 없이 사용할 수 있습니다",
@@ -7104,6 +7238,9 @@
"card_tos_title": "이용약관",
"order_metal_card": "메탈 카드",
"order_metal_card_description": "실물 메탈 카드를 지금 주문하세요",
+ "cashback": "캐시백",
+ "cashback_description": "모든 결제 금액의 1% 돌려받기",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "카드 정지",
"unfreeze_card": "카드 정지 해제",
"freeze_card_description": "카드의 모든 사용 일시 중지",
@@ -7141,6 +7278,19 @@
"retry": "다시 시도",
"on_linea": "Linea 네트워크에서"
},
+ "cashback_screen": {
+ "title": "캐시백",
+ "available_cashback": "사용 가능한 캐시백",
+ "network_fee": "네트워크 수수료",
+ "expected_to_receive": "예상 수령액",
+ "withdraw": "출금",
+ "withdraw_unavailable": "출금할 수 없음",
+ "withdrawal_initiated": "출금이 시작되었습니다",
+ "withdrawal_success": "성공적으로 출금 완료",
+ "withdrawal_failed": "출금에 실패했습니다. 다시 시도하세요.",
+ "no_cashback": "캐시백을 사용할 수 없음",
+ "loading_error": "캐시백을 불러오지 못했습니다. 다시 시도하세요."
+ },
"change_asset": {
"title": "토큰 및 네트워크 변경",
"full_spending_access": "지출에 대한 전체 접근 권한",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "결제 방법 선택",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "토큰 받기 선택",
+ "no_gas": "가스비로 사용할 네이티브 토큰 잔액 없음"
},
"connection_removed_modal": {
"title": "연결 제거 완료",
@@ -7315,7 +7465,10 @@
"service_not_available": "현재 서비스를 이용할 수 없습니다. 잠시 후 다시 시도해 주세요.",
"invalid_referral_code": "잘못된 추천 코드입니다. 확인 후 다시 입력해 주세요.",
"already_referred": "이미 다른 사용자의 추천을 받았습니다.",
- "cannot_use_own_referral_code": "본인의 추천 코드는 사용할 수 없습니다."
+ "cannot_use_own_referral_code": "본인의 추천 코드는 사용할 수 없습니다.",
+ "invalid_bonus_code": "잘못된 보너스 코드",
+ "already_redeemed": "이미 이 보너스 코드를 사용하셨습니다",
+ "reached_maximum": "이 보너스 코드는 최대 사용 한도에 도달했습니다"
},
"claim_reward_error": {
"title": "보상을 수령할 수 없습니다"
@@ -7372,8 +7525,10 @@
"predict": "예측",
"musd_deposit": "mUSD 예치",
"apply_referral_bonus": "추천 코드 보너스",
+ "bonus_code": "보너스 코드",
"uncategorized_event": "미분류 이벤트"
},
+ "code": "코드",
"date": "날짜",
"account": "계정",
"bonus": "보너스",
@@ -7473,7 +7628,16 @@
"show_less": "요약 보기",
"linking_progress": "계정 추가 중...({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} 등록됨",
- "add_all_accounts": "모든 계정 추가"
+ "add_all_accounts": "모든 계정 추가",
+ "environment_selector": "환경",
+ "environment_cancel": "취소",
+ "environment_default": "기본",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "추천 코드",
@@ -7483,6 +7647,10 @@
"invalid_code": "잘못된 추천 코드",
"apply_button": "추천 코드 적용"
},
+ "bonus_code": {
+ "input_placeholder": "보너스 코드 입력",
+ "apply_success": "보너스 코드가 적용되었습니다!"
+ },
"optout": {
"title": "보상 프로그램 영구 탈퇴",
"description": "이 작업을 수행하면 보상 프로그램에서 계정이 완전히 삭제되며, 모든 포인트와 진행 상황이 사라집니다. 이 작업은 되돌릴 수 없습니다.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "브릿지 수수료",
+ "provider_fee": "공급자 수수료",
"network_fee": "네트워크 수수료",
"paid_with": "결제 수단:",
+ "receive_token": "토큰 받기",
"retry_button": "다시 시도",
"total": "총액",
"account": "계정",
- "received_total": "Received total"
+ "received_total": "총 수령액"
},
"summary_title": {
"bridge_approval": "{{approveSymbol}} 승인",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "트랜잭션 찾을 수 없음",
"description": "계속하려면 앱에서 새로 연결해 주세요."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "아직 {{networkName}}에 연결 중입니다...",
"unable_to_connect_network": "{{networkName}}에 연결할 수 없습니다.",
"update_rpc": "RPC 업데이트",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "MetaMask 기본 RPC로 전환",
"check_network_connectivity": "네트워크 연결을 확인하세요.",
"check_network_connectivity_or": "네트워크 연결을 확인하거나",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "MetaMask 기본값으로 업데이트함"
},
"trending": {
"title": "둘러보기",
"trending_tokens": "인기 토큰",
+ "stocks": "주식",
"price_change": "가격 변동",
"all_networks": "모든 네트워크",
"24h": "24시간",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "새로고침",
"primary_action_acknowledge": "컨펌"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "QR 하드웨어 지갑",
+ "hardware_wallet": "하드웨어 지갑"
+ },
+ "common": {
+ "cancel": "취소",
+ "continue": "계속"
+ },
+ "connecting": {
+ "title": "{{device}} 연결",
+ "searching": "{{device}} 찾는 중...",
+ "tips_header": "계속하려면 다음 사항을 확인하세요.",
+ "tip_unlock": "{{device}} 잠금이 해제되어 있어야 합니다",
+ "tip_open_app": "이더리움 앱이 열려 있어야 합니다",
+ "tip_enable_bluetooth": "블루투스가 켜져 있어야 합니다",
+ "tip_dnd_off": "방해 금지 모드는 꺼져 있어야 합니다",
+ "tip_bluetooth_permission": "위치 및 블루투스 권한이 허용되어 있어야 합니다",
+ "tip_bluetooth_permission_v12": "근처 장치 권한이 허용되어 있어야 합니다",
+ "tip_stay_close": "장치와 휴대전화가 서로 가까이 있어야 합니다"
+ },
+ "awaiting_app": {
+ "title": "{{app}} 열기",
+ "message": "{{device}}에서 {{app}} 앱을 여세요",
+ "current_app": "현재 열려 있음: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "{{device}}에서 컨펌",
+ "title_message": "{{device}}에서 서명",
+ "message": "{{device}}에서 검토한 후 컨펌"
+ },
+ "success": {
+ "title": "{{device}} 연결됨"
+ },
+ "errors": {
+ "device_locked": "계속하려면 잠금을 해제한 후 다시 시도하세요",
+ "app_not_open": "장치에서 이더리움 앱을 여세요",
+ "device_disconnected": "{{device}}의 연결이 해제되었습니다. 다시 연결한 후 재시도하세요",
+ "device_not_found": "{{device}}을(를) 찾을 수 없습니다. 연결되어 있는지 확인하세요",
+ "device_not_ready": "장치가 준비되어 있지 않습니다. 확인 후 다시 시도하세요",
+ "blind_signing": "블라인드 서명이 비활성화되어 있습니다. 쟝치 설정에서 활성화해 주세요",
+ "connection_closed": "연결이 끊어졌습니다. 다시 시도하세요",
+ "connection_timeout": "연결 시간이 초과되었습니다. 다시 시도하세요",
+ "user_cancelled": "장치에서 작업이 취소되었습니다",
+ "pending_confirmation": "장치에 보류 중인 작업이 있습니다. 완료하거나 취소하세요",
+ "bluetooth_permission_denied": "장치를 연결하려면 블루투스 권한이 필요합니다",
+ "location_permission_denied": "장치를 스캔하려면 위치 권한이 필요합니다",
+ "nearby_permission_denied": "근처 장치 권한이 필요합니다",
+ "bluetooth_off": "장치에 연결하려면 블루투스를 켜세요",
+ "bluetooth_scan_failed": "장치를 스캔하지 못했습니다. 다시 시도하세요",
+ "bluetooth_connection_failed": "장치의 블루투스를 활성화한 후 계속하세요",
+ "not_supported": "지원되지 않는 작업입니다",
+ "unknown_error": "이 계정에 대한 비밀복구구문 또는 패스프레이즈로 {{device}}이(가) 설정되어 있는지 확인하세요"
+ },
+ "error": {
+ "title": "문제가 발생했습니다",
+ "default_title": "오류가 발생했습니다",
+ "continue": "계속",
+ "retry": "다시 시도",
+ "view_settings": "설정 보기",
+ "device_locked_title": "{{device}} 잠김",
+ "device_disconnected_title": "{{device}}의 연결이 해제됨",
+ "device_not_found_title": "{{device}} 찾을 수 없음",
+ "app_not_open": "이더리움 앱이 열려 있지 않음",
+ "blind_signing_disabled": "블라인드 서명 비활성화됨",
+ "connection_timeout": "장치 응답 없음",
+ "connection_closed": "연결 끊김",
+ "user_cancelled": "작업 취소됨",
+ "pending_confirmation": "보류 중인 컨펌",
+ "bluetooth_required": "블루투스 필요",
+ "bluetooth_permission_denied": "블루투스 권한 필요",
+ "location_permission_denied": "위치 권한 필요",
+ "nearby_devices_permission_denied": "근처 장치 권한 필요",
+ "scan_failed": "스캔 실패",
+ "something_went_wrong": "문제가 발생했습니다"
+ },
+ "device_selection": {
+ "title": "{{device}} 선택",
+ "scanning": "장치 스캔 중...",
+ "no_devices_found": "장치를 찾을 수 없음",
+ "no_devices_hint": "{{device}}의 잠금이 해제되어 있고 블루투스가 활성화되어 있는지 확인하세요",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "연결",
+ "rescan": "다시 스캔",
+ "unknown_device": "알 수 없는 장치",
+ "signal_strength": "신호 {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "토큰",
@@ -7728,12 +7996,15 @@
"defi": "디파이",
"nfts": "NFT",
"import_nfts": "NFT 가져오기",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "컬렉터블을 간편하게 추가하세요",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "익절/손절 미설정"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "{{section}} 불러올 수 없음",
+ "retry": "다시 시도"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/pt.json b/locales/languages/pt.json
index c477bf9fc0b..f1a87ed1ab3 100644
--- a/locales/languages/pt.json
+++ b/locales/languages/pt.json
@@ -103,6 +103,10 @@
"message": "Não há {{ticker}} suficientes para cobrir as taxas. Use um token em outra rede ou adicione mais {{ticker}} para continuar.",
"title": "Fundos insuficientes"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Esta rota de pagamento não está disponível no momento. Experimente mudar o valor, a rede ou o token, e encontraremos a melhor opção.",
"title": "Nenhuma cotação"
@@ -1180,6 +1184,7 @@
"title": "Nova ordem",
"leverage": "Alavancagem",
"limit_price": "Preço limite",
+ "market_price": "Preço de mercado",
"enter_price": "Insira o preço",
"trigger_price": "Preço de disparo",
"liquidation_price": "Preço de liquidação",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Os fundos estão disponíveis para negociação",
"close_order_still_active": "Ordem de encerramento ainda ativa",
"order_submitted": "Ordem enviada",
+ "submitting_your_trade": "Enviando sua negociação",
"order_filled": "Ordem executada",
"order_placed": "Ordem realizada",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "Detalhes da ordem",
"cancel_order": "Cancelar ordem",
"date": "Data",
+ "trigger_condition": "Condição de acionamento",
+ "price": "Preço",
"fee": "Taxa",
"limit_buy": "Limitar posição long",
"limit_price": "Preço limite",
"limit_sell": "Limitar posição short",
"market_buy": "Compra a mercado",
"market_sell": "Mercado short",
+ "market": "Mercado",
"open": "Abrir",
"size": "Tamanho",
+ "original_size": "Tamanho original",
+ "order_value": "Valor da ordem",
+ "reduce_only": "Reduzir apenas",
+ "yes": "Sim",
+ "no": "Não",
+ "price_above": "Preço acima de {{price}}",
+ "price_below": "Preço abaixo de {{price}}",
+ "take_profit": "Take profit",
+ "stop_loss": "Stop loss",
"status": "Status",
"view_explorer": "Ver no Explorer"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Análises de mercado",
- "updated_ago": "Atualizado em {{time}}",
- "disclaimer": "Análises por IA. Isso não é aconselhamento financeiro.",
- "whats_driving_price": "O que está impulsionando o preço?",
- "what_people_saying": "O que as pessoas estão dizendo",
+ "a_closer_look": "Uma análise mais detalhada",
+ "whats_being_said": "O que as pessoas dizem",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Negociar",
"sources_count": "+{{count}} fontes",
- "sources_title": "Maior liquidez"
+ "sources_title": "News sources",
+ "feedback_submitted": "Feedback enviado",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Comentário",
+ "description": "Ajude a aprimorar nossas análises de mercado geradas por IA.",
+ "not_relevant": "Não relevante",
+ "not_accurate": "Inexato",
+ "hard_to_understand": "Difícil de entender",
+ "harmful_or_offensive": "Prejudicial ou ofensivo",
+ "something_else": "Outra coisa",
+ "additional_feedback_label": "Feedback adicional (opcional)",
+ "additional_feedback_placeholder": "Como podemos melhorar?",
+ "characters_remaining": "{{count}} caracteres restantes",
+ "submit": "Enviar"
+ }
},
"predict": {
"title": "Previsões da MetaMask",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Disponível em cerca de 1 minuto",
"withdraw_completed": "Saque concluído",
"withdraw_completed_subtitle": "{{amount}} USDC transferidos para sua carteira",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} transferidos para sua carteira",
"error_title": "Ocorreu algum erro",
"error_description": "Falha ao continuar com a retirada",
"try_again": "Tentar novamente"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Lembre-se de que este valor é uma estimativa e será finalizado após a conclusão da transação. Os pontos podem levar até 1 hora para serem confirmados no seu saldo de recompensas.",
"points_error": "Não é possível carregar pontos neste momento",
"points_error_content": "Você ainda ganhará os pontos por essa transação. Notificaremos você assim que eles forem adicionados à sua conta. Você também poderá conferir sua guia de recompensas em cerca de uma hora.",
- "slippage": "Slippage"
+ "slippage": "Slippage",
+ "price_details": "Detalhes do preço",
+ "prediction_order": "Ordem de previsão",
+ "prediction_order_description": "~{{count}} contratos a {{price}} cada. O valor final pode variar de acordo com a disponibilidade no livro de ordens (até {{slippage}}%).",
+ "metamask_fee_description": "Taxa de serviço para processamento desta previsão",
+ "exchange_fee": "Taxa de câmbio",
+ "exchange_fee_description": "Taxa paga à bolsa ou ao mercado",
+ "total_incl_fees": "incluindo taxas",
+ "close": "Fechar"
},
"error": {
"title": "Não foi possível conectar-se às previsões",
@@ -2399,7 +2440,7 @@
"add_tokens": "Importar tokens",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Deseja importar esses tokens?",
"tokens_detected_in_account": "{{tokenCount}} novo(s) {{tokensLabel}} encontrado(s) nesta conta",
"token_toast": {
"tokens_imported_title": "Tokens importados",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Os números decimais do token não podem ficar em branco.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "Não encontramos nenhum token com esse nome.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Procure qualquer token e importe-o",
"select_token": "Selecionar token",
"address_must_be_smart_contract": "Endereço pessoal detectado. Insira o endereço do contrato do token.",
"billion_abbreviation": "B",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "Adicionar URL da RPC",
"add_block_explorer_url": "Adicionar URL do explorador de blocos",
"networks_desc": "Adicione e edite redes RPC personalizadas",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Redes habilitadas",
+ "networks_test_networks": "Redes de teste",
+ "networks_additional": "Redes adicionais",
"networks_search_placeholder": "Pesquisar redes",
"networks_no_results": "Nenhuma rede encontrada",
"network_name_label": "Nome da rede",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "Redes RPC",
"network_add_network": "Adicionar rede",
"add_chain_title": "Adicionar uma rede",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Pesquisar por nome, ID da rede ou moeda",
+ "add_chain_loading": "Carregando redes…",
+ "add_chain_error": "Não foi possível carregar redes. Tente novamente.",
"add_chain_retry": "Tentar novamente",
- "add_chain_added": "Added",
+ "add_chain_added": "Adicionado",
"add_chain_or": "ou",
"add_chain_custom_link": "Adicionar uma rede personalizada",
"network_add_custom_network": "Adicionar uma rede personalizada",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Adicionar uma rede de teste",
"network_add": "Adicionar",
"network_save": "Salvar",
"remove_network_title": "Deseja remover essa rede?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Guarde-a em um lugar seguro e secreto.",
"private_key_warning": "Esta é a chave privada da conta atual selecionada: {{accountName}}. Nunca divulgue esta chave. Qualquer pessoa que tiver a chave privada poderá controlar totalmente a sua conta, inclusive retirar seus fundos.",
- "seed_phrase_warning_explanation": [
- "Certifique-se de que não há ninguém olhando para a sua tela. ",
- "O suporte da MetaMask nunca solicitará essa informação."
- ],
+ "seed_phrase_warning_explanation": "Certifique-se de que ninguém esteja olhando para a sua tela. O Suporte da MetaMask nunca solicitará isso.",
"private_key_warning_explanation": "Nunca divulgue esta chave. Qualquer pessoa que tiver a chave privada poderá controlar totalmente a sua conta, inclusive retirar seus fundos.",
+ "reveal_srp_description": "Sua Frase de Recuperação Secreta concede acesso total à sua carteira. Não a compartilhe com ninguém.",
"reveal_credential_modal": [
"Sua {{credentialName}} oferece ",
"acesso total à sua conta e fundos.\n\nNão compartilhe com ninguém.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "Frase de recuperação secreta",
"private_key_text": "Chave privada",
"got_it": "Entendi",
- "learn_more": "Saiba mais"
+ "learn_more": "Saiba mais",
+ "copied_to_clipboard": "Copiado para a área de transferência"
},
"screenshot_deterrent": {
"title": "Alerta de segurança",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Tentar acelerar?",
"speedup_tx_message": "Enviar essa tentativa não garante que sua transação original será acelerada. Caso a tentativa de acelerar seja bem-sucedida, será cobrada a taxa de transação acima.",
"nevermind": "Deixa para lá",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Editar taxa de gás",
"edit_priority": "Editar prioridade",
"gas_cancel_fee": "Taxa de cancelamento de gas",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Há uma taxa de transação associada a esta permissão.",
"view_details": "Ver detalhes",
"view_transaction_details": "Ver detalhes da transação",
- "view_data": "View data",
+ "view_data": "Visualizar dados",
"transaction_details": "Detalhes da transação",
"site_url": "URL do site",
"permission_request": "Solicitação de permissão",
@@ -3843,7 +3887,7 @@
"right_button": "Proteger carteira"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Adicionar favorito",
"title_label": "Nome",
"url_label": "URL",
"add_button": "Adicionar",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Desbloquear com Touch ID?",
"enable_faceid": "Desbloquear com Face ID?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "Desbloquear com impressão digital?",
+ "enable_biometrics": "Desbloquear com biometria?",
"enable_device_passcode_ios": "Desbloquear com o código de acesso do dispositivo?",
"enable_device_passcode_android": "Desbloquear com o PIN do dispositivo?"
},
@@ -4724,7 +4768,7 @@
"public_address": "Endereço público",
"public_address_qr_code": "Endereço público",
"coming_soon": "Em breve...",
- "request_payment": "Request payment",
+ "request_payment": "Solicitar pagamento",
"copy": "Copiar",
"scan_address": "Leia o endereço para receber um pagamento",
"copy_address": "Copiar endereço"
@@ -4738,8 +4782,8 @@
"switch_network": "Alterne para a Mainnet ou Sepolia",
"card_title": "Sempre exibir o botão do cartão MetaMask",
"card_desc": "O cartão MetaMask só está disponível para residentes de países selecionados.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "Utilize o ambiente de demonstração do DaimoPay",
+ "daimo_demo_desc": "Alternar entre os ambientes de demonstração e produção do DaimoPay para pagamentos com cartão."
},
"walletconnect_sessions": {
"no_active_sessions": "Você não tem nenhuma sessão ativa",
@@ -4752,10 +4796,10 @@
"close_current_session": "Encerre a sessão atual antes de iniciar outra."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Solicitação de pagamento",
+ "title_complete": "Pagamento concluído",
"confirm": "Pagar",
- "cancel": "Decline",
+ "cancel": "Recusar",
"is_requesting_you_to_pay": "está solicitando que você pague",
"total": "Total:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Nenhum método de pagamento disponível.",
"error_fetching_quotes": "Algo deu errado. Tente novamente.",
"no_quotes_available": "Nenhum provedor disponível.",
+ "quote_unavailable": "Cotação não disponível.",
"providers": "Fornecedores",
+ "quotes_displayed_for": "Cotações exibidas para {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Usado anteriormente",
+ "best_rate": "Melhor taxa",
+ "most_reliable": "Mais confiável",
"continue": "Continuar",
"powered_by_provider": "Desenvolvido por {{provider}}",
"purchased_currency": "{{currency}} comprado",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Erro ao desconectar"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Não disponível",
+ "description": "O token {{token}} não está disponível com o provedor {{provider}} em sua região.",
+ "change_token": "Alterar token",
+ "change_provider": "Alterar fornecedor"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Escolher um provedor"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Entendi",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Alterar fornecedor"
},
"fiat_on_ramp_aggregator": {
"buy": "comprar",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "Depósito em {{currency}}"
},
+ "ramps_order_details": {
+ "title": "Detalhes da ordem",
+ "status": "Status",
+ "processing": "Em processamento",
+ "complete": "Concluído",
+ "failed": "Falha",
+ "cancelled": "Cancelado",
+ "view_on_provider": "Ver no {{provider}}",
+ "order_id": "ID do pedido",
+ "date_and_time": "Data e hora",
+ "fees": "Taxas",
+ "total": "Total",
+ "card_processing_info": "Compras no cartão normalmente levam alguns minutos",
+ "processing_info_modal_description": "Compras com cartão geralmente levam alguns minutos. Entre em contato com o suporte em caso de dúvidas.",
+ "go_to_provider_support": "Acesse a página de suporte do {{provider}}",
+ "error_title": "Ocorreu algum erro",
+ "error_message": "Não foi possível carregar os detalhes do pedido. Tente novamente.",
+ "try_again": "Tentar novamente",
+ "close": "Fechar"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Processando sua compra de {{cryptocurrency}}",
+ "purchase_pending_description": "Isso deve levar apenas alguns minutos...",
+ "purchase_completed_title": "Sua compra de {{amount}} {{cryptocurrency}} foi bem-sucedida!",
+ "purchase_completed_description": "Seu {{cryptocurrency}} já está disponível",
+ "purchase_failed_title": "A compra de {{cryptocurrency}} falhou",
+ "purchase_failed_description": "Tente novamente daqui a pouco",
+ "purchase_cancelled_title": "Sua compra foi cancelada",
+ "purchase_cancelled_description": "Sua compra de {{cryptocurrency}} foi cancelada",
+ "track": "Rastrear"
+ }
+ },
"swaps": {
"title": "Troca",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Para evitar que isso ocorra novamente, certifique-se de sempre manter seu app MetaMask e sistema operacional atualizados com a última versão."
},
"srp_security_quiz": {
+ "question_step": "Questão {{step}} de {{total}}",
"title": "Questionário de segurança",
"introduction": "Para revelar sua Frase de Recuperação Secreta, você precisa responder corretamente duas perguntas.",
"get_started": "Começar",
"learn_more": "Saiba mais",
"try_again": "Tentar novamente",
"continue": "Continuar",
+ "correct": "Correto!",
+ "incorrect": "Incorreto!",
"of": "de",
"question_one": {
"question": "Se você perder sua Frase de Recuperação Secreta, a MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "Ocorreu uma falha na instalação de {{snap}}."
},
"earn": {
- "claimable_bonus_tooltip": "Um bônus anual que pode ser resgatado diariamente a partir da sua carteira.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Ganhe um bônus de {{percentage}}%",
"claimable_bonus": "Bônus resgatável",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Resgatar bônus",
+ "claim_bonus_subtitle": "O bônus será pago em {{networkName}}.",
"empty_state_cta": {
"heading": "Empreste {{tokenSymbol}} e ganhe",
"body": "Empreste seus {{tokenSymbol}} com {{protocol}} e ganhe",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Estamos em manutenção. Estaremos on-line novamente em breve!"
},
- "supply": "Supply",
+ "supply": "Fornecimento",
"deposit": "Depositar",
"approve": "Aprovar",
"approval": "Aprovação",
@@ -5887,7 +5978,7 @@
"network": "Rede",
"health_factor": "Fator de saúde",
"liquidation_risk": "Risco de liquidação",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Liquidez do pool insuficiente",
"available_to_withdraw": "disponível para sacar",
"unknown": "desconhecido",
"how_it_works": "Como funciona",
@@ -5906,7 +5997,7 @@
"lending": "Histórico de posições",
"staking": "Histórico de pagamentos"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Redefinição de limite",
"tron": {
"fee": "Taxa"
},
@@ -5914,32 +6005,60 @@
"ok": "OK",
"continue": "Continuar",
"convert_and_get_percentage_bonus": "Converta e ganhe {{percentage}}%",
+ "your_musd": "Seu mUSD",
+ "balance_breakdown_title": "Seus saldos em mUSD por rede",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "Converter para mUSD",
"get_a_percentage_musd_bonus": "Ganhe {{percentage}}% de bônus em mUSD",
"convert": "Converter",
+ "fetching_quote": "Buscando cotação...",
+ "you_convert": "Você converte",
+ "network_fee": "Taxa de rede",
+ "earning": "Ganho",
+ "quick_convert_description": "Selecione um token para converter todo o seu saldo ou toque no ícone de edição para inserir um valor personalizado.",
+ "no_tokens_to_convert": "Você não possui nenhum token que possa ser convertido em mUSD.",
"toasts": {
"converting": "Convertendo {{token}} → mUSD",
"eta": "~{{time}}",
- "delivered": "Seu mUSD está aqui!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "Falha ao converter mUSD"
},
"education": {
"heading": "RECEBA {{percentage}}% EM \nSTABLECOINS",
- "description": "Converta suas stablecoins em mUSD, a stablecoin lastreada em dólar americano da MetaMask, e receba um bônus de até {{percentage}}%.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Sujeito a termos e condições.",
"primary_button": "Comece já",
"secondary_button": "Agora não"
},
"buy_musd": "Comprar mUSD",
"get_musd": "Obter mUSD",
- "bonus_title": "Obtenha {{percentage}}% sobre suas stablecoins",
- "bonus_description": "Converta suas stablecoins em mUSD e receba um bônus de até {{percentage}}%.",
- "powered_by_relay": "Desenvolvido por Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Desenvolvido por Relay",
+ "max": "Máx.",
+ "quick_convert_button": "Converter",
+ "learn_more": "Saiba mais",
+ "tooltip_title": "Obtenha rendimentos com mUSD",
+ "tooltip_content": "Converta seus USDC, USDT ou DAI para mUSD, a stablecoin com lastro em dólar da MetaMask. Obtenha {{apy}} de rendimento para cada dólar que você possui.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Falha na conversão. Tente novamente.",
+ "confirmation": {
+ "title": "Converter máximo"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Avaliar"
},
"bonus_claim": {
"toasts": {
"claiming": "Seu bônus em mUSD está sendo processado",
"delivered": "Seu bônus em mUSD chegou!",
- "failed": "Bonus claim failed"
+ "failed": "Resgate de bônus falhou"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Pontos serão adicionados automaticamente à sua conta.",
"tooltip_not_opted_in_footer": "Inscreva-se no programa de recompensas para receber seus pontos.",
"tooltip_close": "Fechar"
- }
+ },
+ "your_stablecoins": "Suas stablecoins"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Taxas de transação",
"metamask_fee": "Taxa da MetaMask",
"network_fee": "Taxa de rede",
- "bridge_fee": "Taxa do provedor de ponte"
+ "bridge_fee": "Taxa do provedor de ponte",
+ "provider_fee": "Taxa do provedor"
},
"title": {
"signature": "Solicitação de assinatura",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Trocaremos seus tokens por USDC.e na Polygon, a rede usada pela Predictions. Provedores de swaps talvez cobrem taxas, mas a MetaMask não cobra."
},
"predict_withdraw": {
- "transaction_fee": "A MetaMask fará a conversão para o token desejado para você. A MetaMask não aplica taxas quando você converte para MUSD."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "As taxas de conversão de mUSD incluem custos de rede e podem incluir taxas do provedor."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Taxas"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Impacto do preço",
"price_impact_info_description": "O impacto no preço reflete como sua ordem de troca afeta o preço de mercado do ativo. Ele depende do tamanho da negociação e da liquidez disponível no pool. A MetaMask não influencia nem controla o impacto no preço.",
"price_impact_info_gasless_description": "O impacto no preço reflete como sua ordem de troca afeta o preço de mercado do ativo. Se você não tiver fundos suficientes para o gás, parte do seu token de origem será automaticamente alocada para cobrir taxas, o que aumenta o impacto no preço. A MetaMask não influencia nem controla o impacto no preço.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Cancelar",
"slippage_info_title": "Slippage",
"slippage_info_description": "A % de alteração no preço que você está disposto a permitir antes que sua transação seja cancelada.",
"blockaid_error_title": "Esta transação será estornada",
@@ -6470,13 +6596,14 @@
},
"submit": "Enviar",
"default_slippage_description": "Sua transação não será concluída se a variação de preço for maior que a porcentagem de slippage.",
- "cancel": "Cancelar",
"confirm": "Confirmar",
"exceeding_upper_slippage_warning": "Slippage elevado, que pode resultar em swap desfavorável",
"exceeding_lower_slippage_warning": "Slippage reduzido, que pode resultar em swap desfavorável",
"exceeding_lower_slippage_error": "Insira um valor maior que {{value}}%",
"exceeding_upper_slippage_error": "Não é possível inserir um valor maior que {{value}}%",
- "custom": "Personalizado"
+ "custom": "Personalizado",
+ "invalid_recipient_address": "Endereço inválido",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Novas cotações estão disponíveis",
@@ -6773,12 +6900,16 @@
"title": "Insira a senha",
"description": "Digite a senha da sua carteira para visualizar detalhes do cartão.",
"description_unfreeze": "Digite a senha da sua carteira para retomar os gastos com o seu cartão.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Senha",
"confirm": "Confirmar",
"cancel": "Cancelar",
"error_empty": "Insira sua senha",
"error_incorrect": "Senha incorreta. Tente novamente."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Escolha seu cartão",
"upgrade_title": "Faça upgrade para Metal",
@@ -7094,6 +7225,9 @@
"view_card_details": "Ver detalhes do cartão",
"hide_card_details": "Ocultar detalhes do cartão",
"view_card_details_description": "Número do cartão, data de validade e CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Gerenciar limite",
"manage_spending_limit_description_restricted": "Limitação de gastos ativada",
"manage_spending_limit_description_full": "Acesso total está ativado",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Termos e condições",
"order_metal_card": "Cartão Metal",
"order_metal_card_description": "Peça já o seu Cartão Metal físico",
+ "cashback": "Cashback",
+ "cashback_description": "Receba 1% de volta em todos os seus gastos",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Congelar cartão",
"unfreeze_card": "Descongelar cartão",
"freeze_card_description": "Pausar todos os gastos com seu cartão",
@@ -7141,6 +7278,19 @@
"retry": "Tentar novamente",
"on_linea": "na Linea"
},
+ "cashback_screen": {
+ "title": "Cashback",
+ "available_cashback": "Cashback disponível",
+ "network_fee": "Taxa de rede",
+ "expected_to_receive": "Recebimento previsto",
+ "withdraw": "Sacar",
+ "withdraw_unavailable": "Saque indisponível",
+ "withdrawal_initiated": "O processo de saque foi iniciado",
+ "withdrawal_success": "Saque concluído com sucesso",
+ "withdrawal_failed": "Saque falhou. Tente novamente.",
+ "no_cashback": "Sem cashback disponível",
+ "loading_error": "Falha ao carregar cashback. Tente novamente."
+ },
"change_asset": {
"title": "Alterar token e rede",
"full_spending_access": "Acesso total aos gastos",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Selecione o método de pagamento",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Selecionar token de recebimento",
+ "no_gas": "Sem saldo nativo para gas"
},
"connection_removed_modal": {
"title": "Conexões removidas",
@@ -7315,7 +7465,10 @@
"service_not_available": "Serviço não disponível no momento. Tente novamente em instantes.",
"invalid_referral_code": "Código de indicação inválido. Verifique e tente novamente.",
"already_referred": "Você já foi indicado por outro usuário.",
- "cannot_use_own_referral_code": "Você não pode usar seu próprio código de indicação."
+ "cannot_use_own_referral_code": "Você não pode usar seu próprio código de indicação.",
+ "invalid_bonus_code": "Código de bônus inválido",
+ "already_redeemed": "Você já resgatou este código de bônus",
+ "reached_maximum": "Este código de bônus atingiu o número máximo de utilizações"
},
"claim_reward_error": {
"title": "Falha ao resgatar a recompensa"
@@ -7372,8 +7525,10 @@
"predict": "Previsão",
"musd_deposit": "Depósito em mUSD",
"apply_referral_bonus": "Bônus de código de indicação",
+ "bonus_code": "Código de bônus",
"uncategorized_event": "Evento não categorizado"
},
+ "code": "Código",
"date": "Data",
"account": "Conta",
"bonus": "Bônus",
@@ -7473,7 +7628,16 @@
"show_less": "Exibir menos",
"linking_progress": "Adicionando contas... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} inscrita(s)",
- "add_all_accounts": "Adicionar todas as contas"
+ "add_all_accounts": "Adicionar todas as contas",
+ "environment_selector": "Ambiente",
+ "environment_cancel": "Cancelar",
+ "environment_default": "Padrão",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Código de indicação",
@@ -7483,6 +7647,10 @@
"invalid_code": "Código de indicação inválido",
"apply_button": "Aplicar código de indicação"
},
+ "bonus_code": {
+ "input_placeholder": "Inserir código de bônus",
+ "apply_success": "Código de bônus aplicado!"
+ },
"optout": {
"title": "Sair do programa de recompensas",
"description": "Essa ação removerá suas contas do programa de recompensas e apagará seus pontos e progresso. Essa ação não poderá ser desfeita.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Taxa de ponte",
+ "provider_fee": "Taxa do provedor",
"network_fee": "Taxa de rede",
"paid_with": "Paga com",
+ "receive_token": "Receber token",
"retry_button": "Tentar novamente",
"total": "Total",
"account": "Conta",
- "received_total": "Received total"
+ "received_total": "Total recebido"
},
"summary_title": {
"bridge_approval": "Aprovar {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Conexão não encontrada",
"description": "Para continuar, estabeleça uma nova conexão a partir do aplicativo."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Ainda conectando-se à {{networkName}}...",
"unable_to_connect_network": "Não é possível conectar-se à {{networkName}}.",
"update_rpc": "Atualizar RPC",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Alternar para o RPC padrão da MetaMask",
"check_network_connectivity": "Verifique a conexão de sua rede.",
"check_network_connectivity_or": "Verifique a conexão de sua rede ou",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Atualizado para o padrão da MetaMask"
},
"trending": {
"title": "Explorar",
"trending_tokens": "Tokens em alta",
+ "stocks": "Ações",
"price_change": "Variação de preço",
"all_networks": "Todas as redes",
"24h": "24h",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Recarregar",
"primary_action_acknowledge": "Entendi"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "Carteira de hardware QR",
+ "hardware_wallet": "Carteira de hardware"
+ },
+ "common": {
+ "cancel": "Cancelar",
+ "continue": "Continuar"
+ },
+ "connecting": {
+ "title": "Conecte seu {{device}}",
+ "searching": "Procurando {{device}}...",
+ "tips_header": "Para continuar, certifique-se de que:",
+ "tip_unlock": "Seu {{device}} está desbloqueado",
+ "tip_open_app": "O aplicativo Ethereum está aberto",
+ "tip_enable_bluetooth": "O Bluetooth está ativado",
+ "tip_dnd_off": "O modo \"Não perturbe\" está desativado",
+ "tip_bluetooth_permission": "Permissão de localização e Bluetooth estão concedidas",
+ "tip_bluetooth_permission_v12": "A permissão para dispositivos próximos foi concedida",
+ "tip_stay_close": "Seu dispositivo permanece próximo ao seu telefone"
+ },
+ "awaiting_app": {
+ "title": "Abrir {{app}}",
+ "message": "Abra o aplicativo {{app}} no seu {{device}}",
+ "current_app": "Aberto no momento: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Confirme em {{device}}",
+ "title_message": "Faça login em {{device}}",
+ "message": "Confira e confirme no seu {{device}}"
+ },
+ "success": {
+ "title": "{{device}} conectado"
+ },
+ "errors": {
+ "device_locked": "Desbloqueie-o e tente novamente para continuar",
+ "app_not_open": "Abra o aplicativo Ethereum em seu dispositivo",
+ "device_disconnected": "Seu {{device}} foi desconectado. Reconecte e tente novamente",
+ "device_not_found": "Não foi possível encontrar seu {{device}}. Verifique se ele está conectado",
+ "device_not_ready": "Seu dispositivo não está pronto. Verifique-o e tente novamente",
+ "blind_signing": "A assinatura cega está desativada. Ative-a nas configurações do seu dispositivo",
+ "connection_closed": "A conexão foi perdida. Tente novamente",
+ "connection_timeout": "Conexão expirou. Tente novamente",
+ "user_cancelled": "A ação foi cancelada no seu dispositivo",
+ "pending_confirmation": "Há uma ação pendente em seu dispositivo. Conclua-a ou cancele-a",
+ "bluetooth_permission_denied": "É necessário conceder permissão ao Bluetooth para conectar-se ao seu dispositivo",
+ "location_permission_denied": "É necessário permissão de localização para procurar dispositivos",
+ "nearby_permission_denied": "É necessário permissão para dispositivos próximos",
+ "bluetooth_off": "Ative o Bluetooth para se conectar ao seu dispositivo",
+ "bluetooth_scan_failed": "Falha ao procurar dispositivos. Tente novamente",
+ "bluetooth_connection_failed": "Ative o Bluetooth no seu dispositivo para continuar",
+ "not_supported": "Esta operação não é suportada",
+ "unknown_error": "Certifique-se de que seu {{device}} esteja configurado com a Frase de Recuperação Secreta ou com a senha para esta conta"
+ },
+ "error": {
+ "title": "Ocorreu algum erro",
+ "default_title": "Ocorreu um erro",
+ "continue": "Continuar",
+ "retry": "Tentar novamente",
+ "view_settings": "Ver configurações",
+ "device_locked_title": "{{device}} bloqueado",
+ "device_disconnected_title": "{{device}} desconectado",
+ "device_not_found_title": "{{device}} não encontrado",
+ "app_not_open": "Aplicativo Ethereum não aberto",
+ "blind_signing_disabled": "Assinatura cega desativada",
+ "connection_timeout": "Dispositivo não responde",
+ "connection_closed": "Conexão perdida",
+ "user_cancelled": "Ação cancelada",
+ "pending_confirmation": "Aguardando confirmação",
+ "bluetooth_required": "Bluetooth necessário",
+ "bluetooth_permission_denied": "Necessário permissão para usar Bluetooth",
+ "location_permission_denied": "Necessário permissão de localização",
+ "nearby_devices_permission_denied": "Necessário permissão para dispositivos próximos",
+ "scan_failed": "Verificação falhou",
+ "something_went_wrong": "Ocorreu algum erro"
+ },
+ "device_selection": {
+ "title": "Selecione {{device}}",
+ "scanning": "Procurando dispositivos...",
+ "no_devices_found": "Nenhum dispositivo encontrado",
+ "no_devices_hint": "Certifique-se de que seu {{device}} esteja desbloqueado e o Bluetooth esteja ativado",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Conectar",
+ "rescan": "Verificar novamente",
+ "unknown_device": "Dispositivo desconhecido",
+ "signal_strength": "Sinal: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Tokens",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFTs",
"import_nfts": "Importar NFTs",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Adicione facilmente seus itens colecionáveis",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Sem TP/SL"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "Não foi possível carregar {{section}}",
+ "retry": "Tentar novamente"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/ru.json b/locales/languages/ru.json
index bd2f1764300..6f44ffcf7d6 100644
--- a/locales/languages/ru.json
+++ b/locales/languages/ru.json
@@ -103,6 +103,10 @@
"message": "Недостаточно {{ticker}} для покрытия комиссий. Используйте токен в другой сети или добавьте больше {{ticker}}, чтобы продолжить.",
"title": "Недостаточно средств"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Этот путь оплаты сейчас недоступен. Попробуйте изменить сумму, сеть или токен, и мы подберём оптимальный вариант.",
"title": "Нет котировок"
@@ -1180,6 +1184,7 @@
"title": "Новый ордер",
"leverage": "Кредитное плечо",
"limit_price": "Предельная цена",
+ "market_price": "Рыночная цена",
"enter_price": "Введите цену",
"trigger_price": "Триггерная цена",
"liquidation_price": "Цена ликвидации",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Средства доступны для торговли",
"close_order_still_active": "Закрытие ордера все еще активно",
"order_submitted": "Ордер отправлен",
+ "submitting_your_trade": "Ваша сделка отправляется",
"order_filled": "Ордер выполнен",
"order_placed": "Ордер размещен",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "Информация об ордере",
"cancel_order": "Отменить ордер",
"date": "Дата",
+ "trigger_condition": "Условие срабатывания",
+ "price": "Цена",
"fee": "Комиссия",
"limit_buy": "Ограничить длинные",
"limit_price": "Предельная цена",
"limit_sell": "Ограничить короткие",
"market_buy": "Рыночная длинная",
"market_sell": "Рыночная короткая",
+ "market": "Рынок",
"open": "Открыть",
"size": "Размер",
+ "original_size": "Исходный размер",
+ "order_value": "Стоимость ордера",
+ "reduce_only": "Только уменьшить",
+ "yes": "Да",
+ "no": "Нет",
+ "price_above": "Цена выше {{price}}",
+ "price_below": "Цена ниже {{price}}",
+ "take_profit": "Тейк-профит",
+ "stop_loss": "Стоп-лосс",
"status": "Статус",
"view_explorer": "Смотреть в Обозревателе"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Аналитика рынка",
- "updated_ago": "Обновлено {{time}}",
- "disclaimer": "ИИ-аналитика. Не является финансовой консультацией.",
- "whats_driving_price": "Что влияет на цену?",
- "what_people_saying": "Что говорят люди",
+ "a_closer_look": "Подробный обзор",
+ "whats_being_said": "Что говорят",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Торговать",
"sources_count": "+{{count}} источника(-ов)",
- "sources_title": "Ликвидности"
+ "sources_title": "News sources",
+ "feedback_submitted": "Отзыв отправлен",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Отзыв",
+ "description": "Помогите улучшить наши обзоры рынка, созданные с помощью ИИ.",
+ "not_relevant": "Неактуально",
+ "not_accurate": "Неточно",
+ "hard_to_understand": "Сложно для понимания",
+ "harmful_or_offensive": "Вредно или оскорбительно",
+ "something_else": "Другое",
+ "additional_feedback_label": "Дополнительный отзыв (необязательно)",
+ "additional_feedback_placeholder": "Как мы можем это улучшить?",
+ "characters_remaining": "Осталось {{count}} символа(-ов)",
+ "submit": "Отправить"
+ }
},
"predict": {
"title": "Прогнозы MetaMask",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Доступно примерно через 1 минуту",
"withdraw_completed": "Вывод средств завершен",
"withdraw_completed_subtitle": "{{amount}} USDC переведены в ваш кошелек",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} переведены в ваш кошелек",
"error_title": "Что-то пошло не так",
"error_description": "Не удалось продолжить вывод средств",
"try_again": "Повторить попытку"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Имейте в виду, что это приблизительное значение, которое будет окончательно подтверждено после завершения транзакции. Подтверждение начисления баллов на ваш баланс вознаграждений может занять до 1 часа.",
"points_error": "Мы не можем загрузить баллы сейчас",
"points_error_content": "Вы все равно получите баллы за эту транзакцию. Мы уведомим вас, как только они будут зачислены на ваш счет. Вы также сможете увидеть их на вкладке «Вознаграждения» примерно через час.",
- "slippage": "Проскальзывание"
+ "slippage": "Проскальзывание",
+ "price_details": "Детали цены",
+ "prediction_order": "Ордер на прогноз",
+ "prediction_order_description": "~{{count}} контрактов по {{price}} каждый.\nИтоговая сумма может меняться в зависимости от доступности в книге ордеров (до {{slippage}}%).",
+ "metamask_fee_description": "Сервисный сбор за обработку этого прогноза",
+ "exchange_fee": "Комиссия биржи",
+ "exchange_fee_description": "Комиссия, уплачиваемая бирже или рынку",
+ "total_incl_fees": "вкл. комиссии",
+ "close": "Закрыть"
},
"error": {
"title": "Невозможно подключиться к прогнозам",
@@ -2399,7 +2440,7 @@
"add_tokens": "Импорт токенов",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Хотите импортировать эти токены?",
"tokens_detected_in_account": "{{tokenCount}} новый {{tokensLabel}} найден в этом счете",
"token_toast": {
"tokens_imported_title": "Импортированные токены",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Десятичные знаки токена не могут быть пустыми.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "нам не удалось найти токены с таким именем.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Найдите любой токен и импортируйте его",
"select_token": "Выбрать токен",
"address_must_be_smart_contract": "Обнаружен личный адрес. Введите адрес контракта токена.",
"billion_abbreviation": "Б",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "Добавить URL-адрес RPC",
"add_block_explorer_url": "Добавить URL обозревателя блоков",
"networks_desc": "Добавляйте и редактируйте пользовательские сети RPC",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Включенные сети",
+ "networks_test_networks": "Тестовые сети",
+ "networks_additional": "Дополнительные сети",
"networks_search_placeholder": "Поиск сетей",
"networks_no_results": "Сети не найдены",
"network_name_label": "Имя сети",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "RPC-сети",
"network_add_network": "Добавить сеть",
"add_chain_title": "Добавить сеть",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Поиск по названию, ID блокчейна или валюте",
+ "add_chain_loading": "Загрузка сетей…",
+ "add_chain_error": "Не удалось загрузить сети. Повторите попытку.",
"add_chain_retry": "Повтор",
- "add_chain_added": "Added",
+ "add_chain_added": "Добавлено",
"add_chain_or": "или",
"add_chain_custom_link": "Добавить пользовательскую сеть",
"network_add_custom_network": "Добавить пользовательскую сеть",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Добавить тестовую сеть",
"network_add": "Добавить",
"network_save": "Сохранить",
"remove_network_title": "Вы хотите удалить эту сеть?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Сохраните ее в надежном секретном месте.",
"private_key_warning": "Это закрытый ключ для текущего выбранного счета: {{accountName}}. Никогда никому не сообщайте этот ключ. Любой, у кого есть ваш закрытый ключ, может полностью контролировать ваш счет, включая перевод любых средств.",
- "seed_phrase_warning_explanation": [
- "Убедитесь, что никто не смотрит на ваш экран.",
- "Служба поддержки MetaMask никогда не будет запрашивать этот ключ."
- ],
+ "seed_phrase_warning_explanation": "Убедитесь, что никто не смотрит на ваш экран. Служба поддержки MetaMask никогда не будет задавать этот вопрос.",
"private_key_warning_explanation": "Никогда никому не сообщайте этот ключ. Любой, у кого есть ваш закрытый ключ, может полностью контролировать ваш счет, включая перевод любых ваших средств.",
+ "reveal_srp_description": "Ваша секретная фраза для восстановления предоставляет полный доступ к вашему кошельку. Не сообщайте ее никому.",
"reveal_credential_modal": [
"Ваш {{credentialName}} предоставляет ",
"полный доступ к вашему счету и средствам.\n\nНикому не сообщайте эти сведения.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "секретная фраза для восстановления",
"private_key_text": "Закрытый ключ",
"got_it": "Понятно",
- "learn_more": "Подробнее"
+ "learn_more": "Подробнее",
+ "copied_to_clipboard": "Скопировано в буфер обмена"
},
"screenshot_deterrent": {
"title": "Предупреждение о безопасности",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Попытаться ускорить?",
"speedup_tx_message": "Эта попытка не гарантирует, что ваша первоначальная транзакция будет ускорена. Если попытка ускорения окажется успешной, с вас будет удержана вышеуказанная комиссия за транзакцию.",
"nevermind": "Неважно",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Изменить плату за газ",
"edit_priority": "Изменить приоритет",
"gas_cancel_fee": "Плата за газ при отмене",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Комиссия за транзакцию связана с этим разрешением.",
"view_details": "Просмотр сведений",
"view_transaction_details": "Смотреть реквизиты транзакции",
- "view_data": "View data",
+ "view_data": "Просмотреть данные",
"transaction_details": "Сведения о транзакции",
"site_url": "URL сайта",
"permission_request": "Запрос разрешения",
@@ -3843,7 +3887,7 @@
"right_button": "Защитить кошелек"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Добавить в избранное",
"title_label": "Имя",
"url_label": "URL",
"add_button": "Добавить",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Разблокировать с помощью Touch ID?",
"enable_faceid": "Разблокировать с помощью Face ID?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "Разблокировать по отпечатку пальца?",
+ "enable_biometrics": "Разблокировать с помощью биометрии?",
"enable_device_passcode_ios": "Разблокировать с помощью пароля устройства?",
"enable_device_passcode_android": "Разблокировать с помощью PIN-кода устройства?"
},
@@ -4724,7 +4768,7 @@
"public_address": "Публичный адрес",
"public_address_qr_code": "Публичный адрес",
"coming_soon": "Скоро появится...",
- "request_payment": "Request payment",
+ "request_payment": "Запросить платеж",
"copy": "Копировать",
"scan_address": "Отсканируйте адрес для получения платежа",
"copy_address": "Копировать адрес"
@@ -4738,8 +4782,8 @@
"switch_network": "Переключитесь на мейн-нет или Sepolia",
"card_title": "Всегда показывать кнопку Карты MetaMask",
"card_desc": "Карта MetaMask доступна только для жителей отдельных стран.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "Использовать демо-среду DaimoPay",
+ "daimo_demo_desc": "Переключение между демо- и рабочей средой DaimoPay для платежей по картам."
},
"walletconnect_sessions": {
"no_active_sessions": "У вас нет активных сеансов",
@@ -4752,10 +4796,10 @@
"close_current_session": "Закройте текущий сеанс перед началом нового."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Запрос платежа",
+ "title_complete": "Платеж завершен",
"confirm": "Оплатить",
- "cancel": "Decline",
+ "cancel": "Отклонить",
"is_requesting_you_to_pay": "просит вас заплатить",
"total": "Итого:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Нет доступных способов оплаты.",
"error_fetching_quotes": "Возникла какая-то проблема. Повторите попытку.",
"no_quotes_available": "Нет доступных поставщиков.",
+ "quote_unavailable": "Котировка недоступна.",
"providers": "Поставщики",
+ "quotes_displayed_for": "Котировки отображаются для {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Ранее использовались",
+ "best_rate": "Лучший курс",
+ "most_reliable": "Самый надежный",
"continue": "Продолжить",
"powered_by_provider": "Работает на платформе {{provider}}",
"purchased_currency": "Куплено {{currency}}",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Ошибка выхода"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Недоступен",
+ "description": "{{token}} недоступен через {{provider}} в вашем регионе.",
+ "change_token": "Сменить токен",
+ "change_provider": "Изменить поставщика"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Выберите поставщика"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Понятно",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Изменить поставщика"
},
"fiat_on_ramp_aggregator": {
"buy": "купить",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "Депозит на сумму {{currency}}"
},
+ "ramps_order_details": {
+ "title": "Сведения об ордере",
+ "status": "Статус",
+ "processing": "Обработка...",
+ "complete": "Завершено",
+ "failed": "Ошибка",
+ "cancelled": "Отменено",
+ "view_on_provider": "Просмотр на {{provider}}",
+ "order_id": "Ид. ордера",
+ "date_and_time": "Дата и время",
+ "fees": "Комиссии",
+ "total": "Итого",
+ "card_processing_info": "Покупки по карте обычно занимают несколько минут",
+ "processing_info_modal_description": "Оформление покупки с помощью карты обычно занимает несколько минут. Если у вас возникнут вопросы, вы можете обратиться в службу поддержки.",
+ "go_to_provider_support": "Перейти на страницу поддержки {{provider}}",
+ "error_title": "Что-то пошло не так",
+ "error_message": "Не удалось загрузить сведения об ордере. Повторите попытку.",
+ "try_again": "Повторить попытку",
+ "close": "Закрыть"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Обработка вашей покупки {{cryptocurrency}}...",
+ "purchase_pending_description": "Это должно занять всего несколько минут...",
+ "purchase_completed_title": "Ваша покупка {{amount}} {{cryptocurrency}} успешно выполнена!",
+ "purchase_completed_description": "Ваша {{cryptocurrency}} уже доступна",
+ "purchase_failed_title": "Покупка {{cryptocurrency}} не удалась",
+ "purchase_failed_description": "Попробуйте еще раз через минуту",
+ "purchase_cancelled_title": "Ваша покупка отменена",
+ "purchase_cancelled_description": "Ваша покупка {{cryptocurrency}} была отменена",
+ "track": "Отследить"
+ }
+ },
"swaps": {
"title": "Обменять",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Чтобы этого больше не повторилось, всегда обновляйте приложение MetaMask и ОС до последней версии."
},
"srp_security_quiz": {
+ "question_step": "Вопрос {{step}} из {{total}}",
"title": "Тест по безопасности",
"introduction": "Чтобы увидеть свою секретную фразу для восстановления, вам нужно правильно ответить на два вопроса",
"get_started": "С чего начать",
"learn_more": "Узнайте подробнее",
"try_again": "Повторить попытку",
"continue": "Продолжить",
+ "correct": "Правильно!",
+ "incorrect": "Неправильно!",
"of": "из",
"question_one": {
"question": "Если вы потеряете свою секретную фразу для восстановления, MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "Не удалось установить {{snap}}."
},
"earn": {
- "claimable_bonus_tooltip": "Ежегодный бонус, который можно ежедневно выводить из вашего кошелька.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Заработайте бонус в размере {{percentage}}%",
"claimable_bonus": "Встребуемый бонус",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Получить бонус",
+ "claim_bonus_subtitle": "Бонус будет выплачен в сети {{networkName}}.",
"empty_state_cta": {
"heading": "Давайте взаймы {{tokenSymbol}} и зарабатывайте",
"body": "Одолжите свой {{tokenSymbol}} с помощью {{protocol}} и зарабатывайте",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Мы закрыты на техобслуживание. Скоро вернемся в сеть!"
},
- "supply": "Supply",
+ "supply": "Предложение",
"deposit": "Внести депозит",
"approve": "Одобрить",
"approval": "Одобрение",
@@ -5887,7 +5978,7 @@
"network": "Сеть",
"health_factor": "Фактор здоровья",
"liquidation_risk": "Риск ликвидации",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Недостаточно ликвидности в пуле",
"available_to_withdraw": "доступно для вывода",
"unknown": "неизвестно",
"how_it_works": "Как это работает",
@@ -5906,7 +5997,7 @@
"lending": "История позиций",
"staking": "История выплат"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Сброс разрешения",
"tron": {
"fee": "Комиссия"
},
@@ -5914,32 +6005,60 @@
"ok": "ОК",
"continue": "Продолжить",
"convert_and_get_percentage_bonus": "Конвертируйте и получите {{percentage}}%",
+ "your_musd": "Ваш mUSD",
+ "balance_breakdown_title": "Ваши балансы mUSD по сетям",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "Конвертировать в mUSD",
"get_a_percentage_musd_bonus": "Получите бонус в размере {{percentage}}% в mUSD",
"convert": "Конвертировать",
+ "fetching_quote": "Получение котировки...",
+ "you_convert": "Вы конвертируете",
+ "network_fee": "Комиссия сети",
+ "earning": "Заработок",
+ "quick_convert_description": "Выберите токен, чтобы конвертировать весь баланс, или нажмите на значок редактирования, чтобы ввести свою сумму.",
+ "no_tokens_to_convert": "У вас нет токенов, которые можно конвертировать в mUSD.",
"toasts": {
"converting": "Конвертация {{token}} → mUSD",
"eta": "~{{time}}",
- "delivered": "Ваши mUSD здесь!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "Конвертация mUSD не удалась."
},
"education": {
"heading": "ПОЛУЧИТЕ {{percentage}}% НА\nСТЕЙБЛКОИНЫ",
- "description": "Конвертируйте свои стейблкоины в mUSD, обеспеченный долларом США стейблкоин от MetaMask, и получите бонус до {{percentage}}%.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Применяются условия.",
"primary_button": "Начало работы",
"secondary_button": "Не сейчас"
},
"buy_musd": "Купить mUSD",
"get_musd": "Получить mUSD",
- "bonus_title": "Получите {{percentage}}% на ваши стейблкоины",
- "bonus_description": "Конвертируйте свои стейблкоины в mUSD и получите бонус до {{percentage}}%.",
- "powered_by_relay": "При поддержке Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "При поддержке Relay",
+ "max": "Максимум",
+ "quick_convert_button": "Конвертировать",
+ "learn_more": "Узнайте подробнее",
+ "tooltip_title": "Получайте доходность с mUSD",
+ "tooltip_content": "Конвертируйте свои USDC, USDT или DAI в mUSD, стейблкоин MetaMask, обеспеченный долларом. Получайте доходность в размере {{apy}} с каждого имеющегося у вас доллара.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Ошибка конвертации. Попробуйте еще раз.",
+ "confirmation": {
+ "title": "Конвертировать максимум"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Курс"
},
"bonus_claim": {
"toasts": {
"claiming": "Ваш бонус mUSD bonus обрабатывается",
"delivered": "Ваш бонус в mUSDуже поступил!",
- "failed": "Bonus claim failed"
+ "failed": "Не удалось получить бонус"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Баллы автоматически зачисляются на ваш счет.",
"tooltip_not_opted_in_footer": "Чтобы получать баллы, согласитесь с получением бонусов.",
"tooltip_close": "Закрыть"
- }
+ },
+ "your_stablecoins": "Ваши стейблкоины"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Комиссии за транзакцию",
"metamask_fee": "Комиссия MetaMask",
"network_fee": "Комиссия сети",
- "bridge_fee": "Комиссия поставщика моста"
+ "bridge_fee": "Комиссия поставщика моста",
+ "provider_fee": "Комиссия поставщика"
},
"title": {
"signature": "Запрос подписи",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Мы обменяем ваши токены на USDC.e в Polygon, сети, используемой функцией «Прогнозы». Поставщики услуг свопов могут взимать комиссию, но MetaMask не взимает ее."
},
"predict_withdraw": {
- "transaction_fee": "MetaMask автоматически обменяет ваш токен на желаемый. При обмене на MUSD комиссия MetaMask не взимается."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "В стоимость конвертации mUSD входят расходы сети, а также могут входить сборы поставщика."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Комиссии"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Влияние на цену",
"price_impact_info_description": "Влияние на цену отражает, как ваш своп-ордер влияет на рыночную цену актива. Оно зависит от объёма сделки и доступной ликвидности в пуле. MetaMask не имеет отношения к влиянию на цену и не контролирует его.",
"price_impact_info_gasless_description": "Влияние на цену отражает, как ваш ордер на своп влияет на рыночную цену актива. Если у вас недостаточно средств для оплаты газа, часть вашего исходного токена автоматически выделяется на покрытие комиссий, что увеличивает влияние на цену. MetaMask не влияет на воздействие цену и не контролирует его.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Отмена",
"slippage_info_title": "Проскальзывание",
"slippage_info_description": "Процент изменения цены, который вы готовы допустить, прежде чем транзакция будет отменена.",
"blockaid_error_title": "Эта транзакция будет отменена",
@@ -6470,13 +6596,14 @@
},
"submit": "Отправить",
"default_slippage_description": "Ваша транзакция не будет завершена, если изменение цены превысит допустимый процент проскальзывания.",
- "cancel": "Отмена",
"confirm": "Подтвердить",
"exceeding_upper_slippage_warning": "Высокое проскальзывание может привести к невыгодному свопу",
"exceeding_lower_slippage_warning": "Низкое проскальзывание может привести к невыгодному свопу",
"exceeding_lower_slippage_error": "Введите значение более {{value}}%",
"exceeding_upper_slippage_error": "Вы не можете ввести значение более {{value}}%",
- "custom": "Пользовательские"
+ "custom": "Пользовательские",
+ "invalid_recipient_address": "Недействительный адрес",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Доступны новые котировки",
@@ -6773,12 +6900,16 @@
"title": "Введите пароль",
"description": "Введите пароль от своего кошелька, чтобы просмотреть реквизиты карты.",
"description_unfreeze": "Введите пароль от своего кошелька, чтобы возобновить расходы по карте.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Пароль",
"confirm": "Подтвердить",
"cancel": "Отмена",
"error_empty": "Введите свой пароль",
"error_incorrect": "Неверный пароль. Повторите попытку."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Выберите свою карту",
"upgrade_title": "Повысить уровень до Металлической",
@@ -7094,6 +7225,9 @@
"view_card_details": "Смотреть реквизиты карты",
"hide_card_details": "Скрыть реквизиты карты",
"view_card_details_description": "Номер карты, срок действия и CVV-код",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Управление лимитом",
"manage_spending_limit_description_restricted": "Активирован лимит расходов",
"manage_spending_limit_description_full": "Активирован полный доступ",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Положения и условия",
"order_metal_card": "Металлическая карта",
"order_metal_card_description": "Закажите свою физическую металлическую карту прямо сейчас",
+ "cashback": "Кешбэк",
+ "cashback_description": "Получайте 1% всех трат обратно",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Заблокировать карту",
"unfreeze_card": "Разблокировать карту",
"freeze_card_description": "Приостановить все расходы по вашей карте",
@@ -7141,6 +7278,19 @@
"retry": "Повторить попытку",
"on_linea": "на Linea"
},
+ "cashback_screen": {
+ "title": "Кэшбэк",
+ "available_cashback": "Доступный кешбэк",
+ "network_fee": "Комиссия сети",
+ "expected_to_receive": "Ожидается к получению",
+ "withdraw": "Вывести средства",
+ "withdraw_unavailable": "Вывод недоступен",
+ "withdrawal_initiated": "Вывод инициирован",
+ "withdrawal_success": "Вывод успешно завершен",
+ "withdrawal_failed": "Ошибка вывода. Повторите попытку.",
+ "no_cashback": "Нет доступного кешбэка",
+ "loading_error": "Не удалось загрузить кешбэк. Повторите попытку."
+ },
"change_asset": {
"title": "Измените токен и сеть",
"full_spending_access": "Полный доступ к расходам",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Выбрать способ оплаты",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Выберите токен для получения",
+ "no_gas": "Нет нативного баланса для оплаты газа"
},
"connection_removed_modal": {
"title": "Соединения удалены",
@@ -7315,7 +7465,10 @@
"service_not_available": "В данный момент сервис недоступен. Повторите попытку позже.",
"invalid_referral_code": "Неверный реферальный код. Проверьте и повторите попытку.",
"already_referred": "Вас уже направил другой пользователь.",
- "cannot_use_own_referral_code": "Вы не можете использовать собственный реферальный код."
+ "cannot_use_own_referral_code": "Вы не можете использовать собственный реферальный код.",
+ "invalid_bonus_code": "Недействительный бонусный код",
+ "already_redeemed": "Вы уже использовали этот бонусный код",
+ "reached_maximum": "Этот бонусный код достиг использован максимальное количество раз"
},
"claim_reward_error": {
"title": "Не удалось получить вознаграждение"
@@ -7372,8 +7525,10 @@
"predict": "Прогноз",
"musd_deposit": "Депозит mUSD",
"apply_referral_bonus": "Бонус за реферальный код",
+ "bonus_code": "Бонусный код",
"uncategorized_event": "Событие без категории"
},
+ "code": "Код",
"date": "Дата",
"account": "Счет",
"bonus": "Бонус",
@@ -7473,7 +7628,16 @@
"show_less": "Показать меньше",
"linking_progress": "Добавляются счета... ({{current}} из {{total}})",
"accounts_linked_count": "{{linked}}/{{total}} зарегистрировано",
- "add_all_accounts": "Добавить все счета"
+ "add_all_accounts": "Добавить все счета",
+ "environment_selector": "Среда",
+ "environment_cancel": "Отмена",
+ "environment_default": "По умолчанию",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Реферальный код",
@@ -7483,6 +7647,10 @@
"invalid_code": "Недействительный реферальный код",
"apply_button": "Применить реферальный код"
},
+ "bonus_code": {
+ "input_placeholder": "Введите бонусный код",
+ "apply_success": "Бонусный код применен!"
+ },
"optout": {
"title": "Отказаться от участия в программе вознаграждений",
"description": "Это приведет к удалению ваших счетов из программы вознаграждений, а также к потере баллов и прогресса. Отменить это действие будет невозможно.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Комиссия моста",
+ "provider_fee": "Комиссия поставщика",
"network_fee": "Комиссия сети",
"paid_with": "Оплачено с помощью",
+ "receive_token": "Получить токен",
"retry_button": "Повторить попытку",
"total": "Итого",
"account": "Счет",
- "received_total": "Received total"
+ "received_total": "Всего получено"
},
"summary_title": {
"bridge_approval": "Одобрить {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Соединение не найдено",
"description": "Для продолжения установите новое соединение через приложение."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Все еще выполняется подключение к {{networkName}}...",
"unable_to_connect_network": "Не удалось подключиться к {{networkName}}.",
"update_rpc": "Обновить RPC",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Переключиться на RPC MetaMask по умолчанию",
"check_network_connectivity": "Проверьте подключение к сети.",
"check_network_connectivity_or": "Проверьте подключение к сети или",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Обновлено на настройки MetaMask по умолчанию"
},
"trending": {
"title": "Обзор",
"trending_tokens": "Популярные токены",
+ "stocks": "Акции",
"price_change": "Изменение цены",
"all_networks": "Все сети",
"24h": "24 ч",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Перезагрузить",
"primary_action_acknowledge": "Понятно"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "Аппаратный QR-кошелек",
+ "hardware_wallet": "Аппаратный кошелек"
+ },
+ "common": {
+ "cancel": "Отмена",
+ "continue": "Продолжить"
+ },
+ "connecting": {
+ "title": "Подключите ваше {{device}}",
+ "searching": "Поиск {{device}}...",
+ "tips_header": "Чтобы продолжить, убедитесь, что:",
+ "tip_unlock": "Ваше {{device}} разблокировано",
+ "tip_open_app": "Приложение Ethereum открыто",
+ "tip_enable_bluetooth": "Bluetooth включен",
+ "tip_dnd_off": "Режим «Не беспокоить» выключен",
+ "tip_bluetooth_permission": "Разрешен доступ к геолокации и Bluetooth",
+ "tip_bluetooth_permission_v12": "Разрешен доступ к устройствам поблизости",
+ "tip_stay_close": "Ваше устройство находится рядом с телефоном"
+ },
+ "awaiting_app": {
+ "title": "Откройте {{app}}",
+ "message": "Откройте приложение {{app}} на вашем {{device}}",
+ "current_app": "Сейчас открыто: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Подтвердите на {{device}}",
+ "title_message": "Подпишите на {{device}}",
+ "message": "Проверьте и подтвердите на вашем {{device}}"
+ },
+ "success": {
+ "title": "{{device}} подключено"
+ },
+ "errors": {
+ "device_locked": "Разблокируйте его и попробуйте снова, чтобы продолжить",
+ "app_not_open": "Откройте приложение Ethereum на вашем устройстве",
+ "device_disconnected": "Ваше {{device}} было отключено. Подключитесь снова и повторите попытку.",
+ "device_not_found": "Не удалось найти ваше {{device}}. Убедитесь, что устройство подключено.",
+ "device_not_ready": "Ваше устройство не готово. Проверьте его и повторите попытку.",
+ "blind_signing": "Слепое подписание отключено. Включите его в настройках вашего устройства.",
+ "connection_closed": "Подключение прервано. Повторите попытку.",
+ "connection_timeout": "Время ожидания подключения истекло. Повторите попытку.",
+ "user_cancelled": "Действие было отменено на вашем устройстве",
+ "pending_confirmation": "На вашем устройстве ожидается выполнения действия. Выполните или отмените его.",
+ "bluetooth_permission_denied": "Для подключения к вашему устройству требуется разрешение на использование Bluetooth",
+ "location_permission_denied": "Для поиска устройств требуется разрешение на использование геолокации",
+ "nearby_permission_denied": "Требуется разрешение на доступ к устройствам поблизости",
+ "bluetooth_off": "Включите Bluetooth для подключения к вашему устройству",
+ "bluetooth_scan_failed": "Не удалось выполнить поиск устройств. Повторите попытку",
+ "bluetooth_connection_failed": "Включите Bluetooth на вашем устройстве, чтобы продолжить",
+ "not_supported": "Эта операция не поддерживается",
+ "unknown_error": "Убедитесь, что ваш {{device}} настроен с помощью секретной фразой для восстановления или пароля для этого счета"
+ },
+ "error": {
+ "title": "Что-то пошло не так",
+ "default_title": "Произошла ошибка",
+ "continue": "Продолжить",
+ "retry": "Повтор",
+ "view_settings": "Просмотреть Настройки",
+ "device_locked_title": "{{device}} заблокировано",
+ "device_disconnected_title": "{{device}} отключено",
+ "device_not_found_title": "{{device}} не найдено",
+ "app_not_open": "Приложение Ethereum не открыто",
+ "blind_signing_disabled": "Слепое подписание отключено",
+ "connection_timeout": "Устройство не отвечает",
+ "connection_closed": "Подключение разорвано",
+ "user_cancelled": "Действие отменено",
+ "pending_confirmation": "Ожидание подтверждения",
+ "bluetooth_required": "Требуется Bluetooth",
+ "bluetooth_permission_denied": "Требуется разрешение на Bluetooth",
+ "location_permission_denied": "Требуется разрешение на геолокацию",
+ "nearby_devices_permission_denied": "Требуется разрешение на поиск устройств поблизости",
+ "scan_failed": "Ошибка поиска",
+ "something_went_wrong": "Что-то пошло не так"
+ },
+ "device_selection": {
+ "title": "Выберите {{device}}",
+ "scanning": "Поиск устройств...",
+ "no_devices_found": "Устройства не найдены",
+ "no_devices_hint": "Убедитесь, что ваше {{device}} разблокировано и Bluetooth включен",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Подключиться",
+ "rescan": "Повторить поиск",
+ "unknown_device": "Неизвестное устройство",
+ "signal_strength": "Сигнал: {{rssi}} дБм"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Токены",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT",
"import_nfts": "Импорт NFT",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Легко добавляйте свои предметы коллекционирования",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Нет TP/SL"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "Не удалось загрузить {{section}}",
+ "retry": "Повтор"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/tl.json b/locales/languages/tl.json
index a8add6158cc..a58d4e9991f 100644
--- a/locales/languages/tl.json
+++ b/locales/languages/tl.json
@@ -103,6 +103,10 @@
"message": "Walang sapat na {{ticker}} para sa mga bayarin. Gumamit ng token sa ibang network o magdagdag pa ng {{ticker}} para makapagpatuloy.",
"title": "Hindi sapat ang mga pondo"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Hindi available ang ruta ng pagbabayad na ito sa ngayon. Subukang baguhin ang halaga, network, o token at hahanapin namin ang pinakamainam na opsyon.",
"title": "Walang mga quote"
@@ -1180,6 +1184,7 @@
"title": "Bagong order",
"leverage": "Leverage",
"limit_price": "Limit price",
+ "market_price": "Market price",
"enter_price": "Enter price",
"trigger_price": "Trigger price",
"liquidation_price": "Liquidation price",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Ang mga pondo ay available na i-trade",
"close_order_still_active": "Aktibo parin ang order na isara",
"order_submitted": "Naisumite ang order",
+ "submitting_your_trade": "Isinusumite ang trade mo",
"order_filled": "Na-fill ang order",
"order_placed": "Nailagay ang oder",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1338,16 +1344,28 @@
},
"order_details": {
"title": "Mga Detalye ng Order",
- "cancel_order": "Kanselahin ang Order",
+ "cancel_order": "Kanselahin ang order",
"date": "Petsa",
+ "trigger_condition": "Kondisyon ng trigger",
+ "price": "Presyo",
"fee": "Bayarin",
"limit_buy": "Limitahan ang Long",
"limit_price": "Limit Price",
"limit_sell": "Limitahan ang Short",
"market_buy": "Market Long",
"market_sell": "Market Short",
+ "market": "Market",
"open": "Bukas",
"size": "Laki",
+ "original_size": "Orihinal na laki",
+ "order_value": "Halaga ng order",
+ "reduce_only": "Reduce only",
+ "yes": "Oo",
+ "no": "Hindi",
+ "price_above": "Presyong mas mataas sa {{price}}",
+ "price_below": "Presyong mas mababa sa {{price}}",
+ "take_profit": "Take profit",
+ "stop_loss": "Stop loss",
"status": "Katayuan",
"view_explorer": "Tingnan sa Explorer"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Mga market insight",
- "updated_ago": "Na-update {{time}}",
- "disclaimer": "Mga AI insight. Hindi payong pinansyal.",
- "whats_driving_price": "Ano ang nagtutulak sa presyo?",
- "what_people_saying": "Ano ang sinasabi ng mga tao",
+ "a_closer_look": "Mas malalim na pagtingin",
+ "whats_being_said": "Ano ang sinasabi",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Mag-trade",
"sources_count": "+{{count}} (na) pinagmulan",
- "sources_title": "Mga pinagmulan"
+ "sources_title": "News sources",
+ "feedback_submitted": "Isinumite ang feedback",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Feedback",
+ "description": "Tumulong na mapahusay ang mga pananaw sa market na gawa ng AI.",
+ "not_relevant": "Walang kaugnayan",
+ "not_accurate": "Hindi tumpak",
+ "hard_to_understand": "Mahirap maunawaan",
+ "harmful_or_offensive": "Mapaminsala o nakakasakit",
+ "something_else": "Iba pa",
+ "additional_feedback_label": "Karagdagang feedback (opsyonal)",
+ "additional_feedback_placeholder": "Paano kami mas huhusay?",
+ "characters_remaining": "{{count}} (na) karakter ang natitira",
+ "submit": "Isumite"
+ }
},
"predict": {
"title": "Mga Predisksyon ng MetaMask",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Available sa loob ng humigit-kumulang 1 minuto",
"withdraw_completed": "Tapos ng mag-withdraw",
"withdraw_completed_subtitle": "Nailipat ang {{amount}} USDC sa iyong wallet",
+ "withdraw_any_token_completed_subtitle": "Nailipat ang {{amount}}{{token}} sa iyong wallet",
"error_title": "Mayroong nang mali",
"error_description": "Nabigong magpatuloy sa pag-withdraw",
"try_again": "Subukang muli"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Tandaan na ang halagang ito ay isang pagtatantya lamang at magiging pinal lamang kapag natapos na ang transaksyon. Maaaring umabot ng hanggang 1 oras bago makumpirma ang mga point sa iyong balanse ng mga Reward.",
"points_error": "Hindi namin mai-load ang mga point sa ngayon",
"points_error_content": "Makakaipon ka pa rin ng mga point para sa transaksyong ito. Aabisuhan ka namin kapag naidagdag na ang mga ito sa iyong account. Maaari mo rin itong tingnan sa iyong rewards tab makalipas ang humigit-kumulang isang oras.",
- "slippage": "Slippage"
+ "slippage": "Slippage",
+ "price_details": "Mga detalye ng presyo",
+ "prediction_order": "Order na prediksyon",
+ "prediction_order_description": "~{{count}} (na) kontrata sa halagang {{price}} bawat isa. Puwedeng mag-iba ang pinal na halaga batay sa availability ng order book (hanggang sa {{slippage}}%).",
+ "metamask_fee_description": "Bayad sa serbisyo para pagproseso ng prediksyong ito",
+ "exchange_fee": "Bayad sa palitan",
+ "exchange_fee_description": "Bayad sa palitan o market",
+ "total_incl_fees": "kasama ang mga bayarin",
+ "close": "Isara"
},
"error": {
"title": "Hindi maikonekta sa mga prediksyon",
@@ -2399,7 +2440,7 @@
"add_tokens": "Mag-import ng mga token",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Gusto mo bang i-import ang mga token na ito?",
"tokens_detected_in_account": "{{tokenCount}} (na) bagong {{tokensLabel}} ang natagpuan sa account na ito",
"token_toast": {
"tokens_imported_title": "Na-import na mga token",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Hindi maaaring walang laman ang smga decimal ng token.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "Wala kaming mahanap na anumang token na may ganyang pangalan.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Maghanap ng kahit anong token at i-import ito",
"select_token": "Pumili ng token",
"address_must_be_smart_contract": "Natukoy ang personal na address. Ilagay ang address ng kontrata ng token.",
"billion_abbreviation": "B",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "Magdagdag ng RPC URL",
"add_block_explorer_url": "Magdagdag ng URL ng block explorer",
"networks_desc": "Magdagdag at mag-edit ng mga custom na RPC network",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Mga Naka-enable na Network",
+ "networks_test_networks": "Mga Test Network",
+ "networks_additional": "Mga Karagdagang Network",
"networks_search_placeholder": "Pumili ng mga network",
"networks_no_results": "Walang nahanap na network",
"network_name_label": "Pangalan ng network",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "Mga RPC network",
"network_add_network": "Magdagdag ng network",
"add_chain_title": "Magdagdag ng network",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Maghanap ayon sa pangalan, chain ID, o currency",
+ "add_chain_loading": "Naglo-load ng mga network…",
+ "add_chain_error": "Hindi nakapag-load ng mga network. Subukan ulit.",
"add_chain_retry": "Subukang muli",
- "add_chain_added": "Added",
+ "add_chain_added": "Naidagdag",
"add_chain_or": "o",
"add_chain_custom_link": "Magdagdag ng custom na network",
"network_add_custom_network": "Magdagdag ng custom na network",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Magdagdag ng test network",
"network_add": "Idagdag",
"network_save": "I-save",
"remove_network_title": "Gusto mo bang alisin ang network na ito?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "I-save ito sa isang ligtas at sikretong lugar.",
"private_key_warning": "Ito ang pribadong key para sa kasalukuyang napiling account: {{accountName}}. Huwag ipaalam ang key na ito. Ang sinumang nakakaalam ng iyong pribadong key ay ganap na makokontrol ang iyong account, pati na ang paglilipat ng alinman sa iyong mga pondo.",
- "seed_phrase_warning_explanation": [
- "Tiyaking walang tumitingin sa iyong screen. ",
- "Hindi ito kailanman hihilingin ng Suporta sa MetaMask."
- ],
+ "seed_phrase_warning_explanation": "Siguraduhing walang nakatingin sa screen mo. Hindi ito kailanman hihingin ng Suporta ng MetaMask.",
"private_key_warning_explanation": "Huwag ipaalam ang key na ito. Ang sinumang nakakaalam ng iyong pribadong key ay ganap na makokontrol ang iyong account, pati na ang paglilipat ng alinman sa iyong mga pondo.",
+ "reveal_srp_description": "Maa-access nang buo ang wallet mo sa pamamagitan ng Lihim na Parirala sa Pagbawi. Huwag itong ibahagi sa kahit sino.",
"reveal_credential_modal": [
"Ang iyong {{credentialName}} ay nagbibigay ng ",
"buong access sa iyong account at pondo.\n\nHuwag ibahagi ito sa iba.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "Lihim na Parirala sa Pagbawi (Secret Recovery Phrase)",
"private_key_text": "Pribadong key",
"got_it": "Nakuha ko",
- "learn_more": "Matuto pa"
+ "learn_more": "Matuto pa",
+ "copied_to_clipboard": "Nakopya sa clipboard"
},
"screenshot_deterrent": {
"title": "Alertong pangkaligtasan",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Subukang pabilisin?",
"speedup_tx_message": "Kapag isinumite ang pagsubok na ito, hindi magagarantiya na maa-accelerate ang iyong orihinal na transaksyon. Kung matagumpay ang pagpapabilis, sisingilin ka para sa bayad sa transaksyon sa itaas.",
"nevermind": "Huwag na lang",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "I-edit ang bayad sa gas",
"edit_priority": "I-edit ang priyoridad",
"gas_cancel_fee": "Bayad sa pagkansela ng gas",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "May nauugnay na bayad sa transaksyon sa pahintulot na ito.",
"view_details": "Tingnan ang mga detalye",
"view_transaction_details": "Tingnan ang mga detalye ng transaksyon",
- "view_data": "View data",
+ "view_data": "Tingnan ang data",
"transaction_details": "Mga detalye ng transaksyon",
"site_url": "URL ng Site",
"permission_request": "Kahilingan sa pahintulot",
@@ -3843,7 +3887,7 @@
"right_button": "Protektahan ang wallet"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Magdagdag ng paborito",
"title_label": "Pangalan",
"url_label": "URL",
"add_button": "Idagdag",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "I-unlock gamit ang Touch ID?",
"enable_faceid": "I-unlock gamit ang Face ID?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "I-unlock gamit ang fingerprint?",
+ "enable_biometrics": "I-unlock gamit ang biometrics?",
"enable_device_passcode_ios": "I-unlock gamit ang passcode ng device?",
"enable_device_passcode_android": "I-unlock gamit ang PIN ng device?"
},
@@ -4724,7 +4768,7 @@
"public_address": "Pampublikong address",
"public_address_qr_code": "Pampublikong address",
"coming_soon": "Paparating na...",
- "request_payment": "Request payment",
+ "request_payment": "Humiling ng bayad",
"copy": "Kopyahin",
"scan_address": "I-scan ang address para matanggap ang bayad",
"copy_address": "Kopyahin ang address"
@@ -4738,8 +4782,8 @@
"switch_network": "Mangyaring lumipat sa mainnet o sepolia",
"card_title": "Laging ipakita ang button ng MetaMask Card",
"card_desc": "Available lang ang MetaMask Card sa mga residente ng mga piling bansa.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "Gamitin ang demo environment ng DaimoPay",
+ "daimo_demo_desc": "Mag-toggle sa pagitan ng mga demo at production environment ng DaimoPay para sa mga pagbabayad gamit ang card."
},
"walletconnect_sessions": {
"no_active_sessions": "Wala kang aktibong session",
@@ -4752,10 +4796,10 @@
"close_current_session": "Isara ang kasalukuyang sesyon bago magsimula ng bago."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Kahilingan sa bayad",
+ "title_complete": "Kumpleto ang pagbabayad",
"confirm": "Magbayad",
- "cancel": "Decline",
+ "cancel": "Tumanggi",
"is_requesting_you_to_pay": "ay humihiling sa iyong bayaran ang",
"total": "Kabuuan:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Walang available na mga paraan ng pagbabayad.",
"error_fetching_quotes": "Nagkaproblema. Subukang muli.",
"no_quotes_available": "Wala available na mga provider.",
+ "quote_unavailable": "Hindi available ang quote.",
"providers": "Mga Provider",
+ "quotes_displayed_for": "Mga quote na ipinapakita para sa {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Dating ginamit",
+ "best_rate": "Pinakasulit na rate",
+ "most_reliable": "Pinakamaasahan",
"continue": "Magpatuloy",
"powered_by_provider": "Pinapagana ng {{provider}}",
"purchased_currency": "Bumili ng {{currency}}",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Error sa pag-log out"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Hindi available",
+ "description": "Hindi available ang {{token}} sa {{provider}} sa rehiyon mo.",
+ "change_token": "Baguhin ang token",
+ "change_provider": "Magpalit ng provider"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Pumili ng provider"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Nakuha ko",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Magpalit ng provider"
},
"fiat_on_ramp_aggregator": {
"buy": "bumili",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "{{currency}} Deposito"
},
+ "ramps_order_details": {
+ "title": "Mga detalye ng order",
+ "status": "Katayuan",
+ "processing": "Pinoproseso",
+ "complete": "Kumpleto na",
+ "failed": "Nabigo",
+ "cancelled": "Nakansela",
+ "view_on_provider": "Tingnan sa {{provider}}",
+ "order_id": "Order ID",
+ "date_and_time": "Petsa at oras",
+ "fees": "Mga Bayarin",
+ "total": "Kabuuan",
+ "card_processing_info": "Karaniwang tumatagal ng ilang minuto ang mga pagbili gamit ang Card",
+ "processing_info_modal_description": "Karaniwang tumatagal nang ilang minuto ang pagbili gamit ang card. Puwede kang makipag-ugnayan sa suporta kung may mga tanong ka.",
+ "go_to_provider_support": "Pumunta sa page ng suporta ng {{provider}}",
+ "error_title": "Mayroong nang mali",
+ "error_message": "Hindi mai-load ang mga detalye ng order. Subukan ulit.",
+ "try_again": "Subukang muli",
+ "close": "Isara"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Pinoproseso ang pagbili mo ng {{cryptocurrency}}",
+ "purchase_pending_description": "Tatagal lang ito nang ilang minuto...",
+ "purchase_completed_title": "Matagumpay ang pagbili mo ng {{amount}} {{cryptocurrency}}!",
+ "purchase_completed_description": "Available na ang iyong {{cryptocurrency}}",
+ "purchase_failed_title": "Hindi nakabili ng {{cryptocurrency}}",
+ "purchase_failed_description": "Subukan ulit sa ilang sandali",
+ "purchase_cancelled_title": "Kinansela ang iyong pagbili",
+ "purchase_cancelled_description": "Nakansela ang pagbili mo ng {{cryptocurrency}}",
+ "track": "Subaybayan"
+ }
+ },
"swaps": {
"title": "I-swap",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Para maiwasan na mangyari itong muli, tiyakin na laging naka-update sa pinakabagong bersyon ang iyong MetaMask app at OS."
},
"srp_security_quiz": {
+ "question_step": "Tanong {{step}} sa {{total}}",
"title": "Pagsusulit sa seguridad",
"introduction": "Upang ibunyag ang iyong Lihim na Parirala sa Pagbawi, kailangan mong sagutin nang tama ang dalawang tanong",
"get_started": "Magsimula",
"learn_more": "Matuto pa",
"try_again": "Subukang muli",
"continue": "Magpatuloy",
+ "correct": "Tama!",
+ "incorrect": "Mali!",
"of": "ng",
"question_one": {
"question": "Kung mawala mo ang iyong Lihim na Parirala sa Pagbawi, ang MetaMask ay...",
@@ -5823,11 +5914,11 @@
"error_description": "Nabigo ang pag-install ng {{snap}}."
},
"earn": {
- "claimable_bonus_tooltip": "Ang taunang bonus ay maki-claim araw-araw mula sa iyong wallet.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Kumita ng {{percentage}}% bonus",
"claimable_bonus": "Naki-claim na bonus",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "I-claim ang bonus",
+ "claim_bonus_subtitle": "Ibibigay ang bonus sa {{networkName}}.",
"empty_state_cta": {
"heading": "Magpahiram ng {{tokenSymbol}} at kumita ng",
"body": "Ipahiram ang iyong {{tokenSymbol}} sa {{protocol}} at kumita",
@@ -5887,7 +5978,7 @@
"network": "Network",
"health_factor": "Health factor",
"liquidation_risk": "Panganib ng liquidation",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Hindi sapat na pool liquidity",
"available_to_withdraw": "available para i-withdraw",
"unknown": "hindi Kilala",
"how_it_works": "Paano ito gumagana",
@@ -5906,7 +5997,7 @@
"lending": "Talaan ng posisyon",
"staking": "Talaan ng payout"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Pag-resent ng allowance",
"tron": {
"fee": "Bayarin"
},
@@ -5914,32 +6005,60 @@
"ok": "OK",
"continue": "Magpatuloy",
"convert_and_get_percentage_bonus": "I-convert at makakuha ng {{percentage}}%",
+ "your_musd": "Ang mUSD mo",
+ "balance_breakdown_title": "Ang mga balanse mo sa mUSD ayon sa network",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "I-convert sa mUSD",
"get_a_percentage_musd_bonus": "Makakuha ng {{percentage}}% mUSD bonus",
"convert": "I-convert",
+ "fetching_quote": "Kumukuha ng quote...",
+ "you_convert": "Nagko-convert ka ng",
+ "network_fee": "Bayad sa network",
+ "earning": "Kumikita",
+ "quick_convert_description": "Pumili ng token para mai-convert mo ang buo mong balanse o i-tap ang icon ng pag-edit para maglagay ng gusto mong halaga.",
+ "no_tokens_to_convert": "Wala kang kahit anong token na puwedeng i-convert sa mUSD.",
"toasts": {
"converting": "Ikino-convert ang {{token}} → mUSD",
"eta": "~{{time}}",
- "delivered": "Ang mUSD mo ay narito na!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "Nabigong palitan ang mUSD"
},
"education": {
"heading": "MAKAKUHA NG {{percentage}}% SA\nMGA STABLECOIN",
- "description": "I-convert ang mga stablecoin mo sa mUSD, stablecoin ng MetaMask na suportado ng dolyar ng US, at makakatanggap ng hanggang {{percentage}}% bonus.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Nalalapat ang mga tuntunin.",
"primary_button": "Magsimula",
"secondary_button": "Hindi sa ngayon"
},
"buy_musd": "Bumili ng mUSD",
"get_musd": "Kumuha ng mUSD",
- "bonus_title": "Makakakuha ng {{percentage}}% sa mga stablecoin mo",
- "bonus_description": "I-convert ang mga stablecoin mo sa mUSD at makakatanggap ng hanggang {{percentage}}% na bonus.",
- "powered_by_relay": "Pinapagana ng Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Pinapagana ng Relay",
+ "max": "Pinakamataas",
+ "quick_convert_button": "I-convert",
+ "learn_more": "Matuto pa",
+ "tooltip_title": "Kumita ng yield sa mUSD",
+ "tooltip_content": "I-convert ang USDC, USDT, o DAI mo sa mUSD, ang stablecoin ng MetaMask na sinusuportahan ng dolyar Kumita ng {{apy}} yield sa bawat dolyar na hino-hold mo.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Pumalya ang pag-convert. Subukan ulit.",
+ "confirmation": {
+ "title": "Max ng pag-convert"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Rate"
},
"bonus_claim": {
"toasts": {
"claiming": "Pinoproseso ang mUSD bonus mo",
"delivered": "Ang mUSD na bonus mo ay narito na!",
- "failed": "Bonus claim failed"
+ "failed": "Hindi na-claim ang bonus"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Awtomatikong idadagdag ang mga point sa account mo.",
"tooltip_not_opted_in_footer": "Mag-opt-in sa mga reward para matanggap ang mga point mo.",
"tooltip_close": "Isara"
- }
+ },
+ "your_stablecoins": "Mga stablecoin mo"
},
"stake": {
"stake": "Mag-stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Mga bayad sa transaksyon",
"metamask_fee": "Bayad sa MetaMask",
"network_fee": "Bayad sa network",
- "bridge_fee": "Bayarin sa provider ng bridge"
+ "bridge_fee": "Bayarin sa provider ng bridge",
+ "provider_fee": "Bayad sa provider"
},
"title": {
"signature": "Kahilingan sa paglagda",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Isu-swap namin ang mga token mo para sa USDE.e sa Polygon, ang network na ginamit ng mga Prediksyon. Maaaring maningil ang mga swap provider, ngunit hindi ang MetaMask."
},
"predict_withdraw": {
- "transaction_fee": "Magsu-swap ang MetaMask ng token na gusto mo para sa iyo. Walang hihinging bayad sa MetaMask kapag nag-swap ka sa MUSD."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "Kasama sa mga bayarin sa palitan ng mUSD ang mga gastos sa network at maaaring may kasamang bayarin sa provider."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Mga Bayarin"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Epekto ng presyo",
"price_impact_info_description": "Ipinapakita ng epekto sa presyo kung paano naaapektuhan ng iyong swap order ang market price ng asset. Nakadepende ito sa laki ng trade at sa available na liquidity sa pool. Walang kontrol o impluwensya ang MetaMask sa epekto sa presyo.",
"price_impact_info_gasless_description": "Ipinapakita ng epekto sa presyo kung paano naaapektuhan ng iyong swap order ang market price ng asset. Kung wala kang sapat na pondo para sa gas, awtomatikong ilalaan ang isang bahagi ng source token mo para sa mga bayarin, na magdaragdag sa epekto ng presyo. Walang impluwensya o kontrol ang MetaMask sa epekto sa presyo.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Kanselahin",
"slippage_info_title": "Slippage",
"slippage_info_description": "Ang % ng pagbabago sa presyo na handa mong payagan bago makansela ang iyong transaksyon.",
"blockaid_error_title": "Babawiin ang transaksyong ito",
@@ -6470,13 +6596,14 @@
},
"submit": "Isumite",
"default_slippage_description": "Hindi magpapatuloy ang iyong transaksyon kapag nagbago ang presyo nang higit sa porsiyento ng slippage.",
- "cancel": "Kanselahin",
"confirm": "Kumpirmahin",
"exceeding_upper_slippage_warning": "Mataas na slippage, maaari itong magresulta sa hindi kanais-nais na pag-swap",
"exceeding_lower_slippage_warning": "Mababang slippage, maaari itong magresulta sa hindi kanais-nais na pag-swap",
"exceeding_lower_slippage_error": "Ilagay ang value na mas malaki sa {{value}}%",
"exceeding_upper_slippage_error": "Hindi mo maaaring ilagay ang value na mas malaki sa {{value}}%",
- "custom": "Custom"
+ "custom": "Custom",
+ "invalid_recipient_address": "Di-wastong address",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Available ang mga bagong quote",
@@ -6773,12 +6900,16 @@
"title": "Ilagay ang password",
"description": "Ilagay ang password ng wallet mo para tingnan ang mga detalye ng card.",
"description_unfreeze": "Ilagay ang password ng wallet mo para ipagpatuloy ang paggastos sa iyong card.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Password",
"confirm": "Kumpirmahin",
"cancel": "Kanselahin",
"error_empty": "Ilagay ang password mo",
"error_incorrect": "Mali ang password. Pakisubukan muli."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Piliin ang card mo",
"upgrade_title": "I-upgrade sa Metal",
@@ -7094,6 +7225,9 @@
"view_card_details": "Tingnan ang mga detalye ng card",
"hide_card_details": "Itago ang mga detalye ng card",
"view_card_details_description": "Numero ng card, kailan mag-e-expire at CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Pamahalaan ang limit",
"manage_spending_limit_description_restricted": "Naka-on ang limitadong paggastos",
"manage_spending_limit_description_full": "Naka-on ang buong pag-access",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Mga tuntunin at kundisyon",
"order_metal_card": "Metal Card",
"order_metal_card_description": "I-order ngayon ang pisikal na Metal Card mo",
+ "cashback": "Cashback",
+ "cashback_description": "Makakuha 1% na balik sa lahat ng nagastos mo",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "I-freeze ang card",
"unfreeze_card": "I-unfreeze ang card",
"freeze_card_description": "Ihinto ang lahat ng paggastos sa iyong card",
@@ -7141,6 +7278,19 @@
"retry": "Subukang muli",
"on_linea": "sa Linea"
},
+ "cashback_screen": {
+ "title": "Cashback",
+ "available_cashback": "Available na cashback",
+ "network_fee": "Bayad sa network",
+ "expected_to_receive": "Inaasahang matanggap",
+ "withdraw": "Mag-withdraw",
+ "withdraw_unavailable": "Hindi available ang pag-withdraw",
+ "withdrawal_initiated": "Nasimulan na ang pag-withdraw",
+ "withdrawal_success": "Matagumpay na nakumpleto ang pag-withdraw",
+ "withdrawal_failed": "Pumalya ang pag-withdraw. Subukan ulit mamaya.",
+ "no_cashback": "Walang cashback na available",
+ "loading_error": "Hindi nakapag-load ng cashback. Subukan ulit."
+ },
"change_asset": {
"title": "Palitan ang token at network",
"full_spending_access": "Buong pag-access sa paggastos",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Pumili ng paraan ng pagbabayad",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Piliin ang tatanggaping token",
+ "no_gas": "Walang native na balanse para sa gas"
},
"connection_removed_modal": {
"title": "Naalis na ang mga koneksyon",
@@ -7315,7 +7465,10 @@
"service_not_available": "Hindi available sa sandaling ito ang serbisyo. Pakisubukang muli maya-maya.",
"invalid_referral_code": "Di-wastong referral code. Pakisuriin at subukang muli.",
"already_referred": "Nai-refer ka na ng ibang user.",
- "cannot_use_own_referral_code": "Hindi mo maaaring gamitin ang sarili mong referral code."
+ "cannot_use_own_referral_code": "Hindi mo maaaring gamitin ang sarili mong referral code.",
+ "invalid_bonus_code": "Di-wastong bonus code",
+ "already_redeemed": "Na-redeem mo na ang bonus code na ito",
+ "reached_maximum": "Naabot na ang maximum na bilang ng paggamit para sa bonus code na ito"
},
"claim_reward_error": {
"title": "Nabigong ma-claim ang reward"
@@ -7372,8 +7525,10 @@
"predict": "Prediksyon",
"musd_deposit": "depositong mUSD",
"apply_referral_bonus": "Bonus sa referral code",
+ "bonus_code": "Bonus code",
"uncategorized_event": "Hindi nakakategoryang event"
},
+ "code": "Code",
"date": "Petsa",
"account": "Account",
"bonus": "Bonus",
@@ -7473,7 +7628,16 @@
"show_less": "Magpakita ng mas kaunti",
"linking_progress": "Idinadagdag ang mga account... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} ang na-enroll",
- "add_all_accounts": "Idagdag ang lahat ng account"
+ "add_all_accounts": "Idagdag ang lahat ng account",
+ "environment_selector": "Environment",
+ "environment_cancel": "Kanselahin",
+ "environment_default": "Default",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Referral Code",
@@ -7483,6 +7647,10 @@
"invalid_code": "Hindi valid na referral code",
"apply_button": "Gamitin ang referral code"
},
+ "bonus_code": {
+ "input_placeholder": "Maglagay ng bonus code",
+ "apply_success": "Nailapat na ang bonus code!"
+ },
"optout": {
"title": "Umalis sa Rewards program",
"description": "Ang aksyong ito ay aalis sa iyong account mula sa Rewards program at buburahin ang lahat ng iyong puntos at progreso. Hindi na ito maaaring ibalik.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Bayad sa bridge",
+ "provider_fee": "Bayad sa provider",
"network_fee": "Bayad sa network",
"paid_with": "Magbayad gamit ang",
+ "receive_token": "Tumanggap ng token",
"retry_button": "Subukang muli",
"total": "Kabuuan",
"account": "Account",
- "received_total": "Received total"
+ "received_total": "Kabuuan ng natanggap"
},
"summary_title": {
"bridge_approval": "Aprubahan ang {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Hindi Nahanap ang Koneksyon",
"description": "Magtatag ng bagong koneksyon mula sa app para magpatuloy."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Patuloy na kumukonekta sa {{networkName}}...",
"unable_to_connect_network": "Hindi makakonekta sa {{networkName}}.",
"update_rpc": "I-update ang RPC",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Lumipat sa default RPC ng MetaMask",
"check_network_connectivity": "Tingnan ang konektibidad ng network mo.",
"check_network_connectivity_or": "Tingnan ang konektibidad ng network mo o",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Na-update sa default ng MetaMask"
},
"trending": {
"title": "Tuklasin",
"trending_tokens": "Mga trending na token",
+ "stocks": "Stocks",
"price_change": "Pagbabago ng presyo",
"all_networks": "Lahat ng network",
"24h": "24 na oras",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "I-reload",
"primary_action_acknowledge": "Nakuha ko"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "QR Hardware Wallet",
+ "hardware_wallet": "Hardware Wallet"
+ },
+ "common": {
+ "cancel": "Kanselahin",
+ "continue": "Magpatuloy"
+ },
+ "connecting": {
+ "title": "Ikonekta ang {{device}} mo",
+ "searching": "Hinahanap ang {{device}}...",
+ "tips_header": "Para magpatuloy, siguraduhing:",
+ "tip_unlock": "Naka-unlock ang {{device}} mo",
+ "tip_open_app": "Bukas ang Ethereum app",
+ "tip_enable_bluetooth": "Naka-on ang bluetooth",
+ "tip_dnd_off": "Naka-off ang Do Not Disturb",
+ "tip_bluetooth_permission": "Nagbigay ng pahintulot sa Lokasyon at Bluetooth",
+ "tip_bluetooth_permission_v12": "Nagbigay ng pahintulot sa mga device sa paligid",
+ "tip_stay_close": "Malapit ang device mo sa telepono mo"
+ },
+ "awaiting_app": {
+ "title": "Buksan ang {{app}}",
+ "message": "Buksan ang {{app}} app sa {{device}} mo",
+ "current_app": "Kasalukuyang bukas: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Kumpirmahin sa {{device}}",
+ "title_message": "Mag-sign in sa {{device}}",
+ "message": "Suriin at kumpirmahin sa {{device}} mo"
+ },
+ "success": {
+ "title": "Nakakonekta ang {{device}}"
+ },
+ "errors": {
+ "device_locked": "I-unlock ito at subukan ulit para magpatuloy",
+ "app_not_open": "Buksan ang Ethereum app sa device mo",
+ "device_disconnected": "Nadiskonekta ang {{device}} mo. Ikonekta at subukan ulit",
+ "device_not_found": "Hindi mahanap ang {{device}} mo. Siguraduhing nakakonekta ito",
+ "device_not_ready": "Hindi handa ang device mo. Suriin ito at subukan ulit",
+ "blind_signing": "Naka-disable ang blind signing. I-enable ito sa mga setting ng device mo",
+ "connection_closed": "Nawala ang koneksyon. Subukan ulit",
+ "connection_timeout": "Nag-time out ang koneksyon. Subukan ulit",
+ "user_cancelled": "Nakansela ang aksyon sa device mo",
+ "pending_confirmation": "May nakabinbing aksyon sa device mo. Tapusin o kanselahin ito",
+ "bluetooth_permission_denied": "Kinakailangan ang pahintulot sa bluetooth para kumonekta sa device mo",
+ "location_permission_denied": "Kinakailangan ang pahintulot sa lokasyon para mag-scan ng mga device",
+ "nearby_permission_denied": "Kinakailangan ang pahintulot sa mga device sa paligid",
+ "bluetooth_off": "I-on ang Bluetooth para kumonekta sa device mo",
+ "bluetooth_scan_failed": "Hindi nakapag-scan ng mga device. Subukan ulit",
+ "bluetooth_connection_failed": "I-enable ang Bluetooh sa device mo para magpatuloy",
+ "not_supported": "Hindi sinusuportahan ang operasyong ito",
+ "unknown_error": "Siguraduhing may naka-set up na Lihim na Parirala sa Pagbawi o passphrase ang {{device}} mo para sa account na ito"
+ },
+ "error": {
+ "title": "Mayroong nang mali",
+ "default_title": "May naganap na error",
+ "continue": "Magpatuloy",
+ "retry": "Subukang muli",
+ "view_settings": "Tingnan ang Mga Setting",
+ "device_locked_title": "Naka-lock ang {{device}}",
+ "device_disconnected_title": "Nadiskonekta ang {{device}}",
+ "device_not_found_title": "Hindi nahanap ang {{device}}",
+ "app_not_open": "Hindi Bukas ang Ethereum App",
+ "blind_signing_disabled": "Naka-disable ang Blind Signing",
+ "connection_timeout": "Hindi Tumutugon ang Device",
+ "connection_closed": "Nawala ang Koneksyon",
+ "user_cancelled": "Nakansela ang Aksyon",
+ "pending_confirmation": "Nakabinbin ang Kumpirmasyon",
+ "bluetooth_required": "Kinakailangan ang bluetooth",
+ "bluetooth_permission_denied": "Kinakailangan ang Pahintulot sa Bluetooth",
+ "location_permission_denied": "Kinakailangan ang Pahintulot sa Lokasyon",
+ "nearby_devices_permission_denied": "Kinakailangan ang Pahintulot sa Mga Device sa Paligid",
+ "scan_failed": "Pumalya ang Pag-scan",
+ "something_went_wrong": "Mayroong nang mali"
+ },
+ "device_selection": {
+ "title": "Piliin ang {{device}}",
+ "scanning": "Nagsa-scan ng mga device...",
+ "no_devices_found": "Walang nahanap sa device",
+ "no_devices_hint": "Siguraduhing naka-unlock ang {{device}} mo at naka-enable ang Bluetooth mo",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Kumonekta",
+ "rescan": "I-scan Ulit",
+ "unknown_device": "Hindi Kilalang Device",
+ "signal_strength": "Signal: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Mga Token",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "Mga NFT",
"import_nfts": "Mag-import ng mga NFT",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Madaling idagdag ang mga collectible mo",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Walang TP/SL"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "Hindi nai-load ang {{section}}",
+ "retry": "Subukang muli"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/tr.json b/locales/languages/tr.json
index c883a97cbe1..69ea5ebcc36 100644
--- a/locales/languages/tr.json
+++ b/locales/languages/tr.json
@@ -103,6 +103,10 @@
"message": "Ücretleri karşılamak için yeterli {{ticker}} yok. Devam etmek için başka bir ağda bir token kullanın veya daha fazla {{ticker}} ekleyin.",
"title": "Yetersiz bakiye"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Bu ödeme rotası şu anda kullanılamıyor. Tutarı, ağı veya token'ı değiştirmeyi deneyin ve sizin için en iyi seçeneği bulalım.",
"title": "Teklif yok"
@@ -1180,6 +1184,7 @@
"title": "Yeni emir",
"leverage": "Kaldıraç",
"limit_price": "Limit fiyatı",
+ "market_price": "Piyasa fiyatı",
"enter_price": "Fiyatı gir",
"trigger_price": "Tetikleme fiyatı",
"liquidation_price": "Likidasyon fiyatı",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Fonlar işlem yapmak için kullanılabilir",
"close_order_still_active": "Kapama emri halen aktif",
"order_submitted": "Emir gönderildi",
+ "submitting_your_trade": "İşleminiz gönderiliyor",
"order_filled": "Emir gerçekleşti",
"order_placed": "Emir verildi",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1338,16 +1344,28 @@
},
"order_details": {
"title": "Emir Bilgileri",
- "cancel_order": "Emri İptal Et",
+ "cancel_order": "Emri iptal et",
"date": "Tarih",
+ "trigger_condition": "Tetikleme koşulu",
+ "price": "Fiyat",
"fee": "Ücret",
"limit_buy": "Limit Long",
"limit_price": "Limit Fiyatı",
"limit_sell": "Limit Short",
"market_buy": "Piyasa Long",
"market_sell": "Piyasa Short",
+ "market": "Piyasa",
"open": "Açık",
"size": "Boyut",
+ "original_size": "Orijinal boyut",
+ "order_value": "Emir değeri",
+ "reduce_only": "Yalnızca azalt",
+ "yes": "Evet",
+ "no": "Hayır",
+ "price_above": "Fiyat {{price}} üzerinde",
+ "price_below": "Fiyat {{price}} altında",
+ "take_profit": "Kazancı al",
+ "stop_loss": "Zararda durdur",
"status": "Durum",
"view_explorer": "Blok Gezgininde Görüntüle"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Piyasa içgörüleri",
- "updated_ago": "Güncelleme: {{time}}",
- "disclaimer": "Yapay zeka içgörüleridir. Finansal tavsiye değildir.",
- "whats_driving_price": "Fiyatı ne etkiliyor?",
- "what_people_saying": "İnsanlar ne diyor",
+ "a_closer_look": "Daha yakın bir görünüm",
+ "whats_being_said": "Neler söyleniyor",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "İşlem Yap",
"sources_count": "+{{count}} kaynak",
- "sources_title": "Kaynakları"
+ "sources_title": "News sources",
+ "feedback_submitted": "Geri bildirim gönderildi",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Geri Bildirim",
+ "description": "Yapay zeka ile oluşturulmuş piyasa içgörülerimizi iyileştirmemize yardımcı olun.",
+ "not_relevant": "İlgili değil",
+ "not_accurate": "Doğru değil",
+ "hard_to_understand": "Anlaşılması zor",
+ "harmful_or_offensive": "Zararlı veya rahatsız edici",
+ "something_else": "Başka bir şey",
+ "additional_feedback_label": "Daha fazla geri bildirim (isteğe bağlı)",
+ "additional_feedback_placeholder": "Nasıl gelişebiliriz?",
+ "characters_remaining": "{{count}} karakter kaldı",
+ "submit": "Gönder"
+ }
},
"predict": {
"title": "MetaMask Tahminler",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Yaklaşık 1 dakika sonra kullanılabilir",
"withdraw_completed": "Çekme işlemi tamamlandı",
"withdraw_completed_subtitle": "{{amount}} USDC cüzdanınıza aktarıldı",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} cüzdanınıza taşındı",
"error_title": "Bir şeyler ters gitti",
"error_description": "Çekme işlemine ilerlenemedi",
"try_again": "Tekrar dene"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Bu değerin bir tahmin olduğunu ve işlem tamamlandığında kesinleşeceğini unutmayın. Puanların Ödüller bakiyenizde onaylanması 1 saate kadar sürebilir.",
"points_error": "Şu anda puanları yükleyemiyoruz",
"points_error_content": "Bu işlem için puan kazanmaya devam edeceksiniz. Hesabınıza eklendiğinde sizi bilgilendireceğiz. Ayrıca yaklaşık bir saat içinde ödüller sekmenizi kontrol edebilirsiniz.",
- "slippage": "Kayma"
+ "slippage": "Kayma",
+ "price_details": "Fiyat bilgileri",
+ "prediction_order": "Tahmin emri",
+ "prediction_order_description": "Her biri {{price}} fiyatta ~{{count}} sözleşme. Son tutar, emir defteri kullanılabilirliğe göre değişiklik gösterebilir (%{{slippage}} orana kadar).",
+ "metamask_fee_description": "Bu tahmin işlemine yönelik hizmet ücreti",
+ "exchange_fee": "Borsa ücreti",
+ "exchange_fee_description": "Borsaya veya piyasaya ödenen ücret",
+ "total_incl_fees": "ücretler dahil",
+ "close": "Kapat"
},
"error": {
"title": "Tahminlere bağlanılamıyor",
@@ -2399,7 +2440,7 @@
"add_tokens": "Tokenleri içe aktar",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Bu tokenları içeri aktarmak istiyor musunuz?",
"tokens_detected_in_account": "Bu hesapta {{tokenCount}} yeni {{tokensLabel}} bulundu",
"token_toast": {
"tokens_imported_title": "İçe aktarılan tokenlar",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Token ondalıkları boş olamaz.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "Bu isimde token bulamadık.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Herhangi bir tokenı ara veya içe aktar",
"select_token": "Token seç",
"address_must_be_smart_contract": "Kişisel adres algılandı. Token sözleşme adresini girin.",
"billion_abbreviation": "MR",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "RPC URL adresi ekle",
"add_block_explorer_url": "Blok Gezgini URL adresi ekle",
"networks_desc": "Kişisel RPC ağı ekle ve düzenle",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Etkinleştirilen Ağlar",
+ "networks_test_networks": "Test Ağları",
+ "networks_additional": "İlave Ağlar",
"networks_search_placeholder": "Ağ ara",
"networks_no_results": "Ağ bulunamadı",
"network_name_label": "Ağ adı",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "RPC ağları",
"network_add_network": "Ağ ekle",
"add_chain_title": "Ağ ekle",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Ada, zincir kimliğine veya para birimine göre ara",
+ "add_chain_loading": "Ağlar yükleniyor…",
+ "add_chain_error": "Ağlar yüklenemedi. Lütfen tekrar deneyin.",
"add_chain_retry": "Tekrar Dene",
- "add_chain_added": "Added",
+ "add_chain_added": "Eklendi",
"add_chain_or": "veya",
"add_chain_custom_link": "Özel bir ağ ekle",
"network_add_custom_network": "Özel bir ağ ekle",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Bir test ağı ekle",
"network_add": "Ekle",
"network_save": "Kaydet",
"remove_network_title": "Bu ağı kaldırmak istiyor musunuz?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Güvenli ve gizli bir yere kaydedin.",
"private_key_warning": "Bu, halihazırda seçili hesabın özel anahtarıdır: {{accountName}}. Bu anahtarı hiçbir zaman ifşa etmeyin. Özel anahtara sahip olan herkes, paranızın tamamını transfer etmek de dahil olmak üzere hesabınızı tam olarak kontrol edebilir.",
- "seed_phrase_warning_explanation": [
- "Hiç kimsenin ekranınıza bakmadığından emin olun.",
- "MetaMask Destek bölümü bunu asla talep etmez."
- ],
+ "seed_phrase_warning_explanation": "Hiç kimsenin ekranınıza bakmadığından emin olun. MetaMask Destek ekibi asla bunu istemez.",
"private_key_warning_explanation": "Bu anahtarı hiçbir zaman ifşa etmeyin. Özel anahtarınıza sahip olan herkes, paranızın tamamını transfer etmek de dahil olmak üzere hesabınızı tamamen kontrol edebilir.",
+ "reveal_srp_description": "Gizli Kurtarma İfadeniz cüzdanınıza tam erişim verir. Kimseyle paylaşmayın.",
"reveal_credential_modal": [
"{{credentialName}} bilgileriniz ",
"hesabınıza ve paranıza tam erişim sunar.\n\nBunu hiç kimseyle paylaşmayın.",
@@ -3385,7 +3424,8 @@
"srp_text": "Gizli Kurtarma İfadesi",
"private_key_text": "Özel anahtar",
"got_it": "Anladım",
- "learn_more": "Daha fazla bilgi edin"
+ "learn_more": "Daha fazla bilgi edin",
+ "copied_to_clipboard": "Panoya kopyalandı"
},
"screenshot_deterrent": {
"title": "Güvenlik uyarısı",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Hızlandırmayı dene?",
"speedup_tx_message": "Bu denemeyi göndermek ilk işlemin hızlandırılmasını garanti etmez. Hızlandırma denemesi başarılı olursa sizden yukarıdaki işlem ücreti alınır.",
"nevermind": "Vazgeç",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Gaz ücretini düzenle",
"edit_priority": "Önceliği düzenle",
"gas_cancel_fee": "Gaz iptal ücreti",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Bu izinle ilişkili bir işlem ücreti.",
"view_details": "Ayrıntıları görüntüle",
"view_transaction_details": "İşlem bilgilerini görüntüle",
- "view_data": "View data",
+ "view_data": "Verileri görüntüle",
"transaction_details": "İşlem bilgileri",
"site_url": "Site URL adresi",
"permission_request": "İzin talebi",
@@ -3843,7 +3887,7 @@
"right_button": "Cüzdanı koru"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Favori ekle",
"title_label": "Adı",
"url_label": "URL Adresi",
"add_button": "Ekle",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Touch ID ile kilidi aç?",
"enable_faceid": "Face ID ile kilidi aç?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "Parmak izi ile kilidi aç?",
+ "enable_biometrics": "Biyometri ile kilidi aç?",
"enable_device_passcode_ios": "Cihaz parolası ile kilidi aç?",
"enable_device_passcode_android": "Cihazın PIN kodu ile kilidi aç?"
},
@@ -4724,7 +4768,7 @@
"public_address": "Genel adres",
"public_address_qr_code": "Genel adres",
"coming_soon": "Çok yakında...",
- "request_payment": "Request payment",
+ "request_payment": "Ödeme iste",
"copy": "Hata",
"scan_address": "Ödeme almak için adresi tara",
"copy_address": "Adresi kopyala"
@@ -4738,8 +4782,8 @@
"switch_network": "Lütfen ana ağa veya sepolia ağına geçin",
"card_title": "MetaMask Kartı düğmesini her zaman göster",
"card_desc": "MetaMask Kartı yalnızca seçili ülkelerde ikamet edenler için mevcuttur.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "DaimoPay demo ortamını kullan",
+ "daimo_demo_desc": "Kart ödemeleri için DaimoPay demo ve üretim ortamları arasında değiştir."
},
"walletconnect_sessions": {
"no_active_sessions": "Aktif oturumunuz yok",
@@ -4752,10 +4796,10 @@
"close_current_session": "Yeni bir oturum başlatmadan önce geçerli oturumu kapat."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Ödeme talebi",
+ "title_complete": "Ödeme tamamlandı",
"confirm": "Öde",
- "cancel": "Decline",
+ "cancel": "Reddet",
"is_requesting_you_to_pay": "ödeme yapmanızı istiyor",
"total": "Toplam:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Kullanılabilir ödeme yöntemi yok.",
"error_fetching_quotes": "Bir şeyler ters gitti. Lütfen tekrar deneyin.",
"no_quotes_available": "Sağlayıcı mevcut değil.",
+ "quote_unavailable": "Teklif kullanılamıyor.",
"providers": "Sağlayıcılar",
+ "quotes_displayed_for": "{{paymentMethodName}} için teklifler görüntüleniyor.",
+ "other_options": "Other options",
+ "previously_used": "Daha önce kullanılan",
+ "best_rate": "En iyi oran",
+ "most_reliable": "En güvenilir",
"continue": "Devam et",
"powered_by_provider": "Destekleyen {{provider}}",
"purchased_currency": "{{currency}} Satın Alındı",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Oturum kapatılırken hata"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Mevcut değil",
+ "description": "{{token}} bölgenizde {{provider}} ile kullanılamıyor.",
+ "change_token": "Tokenı değiştir",
+ "change_provider": "Sağlayıcı değiştirin"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Bir sağlayıcı seç"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Anladım",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Sağlayıcı değiştirin"
},
"fiat_on_ramp_aggregator": {
"buy": "al",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "{{currency}} Para Yatırma İşlemi"
},
+ "ramps_order_details": {
+ "title": "Emir bilgileri",
+ "status": "Durum",
+ "processing": "Gerçekleştiriliyor",
+ "complete": "Tamamlandı",
+ "failed": "Başarısız oldu",
+ "cancelled": "İptal edildi",
+ "view_on_provider": "{{provider}} üzerinde görüntüle",
+ "order_id": "Emir Numarası",
+ "date_and_time": "Tarih ve saat",
+ "fees": "Ücretler",
+ "total": "Toplam",
+ "card_processing_info": "Kart ile satın alma işlemleri genellikle birkaç dakika sürer",
+ "processing_info_modal_description": "Kart ile satın alma işlemleri genellikle birkaç dakika sürer. Sorularınız olursa destek ekibine ulaşabilirsiniz.",
+ "go_to_provider_support": "{{provider}} destek sayfasına git",
+ "error_title": "Bir şeyler ters gitti",
+ "error_message": "Emir bilgileri yüklenemiyor. Lütfen tekrar deneyin.",
+ "try_again": "Tekrar dene",
+ "close": "Kapat"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "{{cryptocurrency}} satın alma işleminiz gerçekleşiyor",
+ "purchase_pending_description": "Bu işlem sadece birkaç dakika sürecektir...",
+ "purchase_completed_title": "{{amount}} {{cryptocurrency}} satın alma işleminiz başarılı oldu!",
+ "purchase_completed_description": "{{cryptocurrency}} para biriminiz artık kullanılabilir",
+ "purchase_failed_title": "{{cryptocurrency}} satın alma işlemi başarısız oldu",
+ "purchase_failed_description": "Lütfen kısa bir süre sonra tekrar deneyin",
+ "purchase_cancelled_title": "Satın alma işleminiz iptal edildi",
+ "purchase_cancelled_description": "{{cryptocurrency}} satın alma işleminiz iptal edildi",
+ "track": "Takip Et"
+ }
+ },
"swaps": {
"title": "Takas Et",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Bunun tekrar olmasını önlemek için her zaman MetaMask uygulamanı ve işletim sistemini en yeni sürümde güncel tuttuğundan emin ol."
},
"srp_security_quiz": {
+ "question_step": "Soru {{step}} / {{total}}",
"title": "Güvenlik testi",
"introduction": "Gizli Kurtarma İfadenizi görmek için iki soruyu doğru cevaplamanız gerekmektedir",
"get_started": "Başlarken",
"learn_more": "Daha fazla bilgi edin",
"try_again": "Tekrar dene",
"continue": "Devam et",
+ "correct": "Doğru!",
+ "incorrect": "Yanlış!",
"of": "/",
"question_one": {
"question": "Gizli Kurtarma İfadenizi kaybederseniz MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "{{snap}} yüklemesi başarısız oldu."
},
"earn": {
- "claimable_bonus_tooltip": "Cüzdanınızdan her gün alınabilen yıllık bonus.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "%{{percentage}} bonus kazanın",
"claimable_bonus": "Alınabilir bonus",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Bonusu al",
+ "claim_bonus_subtitle": "Bonus, {{networkName}} üzerinde ödenecektir.",
"empty_state_cta": {
"heading": "{{tokenSymbol}} borç verin ve kazanın",
"body": "{{protocol}} ile {{tokenSymbol}} token'ınızı borç verin ve",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Bakım çalışması nedeniyle kapalıyız. Kısa sürede tekrar burada olacağız!"
},
- "supply": "Supply",
+ "supply": "Arz",
"deposit": "Para Yatır",
"approve": "Onayla",
"approval": "Onay",
@@ -5887,7 +5978,7 @@
"network": "Ağ",
"health_factor": "Sağlık faktörü",
"liquidation_risk": "Likidasyon riski",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Havuz likiditesi yetersiz",
"available_to_withdraw": "çekilebilir",
"unknown": "bilinmeyen",
"how_it_works": "Nasıl çalışır?",
@@ -5906,7 +5997,7 @@
"lending": "Pozisyon geçmişi",
"staking": "Ödeme geçmişi"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Kullanım izni sıfırlandı",
"tron": {
"fee": "Ücret"
},
@@ -5914,32 +6005,60 @@
"ok": "Tamam",
"continue": "Devam et",
"convert_and_get_percentage_bonus": "Dönüştür ve %{{percentage}} al",
+ "your_musd": "mUSD bakiyeniz",
+ "balance_breakdown_title": "Ağa göre mUSD bakiyeleriniz",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "mUSD'ye dönüştür",
"get_a_percentage_musd_bonus": "%{{percentage}} mUSD bonus al",
"convert": "Dönüştür",
+ "fetching_quote": "Teklif alınıyor...",
+ "you_convert": "Dönüştürdüğünüz tutar",
+ "network_fee": "Ağ ücreti",
+ "earning": "Kazanç",
+ "quick_convert_description": "Tüm bakiyenizi dönüştürmek için bir token seçin veya düzenle simgesine dokunarak özel bir miktar girin.",
+ "no_tokens_to_convert": "mUSD'ye dönüştürülebilecek tokenınız yok.",
"toasts": {
"converting": "{{token}} → mUSD dönüştürülüyor",
"eta": "~{{time}}",
- "delivered": "mUSD'niz hazır!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "mUSD dönüştürme işlemi başarısız oldu"
},
"education": {
"heading": "STABİL KRİPTO PARALARDA\n%{{percentage}} AL",
- "description": "Stabil kripto paranızı MetaMask'in Amerikan doları destekli stabil kripto parası olan mUSD'ye dönüştürün ve %{{percentage}} oranına varan bonus kazanın.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Şartlar uygulanır.",
"primary_button": "Başlarken",
"secondary_button": "Şimdi değil"
},
"buy_musd": "mUSD al",
"get_musd": "mUSD kazan",
- "bonus_title": "Stabil kripto paranızda %{{percentage}} bonus kazanın",
- "bonus_description": "Stabil kripto paranızı mUSD'ye dönüştürün ve %{{percentage}} oranına varan bonus kazanın.",
- "powered_by_relay": "Destekleyen Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Destekleyen Relay",
+ "max": "Maksimum",
+ "quick_convert_button": "Dönüştür",
+ "learn_more": "Daha fazla bilgi edin",
+ "tooltip_title": "mUSD ile getiri elde et",
+ "tooltip_content": "USDC, USDT veya DAI'nizi MetaMask'in dolar destekli stabil kripto parası olan mUSD'ye dönüştürün. Tuttuğunuz her dolar için {{apy}} getiri elde edin.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Dönüştürme işlemi başarısız oldu. Tekrar deneyin.",
+ "confirmation": {
+ "title": "Maksimumu dönüştür"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Oran"
},
"bonus_claim": {
"toasts": {
"claiming": "mUSD bonusunuz işleniyor",
"delivered": "mUSD bonusunuz hazır!",
- "failed": "Bonus claim failed"
+ "failed": "Bonus alımı başarısız oldu"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Puanlar otomatik olarak hesabınıza eklenecektir.",
"tooltip_not_opted_in_footer": "Puanlarınızı almak için ödüller programına katılın.",
"tooltip_close": "Kapat"
- }
+ },
+ "your_stablecoins": "Stabil kripto paralarınız"
},
"stake": {
"stake": "Pay",
@@ -6222,7 +6342,8 @@
"transaction_fees": "İşlem ücretleri",
"metamask_fee": "MetaMask ücreti",
"network_fee": "Ağ ücreti",
- "bridge_fee": "Köprü sağlayıcı ücreti"
+ "bridge_fee": "Köprü sağlayıcı ücreti",
+ "provider_fee": "Sağlayıcı ücreti"
},
"title": {
"signature": "İmza talebi",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Tokenlerinizi Tahminler tarafından kullanılan Polygon ağı üzerinden USDC ile takas edeceğiz. Takas sağlayıcıları ücret alabilir ancak MetaMask ücret talep etmez."
},
"predict_withdraw": {
- "transaction_fee": "MetaMask, sizin için istediğiniz tokena takas gerçekleştirecek. MUSD'ye takas gerçekleştirirken MetaMask ücreti uygulanmaz."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "mUSD dönüştürme ücretlerine ağ ücretleri dahildir ve sağlayıcı ücretleri dahil olabilir."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Ücretler"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Piyasa etkisi",
"price_impact_info_description": "Fiyat etkisi, takas emrinizin varlığın piyasa fiyatını nasıl etkilediğini yansıtır. İşlem boyutuna ve havuzdaki mevcut likiditeye bağlıdır. MetaMask'in fiyat etkisi üzerinde bir etkisi veya kontrolü bulunmamaktadır.",
"price_impact_info_gasless_description": "Fiyat etkisi, takas emrinizin varlığın piyasa fiyatını nasıl etkilediğini yansıtır. Gaz için yeterli fon tutmazsanız kaynak token'inizin bir kısmı otomatik olarak ücretleri karşılamak üzere ayrılır, bu nedenle fiyat etkisi artar. MetaMask fiyat etkisini etkilemez veya kontrol etmez.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "İptal",
"slippage_info_title": "Kayma",
"slippage_info_description": "İşleminiz iptal edilmeden önce izin vermek istediğiniz fiyat değişim % oranı.",
"blockaid_error_title": "Bu işlem geri alınacak",
@@ -6470,13 +6596,14 @@
},
"submit": "Gönder",
"default_slippage_description": "Fiyat kayma yüzdesinden daha fazla değişirse işleminiz gerçekleşmez.",
- "cancel": "İptal",
"confirm": "Onayla",
"exceeding_upper_slippage_warning": "Yüksek kayma; bu durum istenmeyen bir takas ile sonuçlanabilir",
"exceeding_lower_slippage_warning": "Düşük kayma; bu durum istenmeyen bir takas ile sonuçlanabilir",
"exceeding_lower_slippage_error": "%{{value}} değerin üzerinde olan bir değer girin",
"exceeding_upper_slippage_error": "%{{value}} değerin üzerinde olan bir değer giremezsiniz",
- "custom": "Özel"
+ "custom": "Özel",
+ "invalid_recipient_address": "Geçersiz adres",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Yeni teklifler mevcut",
@@ -6773,12 +6900,16 @@
"title": "Şifre girin",
"description": "Kart bilgilerini görüntülemek için cüzdan şifrenizi girin.",
"description_unfreeze": "Kartınızda harcama yapmaya devam edebilmek için cüzdan şifrenizi girin.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Şifre",
"confirm": "Onayla",
"cancel": "İptal et",
"error_empty": "Lütfen şifrenizi girin",
"error_incorrect": "Şifre yanlış. Lütfen tekrar deneyin."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Kartınızı seçin",
"upgrade_title": "Metale Yükselt",
@@ -7094,6 +7225,9 @@
"view_card_details": "Kart bilgilerini görüntüle",
"hide_card_details": "Kart bilgilerini gizle",
"view_card_details_description": "Kart numarası, son geçerlilik tarihi ve CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Limiti yönet",
"manage_spending_limit_description_restricted": "Sınırlı harcama açık",
"manage_spending_limit_description_full": "Tam erişim açık",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Şart ve koşullar",
"order_metal_card": "Metal Kart",
"order_metal_card_description": "Hemen Fiziksel Metal Kart siparişinizi verin",
+ "cashback": "Para iadesi",
+ "cashback_description": "Tüm harcamalarda %1 iade al",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Kartı dondur",
"unfreeze_card": "Kartı dondurma işlemini iptal et",
"freeze_card_description": "Kartındaki tüm harcamaları duraklat",
@@ -7141,6 +7278,19 @@
"retry": "Tekrar dene",
"on_linea": "Linea üzerinde"
},
+ "cashback_screen": {
+ "title": "Para iadesi",
+ "available_cashback": "Kullanılabilir para iadesi",
+ "network_fee": "Ağ ücreti",
+ "expected_to_receive": "Alınması beklenen",
+ "withdraw": "Çek",
+ "withdraw_unavailable": "Para çekme kullanılamıyor",
+ "withdrawal_initiated": "Para çekme başlatıldı",
+ "withdrawal_success": "Para çekme başarıyla tamamlandı",
+ "withdrawal_failed": "Para çekme başarısız oldu. Lütfen tekrar deneyin.",
+ "no_cashback": "Para iadesi mevcut değil",
+ "loading_error": "Para iadesi yüklenemedi. Lütfen tekrar deneyin."
+ },
"change_asset": {
"title": "Token'ı ve ağı değiştir",
"full_spending_access": "Tam harcama erişimi",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Ödeme yöntemi seç",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Token al seçeneğini seç",
+ "no_gas": "Gaz için yerel bakiye yok"
},
"connection_removed_modal": {
"title": "Bağlantılar kaldırıldı",
@@ -7315,7 +7465,10 @@
"service_not_available": "Hizmet şu anda kullanılamıyor. Lütfen kısa süre sonra tekrar deneyin.",
"invalid_referral_code": "Referans kodu geçersiz. Lütfen kontrol edip tekrar deneyin.",
"already_referred": "Zaten başka bir kullanıcının referansı ile geldiniz.",
- "cannot_use_own_referral_code": "Kendi referans kodunuzu kullanamazsınız."
+ "cannot_use_own_referral_code": "Kendi referans kodunuzu kullanamazsınız.",
+ "invalid_bonus_code": "Bonus kodu geçersiz",
+ "already_redeemed": "Bu bonus kodunu zaten kullandınız",
+ "reached_maximum": "Bu bonus kodu maksimum kullanım sayısına ulaştı"
},
"claim_reward_error": {
"title": "Ödül alınamadı"
@@ -7372,8 +7525,10 @@
"predict": "Tahmin",
"musd_deposit": "mUSD yatır",
"apply_referral_bonus": "Referans kodu bonusu",
+ "bonus_code": "Bonus kodu",
"uncategorized_event": "Kategorize edilmemiş etkinlik"
},
+ "code": "Kod",
"date": "Tarih",
"account": "Hesap",
"bonus": "Bonus",
@@ -7473,7 +7628,16 @@
"show_less": "Daha az göster",
"linking_progress": "Hesaplar ekleniyor... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} kayıtlı",
- "add_all_accounts": "Tüm hesapları ekle"
+ "add_all_accounts": "Tüm hesapları ekle",
+ "environment_selector": "Ortam",
+ "environment_cancel": "İptal",
+ "environment_default": "Varsayılan",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Referans Kodu",
@@ -7483,6 +7647,10 @@
"invalid_code": "Geçersiz referans kodu",
"apply_button": "Referans kodunu uygula"
},
+ "bonus_code": {
+ "input_placeholder": "Bonus kodunu gir",
+ "apply_success": "Bonus kodu uygulandı!"
+ },
"optout": {
"title": "Ödüller Programını Sil",
"description": "Bu işlem, hesaplarınızı Ödüller programından kaldırır ve tüm puanlarınızı ile ilerlemenizi siler. Bu işlemi geri alamazsınız.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Köprü ücreti",
+ "provider_fee": "Sağlayıcı ücreti",
"network_fee": "Ağ ücreti",
"paid_with": "Ödeme aracı",
+ "receive_token": "Token al",
"retry_button": "Tekrar dene",
"total": "Toplam",
"account": "Hesap",
- "received_total": "Received total"
+ "received_total": "Alınan toplam"
},
"summary_title": {
"bridge_approval": "{{approveSymbol}} onayla",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Bağlantı Bulunamadı",
"description": "Devam etmek için lütfen uygulamadan yeni bir bağlantı kurun."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Halen {{networkName}} ağına bağlanılıyor...",
"unable_to_connect_network": "{{networkName}} ağına bağlanılamıyor.",
"update_rpc": "RPC'yi güncelle",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "MetaMask varsayılan RPC'sine geç",
"check_network_connectivity": "Ağ bağlantınızı kontrol edin.",
"check_network_connectivity_or": "Ağ bağlantınızı kontrol edin veya",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "MetaMask varsayılanına güncellendi"
},
"trending": {
"title": "Keşfet",
"trending_tokens": "Trend tokenlar",
+ "stocks": "Hisse Senetleri",
"price_change": "Fiyat değişikliği",
"all_networks": "Tüm ağlar",
"24h": "24s",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Tekrar Yükle",
"primary_action_acknowledge": "Anladım"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "QR Donanım Cüzdanı",
+ "hardware_wallet": "Donanım Cüzdanı"
+ },
+ "common": {
+ "cancel": "İptal",
+ "continue": "Devam et"
+ },
+ "connecting": {
+ "title": "{{device}} cihazını bağla",
+ "searching": "{{device}} aranıyor...",
+ "tips_header": "Devam etmek için şunlardan emin olun:",
+ "tip_unlock": "{{device}} kilidi açık olmalı",
+ "tip_open_app": "Ethereum uygulaması açık olmalı",
+ "tip_enable_bluetooth": "Bluetooth açık olmalı",
+ "tip_dnd_off": "Rahatsız Etme modu kapalı olmalı",
+ "tip_bluetooth_permission": "Konum ve Bluetooth izni verilmiş olmalı",
+ "tip_bluetooth_permission_v12": "Yakındaki cihazlar izni verilmiş olmalı",
+ "tip_stay_close": "Cihazınız telefonunuza yakın durmalı"
+ },
+ "awaiting_app": {
+ "title": "{{app}} uygulamasını aç",
+ "message": "Lütfen {{device}} cihazınızda {{app}} uygulamasını açın",
+ "current_app": "Şu anda açık: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "{{device}} cihazında onayla",
+ "title_message": "{{device}} cihazında imzala",
+ "message": "{{device}} cihazında incele ve onayla"
+ },
+ "success": {
+ "title": "{{device}} bağlandı"
+ },
+ "errors": {
+ "device_locked": "Devam etmek için kilidini açın ve tekrar deneyin",
+ "app_not_open": "Lütfen cihazınızda Ethereum uygulamasını açın",
+ "device_disconnected": "{{device}} cihazınızın bağlantısı kesildi. Lütfen yeniden bağlayıp tekrar deneyin",
+ "device_not_found": "{{device}} cihazınız bulunamadı. Lütfen bağlı olduğundan emin olun",
+ "device_not_ready": "Cihazınız hazır değil. Lütfen kontrol edip tekrar deneyin",
+ "blind_signing": "Kör imzalama devre dışı bırakıldı. Lütfen cihaz ayarlarınızda etkinleştirin",
+ "connection_closed": "Bağlantı kayboldu. Lütfen tekrar deneyin",
+ "connection_timeout": "Bağlantı zaman aşımına uğradı. Lütfen tekrar deneyin",
+ "user_cancelled": "İşlem cihazınızda iptal edildi",
+ "pending_confirmation": "Cihazınızda bekleyen bir işlem var. Lütfen tamamlayın veya iptal edin",
+ "bluetooth_permission_denied": "Cihazınıza bağlanmak için Bluetooth izni gereklidir",
+ "location_permission_denied": "Cihazları taramak için konum izni gereklidir",
+ "nearby_permission_denied": "Yakındaki cihazlar izni gereklidir",
+ "bluetooth_off": "Cihazınıza bağlanmak için lütfen Bluetooth'u açın",
+ "bluetooth_scan_failed": "Cihazlar taranamadı. Lütfen tekrar deneyin",
+ "bluetooth_connection_failed": "Devam etmek için cihazınızda Bluetooth'u etkinleştirin",
+ "not_supported": "Bu işlem desteklenmiyor",
+ "unknown_error": "{{device}} cihazınızda bu hesap için Gizli Kurtarma İfadesi veya parola kurulumunun yapıldığından emin olun"
+ },
+ "error": {
+ "title": "Bir şeyler ters gitti",
+ "default_title": "Bir hata oluştu",
+ "continue": "Devam et",
+ "retry": "Tekrar Dene",
+ "view_settings": "Ayarları Görüntüle",
+ "device_locked_title": "{{device}} kilitli",
+ "device_disconnected_title": "{{device}} bağlantısı kesilmiş",
+ "device_not_found_title": "{{device}} bulunamadı",
+ "app_not_open": "Ethereum Uygulaması Açık Değil",
+ "blind_signing_disabled": "Kör İmzalama Devre Dışı",
+ "connection_timeout": "Cihaz Yanıt Vermiyor",
+ "connection_closed": "Bağlantı Kayboldu",
+ "user_cancelled": "İşlem İptal Edildi",
+ "pending_confirmation": "Onay Bekliyor",
+ "bluetooth_required": "Bluetooth gerekli",
+ "bluetooth_permission_denied": "Bluetooth İzni Gerekli",
+ "location_permission_denied": "Konum İzni Gerekli",
+ "nearby_devices_permission_denied": "Yakındaki Cihazlar İzni Gerekli",
+ "scan_failed": "Tarama Başarısız Oldu",
+ "something_went_wrong": "Bir şeyler ters gitti"
+ },
+ "device_selection": {
+ "title": "{{device}} cihazını seçin",
+ "scanning": "Cihazlar taranıyor...",
+ "no_devices_found": "Cihaz bulunamadı",
+ "no_devices_hint": "{{device}} cihazınızın kilidinin açık olduğundan ve Bluetooth özelliğinin etkin olduğundan emin olun",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Bağla",
+ "rescan": "Tekrar Tara",
+ "unknown_device": "Bilinmeyen Cihaz",
+ "signal_strength": "Sinyal: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "tokenlar",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT'ler",
"import_nfts": "NFT'leri içe aktar",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Koleksiyon ögelerinizi kolayca ekleyin",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "KA/ZD yok"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "{{section}} yüklenemiyor",
+ "retry": "Tekrar Dene"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/vi.json b/locales/languages/vi.json
index 6d4c66fc49e..d0587d29252 100644
--- a/locales/languages/vi.json
+++ b/locales/languages/vi.json
@@ -103,6 +103,10 @@
"message": "Không đủ {{ticker}} để chi trả phí. Hãy sử dụng token trên mạng khác hoặc nạp thêm {{ticker}} để tiếp tục.",
"title": "Không đủ tiền"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "Lộ trình thanh toán này hiện không khả dụng. Hãy thử thay đổi số lượng, mạng hoặc token và chúng tôi sẽ tìm phương án tốt nhất.",
"title": "Không có báo giá"
@@ -1180,6 +1184,7 @@
"title": "Lệnh mới",
"leverage": "Đòn bẩy",
"limit_price": "Giá giới hạn",
+ "market_price": "Giá thị trường",
"enter_price": "Nhập giá",
"trigger_price": "Giá kích hoạt",
"liquidation_price": "Giá thanh lý",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "Tiền đã sẵn sàng để giao dịch",
"close_order_still_active": "Lệnh đóng vị thế vẫn đang có hiệu lực",
"order_submitted": "Đã gửi lệnh",
+ "submitting_your_trade": "Đang gửi giao dịch của bạn",
"order_filled": "Đã khớp lệnh",
"order_placed": "Đã đặt lệnh",
"order_placement_subtitle": "{{direction}} {{amount}} {{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "Chi tiết lệnh",
"cancel_order": "Hủy lệnh",
"date": "Ngày",
+ "trigger_condition": "Điều kiện kích hoạt",
+ "price": "Giá",
"fee": "Phí",
"limit_buy": "Giới hạn mua khống",
"limit_price": "Giá giới hạn",
"limit_sell": "Giới hạn bán khống",
"market_buy": "Mua khống theo thị trường",
"market_sell": "Bán khống theo thị trường",
+ "market": "Thị trường",
"open": "Mở",
"size": "Kích thước",
+ "original_size": "Kích thước ban đầu",
+ "order_value": "Giá trị lệnh",
+ "reduce_only": "Chỉ giảm",
+ "yes": "Có",
+ "no": "Không",
+ "price_above": "Giá trên {{price}}",
+ "price_below": "Giá dưới {{price}}",
+ "take_profit": "Chốt lời",
+ "stop_loss": "Cắt lỗ",
"status": "Trạng thái",
"view_explorer": "Xem trên Trình khám phá"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "Thông tin thị trường",
- "updated_ago": "Đã cập nhật {{time}}",
- "disclaimer": "Phân tích AI. Không phải lời khuyên tài chính.",
- "whats_driving_price": "Điều gì đang thúc đẩy giá?",
- "what_people_saying": "Mọi người đang bàn luận về điều gì",
+ "a_closer_look": "Xem chi tiết hơn",
+ "whats_being_said": "Những gì đang được nói",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "Giao dịch",
"sources_count": "+{{count}} nguồn",
- "sources_title": "Có tính thanh khoản cao nhất"
+ "sources_title": "News sources",
+ "feedback_submitted": "Đã gửi phản hồi",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "Phản hồi",
+ "description": "Giúp cải thiện thông tin thị trường do AI tạo ra.",
+ "not_relevant": "Không liên quan",
+ "not_accurate": "Không chính xác",
+ "hard_to_understand": "Khó hiểu",
+ "harmful_or_offensive": "Gây hại hoặc xúc phạm",
+ "something_else": "Vấn đề khác",
+ "additional_feedback_label": "Phản hồi bổ sung (không bắt buộc)",
+ "additional_feedback_placeholder": "Chúng tôi có thể làm gì để cải thiện?",
+ "characters_remaining": "Còn lại {{count}} ký tự",
+ "submit": "Gửi"
+ }
},
"predict": {
"title": "MetaMask Dự đoán",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "Sẽ khả dụng sau khoảng 1 phút",
"withdraw_completed": "Rút tiền hoàn tất",
"withdraw_completed_subtitle": "{{amount}} USDC đã được chuyển vào ví của bạn",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} đã được chuyển vào ví của bạn",
"error_title": "Đã xảy ra sự cố",
"error_description": "Không thể thực hiện yêu cầu rút tiền",
"try_again": "Thử lại"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "Lưu ý rằng giá trị này chỉ là ước tính và sẽ được xác nhận sau khi giao dịch hoàn tất. Điểm có thể mất đến 1 giờ để được xác nhận trong số dư Phần thưởng.",
"points_error": "Chúng tôi không thể tải điểm ngay bây giờ",
"points_error_content": "Bạn vẫn sẽ nhận được điểm cho giao dịch này. Chúng tôi sẽ thông báo cho bạn sau khi điểm đã được cộng vào tài khoản. Bạn cũng có thể kiểm tra tab Phần thưởng sau khoảng một giờ.",
- "slippage": "Trượt giá"
+ "slippage": "Trượt giá",
+ "price_details": "Chi tiết giá",
+ "prediction_order": "Lệnh dự đoán",
+ "prediction_order_description": "~{{count}} hợp đồng ở mức {{price}} mỗi hợp đồng. Số tiền cuối cùng có thể thay đổi tùy theo tính khả dụng của sổ lệnh (tối đa {{slippage}}%).",
+ "metamask_fee_description": "Phí dịch vụ để xử lý dự đoán này",
+ "exchange_fee": "Phí sàn giao dịch",
+ "exchange_fee_description": "Phí trả cho sàn giao dịch hoặc thị trường",
+ "total_incl_fees": "bao gồm phí",
+ "close": "Đóng"
},
"error": {
"title": "Không thể kết nối với thị trường dự đoán",
@@ -2399,7 +2440,7 @@
"add_tokens": "Nhập token",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "Bạn có muốn nhập các token này không?",
"tokens_detected_in_account": "Đã tìm thấy {{tokenCount}} {{tokensLabel}} mới trong tài khoản này",
"token_toast": {
"tokens_imported_title": "Token đã nhập",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "Số thập phân Token không thể để trống.",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "Chúng tôi không thể tìm ra Token nào có tên đó.",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "Tìm kiếm bất kỳ token nào và nhập nó",
"select_token": "Chọn token",
"address_must_be_smart_contract": "Đã phát hiện địa chỉ cá nhân. Hãy nhập địa chỉ hợp đồng của Token.",
"billion_abbreviation": "Tỷ",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "Thêm URL RPC",
"add_block_explorer_url": "Thêm URL trình khám phá khối",
"networks_desc": "Thêm và sửa mạng RPC tùy chỉnh",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "Mạng đã bật",
+ "networks_test_networks": "Mạng thử nghiệm",
+ "networks_additional": "Mạng bổ sung",
"networks_search_placeholder": "Tìm kiếm mạng",
"networks_no_results": "Không tìm thấy mạng nào",
"network_name_label": "Tên mạng",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "Mạng RPC",
"network_add_network": "Thêm mạng",
"add_chain_title": "Thêm mạng",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "Tìm kiếm theo tên, ID chuỗi hoặc loại tiền tệ",
+ "add_chain_loading": "Đang tải mạng…",
+ "add_chain_error": "Không thể tải mạng. Vui lòng thử lại.",
"add_chain_retry": "Thử lại",
- "add_chain_added": "Added",
+ "add_chain_added": "Đã thêm",
"add_chain_or": "hoặc",
"add_chain_custom_link": "Thêm mạng tùy chỉnh",
"network_add_custom_network": "Thêm mạng tùy chỉnh",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "Thêm mạng thử nghiệm",
"network_add": "Thêm",
"network_save": "Lưu",
"remove_network_title": "Bạn có muốn gỡ bỏ mạng này?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "Hãy cất giữ nó ở một nơi an toàn và bí mật.",
"private_key_warning": "Đây là khóa cá nhân cho tài khoản đang được chọn: {{accountName}}. Đừng bao giờ tiết lộ khóa này. Bất kỳ ai có khóa cá nhân của bạn đều có toàn quyền kiểm soát tài khoản của bạn, bao gồm cả quyền chuyển bất kỳ khoản tiền nào của bạn đi.",
- "seed_phrase_warning_explanation": [
- "Đảm bảo không có ai đang nhìn vào màn hình của bạn. ",
- "Bộ phận Hỗ trợ MetaMask sẽ không bao giờ yêu cầu điều này."
- ],
+ "seed_phrase_warning_explanation": "Đảm bảo không có ai đang nhìn vào màn hình của bạn. Bộ phận Hỗ trợ MetaMask sẽ không bao giờ yêu cầu thông tin này.",
"private_key_warning_explanation": "Đừng bao giờ tiết lộ khóa này. Bất kỳ ai có khóa cá nhân của bạn đều có toàn quyền kiểm soát tài khoản của bạn, bao gồm cả quyền chuyển bất kỳ khoản tiền nào của bạn đi.",
+ "reveal_srp_description": "Cụm từ khôi phục bí mật cho phép có toàn quyền truy cập vào ví của bạn. Đừng chia sẻ với bất kỳ ai.",
"reveal_credential_modal": [
"{{credentialName}} của bạn cung cấp ",
"toàn quyền truy cập vào tài khoản và tiền của bạn.\n\nĐừng chia sẻ điều này với bất kỳ ai.\n",
@@ -3385,7 +3424,8 @@
"srp_text": "của bạn",
"private_key_text": "Khóa riêng tư",
"got_it": "Đã hiểu",
- "learn_more": "Tìm hiểu thêm"
+ "learn_more": "Tìm hiểu thêm",
+ "copied_to_clipboard": "Đã sao chép vào bộ nhớ đệm"
},
"screenshot_deterrent": {
"title": "Cảnh báo an toàn",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "Cố tăng tốc?",
"speedup_tx_message": "Việc cố tăng tốc này không đảm bảo giao dịch ban đầu của bạn sẽ được tăng tốc. Nếu tăng tốc thành công, bạn sẽ bị tính phí giao dịch ở trên.",
"nevermind": "Đừng bận tâm",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "Sửa phí gas",
"edit_priority": "Sửa mức ưu tiên",
"gas_cancel_fee": "Phí hủy gas",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "Phí giao dịch liên quan tới quyền này.",
"view_details": "Xem chi tiết",
"view_transaction_details": "Xem chi tiết giao dịch",
- "view_data": "View data",
+ "view_data": "Xem dữ liệu",
"transaction_details": "Chi tiết giao dịch",
"site_url": "URL trang web",
"permission_request": "Yêu cầu cấp quyền",
@@ -3843,7 +3887,7 @@
"right_button": "Bảo vệ ví"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "Thêm vào mục yêu thích",
"title_label": "Tên",
"url_label": "URL",
"add_button": "Thêm",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "Mở khóa bằng Touch ID?",
"enable_faceid": "Mở khóa bằng Face ID?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "Mở khóa bằng vân tay?",
+ "enable_biometrics": "Mở khóa bằng sinh trắc học?",
"enable_device_passcode_ios": "Mở khóa bằng mật mã của thiết bị?",
"enable_device_passcode_android": "Mở khóa bằng mã PIN của thiết bị?"
},
@@ -4724,7 +4768,7 @@
"public_address": "Địa chỉ công khai",
"public_address_qr_code": "Địa chỉ công khai",
"coming_soon": "Sắp ra mắt...",
- "request_payment": "Request payment",
+ "request_payment": "Yêu cầu thanh toán",
"copy": "Sao chép",
"scan_address": "Quét địa chỉ để nhận thanh toán",
"copy_address": "Sao chép địa chỉ"
@@ -4738,8 +4782,8 @@
"switch_network": "Vui lòng chuyển qua mạng chính hoặc sepolia",
"card_title": "Luôn hiển thị nút Thẻ MetaMask",
"card_desc": "Thẻ MetaMask chỉ khả dụng cho cư dân của một số quốc gia được chọn.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "Sử dụng môi trường thử nghiệm của DaimoPay",
+ "daimo_demo_desc": "Chuyển đổi giữa môi trường thử nghiệm và môi trường triển khai của DaimoPay cho các giao dịch thanh toán bằng thẻ."
},
"walletconnect_sessions": {
"no_active_sessions": "Bạn không có phiên đang hoạt động nào",
@@ -4752,10 +4796,10 @@
"close_current_session": "Đóng phiên hiện tại trước khi bắt đầu một phiên mới."
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "Yêu cầu thanh toán",
+ "title_complete": "Thanh toán hoàn tất",
"confirm": "Thanh toán",
- "cancel": "Decline",
+ "cancel": "Từ chối",
"is_requesting_you_to_pay": "đang yêu cầu bạn thanh toán",
"total": "Tổng cộng:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "Không có phương thức thanh toán khả dụng.",
"error_fetching_quotes": "Đã xảy ra lỗi. Vui lòng thử lại.",
"no_quotes_available": "Hiện không có nhà cung cấp nào.",
+ "quote_unavailable": "Không có báo giá.",
"providers": "Nhà cung cấp",
+ "quotes_displayed_for": "Báo giá được hiển thị cho {{paymentMethodName}}.",
+ "other_options": "Other options",
+ "previously_used": "Đã sử dụng trước đây",
+ "best_rate": "Tỷ giá tốt nhất",
+ "most_reliable": "Đáng tin cậy nhất",
"continue": "Tiếp tục",
"powered_by_provider": "Cung cấp bởi {{provider}}",
"purchased_currency": "Đã mua {{currency}}",
@@ -4890,14 +4940,19 @@
"logged_out_error": "Lỗi khi đăng xuất"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "Không có sẵn",
+ "description": "{{token}} không khả dụng với {{provider}} tại khu vực của bạn.",
+ "change_token": "Thay đổi token",
+ "change_provider": "Thay đổi nhà cung cấp"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "Chọn nhà cung cấp"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "Tôi đã hiểu",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "Thay đổi nhà cung cấp"
},
"fiat_on_ramp_aggregator": {
"buy": "mua",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "Nạp {{currency}}"
},
+ "ramps_order_details": {
+ "title": "Chi tiết lệnh",
+ "status": "Trạng thái",
+ "processing": "Đang xử lý",
+ "complete": "Hoàn tất",
+ "failed": "Không thành công",
+ "cancelled": "Đã hủy",
+ "view_on_provider": "Xem trên {{provider}}",
+ "order_id": "Mã lệnh",
+ "date_and_time": "Ngày và giờ",
+ "fees": "Phí",
+ "total": "Tổng cộng",
+ "card_processing_info": "Mua bằng thẻ thường chỉ mất vài phút",
+ "processing_info_modal_description": "Giao dịch mua hàng bằng thẻ thường mất vài phút. Bạn có thể liên hệ bộ phận hỗ trợ nếu có câu hỏi.",
+ "go_to_provider_support": "Truy cập trang hỗ trợ của {{provider}}",
+ "error_title": "Đã xảy ra sự cố",
+ "error_message": "Không thể tải chi tiết đơn hàng. Vui lòng thử lại.",
+ "try_again": "Thử lại",
+ "close": "Đóng"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "Đang xử lý giao dịch mua {{cryptocurrency}} của bạn",
+ "purchase_pending_description": "Quá trình này chỉ mất vài phút...",
+ "purchase_completed_title": "Bạn đã mua {{amount}} {{cryptocurrency}} thành công!",
+ "purchase_completed_description": "{{cryptocurrency}} của bạn hiện đã có sẵn",
+ "purchase_failed_title": "Giao dịch mua {{cryptocurrency}} đã thất bại",
+ "purchase_failed_description": "Vui lòng thử lại sau ít phút",
+ "purchase_cancelled_title": "Giao dịch mua của bạn đã bị hủy",
+ "purchase_cancelled_description": "Giao dịch mua {{cryptocurrency}} của bạn đã bị hủy",
+ "track": "Theo dõi"
+ }
+ },
"swaps": {
"title": "Hoán đổi",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "Để tránh trường hợp này xảy ra lại, hãy nhớ luôn cập nhật ứng dụng MetaMask và hệ điều hành của bạn lên phiên bản mới nhất."
},
"srp_security_quiz": {
+ "question_step": "Câu hỏi {{step}}/{{total}}",
"title": "Câu hỏi bảo mật",
"introduction": "Để hiển thị Cụm từ khôi phục bí mật, bạn cần trả lời đúng hai câu hỏi",
"get_started": "Bắt đầu",
"learn_more": "Tìm hiểu thêm",
"try_again": "Thử lại",
"continue": "Tiếp tục",
+ "correct": "Chính xác!",
+ "incorrect": "Không chính xác!",
"of": "của",
"question_one": {
"question": "Nếu bạn làm mất Cụm từ khôi phục bí mật, MetaMask...",
@@ -5823,11 +5914,11 @@
"error_description": "Cài đặt {{snap}} thất bại."
},
"earn": {
- "claimable_bonus_tooltip": "Thưởng hằng năm có thể nhận hằng ngày từ ví của bạn.",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "Nhận thưởng {{percentage}}%",
"claimable_bonus": "Thưởng có thể nhận",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "Nhận thưởng",
+ "claim_bonus_subtitle": "Tiền thưởng sẽ được trả trên {{networkName}}.",
"empty_state_cta": {
"heading": "Cho vay {{tokenSymbol}} và nhận lãi",
"body": "Cho vay {{tokenSymbol}} với {{protocol}} và kiếm lợi nhuận",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "Chúng tôi đang bảo trì. Chúng tôi sẽ sớm hoạt động trở lại!"
},
- "supply": "Supply",
+ "supply": "Cung cấp",
"deposit": "Nạp tiền",
"approve": "Phê duyệt",
"approval": "Phê duyệt",
@@ -5887,7 +5978,7 @@
"network": "Mạng",
"health_factor": "Chỉ số an toàn",
"liquidation_risk": "Rủi ro thanh lý",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "Khả năng thanh khoản của bể không đủ",
"available_to_withdraw": "có sẵn để rút",
"unknown": "không xác định",
"how_it_works": "Cách hoạt động",
@@ -5906,7 +5997,7 @@
"lending": "Lịch sử vị thế",
"staking": "Lịch sử trả thưởng"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "Đặt lại hạn mức",
"tron": {
"fee": "Phí"
},
@@ -5914,32 +6005,60 @@
"ok": "OK",
"continue": "Tiếp tục",
"convert_and_get_percentage_bonus": "Chuyển đổi và nhận {{percentage}}%",
+ "your_musd": "mUSD của bạn",
+ "balance_breakdown_title": "Số dư mUSD của bạn theo mạng",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "Chuyển đổi sang mUSD",
"get_a_percentage_musd_bonus": "Nhận thưởng {{percentage}}% mUSD",
"convert": "Chuyển đổi",
+ "fetching_quote": "Tìm nạp báo giá...",
+ "you_convert": "Bạn chuyển đổi",
+ "network_fee": "Phí mạng",
+ "earning": "Thu nhập",
+ "quick_convert_description": "Chọn một token để chuyển đổi toàn bộ số dư của bạn hoặc nhấn vào biểu tượng chỉnh sửa để nhập số lượng tùy chỉnh.",
+ "no_tokens_to_convert": "Bạn không có token nào có thể chuyển đổi sang mUSD.",
"toasts": {
"converting": "Đang chuyển đổi {{token}} → mUSD",
"eta": "~{{time}}",
- "delivered": "mUSD của bạn đã sẵn sàng!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "Chuyển đổi mUSD thất bại"
},
"education": {
"heading": "NHẬN {{percentage}}%\nĐỒNG ỔN ĐỊNH",
- "description": "Chuyển đổi đồng ổn định sang mUSD, đồng ổn định được MetaMask hỗ trợ bằng đô-la Mỹ, và nhận thưởng lên đến {{percentage}}%.",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "Áp dụng điều khoản.",
"primary_button": "Bắt đầu",
"secondary_button": "Không phải bây giờ"
},
"buy_musd": "Mua mUSD",
"get_musd": "Nhận mUSD",
- "bonus_title": "Nhận {{percentage}}% cho đồng ổn định của bạn",
- "bonus_description": "Chuyển đổi đồng ổn định sang mUSD và nhận thưởng lên đến {{percentage}}%.",
- "powered_by_relay": "Cung cấp bởi Relay"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "Cung cấp bởi Relay",
+ "max": "Tối đa",
+ "quick_convert_button": "Chuyển đổi",
+ "learn_more": "Tìm hiểu thêm",
+ "tooltip_title": "Nhận lợi suất với mUSD",
+ "tooltip_content": "Chuyển đổi USDC, USDT hoặc DAI sang mUSD, đồng ổn định được hỗ trợ bằng đô-la của MetaMask. Nhận lợi suất {{apy}} trên mỗi đô-la bạn nắm giữ.",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "Chuyển đổi thất bại. Hãy thử lại.",
+ "confirmation": {
+ "title": "Chuyển đổi tối đa"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "Tỷ lệ"
},
"bonus_claim": {
"toasts": {
"claiming": "Khoản thưởng mUSD của bạn đang được xử lý",
"delivered": "Khoản thưởng mUSD của bạn đã có!",
- "failed": "Bonus claim failed"
+ "failed": "Nhận thưởng thất bại"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "Điểm sẽ tự động được cộng vào tài khoản của bạn.",
"tooltip_not_opted_in_footer": "Đăng ký tham gia chương trình phần thưởng để nhận điểm.",
"tooltip_close": "Đóng"
- }
+ },
+ "your_stablecoins": "Đồng ổn định của bạn"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "Phí giao dịch",
"metamask_fee": "Phí MetaMask",
"network_fee": "Phí mạng",
- "bridge_fee": "Phí nhà cung cấp cầu nối"
+ "bridge_fee": "Phí nhà cung cấp cầu nối",
+ "provider_fee": "Phí nhà cung cấp"
},
"title": {
"signature": "Yêu cầu chữ ký",
@@ -6260,10 +6381,10 @@
"transaction_fee": "Chúng tôi sẽ hoán đổi token của bạn sang USDC.e trên Polygon, mạng được thị trường Dự đoán sử dụng. Các nhà cung cấp dịch vụ hoán đổi có thể tính phí, nhưng MetaMask thì không."
},
"predict_withdraw": {
- "transaction_fee": "MetaMask sẽ hoán đổi sang token mà bạn mong muốn. Không áp dụng phí MetaMask khi bạn hoán đổi sang MUSD."
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "Phí chuyển đổi mUSD bao gồm phí mạng và có thể bao gồm phí từ nhà cung cấp."
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "Phí"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "Mức tác động giá",
"price_impact_info_description": "Tác động giá phản ánh cách lệnh hoán đổi của bạn ảnh hưởng đến giá thị trường của tài sản. Nó phụ thuộc vào kích thước giao dịch và thanh khoản có sẵn trong bể. MetaMask không ảnh hưởng hoặc kiểm soát tác động giá.",
"price_impact_info_gasless_description": "Tác động giá phản ánh cách lệnh hoán đổi của bạn ảnh hưởng đến giá thị trường của tài sản. Nếu bạn không có đủ tiền để trả phí gas, một phần token gốc của bạn sẽ tự động được phân bổ để trả phí, làm tăng tác động giá. MetaMask không can thiệp hoặc kiểm soát tác động giá.",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "Hủy",
"slippage_info_title": "Trượt giá",
"slippage_info_description": "Phần trăm thay đổi giá mà bạn chấp nhận trước khi giao dịch bị hủy.",
"blockaid_error_title": "Giao dịch này sẽ được hoàn lại",
@@ -6470,13 +6596,14 @@
},
"submit": "Gửi",
"default_slippage_description": "Giao dịch của bạn sẽ không được thực hiện nếu giá thay đổi vượt quá phần trăm trượt giá.",
- "cancel": "Hủy",
"confirm": "Xác nhận",
"exceeding_upper_slippage_warning": "Độ trượt giá cao, điều này có thể dẫn đến giao dịch hoán đổi không thuận lợi",
"exceeding_lower_slippage_warning": "Độ trượt giá thấp, điều này có thể dẫn đến giao dịch hoán đổi không thuận lợi",
"exceeding_lower_slippage_error": "Nhập giá trị lớn hơn {{value}}%",
"exceeding_upper_slippage_error": "Bạn không thể nhập giá trị lớn hơn {{value}}%",
- "custom": "Tùy chỉnh"
+ "custom": "Tùy chỉnh",
+ "invalid_recipient_address": "Địa chỉ không hợp lệ",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "Đã có báo giá mới",
@@ -6773,12 +6900,16 @@
"title": "Nhập mật khẩu",
"description": "Nhập mật khẩu ví của bạn để xem thông tin thẻ.",
"description_unfreeze": "Nhập mật khẩu ví của bạn để tiếp tục chi tiêu bằng thẻ.",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "Mật khẩu",
"confirm": "Xác nhận",
"cancel": "Hủy",
"error_empty": "Vui lòng nhập mật khẩu",
"error_incorrect": "Mật khẩu không chính xác. Vui lòng thử lại."
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "Chọn thẻ của bạn",
"upgrade_title": "Nâng cấp lên Metal",
@@ -7094,6 +7225,9 @@
"view_card_details": "Xem thông tin thẻ",
"hide_card_details": "Ẩn thông tin thẻ",
"view_card_details_description": "Số thẻ, ngày hết hạn và số CVV",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "Quản lý hạn mức",
"manage_spending_limit_description_restricted": "Giới hạn chi tiêu đang bật",
"manage_spending_limit_description_full": "Quyền truy cập đầy đủ đang bật",
@@ -7104,6 +7238,9 @@
"card_tos_title": "Điều khoản và điều kiện",
"order_metal_card": "Thẻ Metal",
"order_metal_card_description": "Đặt hàng Thẻ Metal vật lý ngay",
+ "cashback": "Hoàn tiền",
+ "cashback_description": "Nhận lại 1% cho mọi chi tiêu",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "Khóa thẻ",
"unfreeze_card": "Mở khóa thẻ",
"freeze_card_description": "Tạm dừng mọi chi tiêu trên thẻ của bạn",
@@ -7141,6 +7278,19 @@
"retry": "Thử lại",
"on_linea": "trên Linea"
},
+ "cashback_screen": {
+ "title": "Hoàn tiền",
+ "available_cashback": "Hoàn tiền khả dụng",
+ "network_fee": "Phí mạng",
+ "expected_to_receive": "Dự kiến nhận được",
+ "withdraw": "Rút tiền",
+ "withdraw_unavailable": "Không thể rút tiền",
+ "withdrawal_initiated": "Đã khởi tạo lệnh rút tiền",
+ "withdrawal_success": "Rút tiền thành công",
+ "withdrawal_failed": "Rút tiền thất bại. Vui lòng thử lại.",
+ "no_cashback": "Không có hoàn tiền khả dụng",
+ "loading_error": "Không thể tải thông tin hoàn tiền. Vui lòng thử lại."
+ },
"change_asset": {
"title": "Thay đổi token và mạng",
"full_spending_access": "Quyền truy cập chi tiêu đầy đủ",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "Chọn phương thức thanh toán",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "Chọn token nhận được",
+ "no_gas": "Không có số dư token gốc để trả phí gas"
},
"connection_removed_modal": {
"title": "Kết nối đã bị xóa",
@@ -7315,7 +7465,10 @@
"service_not_available": "Dịch vụ hiện không khả dụng. Vui lòng thử lại sau giây lát.",
"invalid_referral_code": "Mã giới thiệu không hợp lệ. Vui lòng kiểm tra và thử lại.",
"already_referred": "Bạn đã được người dùng khác giới thiệu.",
- "cannot_use_own_referral_code": "Bạn không thể sử dụng mã giới thiệu của chính mình."
+ "cannot_use_own_referral_code": "Bạn không thể sử dụng mã giới thiệu của chính mình.",
+ "invalid_bonus_code": "Mã thưởng không hợp lệ",
+ "already_redeemed": "Bạn đã đổi mã thưởng này",
+ "reached_maximum": "Mã thưởng này đã đạt đến số lượt sử dụng tối đa"
},
"claim_reward_error": {
"title": "Không thể nhận phần thưởng"
@@ -7372,8 +7525,10 @@
"predict": "Dự đoán",
"musd_deposit": "Nạp mUSD",
"apply_referral_bonus": "Thưởng mã giới thiệu",
+ "bonus_code": "Mã thưởng",
"uncategorized_event": "Sự kiện chưa được phân loại"
},
+ "code": "Mã",
"date": "Ngày",
"account": "Tài khoản",
"bonus": "Thưởng",
@@ -7473,7 +7628,16 @@
"show_less": "Thu gọn",
"linking_progress": "Đang thêm tài khoản... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} đã đăng ký tham gia",
- "add_all_accounts": "Thêm tất cả tài khoản"
+ "add_all_accounts": "Thêm tất cả tài khoản",
+ "environment_selector": "Môi trường",
+ "environment_cancel": "Hủy",
+ "environment_default": "Mặc định",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "Mã giới thiệu",
@@ -7483,6 +7647,10 @@
"invalid_code": "Mã giới thiệu không hợp lệ",
"apply_button": "Áp dụng mã giới thiệu"
},
+ "bonus_code": {
+ "input_placeholder": "Nhập mã thưởng",
+ "apply_success": "Mã thưởng đã được áp dụng!"
+ },
"optout": {
"title": "Hủy tham gia chương trình Phần thưởng",
"description": "Hành động này sẽ xóa các tài khoản của bạn khỏi chương trình Phần thưởng cũng như xóa toàn bộ điểm và tiến trình của bạn. Bạn sẽ không thể hoàn tác hành động này.",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "Phí cầu nối",
+ "provider_fee": "Phí nhà cung cấp",
"network_fee": "Phí mạng",
"paid_with": "Đã thanh toán bằng",
+ "receive_token": "Token nhận được",
"retry_button": "Thử lại",
"total": "Tổng cộng",
"account": "Tài khoản",
- "received_total": "Received total"
+ "received_total": "Tổng số lượng nhận được"
},
"summary_title": {
"bridge_approval": "Phê duyệt {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "Không tìm thấy kết nối",
"description": "Vui lòng thiết lập kết nối mới từ ứng dụng để tiếp tục."
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "Vẫn đang kết nối với {{networkName}}...",
"unable_to_connect_network": "Không thể kết nối với {{networkName}}.",
"update_rpc": "Cập nhật RPC",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "Chuyển sang RPC mặc định của MetaMask",
"check_network_connectivity": "Kiểm tra kết nối mạng của bạn.",
"check_network_connectivity_or": "Kiểm tra kết nối mạng của bạn hoặc",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "Đã cập nhật sang dạng mặc định của MetaMask"
},
"trending": {
"title": "Khám phá",
"trending_tokens": "Token xu hướng",
+ "stocks": "Cổ phiếu",
"price_change": "Biến động giá",
"all_networks": "Tất cả mạng",
"24h": "24 giờ",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "Tải lại",
"primary_action_acknowledge": "Tôi đã hiểu"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "Ví cứng QR",
+ "hardware_wallet": "Ví cứng"
+ },
+ "common": {
+ "cancel": "Hủy",
+ "continue": "Tiếp tục"
+ },
+ "connecting": {
+ "title": "Kết nối {{device}} của bạn",
+ "searching": "Đang tìm {{device}}...",
+ "tips_header": "Để tiếp tục, hãy đảm bảo:",
+ "tip_unlock": "{{device}} của bạn đã được mở khóa",
+ "tip_open_app": "Ứng dụng Ethereum đang mở",
+ "tip_enable_bluetooth": "Bluetooth đã được bật",
+ "tip_dnd_off": "Chế độ Không làm phiền đã được tắt",
+ "tip_bluetooth_permission": "Quyền truy cập Vị trí và Bluetooth đã được cấp",
+ "tip_bluetooth_permission_v12": "Quyền truy cập Thiết bị lân cận đã được cấp",
+ "tip_stay_close": "Thiết bị của bạn ở gần điện thoại"
+ },
+ "awaiting_app": {
+ "title": "Mở {{app}}",
+ "message": "Vui lòng mở ứng dụng {{app}} trên {{device}} của bạn",
+ "current_app": "Hiện đang mở: {{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "Xác nhận trên {{device}}",
+ "title_message": "Ký trên {{device}}",
+ "message": "Xem lại và xác nhận trên {{device}} của bạn"
+ },
+ "success": {
+ "title": "{{device}} đã được kết nối"
+ },
+ "errors": {
+ "device_locked": "Vui lòng mở khóa và thử lại để tiếp tục",
+ "app_not_open": "Vui lòng mở ứng dụng Ethereum trên thiết bị của bạn",
+ "device_disconnected": "{{device}} của bạn đã bị ngắt kết nối. Vui lòng kết nối lại và thử lại",
+ "device_not_found": "Không tìm thấy {{device}} của bạn. Vui lòng đảm bảo thiết bị đã được kết nối",
+ "device_not_ready": "Thiết bị của bạn chưa sẵn sàng. Vui lòng kiểm tra và thử lại",
+ "blind_signing": "Tính năng ký mù đang bị tắt. Vui lòng bật trong phần cài đặt thiết bị",
+ "connection_closed": "Đã bị mất kết nối. Vui lòng thử lại",
+ "connection_timeout": "Hết thời gian kết nối. Vui lòng thử lại",
+ "user_cancelled": "Hành động đã bị hủy trên thiết bị của bạn",
+ "pending_confirmation": "Có một hành động đang chờ xử lý trên thiết bị của bạn. Vui lòng hoàn tất hoặc hủy hành động đó",
+ "bluetooth_permission_denied": "Cần quyền truy cập Bluetooth để kết nối với thiết bị của bạn",
+ "location_permission_denied": "Cần quyền truy cập Vị trí để quét thiết bị",
+ "nearby_permission_denied": "Cần quyền truy cập Thiết bị lân cận",
+ "bluetooth_off": "Vui lòng bật Bluetooth để kết nối với thiết bị của bạn",
+ "bluetooth_scan_failed": "Không thể quét thiết bị. Vui lòng thử lại",
+ "bluetooth_connection_failed": "Bật Bluetooth trên thiết bị của bạn để tiếp tục",
+ "not_supported": "Thao tác này không được hỗ trợ",
+ "unknown_error": "Đảm bảo {{device}} của bạn đã được thiết lập với Cụm từ khôi phục bí mật hoặc cụm mật khẩu cho tài khoản này"
+ },
+ "error": {
+ "title": "Đã xảy ra sự cố",
+ "default_title": "Đã xảy ra lỗi",
+ "continue": "Tiếp tục",
+ "retry": "Thử lại",
+ "view_settings": "Xem Cài đặt",
+ "device_locked_title": "{{device}} đã bị khóa",
+ "device_disconnected_title": "{{device}} đã bị ngắt kết nối",
+ "device_not_found_title": "Không tìm thấy {{device}}",
+ "app_not_open": "Ứng dụng Ethereum chưa được mở",
+ "blind_signing_disabled": "Tính năng Ký mù đã bị tắt",
+ "connection_timeout": "Thiết bị không phản hồi",
+ "connection_closed": "Mất kết nối",
+ "user_cancelled": "Hành động đã bị hủy",
+ "pending_confirmation": "Đang chờ xác nhận",
+ "bluetooth_required": "Cần có Bluetooth",
+ "bluetooth_permission_denied": "Yêu cầu quyền truy cập Bluetooth",
+ "location_permission_denied": "Yêu cầu quyền truy cập Vị trí",
+ "nearby_devices_permission_denied": "Yêu cầu quyền truy cập Thiết bị lân cận",
+ "scan_failed": "Quét thất bại",
+ "something_went_wrong": "Đã xảy ra sự cố"
+ },
+ "device_selection": {
+ "title": "Chọn {{device}}",
+ "scanning": "Đang quét thiết bị...",
+ "no_devices_found": "Không tìm thấy thiết bị nào",
+ "no_devices_hint": "Đảm bảo {{device}} đã được mở khóa và Bluetooth đã được bật",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "Kết nối",
+ "rescan": "Quét lại",
+ "unknown_device": "Thiết bị không xác định",
+ "signal_strength": "Tín hiệu: {{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "Token",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT",
"import_nfts": "Nhập NFT",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "Dễ dàng thêm các bộ sưu tập của bạn",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "Không có Chốt lời/Cắt lỗ"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "Không thể tải {{section}}",
+ "retry": "Thử lại"
}
}
-}
+}
\ No newline at end of file
diff --git a/locales/languages/zh.json b/locales/languages/zh.json
index dc713652c2b..dcb32b95f50 100644
--- a/locales/languages/zh.json
+++ b/locales/languages/zh.json
@@ -103,6 +103,10 @@
"message": "{{ticker}} 不足,无法支付费用。请使用其他网络的代币,或添加更多 {{ticker}} 以继续。",
"title": "资金不足"
},
+ "insufficient_pay_token_native_post_quote": {
+ "message": "Not enough {{ticker}} to cover fees. Add {{ticker}} to continue.",
+ "title": "Insufficient funds for post quote"
+ },
"no_pay_token_quotes": {
"message": "目前该支付路径不可用。请调整金额、网络或代币类型,系统将为您匹配最优选项。",
"title": "无报价"
@@ -1180,6 +1184,7 @@
"title": "新订单",
"leverage": "杠杆",
"limit_price": "限价",
+ "market_price": "市价",
"enter_price": "入场价格",
"trigger_price": "触发价格",
"liquidation_price": "清算价格",
@@ -1323,6 +1328,7 @@
"funds_are_available_to_trade": "资金可用于交易",
"close_order_still_active": "平仓订单仍有效",
"order_submitted": "订单已提交",
+ "submitting_your_trade": "正在提交您的交易",
"order_filled": "订单已成交",
"order_placed": "订单已下达",
"order_placement_subtitle": "{{direction}}{{amount}}{{assetSymbol}}",
@@ -1340,14 +1346,26 @@
"title": "订单详情",
"cancel_order": "取消订单",
"date": "日期",
+ "trigger_condition": "触发条件",
+ "price": "价格",
"fee": "费用",
"limit_buy": "限价多头仓位",
"limit_price": "限价",
"limit_sell": "限价空头仓位",
"market_buy": "市价多头仓位",
"market_sell": "市价空头仓位",
+ "market": "市场型",
"open": "未平仓",
"size": "规模",
+ "original_size": "原始规模",
+ "order_value": "订单价值",
+ "reduce_only": "仅减仓",
+ "yes": "是",
+ "no": "否",
+ "price_above": "价格高于 {{price}}",
+ "price_below": "价格低于 {{price}}",
+ "take_profit": "止盈",
+ "stop_loss": "止损",
"status": "状态",
"view_explorer": "在区块链浏览器中查看"
},
@@ -2057,13 +2075,27 @@
},
"market_insights": {
"title": "市场洞察",
- "updated_ago": "更新于 {{time}}",
- "disclaimer": "人工智能分析见解。不构成财务建议。",
- "whats_driving_price": "推动价格的因素有哪些?",
- "what_people_saying": "大家都在说什么",
+ "a_closer_look": "深度观察",
+ "whats_being_said": "市场声音",
+ "footer_disclaimer": "AI summary for information only",
"trade_button": "交易",
"sources_count": "+{{count}} 个来源",
- "sources_title": "来源"
+ "sources_title": "News sources",
+ "feedback_submitted": "反馈已提交",
+ "helpful_prompt": "Was this helpful?",
+ "feedback": {
+ "title": "反馈",
+ "description": "帮助我们改进人工智能生成的市场洞察。",
+ "not_relevant": "不相关",
+ "not_accurate": "不准确",
+ "hard_to_understand": "难以理解",
+ "harmful_or_offensive": "有害或具有攻击性",
+ "something_else": "其他",
+ "additional_feedback_label": "附加反馈(可选)",
+ "additional_feedback_placeholder": "我们如何改进?",
+ "characters_remaining": "还剩 {{count}} 个字符",
+ "submit": "提交"
+ }
},
"predict": {
"title": "MetaMask 预测",
@@ -2276,6 +2308,7 @@
"withdrawing_subtitle": "约 1 分钟后可用",
"withdraw_completed": "提取完成",
"withdraw_completed_subtitle": "{{amount}} USDC 已转移到您的钱包",
+ "withdraw_any_token_completed_subtitle": "{{amount}} {{token}} 已转入您的钱包",
"error_title": "出错了......",
"error_description": "提款处理失败",
"try_again": "请重试"
@@ -2292,7 +2325,15 @@
"points_tooltip_content_2": "请知悉,这一数值为估算值,将在交易完成后最终确定。积分最多可能需要 1 小时才能确认并存入您的奖励余额。",
"points_error": "当前无法加载积分",
"points_error_content": "您仍会获得这笔交易的所有积分。积分到账后我们将通知您。您也可以约一小时后在奖励选项卡中查看。",
- "slippage": "滑点"
+ "slippage": "滑点",
+ "price_details": "价格详情",
+ "prediction_order": "预测订单",
+ "prediction_order_description": "约 {{count}} 份合约,每份价格为 {{price}}。最终金额可能因订单簿可用性而有所不同(滑点最高 {{slippage}}%)。",
+ "metamask_fee_description": "处理此项预测的服务费",
+ "exchange_fee": "交易所费用",
+ "exchange_fee_description": "支付给交易所或市场的费用",
+ "total_incl_fees": "包含费用",
+ "close": "关闭"
},
"error": {
"title": "无法连接预测市场",
@@ -2399,7 +2440,7 @@
"add_tokens": "添加资产",
"are_you_sure_exit": "Are you sure you want to exit?",
"import_token": "Would you like to import this token?",
- "import_tokens": "Would you like to import these tokens?",
+ "import_tokens": "您想要导入这些代币吗?",
"tokens_detected_in_account": "在此账户中找到{{tokenCount}}个新的{{tokensLabel}}",
"token_toast": {
"tokens_imported_title": "已导入的代币",
@@ -2664,7 +2705,7 @@
"decimals_cant_be_empty": "代币小数位数不能为空。",
"decimals_is_required": "Decimal is required. Find it on:",
"no_tokens_found": "我们无法找到具有该名称的任何代币。",
- "tokens_empty_description": "Search for any token and import it",
+ "tokens_empty_description": "搜索任意代币并导入",
"select_token": "选择代币",
"address_must_be_smart_contract": "检测到个人地址。输入代币合约地址。",
"billion_abbreviation": "十亿",
@@ -2995,9 +3036,9 @@
"add_rpc_url": "添加 RPC(远程过程调用)URL",
"add_block_explorer_url": "添加区块浏览器 URL",
"networks_desc": "添加并编辑自定义 RPC 网络",
- "networks_enabled": "Enabled Networks",
- "networks_test_networks": "Test Networks",
- "networks_additional": "Additional Networks",
+ "networks_enabled": "启用的网络",
+ "networks_test_networks": "测试网络",
+ "networks_additional": "其他网络",
"networks_search_placeholder": "搜索网络",
"networks_no_results": "未找到网络",
"network_name_label": "网络名称",
@@ -3017,15 +3058,15 @@
"network_rpc_networks": "RPC 网络",
"network_add_network": "添加网络",
"add_chain_title": "添加网络",
- "add_chain_search_placeholder": "Search by name, chain ID, or currency",
- "add_chain_loading": "Loading networks…",
- "add_chain_error": "Failed to load networks. Please try again.",
+ "add_chain_search_placeholder": "按名称、链 ID或货币搜索",
+ "add_chain_loading": "加载网络中……",
+ "add_chain_error": "加载网络失败。请重试。",
"add_chain_retry": "重试",
- "add_chain_added": "Added",
+ "add_chain_added": "已添加",
"add_chain_or": "或",
"add_chain_custom_link": "添加自定义网络",
"network_add_custom_network": "添加自定义网络",
- "network_add_test_network": "Add a test network",
+ "network_add_test_network": "添加测试网络",
"network_add": "添加",
"network_save": "保存",
"remove_network_title": "是否要删除此网络?",
@@ -3349,11 +3390,9 @@
],
"private_key_explanation": "将它保存在安全、秘密的地方。",
"private_key_warning": "这是当前选定账户的私钥:{{accountName}}。请勿泄露此密钥。任何得到您的私钥的人都可以完全控制您的账户,包括转走所有资金。",
- "seed_phrase_warning_explanation": [
- "确保没有人在看您的屏幕。",
- "MetaMask 支持部门绝对不会要求您提供这个。"
- ],
+ "seed_phrase_warning_explanation": "确保没人窥视您的屏幕。MetaMask 支持团队绝不会索要此类信息。",
"private_key_warning_explanation": "请勿泄露此密钥。任何得到您的私钥的人都可以完全控制您的账户,包括转账所有资金。",
+ "reveal_srp_description": "您的私钥助记词可授予对您钱包的完全访问权限。切勿与任何人分享。",
"reveal_credential_modal": [
"您的 {{credentialName}} 提供 ",
"对您的账户和资金的完全访问权限。\n\n请勿与任何人分享。\n",
@@ -3385,7 +3424,8 @@
"srp_text": "私钥助记词,",
"private_key_text": "私钥",
"got_it": "知道了",
- "learn_more": "了解详情"
+ "learn_more": "了解详情",
+ "copied_to_clipboard": "已复制到剪贴板"
},
"screenshot_deterrent": {
"title": "安全警告",
@@ -3661,6 +3701,10 @@
"speedup_tx_title": "尝试加速?",
"speedup_tx_message": "提交此尝试不能保证将会加速您的初始交易。如果加速尝试成功,将向您收取以上交易费。",
"nevermind": "没关系",
+ "cancel_speedup_speedup_title": "Speed up Transaction",
+ "cancel_speedup_cancel_title": "Cancel Transaction",
+ "cancel_speedup_speedup_message": "This network fee will replace the original.",
+ "cancel_speedup_cancel_message": "This transaction will be canceled and this network fee will replace the original.",
"edit_network_fee": "编辑网络费",
"edit_priority": "编辑优先级",
"gas_cancel_fee": "燃料取消费",
@@ -3762,7 +3806,7 @@
"transaction_fee_explanation": "交易费用与此权限关联。",
"view_details": "查看详细信息",
"view_transaction_details": "查看交易详情",
- "view_data": "View data",
+ "view_data": "查看数据",
"transaction_details": "交易详情",
"site_url": "站点 URL",
"permission_request": "权限请求",
@@ -3843,7 +3887,7 @@
"right_button": "保护钱包"
},
"add_favorite": {
- "title": "Add favorite",
+ "title": "添加到收藏夹",
"title_label": "名称",
"url_label": "URL",
"add_button": "添加",
@@ -4197,8 +4241,8 @@
"biometrics": {
"enable_touchid": "使用 Touch ID 登录?",
"enable_faceid": "使用 Face ID 登录?",
- "enable_fingerprint": "Unlock with fingerprint?",
- "enable_biometrics": "Unlock with biometrics?",
+ "enable_fingerprint": "使用指纹解锁?",
+ "enable_biometrics": "使用生物识别解锁?",
"enable_device_passcode_ios": "使用设备密码登录?",
"enable_device_passcode_android": "使用设备 PIN 登录?"
},
@@ -4724,7 +4768,7 @@
"public_address": "公钥",
"public_address_qr_code": "公钥",
"coming_soon": "即将推出...",
- "request_payment": "Request payment",
+ "request_payment": "请求付款",
"copy": "复制",
"scan_address": "扫描地址以接收付款",
"copy_address": "复制地址"
@@ -4738,8 +4782,8 @@
"switch_network": "请切换为 mainnet 或 sepolia",
"card_title": "始终显示 MetaMask 卡按钮",
"card_desc": "MetaMask 卡仅对特定国家/地区的居民开放.",
- "daimo_demo_title": "Use DaimoPay demo environment",
- "daimo_demo_desc": "Toggle between DaimoPay demo and production environments for card payments."
+ "daimo_demo_title": "使用 DaimoPay 演示环境",
+ "daimo_demo_desc": "在 DaimoPay 演示环境和生产环境之间切换,以进行用卡支付。"
},
"walletconnect_sessions": {
"no_active_sessions": "您没有活动会话",
@@ -4752,10 +4796,10 @@
"close_current_session": "在开始新会话之前,请先关闭当前会话。"
},
"paymentRequest": {
- "title": "Payment request",
- "title_complete": "Payment complete",
+ "title": "付款请求",
+ "title_complete": "付款完成",
"confirm": "支付",
- "cancel": "Decline",
+ "cancel": "拒绝",
"is_requesting_you_to_pay": "请求您付款",
"total": "总额:"
},
@@ -4790,7 +4834,13 @@
"no_payment_methods_available": "无可用支付方式。",
"error_fetching_quotes": "出错了。请重试。",
"no_quotes_available": "没有可用的提供商。",
+ "quote_unavailable": "报价不可用。",
"providers": "提供商",
+ "quotes_displayed_for": "为 {{paymentMethodName}} 显示的报价。",
+ "other_options": "Other options",
+ "previously_used": "先前使用的",
+ "best_rate": "最优价格",
+ "most_reliable": "最可靠",
"continue": "继续",
"powered_by_provider": "由 {{provider}} 提供技术支持",
"purchased_currency": "已购买 {{currency}}",
@@ -4890,14 +4940,19 @@
"logged_out_error": "退出登录时出错"
},
"token_unavailable_modal": {
- "title": "Not available",
- "description": "{{token}} is not available with {{provider}} in your region.",
- "change_token": "Change token",
- "change_provider": "Change provider"
+ "title": "不可用",
+ "description": "{{token}} 无法通过 {{provider}} 在您所在的地区使用。",
+ "change_token": "更换代币",
+ "change_provider": "更换提供商"
},
"provider_picker_modal": {
- "title": "Choose a provider"
- }
+ "title": "选择提供商"
+ },
+ "contact_provider_support": "Contact {{provider}} support",
+ "got_it": "知道了",
+ "encountered_error": "We've encountered an error",
+ "no_quotes_error": "We encountered a problem fetching quotes from {{provider}}. Try a different amount or changing provider.",
+ "change_provider_button": "更换提供商"
},
"fiat_on_ramp_aggregator": {
"buy": "买入",
@@ -5126,6 +5181,39 @@
},
"deposit_order_title": "{{currency}} 存款"
},
+ "ramps_order_details": {
+ "title": "订单详情",
+ "status": "状态",
+ "processing": "处理中",
+ "complete": "完成",
+ "failed": "失败",
+ "cancelled": "已取消",
+ "view_on_provider": "在 {{provider}} 上查看",
+ "order_id": "订单 ID",
+ "date_and_time": "日期和时间",
+ "fees": "费用",
+ "total": "总额",
+ "card_processing_info": "卡支付通常需要几分钟时间",
+ "processing_info_modal_description": "用卡购买通常需要几分钟时间。如有疑问,您可以联系支持团队。",
+ "go_to_provider_support": "前往 {{provider}} 支持页面",
+ "error_title": "出错了......",
+ "error_message": "无法加载订单详情。请重试。",
+ "try_again": "请重试",
+ "close": "关闭"
+ },
+ "ramps_v2": {
+ "notifications": {
+ "purchase_pending_title": "正在处理您的 {{cryptocurrency}} 购买",
+ "purchase_pending_description": "整个过程通常只有几分钟时间...",
+ "purchase_completed_title": "您已成功购买 {{amount}} {{cryptocurrency}}!",
+ "purchase_completed_description": "您的 {{cryptocurrency}} 现在可用",
+ "purchase_failed_title": "购买 {{cryptocurrency}} 失败",
+ "purchase_failed_description": "请稍后重试",
+ "purchase_cancelled_title": "您的购买已取消",
+ "purchase_cancelled_description": "您对 {{cryptocurrency}} 的购买已被取消",
+ "track": "追踪"
+ }
+ },
"swaps": {
"title": "交换",
"onboarding": {
@@ -5642,12 +5730,15 @@
"new_wallet_needed_description_part_three": "为了避免再次发生这种情况,请确保将 MetaMask 应用程序和操作系统始终更新至最新版本。"
},
"srp_security_quiz": {
+ "question_step": "第 {{step}} 个问题,共 {{total}} 个",
"title": "安全问答",
"introduction": "要查看私钥助记词,您需要答对两个问题",
"get_started": "开始",
"learn_more": "了解详情",
"try_again": "请重试",
"continue": "继续",
+ "correct": "正确!",
+ "incorrect": "错误!",
"of": "/",
"question_one": {
"question": "如果您丢失了私钥助记词,MetaMask......",
@@ -5823,11 +5914,11 @@
"error_description": "{{snap}}安装失败。"
},
"earn": {
- "claimable_bonus_tooltip": "可从您的钱包中每日领取的年度奖励。",
+ "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.",
"earn_a_percentage_bonus": "获得 {{percentage}}% 的奖励",
"claimable_bonus": "可领取奖励",
- "claim_bonus": "Claim bonus",
- "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.",
+ "claim_bonus": "领取奖励",
+ "claim_bonus_subtitle": "奖励将在 {{networkName}} 上发放。",
"empty_state_cta": {
"heading": "借出 {{tokenSymbol}} 并赚取",
"body": "通过 {{protocol}} 借出您的 {{tokenSymbol}},",
@@ -5838,7 +5929,7 @@
"service_interruption_banner": {
"maintenance_message": "我们正在进行系统维护。即将恢复服务!"
},
- "supply": "Supply",
+ "supply": "供应",
"deposit": "保证金",
"approve": "批准",
"approval": "批准",
@@ -5887,7 +5978,7 @@
"network": "网络",
"health_factor": "健康系数",
"liquidation_risk": "清算风险",
- "insufficient_pool_liquidity": "Insufficient pool liquidity",
+ "insufficient_pool_liquidity": "资金池流动性不足",
"available_to_withdraw": "可提取",
"unknown": "未知",
"how_it_works": "如何运行",
@@ -5906,7 +5997,7 @@
"lending": "头寸历史",
"staking": "奖励发放记录"
},
- "allowance_reset": "Allowance reset",
+ "allowance_reset": "授权额度重设",
"tron": {
"fee": "费用"
},
@@ -5914,32 +6005,60 @@
"ok": "OK",
"continue": "继续",
"convert_and_get_percentage_bonus": "转换并获得 {{percentage}}%",
+ "your_musd": "您的 mUSD",
+ "balance_breakdown_title": "您在各网络的 mUSD 余额",
+ "balance_amount": "{{amount}} mUSD",
+ "balance_amount_with_symbol": "{{amount}} {{symbol}}",
+ "balance_fiat_unavailable": "—",
+ "convert_to_musd": "兑换为 mUSD",
"get_a_percentage_musd_bonus": "获取 {{percentage}}% mUSD 奖励",
"convert": "兑换",
+ "fetching_quote": "正在获取报价...",
+ "you_convert": "您兑换",
+ "network_fee": "网络费",
+ "earning": "收益",
+ "quick_convert_description": "选择代币以兑换您的全部余额,或点击编辑图标输入自定义金额。",
+ "no_tokens_to_convert": "您没有任何可以兑换为 mUSD 的代币。",
"toasts": {
"converting": "正在将 {{token}} 兑换为 mUSD",
"eta": "大约 {{time}}",
- "delivered": "您的 mUSD 已到账!",
+ "delivered": "mUSD conversion successful",
+ "delivered_description": "Bonus will be claimable within a day.",
"failed": "mUSD 兑换失败"
},
"education": {
"heading": "获取 {{percentage}}% 稳定币收益",
- "description": "将您的稳定币兑换为 mUSD(MetaMask 发行的美元抵押稳定币),即可获得高达 {{percentage}}% 的奖励。",
+ "description": "Convert your stablecoins to mUSD and earn up to a {{percentage}}% annualized bonus that you can claim daily.",
"terms_apply": "具体条款适用。",
"primary_button": "开始",
"secondary_button": "暂时不"
},
"buy_musd": "购买 mUSD",
"get_musd": "获取 mUSD",
- "bonus_title": "获得稳定币的 {{percentage}}% 奖励",
- "bonus_description": "将您的稳定币兑换为 mUSD,即可获得高达 {{percentage}}% 的奖励。",
- "powered_by_relay": "由 Relay 提供技术支持"
+ "bonus_title": "Get {{percentage}}% on your stablecoins",
+ "bonus_description": "Convert your stablecoins to mUSD and get a {{percentage}}% annualized bonus.",
+ "powered_by_relay": "由 Relay 提供技术支持",
+ "max": "最大值",
+ "quick_convert_button": "兑换",
+ "learn_more": "了解详情",
+ "tooltip_title": "通过 mUSD 赚取收益",
+ "tooltip_content": "将您的 USDC、USDT 或 DAI 兑换为 mUSD——MetaMask 推出的美元稳定币。您持有的每一美元,即可赚取 {{apy}} 收益。",
+ "quick_convert": {
+ "title": "Convert and get {{percentage}}%",
+ "subtitle": "Convert your stablecoins to mUSD and receive up to a {{percentage}}% annualized bonus that you can claim daily.",
+ "inline_failed_message": "兑换失败。请重试。",
+ "confirmation": {
+ "title": "兑换全部"
+ }
+ },
+ "percentage_bonus": "{{percentage}}% bonus",
+ "rate": "费率"
},
"bonus_claim": {
"toasts": {
"claiming": "您的 mUSD 奖励正在处理中",
"delivered": "您的 mUSD 奖励已到账!",
- "failed": "Bonus claim failed"
+ "failed": "领取奖励失败"
}
},
"rewards": {
@@ -5950,7 +6069,8 @@
"tooltip_opted_in_footer": "积分将自动添加至您的账户。",
"tooltip_not_opted_in_footer": "参与奖励计划即可获得积分。",
"tooltip_close": "关闭"
- }
+ },
+ "your_stablecoins": "您的稳定币"
},
"stake": {
"stake": "Stake",
@@ -6222,7 +6342,8 @@
"transaction_fees": "交易费用",
"metamask_fee": "MetaMask 费用",
"network_fee": "网络费",
- "bridge_fee": "桥接服务商费用"
+ "bridge_fee": "桥接服务商费用",
+ "provider_fee": "服务商费用"
},
"title": {
"signature": "签名请求",
@@ -6260,10 +6381,10 @@
"transaction_fee": "我们将在预测所使用的 Polygon 网络上将您的代币兑换为 USDC.e。兑换提供商可能收取费用,但 MetaMask 不收取费用。"
},
"predict_withdraw": {
- "transaction_fee": "MetaMask 将为您兑换为您想要的代币。兑换至 MUSD 时,MetaMask 不收取任何费用。"
+ "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to mUSD."
},
"musd_conversion": {
- "transaction_fee": "mUSD 兑换费用包含网络成本,且可能包含服务商费用。"
+ "transaction_fee": "mUSD conversion fees include network costs and may include provider fees. No MetaMask fee applies when you convert to mUSD."
},
"title": {
"transaction_fee": "费用"
@@ -6451,6 +6572,11 @@
"price_impact_info_title": "价格影响",
"price_impact_info_description": "价格影响反映您的兑换订单对资产市场价格的影响程度。它取决于交易规模及资金池的可用流动性状况。MetaMask 不参与亦不控制价格影响。",
"price_impact_info_gasless_description": "价格影响反映您的兑换订单对资产市场价格的影响程度。如果您没有足够的资金支付燃料费,系统将自动分配部分源代币用于支付费用,这会扩大价格影响。MetaMask 不参与亦不控制价格影响。",
+ "price_impact_warning_description": "This trade has an estimated {{priceImpact}} price impact, which reflects how much your trade changes the market price. The quote already reflects this.",
+ "price_impact_high": "High price impact",
+ "price_impact_execution_description": "You'll lose approximately {{priceImpact}} of your token's value on this swap. Try lowering the amount or choosing a more liquid route.",
+ "proceed": "Proceed",
+ "cancel": "取消",
"slippage_info_title": "滑点",
"slippage_info_description": "在交易取消前,您愿意接受的价格波动百分比。",
"blockaid_error_title": "此交易将被撤销",
@@ -6470,13 +6596,14 @@
},
"submit": "提交",
"default_slippage_description": "如果价格变化超过滑移百分比,您的交易将无法成功。",
- "cancel": "取消",
"confirm": "确认",
"exceeding_upper_slippage_warning": "高滑移,这可能导致不利兑换",
"exceeding_lower_slippage_warning": "低滑移,这可能导致不利兑换",
"exceeding_lower_slippage_error": "输入一个大于 {{value}}% 的值",
"exceeding_upper_slippage_error": "您输入的数值不能大于 {{value}}%",
- "custom": "自定义"
+ "custom": "自定义",
+ "invalid_recipient_address": "地址无效",
+ "got_it": "Got it"
},
"quote_expired_modal": {
"title": "有新的报价",
@@ -6773,12 +6900,16 @@
"title": "输入密码",
"description": "输入您的钱包密码以查看卡详情。",
"description_unfreeze": "请输入您的钱包密码以恢复卡消费。",
+ "description_view_pin": "Enter your wallet password to view your card PIN.",
"placeholder": "密码",
"confirm": "确认",
"cancel": "取消",
"error_empty": "请输入您的密码",
"error_incorrect": "密码错误。请重试。"
},
+ "view_pin_bottomsheet": {
+ "title": "Your Card PIN"
+ },
"choose_your_card": {
"title": "选择您的卡",
"upgrade_title": "升级至金属卡",
@@ -7094,6 +7225,9 @@
"view_card_details": "查看卡详情",
"hide_card_details": "隐藏卡详情",
"view_card_details_description": "卡号、有效期和 CVV 码",
+ "view_pin": "View PIN",
+ "view_pin_description": "View your card PIN securely",
+ "view_pin_error": "Failed to load PIN. Please try again.",
"manage_spending_limit": "管理限额",
"manage_spending_limit_description_restricted": "限额消费已开启",
"manage_spending_limit_description_full": "全部权限已开启",
@@ -7104,6 +7238,9 @@
"card_tos_title": "条款和条件",
"order_metal_card": "金属卡",
"order_metal_card_description": "立即订购您的实体金属卡",
+ "cashback": "返现",
+ "cashback_description": "所有消费享 1% 返现",
+ "cashback_description_metal": "Earn 3% back on all spending",
"freeze_card": "冻结卡",
"unfreeze_card": "解冻卡",
"freeze_card_description": "暂停卡所有消费",
@@ -7141,6 +7278,19 @@
"retry": "请重试",
"on_linea": "在 Linea 上"
},
+ "cashback_screen": {
+ "title": "返现",
+ "available_cashback": "可用返现",
+ "network_fee": "网络费",
+ "expected_to_receive": "预计收到",
+ "withdraw": "提取",
+ "withdraw_unavailable": "提现不可用",
+ "withdrawal_initiated": "已发起提现",
+ "withdrawal_success": "提现成功完成",
+ "withdrawal_failed": "提现失败。请重试。",
+ "no_cashback": "无可用返现",
+ "loading_error": "加载返现失败。请重试。"
+ },
"change_asset": {
"title": "更改代币和网络",
"full_spending_access": "全额消费权限",
@@ -7243,8 +7393,8 @@
},
"pay_with_modal": {
"title": "选择付款方式",
- "title_receive": "Select receive token",
- "no_gas": "No native balance for gas"
+ "title_receive": "选择收款代币",
+ "no_gas": "无支付燃料所需的原生代币余额"
},
"connection_removed_modal": {
"title": "连接已删除",
@@ -7315,7 +7465,10 @@
"service_not_available": "服务暂不可用。请稍后再试。",
"invalid_referral_code": "推荐码无效。请检查后重试。",
"already_referred": "您已被其他用户推荐过。",
- "cannot_use_own_referral_code": "您无法使用您本人的推荐码。"
+ "cannot_use_own_referral_code": "您无法使用您本人的推荐码。",
+ "invalid_bonus_code": "奖励代码无效",
+ "already_redeemed": "您已兑换过此奖励代码",
+ "reached_maximum": "此奖励代码已达到最大使用次数"
},
"claim_reward_error": {
"title": "领取奖励失败"
@@ -7372,8 +7525,10 @@
"predict": "预测",
"musd_deposit": "mUSD 保证金",
"apply_referral_bonus": "推荐码奖励",
+ "bonus_code": "奖励代码",
"uncategorized_event": "未归类事件"
},
+ "code": "代码",
"date": "日期",
"account": "账户",
"bonus": "奖励",
@@ -7473,7 +7628,16 @@
"show_less": "收起",
"linking_progress": "正在添加账户……({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}{{total}} 已加入",
- "add_all_accounts": "添加全部账户"
+ "add_all_accounts": "添加全部账户",
+ "environment_selector": "环境",
+ "environment_cancel": "取消",
+ "environment_default": "默认",
+ "off_device_accounts_banner_title": "Missing Enrolled Accounts",
+ "off_device_accounts_banner_description": "There are accounts that were enrolled into the rewards program but are not found on this device.",
+ "off_device_accounts_banner_cta": "View",
+ "off_device_accounts_sheet_title": "Missing Enrolled Accounts",
+ "off_device_accounts_sheet_description": "These might belong to a wallet that has not yet been added after reinstalling MetaMask. Don't recognize any of these addresses?",
+ "off_device_accounts_sheet_let_us_know": "Let us know"
},
"referred_by_code": {
"title": "推荐码",
@@ -7483,6 +7647,10 @@
"invalid_code": "推荐码无效",
"apply_button": "应用推荐码"
},
+ "bonus_code": {
+ "input_placeholder": "输入奖励代码",
+ "apply_success": "奖励代码已应用!"
+ },
"optout": {
"title": "注销奖励计划账户",
"description": "此操作将从奖励计划中注销您的账户,并清空您的积分与进度记录。此操作无法撤销。",
@@ -7619,12 +7787,14 @@
},
"label": {
"bridge_fee": "跨链桥费用",
+ "provider_fee": "服务商费用",
"network_fee": "网络费",
"paid_with": "已使用以下支付方式付款:",
+ "receive_token": "收款代币",
"retry_button": "请重试",
"total": "总额",
"account": "账户",
- "received_total": "Received total"
+ "received_total": "收款总额"
},
"summary_title": {
"bridge_approval": "批准 {{approveSymbol}}",
@@ -7663,20 +7833,29 @@
"show_not_found": {
"title": "未找到连接",
"description": "请从应用中重新建立连接以继续。"
+ },
+ "show_internal_error": {
+ "title": "Something went wrong",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "show_method_error": {
+ "title": "Request failed",
+ "description": "The request could not be completed. Please try again."
}
},
"network_connection_banner": {
"still_connecting_network": "仍在连接到 {{networkName}}……",
"unable_to_connect_network": "无法连接到 {{networkName}}。",
"update_rpc": "更新 RPC(远程过程调用)",
- "switch_to_metamask_default_rpc": "Switch to MetaMask default RPC",
+ "switch_to_metamask_default_rpc": "切换到 MetaMask 默认 RPC",
"check_network_connectivity": "请检查您的网络连接。",
"check_network_connectivity_or": "请检查您的网络连接或",
- "updated_to_metamask_default": "Updated to MetaMask default"
+ "updated_to_metamask_default": "已更新至 MetaMask 默认设置"
},
"trending": {
"title": "探索",
"trending_tokens": "热门代币",
+ "stocks": "股票",
"price_change": "价格变化",
"all_networks": "所有网络",
"24h": "24 小时",
@@ -7720,6 +7899,95 @@
"primary_action_reload": "重新加载",
"primary_action_acknowledge": "知道了"
},
+ "hardware_wallet": {
+ "device_names": {
+ "ledger": "Ledger",
+ "qr": "二维码硬件钱包",
+ "hardware_wallet": "硬件钱包"
+ },
+ "common": {
+ "cancel": "取消",
+ "continue": "继续"
+ },
+ "connecting": {
+ "title": "连接您的 {{device}}",
+ "searching": "正在查找 {{device}}……",
+ "tips_header": "若要继续,请确保:",
+ "tip_unlock": "您的 {{device}} 已解锁",
+ "tip_open_app": "以太坊应用已打开",
+ "tip_enable_bluetooth": "蓝牙已开启",
+ "tip_dnd_off": "勿扰模式已关闭",
+ "tip_bluetooth_permission": "位置和蓝牙许可已授予",
+ "tip_bluetooth_permission_v12": "附近设备许可已授予",
+ "tip_stay_close": "您的设备需保持靠近手机"
+ },
+ "awaiting_app": {
+ "title": "打开 {{app}}",
+ "message": "请在您的 {{device}} 上打开 {{app}} 应用",
+ "current_app": "当前打开:{{app}}"
+ },
+ "awaiting_confirmation": {
+ "title_transaction": "在 {{device}} 上确认",
+ "title_message": "在 {{device}} 上签名",
+ "message": "在您的 {{device}} 上查看并确认"
+ },
+ "success": {
+ "title": "{{device}} 已连接"
+ },
+ "errors": {
+ "device_locked": "解锁并重试以继续",
+ "app_not_open": "请在您的设备上打开以太坊应用",
+ "device_disconnected": "您的 {{device}} 已断开连接。请重新连接并重试",
+ "device_not_found": "无法找到您的 {{device}}。请确保其已连接",
+ "device_not_ready": "您的设备尚未就绪。请检查并重试",
+ "blind_signing": "盲签功能已禁用。请在您的设备设置中启用",
+ "connection_closed": "连接已断开。请重试",
+ "connection_timeout": "连接超时。请重试",
+ "user_cancelled": "操作已在您的设备上取消",
+ "pending_confirmation": "您的设备上有一个待定操作。请完成或取消它",
+ "bluetooth_permission_denied": "需要蓝牙许可才能连接到您的设备",
+ "location_permission_denied": "需要位置许可才能扫描设备",
+ "nearby_permission_denied": "需要附近设备许可",
+ "bluetooth_off": "请开启蓝牙以连接到您的设备",
+ "bluetooth_scan_failed": "扫描设备失败。请重试",
+ "bluetooth_connection_failed": "在您的设备上启用蓝牙以继续",
+ "not_supported": "不支持此操作",
+ "unknown_error": "确保您的 {{device}} 已使用此账户的私钥助记词或密语进行设置"
+ },
+ "error": {
+ "title": "出错了......",
+ "default_title": "发生错误",
+ "continue": "继续",
+ "retry": "重试",
+ "view_settings": "查看设置",
+ "device_locked_title": "{{device}} 已锁定",
+ "device_disconnected_title": "{{device}} 已断开连接",
+ "device_not_found_title": "未找到 {{device}}",
+ "app_not_open": "以太坊应用未打开",
+ "blind_signing_disabled": "盲签功能已禁用",
+ "connection_timeout": "设备无响应",
+ "connection_closed": "连接已断开",
+ "user_cancelled": "操作已取消",
+ "pending_confirmation": "待定确认",
+ "bluetooth_required": "需要蓝牙",
+ "bluetooth_permission_denied": "需要蓝牙许可",
+ "location_permission_denied": "需要位置许可",
+ "nearby_devices_permission_denied": "需要附近设备许可",
+ "scan_failed": "扫描失败",
+ "something_went_wrong": "出错了......"
+ },
+ "device_selection": {
+ "title": "选择 {{device}}",
+ "scanning": "正在扫描设备……",
+ "no_devices_found": "未找到设备",
+ "no_devices_hint": "请确保您的 {{device}} 已解锁,并且蓝牙已启用",
+ "tips": "Unlock your {{device}}, enable Bluetooth, and ensure Do Not Disturb is turned off",
+ "connect": "连接",
+ "rescan": "重新扫描",
+ "unknown_device": "未知设备",
+ "signal_strength": "信号强度:{{rssi}} dBm"
+ }
+ },
"homepage": {
"sections": {
"tokens": "代币",
@@ -7728,12 +7996,15 @@
"defi": "DeFi",
"nfts": "NFT",
"import_nfts": "导入 NFT",
- "import_nfts_description": "Easily add your collectibles",
- "more_predictions": "More predictions"
+ "import_nfts_description": "轻松添加您的收藏品",
+ "view_more": "View more",
+ "positions": {
+ "no_tp_sl": "无止盈/止损"
+ }
},
"error": {
- "unable_to_load": "Unable to load {{section}}",
- "retry": "Retry"
+ "unable_to_load": "无法加载 {{section}}",
+ "retry": "重试"
}
}
-}
+}
\ No newline at end of file
From d7de0da2cfd0e0d90d951c5ad82e94b87a11980e Mon Sep 17 00:00:00 2001
From: Kirill Zyusko
Date: Fri, 6 Mar 2026 19:59:01 +0100
Subject: [PATCH 06/11] feat: skeleton migration (Card scope) (#26889)
## **Description**
Migrate `Skeleton` component (Card scope only) from internal component
to design system component.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/DSYS-274
## **Manual testing steps**
```gherkin
Feature: my feature name
Scenario: user [verb for user action]
Given [describe expected initial app state]
When user [verb for user action]
Then [describe expected outcome]
```
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/3b82cbe7-977a-4e0f-afe1-ad5c9dc95e1e
### **After**
https://github.com/user-attachments/assets/db1812f4-083d-45a9-996f-4a8cdbea5e20
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Medium Risk**
> Medium risk because it swaps the loading placeholder implementation in
Card UI flows; visual/animation behavior and prop compatibility may
differ, potentially impacting loading states and tests.
>
> **Overview**
> Introduces a new `components-temp/Skeleton` wrapper around the
design-system `Skeleton`, disabling autoplay in E2E/Jest via
`autoPlay={!isE2E && !process.env.JEST_WORKER_ID}`.
>
> Updates Card-scope screens (`CardHome` and `SpendingLimitProgressBar`)
to import this new Skeleton instead of the legacy internal component,
and adjusts the legacy `components/Skeleton` deprecation notice to point
to the temp wrapper.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
12decef2d7f5468428bca5d60090b25d928e26fc. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../components-temp/Skeleton/Skeleton.tsx | 12 ++++++++++++
.../components-temp/Skeleton/index.ts | 2 ++
.../components/Skeleton/Skeleton.tsx | 2 +-
app/components/UI/Card/Views/CardHome/CardHome.tsx | 2 +-
.../SpendingLimitProgressBar.tsx | 2 +-
5 files changed, 17 insertions(+), 3 deletions(-)
create mode 100644 app/component-library/components-temp/Skeleton/Skeleton.tsx
create mode 100644 app/component-library/components-temp/Skeleton/index.ts
diff --git a/app/component-library/components-temp/Skeleton/Skeleton.tsx b/app/component-library/components-temp/Skeleton/Skeleton.tsx
new file mode 100644
index 00000000000..1715acc1533
--- /dev/null
+++ b/app/component-library/components-temp/Skeleton/Skeleton.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import {
+ Skeleton as DSRNSkeleton,
+ SkeletonProps,
+} from '@metamask/design-system-react-native';
+import { isE2E } from '../../../util/test/utils';
+
+const Skeleton: React.FC = (props) => (
+
+);
+
+export default Skeleton;
diff --git a/app/component-library/components-temp/Skeleton/index.ts b/app/component-library/components-temp/Skeleton/index.ts
new file mode 100644
index 00000000000..dac673616cb
--- /dev/null
+++ b/app/component-library/components-temp/Skeleton/index.ts
@@ -0,0 +1,2 @@
+export { default as Skeleton } from './Skeleton';
+export type { SkeletonProps } from '@metamask/design-system-react-native';
diff --git a/app/component-library/components/Skeleton/Skeleton.tsx b/app/component-library/components/Skeleton/Skeleton.tsx
index 32ca02d1738..90aa05d11f9 100644
--- a/app/component-library/components/Skeleton/Skeleton.tsx
+++ b/app/component-library/components/Skeleton/Skeleton.tsx
@@ -13,7 +13,7 @@ import { SkeletonProps } from './Skeleton.types';
import { isE2E } from '../../../util/test/utils';
/**
- * @deprecated Please update your code to use `Skeleton` from `@metamask/design-system-react-native`.
+ * @deprecated Please update your code to use `Skeleton` from `app/component-library/components-temp/Skeleton`.
* The API may have changed — compare props before migrating.
* @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/Skeleton/README.md}
* @since @metamask/design-system-react-native@0.7.0
diff --git a/app/components/UI/Card/Views/CardHome/CardHome.tsx b/app/components/UI/Card/Views/CardHome/CardHome.tsx
index c0d33dcd3cc..2c9b0efc8e8 100644
--- a/app/components/UI/Card/Views/CardHome/CardHome.tsx
+++ b/app/components/UI/Card/Views/CardHome/CardHome.tsx
@@ -65,7 +65,7 @@ import {
import { useOpenSwaps } from '../../hooks/useOpenSwaps';
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
-import { Skeleton } from '../../../../../component-library/components/Skeleton';
+import { Skeleton } from '../../../../../component-library/components-temp/Skeleton';
import {
DEPOSIT_SUPPORTED_TOKENS,
SPENDING_LIMIT_UNSUPPORTED_TOKENS,
diff --git a/app/components/UI/Card/components/SpendingLimitProgressBar/SpendingLimitProgressBar.tsx b/app/components/UI/Card/components/SpendingLimitProgressBar/SpendingLimitProgressBar.tsx
index 944ca769855..9cb24135458 100644
--- a/app/components/UI/Card/components/SpendingLimitProgressBar/SpendingLimitProgressBar.tsx
+++ b/app/components/UI/Card/components/SpendingLimitProgressBar/SpendingLimitProgressBar.tsx
@@ -7,7 +7,7 @@ import Text, {
import createStyles from './SpendingLimitProgressBar.styles';
import { useTheme } from '../../../../../util/theme';
import ProgressBar from 'react-native-progress/Bar';
-import { Skeleton } from '../../../../../component-library/components/Skeleton';
+import { Skeleton } from '../../../../../component-library/components-temp/Skeleton';
import { CardHomeSelectors } from '../../Views/CardHome/CardHome.testIds';
interface SpendingLimitProgressBarProps {
From 010ee1b635bfa01fbd50b00040cb3d5ddcf08d7c Mon Sep 17 00:00:00 2001
From: George Gkasdrogkas
Date: Fri, 6 Mar 2026 21:04:13 +0200
Subject: [PATCH 07/11] feat: Swaps quotes selector (#26640)
## **Description**
Add select quotes functionality in swaps.
## **Changelog**
CHANGELOG entry: add select quotes functionality
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/SWAPS-3642,
https://consensyssoftware.atlassian.net/browse/SWAPS-4149,
https://consensyssoftware.atlassian.net/browse/SWAPS-4203
## **Manual testing steps**
```gherkin
Ensure AC pass
```
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Medium Risk**
> Touches quote-selection and fee-formatting logic and introduces a new
navigation flow, which could affect which quote is executed and what
costs are displayed if edge cases (expired/refreshing quotes, missing
requestId) are mishandled.
>
> **Overview**
> Adds a new **Quote Selector** screen that lists sorted swap/bridge
quotes by estimated *total cost* and lets users pick a specific quote;
the `rate` row in `QuoteDetailsCard` now navigates to this selector via
new `rate-info-button`/`rate-arrow-button` actions.
>
> Introduces `selectedQuoteRequestId` to the bridge Redux slice and
threads it through `selectBridgeQuotes` and `useBridgeQuoteData` so a
manually chosen quote becomes the `activeQuote` (with auto-reset when
the selection is no longer valid/available).
>
> Refactors fiat/amount display by adding `useDisplayCurrencyValue` and
switching `TokenInputArea` to use it (and the renamed
`useFormattedBalanceWithThreshold`), and updates `formatNetworkFee` to
correctly handle gasless quotes via `includedTxFees` with a
`gasFee.effective` fallback. Includes new route wiring
(`Routes.BRIDGE.QUOTE_SELECTOR_VIEW`), i18n strings, and extensive
new/updated unit tests covering the selector UI, analytics
(`useTrackAllQuotesSortedEvent`), and various BridgeView behaviors
(Blockaid banner, approval disclaimer, input constraints,
sponsored-network handling).
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
701dd2443e6be0a63b44d76559efbecaa724498e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../Views/BridgeView/BridgeView.test.tsx | 485 ++++++++++++-
.../UI/Bridge/_mocks_/bridgeReducerState.ts | 1 +
.../QuoteDetailsCard.test.tsx | 101 ++-
.../QuoteDetailsCard/QuoteDetailsCard.tsx | 86 +--
.../QuoteDetailsCard.test.tsx.snap | 291 ++++----
.../QuoteSelectorView/QuoteList.test.tsx | 309 +++++++++
.../QuoteSelectorView/QuoteList.tsx | 9 +
.../QuoteSelectorView/QuoteRow.test.tsx | 349 ++++++++++
.../components/QuoteSelectorView/QuoteRow.tsx | 145 ++++
.../components/QuoteSelectorView/constants.ts | 26 +
.../QuoteSelectorView/index.test.tsx | 655 ++++++++++++++++++
.../components/QuoteSelectorView/index.tsx | 149 ++++
.../TokenInputArea/TokenInputArea.test.tsx | 382 +++++++++-
.../components/TokenInputArea/index.tsx | 38 +-
.../Bridge/hooks/useBridgeQuoteData/index.ts | 26 +-
.../useBridgeQuoteData.test.ts | 183 +++++
.../useDisplayCurrencyValue/index.test.ts | 322 +++++++++
.../hooks/useDisplayCurrencyValue/index.ts | 41 ++
.../index.test.ts | 84 +--
.../index.ts | 2 +-
.../index.test.ts | 517 ++++++++++++++
.../useTrackAllQuotesSortedEvent/index.ts | 58 ++
app/components/UI/Bridge/routes.tsx | 6 +
.../UI/Bridge/utils/formatNetworkFee.test.ts | 279 +++++---
.../UI/Bridge/utils/formatNetworkFee.ts | 53 +-
app/constants/navigation/Routes.ts | 1 +
app/core/redux/slices/bridge/index.test.ts | 75 ++
app/core/redux/slices/bridge/index.ts | 60 +-
locales/languages/en.json | 4 +
29 files changed, 4342 insertions(+), 395 deletions(-)
create mode 100644 app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
create mode 100644 app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
create mode 100644 app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.test.tsx
create mode 100644 app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
create mode 100644 app/components/UI/Bridge/components/QuoteSelectorView/constants.ts
create mode 100644 app/components/UI/Bridge/components/QuoteSelectorView/index.test.tsx
create mode 100644 app/components/UI/Bridge/components/QuoteSelectorView/index.tsx
create mode 100644 app/components/UI/Bridge/hooks/useDisplayCurrencyValue/index.test.ts
create mode 100644 app/components/UI/Bridge/hooks/useDisplayCurrencyValue/index.ts
rename app/components/UI/Bridge/hooks/{useTokenInputAreaFormattedBalance => useFormattedBalanceWithThreshold}/index.test.ts (78%)
rename app/components/UI/Bridge/hooks/{useTokenInputAreaFormattedBalance => useFormattedBalanceWithThreshold}/index.ts (95%)
create mode 100644 app/components/UI/Bridge/hooks/useTrackAllQuotesSortedEvent/index.test.ts
create mode 100644 app/components/UI/Bridge/hooks/useTrackAllQuotesSortedEvent/index.ts
diff --git a/app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx b/app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx
index 370fae1bf07..502b9bb944e 100644
--- a/app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx
+++ b/app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx
@@ -14,10 +14,11 @@ import { Hex } from '@metamask/utils';
import BridgeView from '.';
import type { BridgeRouteParams } from '../../hooks/useSwapBridgeNavigation';
import { createBridgeTestState } from '../../testUtils';
+import { BridgeViewMode } from '../../types';
import {
- MetaMetricsSwapsEventSource,
RequestStatus,
type QuoteResponse,
+ MetaMetricsSwapsEventSource,
} from '@metamask/bridge-controller';
import { SolScope } from '@metamask/keyring-api';
import { mockUseBridgeQuoteData } from '../../_mocks_/useBridgeQuoteData.mock';
@@ -216,6 +217,17 @@ jest.mock('../../../../../selectors/bridge', () => ({
selectSourceWalletAddress: jest.fn(),
}));
+jest.mock(
+ '../../../../../selectors/featureFlagController/gasFeesSponsored',
+ () => ({
+ getGasFeesSponsoredNetworkEnabled: jest.fn(
+ () => (chainId: string) =>
+ // Return true for Polygon (0x89) to test sponsored quotes
+ chainId === '0x89',
+ ),
+ }),
+);
+
const mockNavigate = jest.fn();
const mockRoute = {
params: {
@@ -1676,4 +1688,475 @@ describe('BridgeView', () => {
expect(mockUseIsGasIncluded7702Supported).toHaveBeenCalledWith('0x1');
});
});
+
+ describe('Blockaid Security Alert', () => {
+ it('displays blockaid error banner when blockaid error exists', async () => {
+ const mockQuote = mockQuoteWithMetadata;
+ const testState = createBridgeTestState({
+ bridgeControllerOverrides: {
+ quotesLoadingStatus: RequestStatus.FETCHED,
+ quotes: [mockQuote as unknown as QuoteResponse],
+ quotesLastFetched: Date.now(),
+ },
+ bridgeReducerOverrides: {
+ sourceAmount: '1.0',
+ },
+ });
+
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ blockaidError: 'This transaction may be a security risk',
+ activeQuote: mockQuote,
+ }));
+
+ const { getByText } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ await waitFor(() => {
+ expect(getByText(strings('bridge.blockaid_error_title'))).toBeTruthy();
+ expect(
+ getByText('This transaction may be a security risk'),
+ ).toBeTruthy();
+ });
+ });
+ });
+
+ describe('Approval Disclaimer', () => {
+ it('displays approval needed text when quote requires approval', async () => {
+ const mockQuote = {
+ ...mockQuoteWithMetadata,
+ approval: {
+ chainId: '0x1',
+ to: '0xToken',
+ data: '0xApprovalData',
+ },
+ };
+
+ const testState = createBridgeTestState({
+ bridgeControllerOverrides: {
+ quotesLoadingStatus: RequestStatus.FETCHED,
+ quotes: [mockQuote as unknown as QuoteResponse],
+ quotesLastFetched: Date.now(),
+ },
+ bridgeReducerOverrides: {
+ sourceAmount: '1.5',
+ sourceToken: {
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ chainId: '0x1' as Hex,
+ decimals: 6,
+ image: '',
+ name: 'USD Coin',
+ symbol: 'USDC',
+ },
+ },
+ });
+
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: mockQuote,
+ }));
+
+ const { getByText } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ await waitFor(() => {
+ const approvalText = strings('bridge.approval_needed', {
+ amount: '1.5',
+ symbol: 'USDC',
+ });
+ expect(getByText(approvalText, { exact: false })).toBeTruthy();
+ });
+ });
+
+ it('does not display approval text when quote does not require approval', async () => {
+ const mockQuote = {
+ ...mockQuoteWithMetadata,
+ approval: null,
+ };
+
+ const testState = createBridgeTestState({
+ bridgeControllerOverrides: {
+ quotesLoadingStatus: RequestStatus.FETCHED,
+ quotes: [mockQuote as unknown as QuoteResponse],
+ quotesLastFetched: Date.now(),
+ },
+ bridgeReducerOverrides: {
+ sourceAmount: '1.0',
+ },
+ });
+
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: mockQuote,
+ }));
+
+ const { queryByText } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ await waitFor(() => {
+ // Should not find approval text in the document
+ expect(queryByText(/approval needed/i, { exact: false })).toBeNull();
+ });
+ });
+ });
+
+ describe('Quote Details Card', () => {
+ it('displays quote details card when active quote exists', async () => {
+ const mockQuote = mockQuoteWithMetadata;
+ const testState = createBridgeTestState({
+ bridgeControllerOverrides: {
+ quotesLoadingStatus: RequestStatus.FETCHED,
+ quotes: [mockQuote as unknown as QuoteResponse],
+ quotesLastFetched: Date.now(),
+ },
+ bridgeReducerOverrides: {
+ sourceAmount: '1.0',
+ },
+ });
+
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: mockQuote,
+ }));
+
+ const { getByTestId } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ await waitFor(() => {
+ // QuoteDetailsCard should be rendered
+ expect(
+ getByTestId(BridgeViewSelectorsIDs.BRIDGE_VIEW_SCROLL),
+ ).toBeTruthy();
+ });
+ });
+
+ it('does not display quote details card when no active quote', () => {
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: null,
+ }));
+
+ const { getByTestId, queryByTestId } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: mockState },
+ );
+
+ // Verify ScrollView exists
+ expect(
+ getByTestId(BridgeViewSelectorsIDs.BRIDGE_VIEW_SCROLL),
+ ).toBeTruthy();
+
+ // QuoteDetailsCard should not be rendered when no quote
+ expect(queryByTestId('quote-details-card')).toBeNull();
+ });
+ });
+
+ describe('Tap Outside to Close Keypad', () => {
+ // TODO: Re-enable after fixing component tree navigation for onResponderRelease
+ it.skip('closes keypad when tapping outside input area', async () => {
+ const { getByTestId, queryByTestId } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: mockState },
+ );
+
+ // Verify keypad is open initially (opened by useBridgeViewOnFocus on mount)
+ await waitFor(() => {
+ expect(queryByTestId('keypad-delete-button')).toBeTruthy();
+ });
+
+ // Tap outside the input area - the onResponderRelease is on the main content Box
+ // which wraps the ScrollView, so we need to go up two parents
+ const scrollView = getByTestId(BridgeViewSelectorsIDs.BRIDGE_VIEW_SCROLL);
+ const container = scrollView.parent?.parent;
+ await act(async () => {
+ // Call the onResponderRelease handler from the parent Box
+ if (container?.props.onResponderRelease) {
+ container.props.onResponderRelease();
+ }
+ });
+
+ // Keypad should be closed
+ await waitFor(() => {
+ expect(queryByTestId('keypad-delete-button')).toBeNull();
+ });
+ });
+ });
+
+ describe('Sponsored Quote Badge', () => {
+ it('renders when quote is on a sponsored network', async () => {
+ const polygonChainId = '0x89' as Hex;
+
+ const testState = createBridgeTestState({
+ bridgeControllerOverrides: {
+ quotesLoadingStatus: RequestStatus.FETCHED,
+ quotes: [mockQuoteWithMetadata as unknown as QuoteResponse],
+ quotesLastFetched: Date.now(),
+ },
+ bridgeReducerOverrides: {
+ sourceAmount: '1.0',
+ sourceToken: {
+ address: '0x0000000000000000000000000000000000000000',
+ chainId: polygonChainId,
+ decimals: 18,
+ image: '',
+ name: 'Polygon',
+ symbol: 'POL',
+ },
+ destToken: {
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ chainId: polygonChainId,
+ decimals: 6,
+ image: '',
+ name: 'USD Coin',
+ symbol: 'USDC',
+ },
+ },
+ });
+
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: mockQuoteWithMetadata,
+ }));
+
+ const { getByTestId } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ // Verify the component renders with the sponsored network tokens
+ await waitFor(() => {
+ const sourceTokenArea = getByTestId(
+ BridgeViewSelectorsIDs.SOURCE_TOKEN_AREA,
+ );
+ expect(sourceTokenArea).toBeTruthy();
+ // Both tokens are on Polygon (0x89), which is mocked as a sponsored network
+ });
+ });
+
+ it('renders when tokens are on different chains', () => {
+ const testState = createBridgeTestState({
+ bridgeReducerOverrides: {
+ sourceToken: {
+ address: '0x0000000000000000000000000000000000000000',
+ chainId: '0x1' as Hex, // Ethereum
+ decimals: 18,
+ image: '',
+ name: 'Ether',
+ symbol: 'ETH',
+ },
+ destToken: {
+ address: '0x0000000000000000000000000000000000000000',
+ chainId: '0x89' as Hex, // Polygon
+ decimals: 18,
+ image: '',
+ name: 'Polygon',
+ symbol: 'POL',
+ },
+ },
+ });
+
+ const { getByTestId } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ // Verify the component renders with tokens on different chains
+ const sourceTokenArea = getByTestId(
+ BridgeViewSelectorsIDs.SOURCE_TOKEN_AREA,
+ );
+ expect(sourceTokenArea).toBeTruthy();
+ // Tokens are on different chains, so sponsored feature won't apply
+ });
+ });
+
+ describe('Bridge View Mode', () => {
+ it('initializes bridge view mode from route params', async () => {
+ mockRoute.params = {
+ bridgeViewMode: BridgeViewMode.Bridge,
+ sourcePage: 'test',
+ location: MetaMetricsSwapsEventSource.MainView,
+ };
+
+ const { store } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: mockState },
+ );
+
+ await waitFor(() => {
+ expect(store.getState().bridge.bridgeViewMode).toBe(
+ BridgeViewMode.Bridge,
+ );
+ });
+ });
+
+ it('does not override existing bridge view mode', async () => {
+ mockRoute.params = {
+ bridgeViewMode: BridgeViewMode.Bridge,
+ sourcePage: 'test',
+ location: MetaMetricsSwapsEventSource.MainView,
+ };
+
+ const testState = {
+ ...mockState,
+ bridge: {
+ ...mockState.bridge,
+ bridgeViewMode: BridgeViewMode.Swap,
+ },
+ };
+
+ const { store } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ await waitFor(() => {
+ // Should keep existing mode
+ expect(store.getState().bridge.bridgeViewMode).toBe(
+ BridgeViewMode.Swap,
+ );
+ });
+ });
+ });
+
+ describe('Keypad Input Constraints', () => {
+ it('prevents input when max length is reached', async () => {
+ // MAX_INPUT_LENGTH is 36 characters
+ const maxLengthAmount = '123456789012345678901234567890123456'; // 36 chars
+ const testState = {
+ ...mockState,
+ bridge: {
+ ...mockState.bridge,
+ sourceAmount: maxLengthAmount,
+ },
+ };
+
+ const { getByText, getByTestId, store } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ const initialAmount = store.getState().bridge.sourceAmount;
+ expect(initialAmount).toBe(maxLengthAmount);
+ // Open the keypad first by pressing on the source token input
+ const sourceInput = getByTestId('source-token-area-input');
+ await act(async () => {
+ sourceInput.props.onPressIn();
+ });
+
+ // Try to add another digit
+ fireEvent.press(getByText('9'));
+
+ await waitFor(() => {
+ const currentAmount = store.getState().bridge.sourceAmount;
+ // Amount should not change when max length is reached
+ expect(currentAmount).toBe(initialAmount);
+ });
+ });
+ });
+
+ describe('Missing Wallet Address', () => {
+ it('disables submit when wallet address is not available', async () => {
+ const mockQuote = mockQuoteWithMetadata;
+
+ // Mock selectSourceWalletAddress to return undefined
+ const { selectSourceWalletAddress } = jest.requireMock(
+ '../../../../../selectors/bridge',
+ );
+ selectSourceWalletAddress.mockReturnValueOnce(undefined);
+
+ const testState = createBridgeTestState({
+ bridgeControllerOverrides: {
+ quotesLoadingStatus: RequestStatus.FETCHED,
+ quotes: [mockQuote as unknown as QuoteResponse],
+ quotesLastFetched: Date.now(),
+ },
+ bridgeReducerOverrides: {
+ sourceAmount: '1.0',
+ },
+ });
+
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: mockQuote,
+ }));
+
+ const { queryByTestId } = renderScreen(
+ BridgeView,
+ {
+ name: Routes.BRIDGE.ROOT,
+ },
+ { state: testState },
+ );
+
+ // When wallet address is missing, the bottom content should not render the confirm button
+ await waitFor(() => {
+ // The confirm button should not be present or should be disabled
+ const confirmButton = queryByTestId('swaps-confirm-button');
+ if (confirmButton) {
+ expect(confirmButton.props.accessibilityState.disabled).toBe(true);
+ }
+ // It's acceptable if the button doesn't render at all when wallet address is missing
+ });
+
+ // Restore the mock for other tests
+ selectSourceWalletAddress.mockReturnValue(
+ '0x1234567890123456789012345678901234567890',
+ );
+ });
+ });
});
diff --git a/app/components/UI/Bridge/_mocks_/bridgeReducerState.ts b/app/components/UI/Bridge/_mocks_/bridgeReducerState.ts
index 7e7c1160d8e..0c557415e99 100644
--- a/app/components/UI/Bridge/_mocks_/bridgeReducerState.ts
+++ b/app/components/UI/Bridge/_mocks_/bridgeReducerState.ts
@@ -37,4 +37,5 @@ export const mockBridgeReducerState: BridgeState = {
isDestTokenManuallySet: false,
tokenSelectorNetworkFilter: undefined,
visiblePillChainIds: undefined,
+ selectedQuoteRequestId: undefined,
};
diff --git a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx
index d29acb19232..2eb80a1f1bb 100644
--- a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx
+++ b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx
@@ -596,27 +596,32 @@ describe('QuoteDetailsCard', () => {
});
});
- it('handles quote info navigation', () => {
- const { getByLabelText, getByText, getByTestId } = renderScreen(
+ it('navigates to quote selector when rate info button is pressed', () => {
+ const { getByTestId } = renderScreen(
QuoteDetailsCardTestScreen,
{ name: Routes.BRIDGE.ROOT },
{ state: testState },
);
- const quoteTooltip = getByLabelText('Rate tooltip');
- fireEvent.press(quoteTooltip);
+ fireEvent.press(getByTestId('rate-info-button'));
- expect(mockNavigate).toHaveBeenCalledWith('RootModalFlow', {
- params: {
- title: strings('bridge.quote_info_title'),
- tooltip: strings('bridge.quote_info_content'),
- footerText: undefined,
- buttonText: undefined,
- },
- screen: 'tooltipModal',
- });
- expect(getByText('Price impact')).toBeTruthy();
- expect(getByTestId('price-impact-info-button')).toBeTruthy();
+ expect(mockNavigate).toHaveBeenCalledWith(
+ Routes.BRIDGE.QUOTE_SELECTOR_VIEW,
+ );
+ });
+
+ it('navigates to quote selector when rate arrow button is pressed', () => {
+ const { getByTestId } = renderScreen(
+ QuoteDetailsCardTestScreen,
+ { name: Routes.BRIDGE.ROOT },
+ { state: testState },
+ );
+
+ fireEvent.press(getByTestId('rate-arrow-button'));
+
+ expect(mockNavigate).toHaveBeenCalledWith(
+ Routes.BRIDGE.QUOTE_SELECTOR_VIEW,
+ );
});
it('renders price impact info button for low price impact values', () => {
@@ -688,6 +693,72 @@ describe('QuoteDetailsCard', () => {
expect(getByTestId('price-impact-info-button')).toBeTruthy();
});
+ describe('minimum received row', () => {
+ it('displays minimum received row when minToTokenAmount is present', () => {
+ const mockModule = jest.requireMock('../../hooks/useBridgeQuoteData');
+ mockModule.useBridgeQuoteData.mockImplementationOnce(() => ({
+ quoteFetchError: null,
+ activeQuote: {
+ ...mockQuotes[0],
+ minToTokenAmount: {
+ amount: '23.50',
+ usd: null,
+ valueInCurrency: null,
+ },
+ },
+ destTokenAmount: '24.44',
+ isLoading: false,
+ formattedQuoteData: {
+ networkFee: '0.01',
+ estimatedTime: '1 min',
+ rate: '1 ETH = 24.4 USDC',
+ priceImpact: '-0.06%',
+ slippage: '0.5%',
+ },
+ shouldShowPriceImpactWarning: false,
+ }));
+
+ const { getByText } = renderScreen(
+ QuoteDetailsCardTestScreen,
+ { name: Routes.BRIDGE.ROOT },
+ { state: testState },
+ );
+
+ expect(getByText(strings('bridge.minimum_received'))).toBeOnTheScreen();
+ // formatMinimumReceived formats "23.50" followed by the dest token symbol "ETH"
+ expect(getByText(/23\.5 ETH/)).toBeOnTheScreen();
+ });
+
+ it('does not display minimum received row when minToTokenAmount is absent', () => {
+ const mockModule = jest.requireMock('../../hooks/useBridgeQuoteData');
+ mockModule.useBridgeQuoteData.mockImplementationOnce(() => ({
+ quoteFetchError: null,
+ activeQuote: {
+ ...mockQuotes[0],
+ minToTokenAmount: undefined,
+ },
+ destTokenAmount: '24.44',
+ isLoading: false,
+ formattedQuoteData: {
+ networkFee: '0.01',
+ estimatedTime: '1 min',
+ rate: '1 ETH = 24.4 USDC',
+ priceImpact: '-0.06%',
+ slippage: '0.5%',
+ },
+ shouldShowPriceImpactWarning: false,
+ }));
+
+ const { queryByText } = renderScreen(
+ QuoteDetailsCardTestScreen,
+ { name: Routes.BRIDGE.ROOT },
+ { state: testState },
+ );
+
+ expect(queryByText(strings('bridge.minimum_received'))).toBeNull();
+ });
+ });
+
describe('rewards functionality', () => {
const { useRewards } = jest.requireMock('../../hooks/useRewards');
diff --git a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx
index ecbba26db57..aa3810b43bb 100644
--- a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx
+++ b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
-import { TouchableOpacity, Platform, UIManager } from 'react-native';
+import { TouchableOpacity, Platform, UIManager, Pressable } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { strings } from '../../../../../../locales/i18n';
import { useTheme } from '../../../../../util/theme';
@@ -43,6 +43,7 @@ import TagColored, {
import { useShouldRenderGasSponsoredBanner } from '../../hooks/useShouldRenderGasSponsoredBanner';
import { isGaslessQuote } from '../../utils/isGaslessQuote';
import { QuoteDetailsCardProps } from './QuoteDetailsCard.types';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
import { getPriceImpactViewData } from '../../utils/getPriceImpactViewData';
import {
TextVariant as TextVariantLegacy,
@@ -63,6 +64,7 @@ const QuoteDetailsCard: React.FC = ({
hasInsufficientBalance,
location,
}) => {
+ const tw = useTailwind();
const theme = useTheme();
const navigation = useNavigation();
const styles = createStyles(theme);
@@ -109,6 +111,10 @@ const QuoteDetailsCard: React.FC = ({
});
};
+ const handleRatePress = () => {
+ navigation.navigate(Routes.BRIDGE.QUOTE_SELECTOR_VIEW);
+ };
+
const handlePriceImpactPress = () => {
navigation.navigate(Routes.BRIDGE.MODALS.ROOT, {
screen: Routes.BRIDGE.MODALS.PRICE_IMPACT_MODAL,
@@ -131,6 +137,7 @@ const QuoteDetailsCard: React.FC = ({
[formattedQuoteData?.priceImpact],
);
+ // Early return for invalid states
if (
!sourceToken?.chainId ||
!destToken?.chainId ||
@@ -143,44 +150,45 @@ const QuoteDetailsCard: React.FC = ({
return (
-
-
- {strings('bridge.rate')}
-
-
-
- ),
- tooltip: {
- title: strings('bridge.quote_info_title'),
- content: strings('bridge.quote_info_content'),
- size: TooltipSizes.Sm,
- iconName: IconNameLegacy.Info,
- },
- }}
- value={{
- label: (
-
- {formattedQuoteData.rate}
-
- ),
- }}
- />
+
+
+ {strings('bridge.rate')}
+
+
+
+
+
+
+
+ {formattedQuoteData?.rate}
+
+
+
+
+
+
{shouldShowGasSponsored ? (
+
+ Rate
+
-
-
+
+
+
-
-
- Rate
-
-
-
- 0:30
-
-
-
-
-
-
-
-
+ "color": "#66676a",
+ "height": 16,
+ "width": 16,
+ },
+ undefined,
+ ]
+ }
+ />
+
+
+
+ 1 ETH = 24.4 USDC
+
-
-
-
- 1 ETH = 24.4 USDC
-
-
-
+ "color": "#66676a",
+ "height": 16,
+ "width": 16,
+ },
+ undefined,
+ ]
+ }
+ />
({
+ QuoteRow: ({ provider, quoteRequestId }: QuoteRowProps) => {
+ const { Text } = jest.requireActual('react-native');
+ return (
+
+ {provider.name} - {quoteRequestId}
+
+ );
+ },
+}));
+
+jest.mock('../../hooks/useShouldRenderGasSponsoredBanner', () => ({
+ useShouldRenderGasSponsoredBanner: jest.fn(),
+}));
+
+describe('QuoteList', () => {
+ const mockOnPress = jest.fn();
+
+ const createMockQuote = (
+ overrides: Partial = {},
+ ): QuoteRowProps => ({
+ provider: { name: 'Lifi' },
+ formattedTotalCost: '$100.00',
+ quoteRequestId: 'quote-123',
+ onPress: mockOnPress,
+ ...overrides,
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('rendering', () => {
+ it('renders empty list when data is empty array', () => {
+ const { queryByTestId } = render();
+
+ expect(queryByTestId(/quote-row-/)).toBeNull();
+ });
+
+ it('renders single QuoteRow when data has one item', () => {
+ const quote = createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'quote-1',
+ });
+
+ const { getByTestId, getByText } = render();
+
+ expect(getByTestId('quote-row-quote-1')).toBeTruthy();
+ expect(getByText('Lifi - quote-1')).toBeTruthy();
+ });
+
+ it('renders multiple QuoteRows when data has multiple items', () => {
+ const quotes = [
+ createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'quote-1',
+ }),
+ createMockQuote({
+ provider: { name: 'Socket' },
+ quoteRequestId: 'quote-2',
+ }),
+ createMockQuote({
+ provider: { name: 'Hop' },
+ quoteRequestId: 'quote-3',
+ }),
+ ];
+
+ const { getByTestId, getByText } = render();
+
+ expect(getByTestId('quote-row-quote-1')).toBeTruthy();
+ expect(getByTestId('quote-row-quote-2')).toBeTruthy();
+ expect(getByTestId('quote-row-quote-3')).toBeTruthy();
+ expect(getByText('Lifi - quote-1')).toBeTruthy();
+ expect(getByText('Socket - quote-2')).toBeTruthy();
+ expect(getByText('Hop - quote-3')).toBeTruthy();
+ });
+
+ it('renders correct number of QuoteRows matching data length', () => {
+ const quotes = [
+ createMockQuote({ quoteRequestId: 'quote-1' }),
+ createMockQuote({ quoteRequestId: 'quote-2' }),
+ createMockQuote({ quoteRequestId: 'quote-3' }),
+ createMockQuote({ quoteRequestId: 'quote-4' }),
+ createMockQuote({ quoteRequestId: 'quote-5' }),
+ ];
+
+ const { getAllByText } = render();
+
+ const renderedRows = getAllByText(/Lifi - quote-/);
+ expect(renderedRows).toHaveLength(5);
+ });
+ });
+
+ describe('prop passing', () => {
+ it('passes all props to QuoteRow components', () => {
+ const quote = createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'quote-1',
+ formattedTotalCost: '$200.50',
+ isLowestCost: true,
+ selected: true,
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('Lifi - quote-1')).toBeTruthy();
+ });
+
+ it('passes different props to different QuoteRow components', () => {
+ const quotes = [
+ createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'quote-1',
+ formattedTotalCost: '$100.00',
+ }),
+ createMockQuote({
+ provider: { name: 'Socket' },
+ quoteRequestId: 'quote-2',
+ formattedTotalCost: '$200.00',
+ }),
+ ];
+
+ const { getByText } = render();
+
+ expect(getByText('Lifi - quote-1')).toBeTruthy();
+ expect(getByText('Socket - quote-2')).toBeTruthy();
+ });
+ });
+
+ describe('key generation', () => {
+ it('uses quoteRequestId as key for each QuoteRow', () => {
+ const quotes = [
+ createMockQuote({ quoteRequestId: 'unique-id-1' }),
+ createMockQuote({ quoteRequestId: 'unique-id-2' }),
+ createMockQuote({ quoteRequestId: 'unique-id-3' }),
+ ];
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-row-unique-id-1')).toBeTruthy();
+ expect(getByTestId('quote-row-unique-id-2')).toBeTruthy();
+ expect(getByTestId('quote-row-unique-id-3')).toBeTruthy();
+ });
+ });
+
+ describe('edge cases', () => {
+ it('handles quotes with same provider but different IDs', () => {
+ const quotes = [
+ createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'lifi-quote-1',
+ }),
+ createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'lifi-quote-2',
+ }),
+ ];
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-row-lifi-quote-1')).toBeTruthy();
+ expect(getByTestId('quote-row-lifi-quote-2')).toBeTruthy();
+ });
+
+ it('handles quotes with all optional props undefined', () => {
+ const quote = createMockQuote({
+ provider: { name: 'Socket' },
+ quoteRequestId: 'minimal-quote',
+ isLowestCost: undefined,
+ selected: undefined,
+ loading: undefined,
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('Socket - minimal-quote')).toBeTruthy();
+ });
+
+ it('handles large list of quotes', () => {
+ const quotes = Array.from({ length: 50 }, (_, i) =>
+ createMockQuote({
+ provider: { name: `Provider${i}` },
+ quoteRequestId: `quote-${i}`,
+ }),
+ );
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-row-quote-0')).toBeTruthy();
+ expect(getByTestId('quote-row-quote-25')).toBeTruthy();
+ expect(getByTestId('quote-row-quote-49')).toBeTruthy();
+ });
+ });
+
+ describe('data updates', () => {
+ it('updates when data changes from empty to populated', () => {
+ const { queryByTestId, rerender } = render();
+
+ expect(queryByTestId(/quote-row-/)).toBeNull();
+
+ const quotes = [createMockQuote({ quoteRequestId: 'new-quote' })];
+ rerender();
+
+ expect(queryByTestId('quote-row-new-quote')).toBeTruthy();
+ });
+
+ it('updates when data changes from populated to empty', () => {
+ const quotes = [createMockQuote({ quoteRequestId: 'quote-1' })];
+ const { getByTestId, queryByTestId, rerender } = render(
+ ,
+ );
+
+ expect(getByTestId('quote-row-quote-1')).toBeTruthy();
+
+ rerender();
+
+ expect(queryByTestId('quote-row-quote-1')).toBeNull();
+ });
+
+ it('updates when data array changes', () => {
+ const initialQuotes = [
+ createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'quote-1',
+ }),
+ ];
+
+ const { getByTestId, queryByTestId, rerender } = render(
+ ,
+ );
+
+ expect(getByTestId('quote-row-quote-1')).toBeTruthy();
+
+ const updatedQuotes = [
+ createMockQuote({
+ provider: { name: 'Socket' },
+ quoteRequestId: 'quote-2',
+ }),
+ createMockQuote({
+ provider: { name: 'Hop' },
+ quoteRequestId: 'quote-3',
+ }),
+ ];
+
+ rerender();
+
+ expect(queryByTestId('quote-row-quote-1')).toBeNull();
+ expect(getByTestId('quote-row-quote-2')).toBeTruthy();
+ expect(getByTestId('quote-row-quote-3')).toBeTruthy();
+ });
+ });
+
+ describe('ordering', () => {
+ it('renders QuoteRows in the same order as data array', () => {
+ const quotes = [
+ createMockQuote({
+ provider: { name: 'First' },
+ quoteRequestId: 'quote-1',
+ }),
+ createMockQuote({
+ provider: { name: 'Second' },
+ quoteRequestId: 'quote-2',
+ }),
+ createMockQuote({
+ provider: { name: 'Third' },
+ quoteRequestId: 'quote-3',
+ }),
+ ];
+
+ const { getAllByText } = render();
+
+ const rows = getAllByText(/- quote-/);
+ expect(rows[0]).toHaveTextContent('First - quote-1');
+ expect(rows[1]).toHaveTextContent('Second - quote-2');
+ expect(rows[2]).toHaveTextContent('Third - quote-3');
+ });
+
+ it('maintains order when data is reversed', () => {
+ const quotes = [
+ createMockQuote({
+ provider: { name: 'First' },
+ quoteRequestId: 'quote-1',
+ }),
+ createMockQuote({
+ provider: { name: 'Second' },
+ quoteRequestId: 'quote-2',
+ }),
+ ];
+
+ const { getAllByText, rerender } = render();
+
+ let rows = getAllByText(/- quote-/);
+ expect(rows[0]).toHaveTextContent('First - quote-1');
+ expect(rows[1]).toHaveTextContent('Second - quote-2');
+
+ const reversedQuotes = [...quotes].reverse();
+ rerender();
+
+ rows = getAllByText(/- quote-/);
+ expect(rows[0]).toHaveTextContent('Second - quote-2');
+ expect(rows[1]).toHaveTextContent('First - quote-1');
+ });
+ });
+});
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
new file mode 100644
index 00000000000..73c2ca6c91d
--- /dev/null
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import { QuoteRow, QuoteRowProps } from './QuoteRow';
+
+interface Props {
+ data: QuoteRowProps[];
+}
+
+export const QuoteList = ({ data }: Props) =>
+ data.map((quote) => );
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.test.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.test.tsx
new file mode 100644
index 00000000000..1a954ce1910
--- /dev/null
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.test.tsx
@@ -0,0 +1,349 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react-native';
+import { QuoteRow, QuoteRowProps } from './QuoteRow';
+import { strings } from '../../../../../../locales/i18n';
+import { BridgeToken } from '../../types';
+import { CHAIN_IDS } from '@metamask/transaction-controller';
+
+jest.mock('../../../../../util/theme', () => ({
+ useTheme: jest.fn(() => ({
+ colors: {
+ success: { default: '#28A745' },
+ },
+ })),
+}));
+
+const mockDestToken: BridgeToken = {
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ symbol: 'USDC',
+ decimals: 6,
+ chainId: CHAIN_IDS.MAINNET,
+};
+
+jest.mock('react-redux', () => {
+ const actual = jest.requireActual('react-redux');
+ return {
+ ...actual,
+ useSelector: jest.fn((selector) => {
+ const mockState = {
+ bridge: { destToken: mockDestToken },
+ };
+ try {
+ return selector(mockState);
+ } catch {
+ return null;
+ }
+ }),
+ };
+});
+
+jest.mock('../../hooks/useDisplayCurrencyValue', () => ({
+ useDisplayCurrencyValue: jest.fn(() => '$50.00'),
+}));
+
+import { useSelector } from 'react-redux';
+import { useDisplayCurrencyValue } from '../../hooks/useDisplayCurrencyValue';
+
+const mockUseSelector = useSelector as jest.MockedFunction;
+const mockUseDisplayCurrencyValue =
+ useDisplayCurrencyValue as jest.MockedFunction<
+ typeof useDisplayCurrencyValue
+ >;
+
+describe('QuoteRow', () => {
+ const mockOnPress = jest.fn();
+
+ const defaultProps: QuoteRowProps = {
+ provider: { name: 'Lifi' },
+ formattedTotalCost: '$100.50',
+ quoteRequestId: 'quote-123',
+ onPress: mockOnPress,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseDisplayCurrencyValue.mockReturnValue('$50.00');
+ mockUseSelector.mockImplementation((selector) => {
+ const mockState = { bridge: { destToken: mockDestToken } };
+ try {
+ return selector(mockState);
+ } catch {
+ return null;
+ }
+ });
+ });
+
+ describe('rendering', () => {
+ it('renders provider name', () => {
+ // Act
+ const { getByText } = render();
+
+ // Assert
+ expect(getByText('Lifi')).toBeTruthy();
+ });
+
+ it('renders formatted total cost with label', () => {
+ // Act
+ const { getByText } = render();
+
+ // Assert
+ expect(
+ getByText(`${strings('bridge.total_cost')}: $100.50`),
+ ).toBeTruthy();
+ });
+
+ it('renders receive amount and destToken symbol', () => {
+ // Act
+ const { getByText } = render(
+ ,
+ );
+
+ // Assert
+ expect(getByText(/500.*USDC/)).toBeTruthy();
+ });
+
+ it('renders fiat currency value with tilde prefix', () => {
+ // Arrange
+ mockUseDisplayCurrencyValue.mockReturnValue('$50.00');
+
+ // Act
+ const { getByText } = render(
+ ,
+ );
+
+ // Assert
+ expect(getByText('~ $50.00')).toBeTruthy();
+ });
+
+ it('passes receiveAmount and destToken to useDisplayCurrencyValue', () => {
+ // Act
+ render();
+
+ // Assert
+ expect(mockUseDisplayCurrencyValue).toHaveBeenCalledWith(
+ '500',
+ mockDestToken,
+ );
+ });
+
+ it('passes undefined receiveAmount to useDisplayCurrencyValue when not provided', () => {
+ // Act
+ render();
+
+ // Assert
+ expect(mockUseDisplayCurrencyValue).toHaveBeenCalledWith(
+ undefined,
+ mockDestToken,
+ );
+ });
+ });
+
+ describe('receive amount formatting', () => {
+ it('formats large receive amounts with locale separators', () => {
+ // Act
+ const { getByText } = render(
+ ,
+ );
+
+ // Assert — formatAmountWithLocaleSeparators('1000.5') → '~ 1,000.5 USDC' in en locale
+ expect(getByText('~ 1,000.5 USDC')).toBeTruthy();
+ });
+
+ it('does not format when receiveAmount is 0', () => {
+ // Act
+ const { getByText } = render(
+ ,
+ );
+
+ // Assert — raw amount returned when value is 0, tilde prefix still present
+ expect(getByText('~ 0 USDC')).toBeTruthy();
+ });
+
+ it('does not format when destToken is undefined', () => {
+ // Arrange
+ mockUseSelector.mockReturnValue(undefined);
+
+ // Act
+ const { queryByText, getByText } = render(
+ ,
+ );
+
+ // Assert — raw amount rendered without symbol; no formatted grouping
+ expect(queryByText('1,000')).toBeNull();
+ expect(getByText(/~ 1000/)).toBeTruthy();
+ });
+
+ it('renders nothing for destToken symbol when destToken is undefined', () => {
+ // Arrange
+ mockUseSelector.mockReturnValue(undefined);
+
+ // Act
+ const { queryByText } = render(
+ ,
+ );
+
+ // Assert — no USDC symbol rendered
+ expect(queryByText(/USDC/)).toBeNull();
+ });
+ });
+
+ describe('lowest cost badge', () => {
+ it('renders lowest cost badge when isLowestCost is true', () => {
+ // Act
+ const { getByText } = render();
+
+ // Assert
+ expect(getByText(strings('bridge.lowest_cost'))).toBeTruthy();
+ });
+
+ it('does not render lowest cost badge when isLowestCost is false', () => {
+ // Act
+ const { queryByText } = render();
+
+ // Assert
+ expect(queryByText(strings('bridge.lowest_cost'))).toBeNull();
+ });
+
+ it('does not render lowest cost badge when loading is true even if isLowestCost', () => {
+ // Act
+ const { queryByText } = render(
+ ,
+ );
+
+ // Assert
+ expect(queryByText(strings('bridge.lowest_cost'))).toBeNull();
+ });
+ });
+
+ describe('loading state', () => {
+ it('hides provider name via Skeleton when loading is true', () => {
+ // Act
+ const { queryByText } = render();
+
+ // Assert — Skeleton with hideChildren hides text content
+ expect(queryByText('Lifi')).toBeNull();
+ });
+
+ it('hides total cost via Skeleton when loading is true', () => {
+ // Act
+ const { queryByText } = render();
+
+ // Assert
+ expect(
+ queryByText(`${strings('bridge.total_cost')}: $100.50`),
+ ).toBeNull();
+ });
+
+ it('shows provider name and cost when loading is false', () => {
+ // Act
+ const { getByText } = render();
+
+ // Assert
+ expect(getByText('Lifi')).toBeTruthy();
+ expect(
+ getByText(`${strings('bridge.total_cost')}: $100.50`),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('interactions', () => {
+ it('calls onPress with quoteRequestId when pressed', () => {
+ // Act
+ const { getByText } = render();
+ fireEvent.press(getByText('Lifi'));
+
+ // Assert
+ expect(mockOnPress).toHaveBeenCalledWith('quote-123');
+ expect(mockOnPress).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onPress when pressing the total cost text', () => {
+ // Act
+ const { getByText } = render();
+ fireEvent.press(getByText(`${strings('bridge.total_cost')}: $100.50`));
+
+ // Assert
+ expect(mockOnPress).toHaveBeenCalledWith('quote-123');
+ });
+
+ it('calls onPress with correct id for different quoteRequestId values', () => {
+ // Arrange
+ const { rerender, getByText } = render(
+ ,
+ );
+
+ // Act
+ fireEvent.press(getByText('Lifi'));
+ expect(mockOnPress).toHaveBeenCalledWith('quote-456');
+
+ rerender();
+ fireEvent.press(getByText('Lifi'));
+
+ // Assert
+ expect(mockOnPress).toHaveBeenCalledWith('quote-789');
+ });
+ });
+
+ describe('selected state', () => {
+ it('renders correctly when selected is true', () => {
+ // Act
+ const { UNSAFE_root } = render();
+
+ // Assert
+ expect(UNSAFE_root).toBeTruthy();
+ });
+
+ it('renders correctly when selected is false', () => {
+ // Act
+ const { UNSAFE_root } = render(
+ ,
+ );
+
+ // Assert
+ expect(UNSAFE_root).toBeTruthy();
+ });
+ });
+
+ describe('complex scenarios', () => {
+ it('renders correctly with all props provided', () => {
+ // Arrange
+ mockUseDisplayCurrencyValue.mockReturnValue('$25.00');
+
+ // Act
+ const { getByText } = render(
+ ,
+ );
+
+ // Assert
+ expect(getByText('Lifi')).toBeTruthy();
+ expect(
+ getByText(`${strings('bridge.total_cost')}: $100.50`),
+ ).toBeTruthy();
+ expect(getByText(strings('bridge.lowest_cost'))).toBeTruthy();
+ expect(getByText('~ 250.5 USDC')).toBeTruthy();
+ expect(getByText('~ $25.00')).toBeTruthy();
+ });
+
+ it('renders correctly with minimal props', () => {
+ // Act
+ const { getByText, queryByText } = render(
+ ,
+ );
+
+ // Assert
+ expect(getByText('Socket')).toBeTruthy();
+ expect(getByText(`${strings('bridge.total_cost')}: $50.00`)).toBeTruthy();
+ expect(queryByText(strings('bridge.lowest_cost'))).toBeNull();
+ });
+ });
+});
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
new file mode 100644
index 00000000000..759272b2c4a
--- /dev/null
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
@@ -0,0 +1,145 @@
+import React, { useMemo } from 'react';
+import { TouchableOpacity, View } from 'react-native';
+import {
+ Box,
+ BoxAlignItems,
+ BoxBackgroundColor,
+ BoxFlexDirection,
+ BoxJustifyContent,
+ FontWeight,
+ Skeleton,
+ Text,
+ TextColor,
+ TextVariant as TextVariantDS,
+} from '@metamask/design-system-react-native';
+import { strings } from '../../../../../../locales/i18n';
+import { useTheme } from '../../../../../util/theme';
+import TagBase, {
+ TagSeverity,
+ TagShape,
+} from '../../../../../component-library/base-components/TagBase';
+import { TextVariant } from '../../../../../component-library/components/Texts/Text';
+import { useDisplayCurrencyValue } from '../../hooks/useDisplayCurrencyValue';
+import { useSelector } from 'react-redux';
+import { selectDestToken } from '../../../../../core/redux/slices/bridge';
+import { formatAmountWithLocaleSeparators } from '../../utils/formatAmountWithLocaleSeparators';
+import { limitToMaximumDecimalPlaces } from '../../../../../util/number';
+
+export interface QuoteRowProps {
+ provider: {
+ name: string;
+ };
+ isLowestCost?: boolean;
+ formattedTotalCost: string;
+ selected?: boolean;
+ quoteRequestId: string;
+ onPress: (quoteRequestId: string) => void;
+ loading?: boolean;
+ receiveAmount?: string;
+}
+
+export const QuoteRow = ({
+ selected,
+ provider,
+ isLowestCost,
+ formattedTotalCost,
+ onPress,
+ quoteRequestId,
+ loading,
+ receiveAmount,
+}: QuoteRowProps) => {
+ const theme = useTheme();
+ const destToken = useSelector(selectDestToken);
+ const formattedReceiveAmountFiat = useDisplayCurrencyValue(
+ receiveAmount,
+ destToken,
+ );
+ const formattedReceiveAmount = useMemo(
+ () =>
+ receiveAmount && receiveAmount !== '0' && destToken
+ ? formatAmountWithLocaleSeparators(
+ limitToMaximumDecimalPlaces(parseFloat(receiveAmount)),
+ )
+ : receiveAmount,
+ [receiveAmount, destToken],
+ );
+
+ return (
+ onPress(quoteRequestId)}>
+
+
+
+
+
+
+ {provider.name}
+
+
+
+
+ {isLowestCost && !loading && (
+
+ {strings('bridge.lowest_cost')}
+
+ )}
+
+
+
+
+ {strings('bridge.total_cost')}: {formattedTotalCost}
+
+
+
+
+
+
+ ~ {formattedReceiveAmount} {destToken?.symbol}
+
+
+
+
+ ~ {formattedReceiveAmountFiat}
+
+
+
+
+
+ );
+};
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/constants.ts b/app/components/UI/Bridge/components/QuoteSelectorView/constants.ts
new file mode 100644
index 00000000000..27bb4ead29a
--- /dev/null
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/constants.ts
@@ -0,0 +1,26 @@
+import { QuoteRowProps } from './QuoteRow';
+
+export const QUOTES_PLACEHOLDER_DATA = [
+ {
+ provider: {
+ name: 'test_provider',
+ },
+ formattedTotalCost: '1234.2',
+ quoteRequestId: '1',
+ onPress: () => {
+ // Placeholder for loading state
+ },
+ loading: true,
+ },
+ {
+ provider: {
+ name: 'test_2',
+ },
+ formattedTotalCost: '23.0',
+ quoteRequestId: '2',
+ onPress: () => {
+ // Placeholder for loading state
+ },
+ loading: true,
+ },
+] satisfies QuoteRowProps[];
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/index.test.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/index.test.tsx
new file mode 100644
index 00000000000..c7df7f9dd6e
--- /dev/null
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/index.test.tsx
@@ -0,0 +1,655 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import { QuoteSelectorView } from './index';
+import { strings } from '../../../../../../locales/i18n';
+import { BigNumber } from 'ethers';
+
+const mockNavigate = jest.fn();
+const mockGoBack = jest.fn();
+const mockSetOptions = jest.fn();
+
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ goBack: mockGoBack,
+ setOptions: mockSetOptions,
+ }),
+}));
+
+const mockUseBridgeQuoteData = jest.fn();
+jest.mock('../../hooks/useBridgeQuoteData', () => ({
+ useBridgeQuoteData: () => mockUseBridgeQuoteData(),
+}));
+
+const mockUseLatestBalance = jest.fn();
+jest.mock('../../hooks/useLatestBalance', () => ({
+ useLatestBalance: (params: unknown) => mockUseLatestBalance(params),
+}));
+
+const mockFormatFiat = jest.fn();
+jest.mock('../../../../../util/formatFiat', () => ({
+ __esModule: true,
+ default: (...args: unknown[]) => mockFormatFiat(...args),
+}));
+
+const mockIsGaslessQuote = jest.fn();
+jest.mock('../../utils/isGaslessQuote', () => ({
+ isGaslessQuote: (quote: unknown) => mockIsGaslessQuote(quote),
+}));
+
+const mockTrackAllQuotesSortedEvent = jest.fn();
+const mockUseTrackAllQuotesSortedEvent = jest.fn();
+jest.mock('../../hooks/useTrackAllQuotesSortedEvent', () => ({
+ useTrackAllQuotesSortedEvent: (params: unknown) =>
+ mockUseTrackAllQuotesSortedEvent(params),
+}));
+
+const mockSourceToken = {
+ address: '0x1234',
+ decimals: 18,
+ chainId: '0x1',
+ symbol: 'ETH',
+};
+
+const mockCurrency = 'USD';
+const mockDispatch = jest.fn();
+
+const mockSelectedQuoteRequestId: string | null = null;
+
+jest.mock('react-redux', () => {
+ const actual = jest.requireActual('react-redux');
+ return {
+ ...actual,
+ useSelector: jest.fn((selector) => {
+ // Call the selector with a mock state to see which selector it is
+ const mockState = {
+ bridge: {
+ sourceToken: mockSourceToken,
+ selectedQuoteRequestId: mockSelectedQuoteRequestId,
+ },
+ engine: {
+ backgroundState: {
+ CurrencyRateController: {
+ currentCurrency: mockCurrency,
+ },
+ },
+ },
+ };
+
+ try {
+ return selector(mockState);
+ } catch {
+ return null;
+ }
+ }),
+ useDispatch: () => mockDispatch,
+ };
+});
+
+jest.mock('../../../../Base/ScreenView', () => {
+ const { View } = jest.requireActual('react-native');
+ return {
+ __esModule: true,
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ };
+});
+
+jest.mock('./QuoteList', () => ({
+ QuoteList: ({ data }: { data: unknown[] }) => {
+ const { View, Text } = jest.requireActual('react-native');
+ return (
+
+ {data.length}
+
+ );
+ },
+}));
+
+describe('QuoteSelectorView', () => {
+ const mockQuote = {
+ quote: {
+ requestId: 'quote-1',
+ srcChainId: 1,
+ destChainId: 137,
+ srcTokenAmount: '1000000000000000000',
+ destTokenAmount: '1000000',
+ srcAsset: {
+ chainId: 1,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ decimals: 18,
+ icon: '',
+ },
+ destAsset: {
+ chainId: 137,
+ address: '0x0000000000000000000000000000000000000001',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ decimals: 6,
+ icon: '',
+ },
+ feeData: {
+ metabridge: {
+ amount: '0',
+ asset: {
+ chainId: 1,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ decimals: 18,
+ icon: '',
+ },
+ },
+ },
+ bridges: ['lifi'],
+ steps: [],
+ refuel: undefined,
+ },
+ sentAmount: {
+ amount: '1',
+ usd: '9999',
+ valueInCurrency: '2000',
+ },
+ totalNetworkFee: {
+ amount: '0.01',
+ usd: '9999',
+ valueInCurrency: '20',
+ },
+ estimatedProcessingTimeInSeconds: 60,
+ adjustedReturn: {
+ usd: '9999',
+ valueInCurrency: '1980',
+ },
+ };
+
+ const mockLatestBalance = {
+ displayBalance: '1000',
+ atomicBalance: BigNumber.from('1000000000000000000'),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [],
+ bestQuote: null,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+ mockUseLatestBalance.mockReturnValue(mockLatestBalance);
+ mockFormatFiat.mockReturnValue('$2,020.00');
+ mockIsGaslessQuote.mockReturnValue(false);
+ mockUseTrackAllQuotesSortedEvent.mockReturnValue(
+ mockTrackAllQuotesSortedEvent,
+ );
+ });
+
+ describe('rendering', () => {
+ it('renders ScreenView component', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('screen-view')).toBeTruthy();
+ });
+
+ it('renders info text with correct translation', () => {
+ const { getByText } = render();
+
+ expect(getByText(strings('bridge.select_quote_info'))).toBeTruthy();
+ });
+
+ it('renders QuoteList component', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list')).toBeTruthy();
+ });
+ });
+
+ describe('navigation setup', () => {
+ it('sets navigation options on mount', () => {
+ render();
+
+ expect(mockSetOptions).toHaveBeenCalled();
+ });
+
+ it('calls goBack when back action is triggered', () => {
+ render();
+
+ expect(mockSetOptions).toHaveBeenCalled();
+ });
+ });
+
+ describe('quote data transformation', () => {
+ it('shows placeholder data when validQuotes is empty', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [],
+ bestQuote: null,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ // QUOTES_PLACEHOLDER_DATA has 2 items
+ expect(getByTestId('quote-list-count')).toHaveTextContent('2');
+ });
+
+ it('transforms single validQuote to data array with one item', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list-count')).toHaveTextContent('1');
+ });
+
+ it('transforms multiple validQuotes to data array with multiple items', () => {
+ const quotes = [
+ mockQuote,
+ { ...mockQuote, quote: { ...mockQuote.quote, requestId: 'quote-2' } },
+ { ...mockQuote, quote: { ...mockQuote.quote, requestId: 'quote-3' } },
+ ];
+
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: quotes,
+ bestQuote: quotes[0],
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list-count')).toHaveTextContent('3');
+ });
+ });
+
+ describe('loading state', () => {
+ it('sets loading to true when isLoading is true', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: true,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list')).toBeTruthy();
+ });
+
+ it('sets loading to false when isLoading is false', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list')).toBeTruthy();
+ });
+ });
+
+ describe('useLatestBalance integration', () => {
+ it('calls useLatestBalance hook with correct parameters', () => {
+ render();
+
+ expect(mockUseLatestBalance).toHaveBeenCalledWith({
+ address: mockSourceToken.address,
+ decimals: mockSourceToken.decimals,
+ chainId: mockSourceToken.chainId,
+ });
+ });
+
+ it('uses latestBalance in quote data', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockUseLatestBalance).toHaveBeenCalled();
+ });
+ });
+
+ describe('isGaslessQuote integration', () => {
+ it('calls isGaslessQuote for each quote', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockIsGaslessQuote).toHaveBeenCalledWith(mockQuote.quote);
+ });
+
+ it('passes isGasless result to quote data', () => {
+ mockIsGaslessQuote.mockReturnValue(true);
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockIsGaslessQuote).toHaveBeenCalled();
+ });
+ });
+
+ describe('formattedTotalCost calculation', () => {
+ it('uses valueInCurrency (not usd) for sentAmount in non-gasless quotes', () => {
+ mockIsGaslessQuote.mockReturnValue(false);
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockIsGaslessQuote).toHaveBeenCalledWith(mockQuote.quote);
+ expect(mockFormatFiat).toHaveBeenCalled();
+ const [totalCostArg] = mockFormatFiat.mock.calls[0];
+ // sentAmount.valueInCurrency (2000) + totalNetworkFee.valueInCurrency (20) = 2020
+ // NOT sentAmount.usd (9999) + totalNetworkFee.usd (9999) = 19998
+ expect(totalCostArg.toString()).toBe('2020');
+ });
+
+ it('uses valueInCurrency (not usd) for sentAmount in gasless quotes', () => {
+ mockIsGaslessQuote.mockReturnValue(true);
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockIsGaslessQuote).toHaveBeenCalledWith(mockQuote.quote);
+ expect(mockFormatFiat).toHaveBeenCalled();
+ const [totalCostArg] = mockFormatFiat.mock.calls[0];
+ // sentAmount.valueInCurrency (2000) + includedTxFees.valueInCurrency (absent → 0) = 2000
+ // NOT sentAmount.usd (9999)
+ expect(totalCostArg.toString()).toBe('2000');
+ });
+
+ it('handles multiple quotes with mixed gasless and non-gasless', () => {
+ const gaslessQuote = {
+ ...mockQuote,
+ quote: { ...mockQuote.quote, requestId: 'gasless-quote' },
+ };
+ const regularQuote = {
+ ...mockQuote,
+ quote: { ...mockQuote.quote, requestId: 'regular-quote' },
+ };
+
+ mockIsGaslessQuote.mockReturnValueOnce(true).mockReturnValueOnce(false);
+
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [gaslessQuote, regularQuote],
+ bestQuote: gaslessQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list-count')).toHaveTextContent('2');
+ expect(mockIsGaslessQuote).toHaveBeenCalledTimes(2);
+ // formatFiat called once per quote — verify both use valueInCurrency
+ expect(mockFormatFiat.mock.calls[0][0].toString()).toBe('2000'); // gasless: sentAmount only
+ expect(mockFormatFiat.mock.calls[1][0].toString()).toBe('2020'); // non-gasless: + network fee
+ });
+ });
+
+ describe('bestQuote identification', () => {
+ it('marks quote as isLowestCost when it matches bestQuote', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockUseBridgeQuoteData).toHaveBeenCalled();
+ });
+
+ it('does not mark quote as isLowestCost when it does not match bestQuote', () => {
+ const anotherQuote = {
+ ...mockQuote,
+ quote: { ...mockQuote.quote, requestId: 'quote-2' },
+ };
+
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote, anotherQuote],
+ bestQuote: anotherQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list-count')).toHaveTextContent('2');
+ });
+ });
+
+ describe('quote data fields', () => {
+ it('includes provider name from bridges', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockUseBridgeQuoteData).toHaveBeenCalled();
+ });
+
+ it('includes quoteRequestId from quote', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockUseBridgeQuoteData).toHaveBeenCalled();
+ });
+
+ it('includes gasSponsored flag from quote', () => {
+ const sponsoredQuote = {
+ ...mockQuote,
+ quote: { ...mockQuote.quote, gasSponsored: true },
+ };
+
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [sponsoredQuote],
+ bestQuote: sponsoredQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list-count')).toHaveTextContent('1');
+ });
+ });
+
+ describe('memoization', () => {
+ it('recalculates data when validQuotes changes', () => {
+ const { rerender, getByTestId } = render();
+
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ rerender();
+
+ expect(getByTestId('quote-list')).toBeTruthy();
+ });
+
+ it('renders correctly when quotes are available', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [mockQuote],
+ bestQuote: mockQuote,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('quote-list-count')).toHaveTextContent('1');
+ });
+ });
+
+ describe('navigation back behavior', () => {
+ it('navigates back when quoteFetchError exists and not loading', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [],
+ bestQuote: null,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: 'Network error',
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockGoBack).toHaveBeenCalled();
+ });
+
+ it('navigates back when blockaidError exists and not loading', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [],
+ bestQuote: null,
+ isLoading: false,
+ blockaidError: 'Blockaid validation failed',
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockGoBack).toHaveBeenCalled();
+ });
+
+ it('navigates back when quotes are expired and not loading', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [],
+ bestQuote: null,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: true,
+ });
+
+ render();
+
+ expect(mockGoBack).toHaveBeenCalled();
+ });
+
+ it('navigates back when loading and error exists', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [],
+ bestQuote: null,
+ isLoading: true,
+ blockaidError: null,
+ quoteFetchError: 'Network error',
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockGoBack).toHaveBeenCalled();
+ });
+
+ it('does not navigate back when no errors and not expired', () => {
+ mockUseBridgeQuoteData.mockReturnValue({
+ validQuotes: [],
+ bestQuote: null,
+ isLoading: false,
+ blockaidError: null,
+ quoteFetchError: null,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockGoBack).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('useTrackAllQuotesSortedEvent integration', () => {
+ it('calls useTrackAllQuotesSortedEvent with latestSourceBalance', () => {
+ render();
+
+ expect(mockUseTrackAllQuotesSortedEvent).toHaveBeenCalledWith(
+ mockLatestBalance,
+ );
+ });
+ });
+});
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/index.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/index.tsx
new file mode 100644
index 00000000000..a416c12c30d
--- /dev/null
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/index.tsx
@@ -0,0 +1,149 @@
+import ScreenView from '../../../../Base/ScreenView';
+import {
+ Box,
+ Text,
+ TextColor,
+ TextVariant,
+} from '@metamask/design-system-react-native';
+import React, { useCallback, useEffect, useMemo } from 'react';
+import { strings } from '../../../../../../locales/i18n';
+import { useNavigation } from '@react-navigation/native';
+import { getHeaderCompactStandardNavbarOptions } from '../../../../../component-library/components-temp/HeaderCompactStandard';
+import { useDispatch, useSelector } from 'react-redux';
+import {
+ selectDestToken,
+ selectSelectedQuoteRequestId,
+ selectSourceToken,
+ setSelectedQuoteRequestId,
+} from '../../../../../core/redux/slices/bridge';
+import { useBridgeQuoteData } from '../../hooks/useBridgeQuoteData';
+import { BigNumber } from 'bignumber.js';
+import { QuoteList } from './QuoteList';
+import { QuoteRowProps } from './QuoteRow';
+import { isGaslessQuote } from '../../utils/isGaslessQuote';
+import { useLatestBalance } from '../../hooks/useLatestBalance';
+import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController';
+import formatFiat from '../../../../../util/formatFiat';
+import { startCase } from 'lodash';
+import { QUOTES_PLACEHOLDER_DATA } from './constants';
+import { useTrackAllQuotesSortedEvent } from '../../hooks/useTrackAllQuotesSortedEvent';
+import { fromTokenMinimalUnit } from '../../../../../util/number';
+
+export const QuoteSelectorView = () => {
+ const navigation = useNavigation();
+ const dispatch = useDispatch();
+ const selectedQuoteRequestId = useSelector(selectSelectedQuoteRequestId);
+ const currency = useSelector(selectCurrentCurrency);
+ const {
+ validQuotes,
+ bestQuote,
+ isLoading,
+ blockaidError,
+ quoteFetchError,
+ isExpired,
+ willRefresh,
+ } = useBridgeQuoteData();
+ const sourceToken = useSelector(selectSourceToken);
+ const destToken = useSelector(selectDestToken);
+ const latestSourceBalance = useLatestBalance({
+ address: sourceToken?.address,
+ decimals: sourceToken?.decimals,
+ chainId: sourceToken?.chainId,
+ });
+
+ const trackAllQuotesSortedEvent =
+ useTrackAllQuotesSortedEvent(latestSourceBalance);
+
+ const onQuoteSelect = useCallback(
+ (requestId: string) => {
+ const quote = validQuotes.find(
+ ({ quote: _quote }) => _quote.requestId === requestId,
+ );
+
+ if (!quote) {
+ return;
+ }
+
+ trackAllQuotesSortedEvent(quote.quote);
+ dispatch(setSelectedQuoteRequestId(requestId));
+ navigation.goBack();
+ },
+ [dispatch, navigation, trackAllQuotesSortedEvent, validQuotes],
+ );
+
+ const data = useMemo(() => {
+ if (validQuotes.length === 0) {
+ return QUOTES_PLACEHOLDER_DATA;
+ }
+
+ return validQuotes.map(
+ (quote) =>
+ ({
+ formattedTotalCost: formatFiat(
+ new BigNumber(quote.sentAmount.valueInCurrency ?? '0').plus(
+ isGaslessQuote(quote.quote)
+ ? (quote.includedTxFees?.valueInCurrency ?? '0')
+ : (quote.totalNetworkFee?.valueInCurrency ??
+ quote.gasFee?.effective?.valueInCurrency ??
+ '0'),
+ ),
+ currency,
+ ),
+ receiveAmount: destToken
+ ? fromTokenMinimalUnit(
+ quote.quote.destTokenAmount,
+ destToken.decimals,
+ )
+ : undefined,
+ provider: {
+ name: startCase(quote.quote.bridges[0]),
+ },
+ quoteRequestId: quote.quote.requestId,
+ onPress: onQuoteSelect,
+ loading: isLoading,
+ isLowestCost: quote.quote.requestId === bestQuote?.quote.requestId,
+ selected:
+ !isLoading &&
+ (!selectedQuoteRequestId
+ ? quote.quote.requestId === bestQuote?.quote.requestId
+ : quote.quote.requestId === selectedQuoteRequestId),
+ }) satisfies QuoteRowProps,
+ );
+ }, [
+ validQuotes,
+ onQuoteSelect,
+ bestQuote,
+ isLoading,
+ currency,
+ selectedQuoteRequestId,
+ destToken,
+ ]);
+
+ useEffect(() => {
+ navigation.setOptions(
+ getHeaderCompactStandardNavbarOptions({
+ title: strings('bridge.select_quote'),
+ onBack: () => navigation.goBack(),
+ includesTopInset: true,
+ }),
+ );
+ }, [navigation]);
+
+ // Go back to bridge view only if there's an error or quotes are expired
+ useEffect(() => {
+ if (quoteFetchError || blockaidError || (isExpired && !willRefresh)) {
+ navigation.goBack();
+ }
+ }, [quoteFetchError, blockaidError, isExpired, navigation, willRefresh]);
+
+ return (
+
+
+
+ {strings('bridge.select_quote_info')}
+
+
+
+
+ );
+};
diff --git a/app/components/UI/Bridge/components/TokenInputArea/TokenInputArea.test.tsx b/app/components/UI/Bridge/components/TokenInputArea/TokenInputArea.test.tsx
index c1cc53cd1ef..08c67960123 100644
--- a/app/components/UI/Bridge/components/TokenInputArea/TokenInputArea.test.tsx
+++ b/app/components/UI/Bridge/components/TokenInputArea/TokenInputArea.test.tsx
@@ -43,8 +43,12 @@ jest.mock('../../hooks/useShouldRenderMaxOption', () => ({
useShouldRenderMaxOption: jest.fn(() => true),
}));
-jest.mock('../../hooks/useTokenInputAreaFormattedBalance', () => ({
- useTokenInputAreaFormattedBalance: jest.fn(() => '100'),
+jest.mock('../../hooks/useFormattedBalanceWithThreshold', () => ({
+ useFormattedBalanceWithThreshold: jest.fn(() => '100'),
+}));
+
+jest.mock('../../hooks/useDisplayCurrencyValue', () => ({
+ useDisplayCurrencyValue: jest.fn(() => '$100.00'),
}));
import { useShouldRenderMaxOption } from '../../hooks/useShouldRenderMaxOption';
@@ -53,10 +57,16 @@ const mockUseShouldRenderMaxOption =
typeof useShouldRenderMaxOption
>;
-import { useTokenInputAreaFormattedBalance } from '../../hooks/useTokenInputAreaFormattedBalance';
-const mockUseTokenInputAreaFormattedBalance =
- useTokenInputAreaFormattedBalance as jest.MockedFunction<
- typeof useTokenInputAreaFormattedBalance
+import { useFormattedBalanceWithThreshold } from '../../hooks/useFormattedBalanceWithThreshold';
+const mockUseFormattedBalanceWithThreshold =
+ useFormattedBalanceWithThreshold as jest.MockedFunction<
+ typeof useFormattedBalanceWithThreshold
+ >;
+
+import { useDisplayCurrencyValue } from '../../hooks/useDisplayCurrencyValue';
+const mockUseDisplayCurrencyValue =
+ useDisplayCurrencyValue as jest.MockedFunction<
+ typeof useDisplayCurrencyValue
>;
const mockOnTokenPress = jest.fn();
@@ -69,7 +79,8 @@ describe('TokenInputArea', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseShouldRenderMaxOption.mockReturnValue(true);
- mockUseTokenInputAreaFormattedBalance.mockReturnValue('100');
+ mockUseFormattedBalanceWithThreshold.mockReturnValue('100');
+ mockUseDisplayCurrencyValue.mockReturnValue('$100.00');
});
it('renders with initial state', () => {
@@ -668,4 +679,361 @@ describe('TokenInputArea', () => {
expect(mockInputBlur).toHaveBeenCalledTimes(1);
});
});
+
+ describe('currency value display', () => {
+ const mockToken: BridgeToken = {
+ address: '0x1234567890123456789012345678901234567890',
+ symbol: 'TEST',
+ decimals: 18,
+ chainId: '0x1' as `0x${string}`,
+ };
+
+ it('shows currency value when token, amount > 0, and currencyValue are all provided', () => {
+ // Arrange
+ mockUseDisplayCurrencyValue.mockReturnValue('$50.00');
+
+ // Act
+ const { getByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(getByText('$50.00')).toBeTruthy();
+ expect(mockUseDisplayCurrencyValue).toHaveBeenCalledWith('1', mockToken);
+ });
+
+ it('hides currency value when amount is 0', () => {
+ // Arrange
+ mockUseDisplayCurrencyValue.mockReturnValue('$0.00');
+
+ // Act
+ const { queryByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert — condition Number(amount) > 0 is false
+ expect(queryByText('$0.00')).toBeNull();
+ });
+
+ it('hides currency value when amount is undefined', () => {
+ // Act
+ const { queryByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(queryByText('$100.00')).toBeNull();
+ });
+
+ it('hides currency value when no token is provided', () => {
+ // Act
+ const { queryByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(queryByText('$100.00')).toBeNull();
+ });
+
+ it('passes amount and token to useDisplayCurrencyValue', () => {
+ // Act
+ renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(mockUseDisplayCurrencyValue).toHaveBeenCalledWith('5', mockToken);
+ });
+
+ it('passes undefined amount and token when both omitted', () => {
+ // Act
+ renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(mockUseDisplayCurrencyValue).toHaveBeenCalledWith(
+ undefined,
+ undefined,
+ );
+ });
+ });
+
+ describe('token button vs select button', () => {
+ const mockToken: BridgeToken = {
+ address: '0x1234567890123456789012345678901234567890',
+ symbol: 'TEST',
+ decimals: 18,
+ chainId: '0x1' as `0x${string}`,
+ };
+
+ it('shows TokenButton with symbol when token is provided', () => {
+ // Act
+ const { getByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(getByText('TEST')).toBeTruthy();
+ });
+
+ it('shows "Swap from" button when no token and isSourceToken is true', () => {
+ // Act
+ const { getByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(getByText('Swap from')).toBeTruthy();
+ });
+
+ it('shows "Swap to" button when no token and isSourceToken is false', () => {
+ // Act
+ const { getByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(getByText('Swap to')).toBeTruthy();
+ });
+ });
+
+ describe('loading state', () => {
+ it('does not render input when isLoading is true', () => {
+ // Act
+ const { queryByTestId } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert — input replaced by Skeleton
+ expect(queryByTestId('token-input-input')).toBeNull();
+ });
+
+ it('renders input when isLoading is false', () => {
+ // Act
+ const { getByTestId } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(getByTestId('token-input-input')).toBeTruthy();
+ });
+ });
+
+ describe('subtitle display', () => {
+ it('source token shows formattedBalance as subtitle', () => {
+ // Arrange
+ mockUseFormattedBalanceWithThreshold.mockReturnValue('42.5 ETH');
+ const mockToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ decimals: 18,
+ chainId: '0x1' as `0x${string}`,
+ };
+
+ // Act
+ const { getByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert
+ expect(getByText('42.5 ETH')).toBeTruthy();
+ });
+
+ it('destination token shows formatted token address as subtitle', () => {
+ // Arrange — use a non-native EVM token address
+ const mockToken: BridgeToken = {
+ address: '0x1234567890123456789012345678901234567890',
+ symbol: 'USDC',
+ decimals: 6,
+ chainId: '0x1' as `0x${string}`,
+ };
+
+ // Act
+ const { queryByText, getByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert — shows formatted address (0x1234...7890), not formattedBalance ('100')
+ expect(queryByText('100')).toBeNull();
+ expect(getByText(/0x.*\.\.\./)).toBeTruthy();
+ });
+
+ it('destination native token shows no address subtitle', () => {
+ // Arrange — native (zero) address should produce no subtitle
+ const nativeToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ decimals: 18,
+ chainId: '0x1' as `0x${string}`,
+ };
+
+ // Act
+ const { queryByText } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ // Assert — no formatted address rendered for native asset
+ expect(queryByText(/0x.*\.\.\./)).toBeNull();
+ });
+ });
+
+ describe('onInputPress callback', () => {
+ it('fires onInputPress when the input is pressed', () => {
+ // Act
+ const { getByTestId } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ fireEvent(getByTestId('token-input-input'), 'pressIn');
+
+ // Assert
+ expect(mockOnInputPress).toHaveBeenCalledTimes(1);
+ });
+
+ it('fires onInputPress on input focus', () => {
+ // Act
+ const { getByTestId } = renderScreen(
+ () => (
+
+ ),
+ { name: 'TokenInputArea' },
+ { state: initialState },
+ );
+
+ fireEvent(getByTestId('token-input-input'), 'focus');
+
+ // Assert — both onFocus and onInputPress are fired
+ expect(mockOnFocus).toHaveBeenCalledTimes(1);
+ expect(mockOnInputPress).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/app/components/UI/Bridge/components/TokenInputArea/index.tsx b/app/components/UI/Bridge/components/TokenInputArea/index.tsx
index 3b7b4402c6c..6cee31cc355 100644
--- a/app/components/UI/Bridge/components/TokenInputArea/index.tsx
+++ b/app/components/UI/Bridge/components/TokenInputArea/index.tsx
@@ -15,12 +15,7 @@ import Text, {
} from '../../../../../component-library/components/Texts/Text';
import Input from '../../../../../component-library/components/Form/TextField/foundation/Input';
import { TokenButton } from '../TokenButton';
-import {
- selectCurrentCurrency,
- selectCurrencyRates,
-} from '../../../../../selectors/currencyRateController';
-import { selectTokenMarketData } from '../../../../../selectors/tokenRatesController';
-import { selectNetworkConfigurations } from '../../../../../selectors/networkController';
+import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController';
import { BigNumber } from 'ethers';
import { BridgeToken } from '../../types';
import { Skeleton } from '../../../../../component-library/components/Skeleton';
@@ -34,10 +29,6 @@ import {
setDestTokenExchangeRate,
setSourceTokenExchangeRate,
} from '../../../../../core/redux/slices/bridge';
-///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
-import { selectMultichainAssetsRates } from '../../../../../selectors/multichain';
-///: END:ONLY_INCLUDE_IF(keyring-snaps)
-import { getDisplayCurrencyValue } from '../../utils/exchange-rates';
import { useBridgeExchangeRates } from '../../hooks/useBridgeExchangeRates';
import useIsInsufficientBalance from '../../hooks/useInsufficientBalance';
import { isCaipAssetType, parseCaipAssetType } from '@metamask/utils';
@@ -49,7 +40,8 @@ import { useTokenAddress } from '../../hooks/useTokenAddress';
import { useShouldRenderMaxOption } from '../../hooks/useShouldRenderMaxOption';
import { useAutoSizingFont } from '../../hooks/useAutoSizingFont';
import { formatAmountWithLocaleSeparators } from '../../utils/formatAmountWithLocaleSeparators';
-import { useTokenInputAreaFormattedBalance } from '../../hooks/useTokenInputAreaFormattedBalance';
+import { useFormattedBalanceWithThreshold } from '../../hooks/useFormattedBalanceWithThreshold';
+import { useDisplayCurrencyValue } from '../../hooks/useDisplayCurrencyValue';
export const MAX_INPUT_LENGTH = 36;
@@ -201,35 +193,15 @@ export const TokenInputArea = forwardRef<
});
};
- // // Data for fiat value calculation
- const evmMultiChainMarketData = useSelector(selectTokenMarketData);
- const evmMultiChainCurrencyRates = useSelector(selectCurrencyRates);
- const networkConfigurationsByChainId = useSelector(
- selectNetworkConfigurations,
- );
-
const isInsufficientBalance = useIsInsufficientBalance({
amount,
token,
latestAtomicBalance,
});
- let nonEvmMultichainAssetRates = {};
- ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
- nonEvmMultichainAssetRates = useSelector(selectMultichainAssetsRates);
- ///: END:ONLY_INCLUDE_IF(keyring-snaps)
-
- const currencyValue = getDisplayCurrencyValue({
- token,
- amount,
- evmMultiChainMarketData,
- networkConfigurationsByChainId,
- evmMultiChainCurrencyRates,
- currentCurrency,
- nonEvmMultichainAssetRates,
- });
+ const currencyValue = useDisplayCurrencyValue(amount, token);
- const formattedBalance = useTokenInputAreaFormattedBalance(
+ const formattedBalance = useFormattedBalanceWithThreshold(
tokenBalance,
token,
);
diff --git a/app/components/UI/Bridge/hooks/useBridgeQuoteData/index.ts b/app/components/UI/Bridge/hooks/useBridgeQuoteData/index.ts
index c6d17ece693..2ee49117a64 100644
--- a/app/components/UI/Bridge/hooks/useBridgeQuoteData/index.ts
+++ b/app/components/UI/Bridge/hooks/useBridgeQuoteData/index.ts
@@ -1,4 +1,4 @@
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import {
selectBridgeControllerState,
selectSourceToken,
@@ -10,6 +10,8 @@ import {
selectBridgeFeatureFlags,
selectIsSolanaSwap,
selectIsSolanaToNonSolana,
+ selectSelectedQuoteRequestId,
+ setSelectedQuoteRequestId,
} from '../../../../../core/redux/slices/bridge';
import { RequestStatus, isNonEvmChainId } from '@metamask/bridge-controller';
import { areAddressesEqual } from '../../../../../util/address';
@@ -37,6 +39,7 @@ interface UseBridgeQuoteDataParams {
export const useBridgeQuoteData = ({
latestSourceAtomicBalance,
}: UseBridgeQuoteDataParams = {}) => {
+ const dispatch = useDispatch();
const bridgeControllerState = useSelector(selectBridgeControllerState);
const sourceToken = useSelector(selectSourceToken);
const destToken = useSelector(selectDestToken);
@@ -48,6 +51,7 @@ export const useBridgeQuoteData = ({
const bridgeFeatureFlags = useSelector(selectBridgeFeatureFlags);
const isSolanaSwap = useSelector(selectIsSolanaSwap);
const isSolanaToNonSolana = useSelector(selectIsSolanaToNonSolana);
+ const selectedQuoteRequestId = useSelector(selectSelectedQuoteRequestId);
const { validateBridgeTx } = useValidateBridgeTx();
const [blockaidError, setBlockaidError] = useState(null);
@@ -84,8 +88,20 @@ export const useBridgeQuoteData = ({
[quotes?.sortedQuotes],
);
+ // Determine the active quote:
+ // 1. If user manually selected a quote, use that
+ // 2. Otherwise, use the best quote
+ // 3. If expired and not refreshing, use undefined
+ const manuallySelectedQuote = selectedQuoteRequestId
+ ? allQuotes.find(
+ (quote) => quote.quote.requestId === selectedQuoteRequestId,
+ )
+ : undefined;
+
const activeQuote =
- isExpired && !willRefresh && !isSubmittingTx ? undefined : bestQuote;
+ isExpired && !willRefresh && !isSubmittingTx
+ ? undefined
+ : (manuallySelectedQuote ?? bestQuote);
// Validate that the quote's source asset matches the selected source token
// This prevents showing stale quote data when user changes source token on the same chain
@@ -287,6 +303,12 @@ export const useBridgeQuoteData = ({
validateQuote();
}, [validateQuote]);
+ useEffect(() => {
+ if (!manuallySelectedQuote) {
+ dispatch(setSelectedQuoteRequestId(undefined));
+ }
+ }, [manuallySelectedQuote, dispatch]);
+
return {
bestQuote,
quoteFetchError,
diff --git a/app/components/UI/Bridge/hooks/useBridgeQuoteData/useBridgeQuoteData.test.ts b/app/components/UI/Bridge/hooks/useBridgeQuoteData/useBridgeQuoteData.test.ts
index 8c8ac39d6b7..fa059f89a81 100644
--- a/app/components/UI/Bridge/hooks/useBridgeQuoteData/useBridgeQuoteData.test.ts
+++ b/app/components/UI/Bridge/hooks/useBridgeQuoteData/useBridgeQuoteData.test.ts
@@ -1292,6 +1292,189 @@ describe('useBridgeQuoteData', () => {
});
});
+ // Test manually selected quote via selectedQuoteRequestId
+ describe('manually selected quote', () => {
+ it('uses manually selected quote when selectedQuoteRequestId matches a quote in sortedQuotes', () => {
+ const manuallySelectedQuote = {
+ ...mockQuoteWithMetadata,
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ requestId: 'selected-quote-id',
+ },
+ };
+
+ const recommendedQuote = {
+ ...mockQuoteWithMetadata,
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ requestId: 'best-quote-id',
+ },
+ };
+
+ (selectBridgeQuotes as unknown as jest.Mock).mockImplementation(() => ({
+ recommendedQuote,
+ sortedQuotes: [recommendedQuote, manuallySelectedQuote],
+ alternativeQuotes: [],
+ }));
+
+ const bridgeReducerOverrides = {
+ selectedQuoteRequestId: 'selected-quote-id',
+ };
+
+ const testState = createBridgeTestState({
+ bridgeReducerOverrides,
+ });
+
+ const { result } = renderHookWithProvider(() => useBridgeQuoteData(), {
+ state: testState,
+ });
+
+ expect(result.current.activeQuote).toEqual(manuallySelectedQuote);
+ expect(result.current.bestQuote).toEqual(recommendedQuote);
+ });
+
+ it('falls back to bestQuote when selectedQuoteRequestId does not match any sortedQuote', () => {
+ const recommendedQuote = { ...mockQuoteWithMetadata };
+
+ (selectBridgeQuotes as unknown as jest.Mock).mockImplementation(() => ({
+ recommendedQuote,
+ sortedQuotes: [recommendedQuote],
+ alternativeQuotes: [],
+ }));
+
+ const bridgeReducerOverrides = {
+ selectedQuoteRequestId: 'non-existent-quote-id',
+ };
+
+ const testState = createBridgeTestState({
+ bridgeReducerOverrides,
+ });
+
+ const { result } = renderHookWithProvider(() => useBridgeQuoteData(), {
+ state: testState,
+ });
+
+ expect(result.current.activeQuote).toEqual(recommendedQuote);
+ expect(result.current.bestQuote).toEqual(recommendedQuote);
+ });
+
+ it('dispatches setSelectedQuoteRequestId(undefined) when manuallySelectedQuote is undefined', async () => {
+ (selectBridgeQuotes as unknown as jest.Mock).mockImplementation(() => ({
+ recommendedQuote: mockQuoteWithMetadata,
+ sortedQuotes: [],
+ alternativeQuotes: [],
+ }));
+
+ // selectedQuoteRequestId is set but sortedQuotes is empty so manuallySelectedQuote will be undefined
+ const bridgeReducerOverrides = {
+ selectedQuoteRequestId: 'some-quote-id',
+ };
+
+ const testState = createBridgeTestState({
+ bridgeReducerOverrides,
+ });
+
+ const { store } = renderHookWithProvider(() => useBridgeQuoteData(), {
+ state: testState,
+ });
+
+ // After the effect runs, selectedQuoteRequestId should be cleared in the store
+ await waitFor(() => {
+ expect(
+ (store.getState() as { bridge: { selectedQuoteRequestId?: string } })
+ .bridge.selectedQuoteRequestId,
+ ).toBeUndefined();
+ });
+ });
+
+ it('does not override activeQuote with manually selected when expired and not refreshing', () => {
+ const manuallySelectedQuote = {
+ ...mockQuoteWithMetadata,
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ requestId: 'selected-quote-id',
+ },
+ };
+
+ const recommendedQuote = {
+ ...mockQuoteWithMetadata,
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ requestId: 'best-quote-id',
+ },
+ };
+
+ (selectBridgeQuotes as unknown as jest.Mock).mockImplementation(() => ({
+ recommendedQuote,
+ sortedQuotes: [recommendedQuote, manuallySelectedQuote],
+ alternativeQuotes: [],
+ }));
+
+ (isQuoteExpired as jest.Mock).mockReturnValue(true);
+ (shouldRefreshQuote as jest.Mock).mockReturnValue(false);
+
+ const bridgeReducerOverrides = {
+ selectedQuoteRequestId: 'selected-quote-id',
+ isSubmittingTx: false,
+ };
+
+ const testState = createBridgeTestState({
+ bridgeReducerOverrides,
+ });
+
+ const { result } = renderHookWithProvider(() => useBridgeQuoteData(), {
+ state: testState,
+ });
+
+ // When expired and not refreshing and not submitting, activeQuote should be undefined
+ expect(result.current.activeQuote).toBeUndefined();
+ expect(result.current.isExpired).toBe(true);
+ });
+
+ it('keeps activeQuote as manually selected when expired but still submitting', () => {
+ const manuallySelectedQuote = {
+ ...mockQuoteWithMetadata,
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ requestId: 'selected-quote-id',
+ },
+ };
+
+ const recommendedQuote = {
+ ...mockQuoteWithMetadata,
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ requestId: 'best-quote-id',
+ },
+ };
+
+ (selectBridgeQuotes as unknown as jest.Mock).mockImplementation(() => ({
+ recommendedQuote,
+ sortedQuotes: [recommendedQuote, manuallySelectedQuote],
+ alternativeQuotes: [],
+ }));
+
+ (isQuoteExpired as jest.Mock).mockReturnValue(true);
+ (shouldRefreshQuote as jest.Mock).mockReturnValue(false);
+
+ const bridgeReducerOverrides = {
+ selectedQuoteRequestId: 'selected-quote-id',
+ isSubmittingTx: true,
+ };
+
+ const testState = createBridgeTestState({
+ bridgeReducerOverrides,
+ });
+
+ const { result } = renderHookWithProvider(() => useBridgeQuoteData(), {
+ state: testState,
+ });
+
+ // When isSubmittingTx is true, activeQuote should remain (even if expired)
+ expect(result.current.activeQuote).toEqual(manuallySelectedQuote);
+ });
+ });
+
// Test willRefresh scenarios
describe('willRefresh behavior', () => {
it('sets willRefresh to true when conditions are met', () => {
diff --git a/app/components/UI/Bridge/hooks/useDisplayCurrencyValue/index.test.ts b/app/components/UI/Bridge/hooks/useDisplayCurrencyValue/index.test.ts
new file mode 100644
index 00000000000..58c1d2b14dc
--- /dev/null
+++ b/app/components/UI/Bridge/hooks/useDisplayCurrencyValue/index.test.ts
@@ -0,0 +1,322 @@
+import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider';
+import { useDisplayCurrencyValue } from './index';
+import { getDisplayCurrencyValue } from '../../utils/exchange-rates';
+import { selectTokenMarketData } from '../../../../../selectors/tokenRatesController';
+import {
+ selectCurrencyRates,
+ selectCurrentCurrency,
+} from '../../../../../selectors/currencyRateController';
+import { selectNetworkConfigurations } from '../../../../../selectors/networkController';
+import { selectMultichainAssetsRates } from '../../../../../selectors/multichain';
+import { BridgeToken } from '../../types';
+import { CHAIN_IDS } from '@metamask/transaction-controller';
+
+jest.mock('../../utils/exchange-rates');
+jest.mock('../../../../../selectors/tokenRatesController');
+jest.mock('../../../../../selectors/currencyRateController');
+jest.mock('../../../../../selectors/networkController');
+jest.mock('../../../../../selectors/multichain', () => ({
+ selectMultichainAssetsRates: jest.fn(),
+}));
+
+const mockGetDisplayCurrencyValue =
+ getDisplayCurrencyValue as jest.MockedFunction<
+ typeof getDisplayCurrencyValue
+ >;
+const mockSelectTokenMarketData = selectTokenMarketData as jest.MockedFunction<
+ typeof selectTokenMarketData
+>;
+const mockSelectCurrencyRates = selectCurrencyRates as jest.MockedFunction<
+ typeof selectCurrencyRates
+>;
+const mockSelectCurrentCurrency = selectCurrentCurrency as jest.MockedFunction<
+ typeof selectCurrentCurrency
+>;
+const mockSelectNetworkConfigurations =
+ selectNetworkConfigurations as jest.MockedFunction<
+ typeof selectNetworkConfigurations
+ >;
+const mockSelectMultichainAssetsRates =
+ selectMultichainAssetsRates as jest.MockedFunction<
+ typeof selectMultichainAssetsRates
+ >;
+
+const MOCK_MARKET_DATA = {
+ '0x1': { '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { price: 1 } },
+} as unknown as ReturnType;
+
+const MOCK_CURRENCY_RATES = {
+ ETH: { conversionRate: 2500, usdConversionRate: 2500 },
+} as unknown as ReturnType;
+
+const MOCK_NETWORK_CONFIGS = {
+ '0x1': { nativeCurrency: 'ETH' },
+} as unknown as ReturnType;
+
+const MOCK_MULTICHAIN_RATES = {} as ReturnType<
+ typeof selectMultichainAssetsRates
+>;
+
+const makeToken = (overrides?: Partial): BridgeToken => ({
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ symbol: 'USDC',
+ decimals: 6,
+ chainId: CHAIN_IDS.MAINNET,
+ ...overrides,
+});
+
+describe('useDisplayCurrencyValue', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockSelectTokenMarketData.mockReturnValue(MOCK_MARKET_DATA);
+ mockSelectCurrencyRates.mockReturnValue(MOCK_CURRENCY_RATES);
+ mockSelectCurrentCurrency.mockReturnValue('USD');
+ mockSelectNetworkConfigurations.mockReturnValue(MOCK_NETWORK_CONFIGS);
+ mockSelectMultichainAssetsRates.mockReturnValue(MOCK_MULTICHAIN_RATES);
+ mockGetDisplayCurrencyValue.mockReturnValue('$0.00');
+ });
+
+ describe('return value', () => {
+ it('returns the value from getDisplayCurrencyValue', () => {
+ // Arrange
+ mockGetDisplayCurrencyValue.mockReturnValue('$100.00');
+ const token = makeToken();
+
+ // Act
+ const { result } = renderHookWithProvider(
+ () => useDisplayCurrencyValue('100', token),
+ { state: {} },
+ );
+
+ // Assert
+ expect(result.current).toBe('$100.00');
+ });
+
+ it('returns "$0.00" when amount is undefined', () => {
+ // Arrange
+ mockGetDisplayCurrencyValue.mockReturnValue('$0.00');
+
+ // Act
+ const { result } = renderHookWithProvider(
+ () => useDisplayCurrencyValue(undefined, makeToken()),
+ { state: {} },
+ );
+
+ // Assert
+ expect(result.current).toBe('$0.00');
+ });
+
+ it('returns "$0.00" when token is undefined', () => {
+ // Arrange
+ mockGetDisplayCurrencyValue.mockReturnValue('$0.00');
+
+ // Act
+ const { result } = renderHookWithProvider(
+ () => useDisplayCurrencyValue('100', undefined),
+ { state: {} },
+ );
+
+ // Assert
+ expect(result.current).toBe('$0.00');
+ });
+
+ it('returns "$0.00" when both amount and token are undefined', () => {
+ // Arrange
+ mockGetDisplayCurrencyValue.mockReturnValue('$0.00');
+
+ // Act
+ const { result } = renderHookWithProvider(
+ () => useDisplayCurrencyValue(undefined, undefined),
+ { state: {} },
+ );
+
+ // Assert
+ expect(result.current).toBe('$0.00');
+ });
+ });
+
+ describe('selector forwarding', () => {
+ it('forwards amount and token to getDisplayCurrencyValue', () => {
+ // Arrange
+ const token = makeToken();
+
+ // Act
+ renderHookWithProvider(() => useDisplayCurrencyValue('50', token), {
+ state: {},
+ });
+
+ // Assert
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledWith(
+ expect.objectContaining({ amount: '50', token }),
+ );
+ });
+
+ it('forwards evmMultiChainMarketData from selectTokenMarketData', () => {
+ // Arrange
+ const customMarketData = {
+ '0x1': { '0xabc': { price: 2.5 } },
+ } as unknown as ReturnType;
+ mockSelectTokenMarketData.mockReturnValue(customMarketData);
+
+ // Act
+ renderHookWithProvider(() => useDisplayCurrencyValue('1', makeToken()), {
+ state: {},
+ });
+
+ // Assert
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledWith(
+ expect.objectContaining({ evmMultiChainMarketData: customMarketData }),
+ );
+ });
+
+ it('forwards evmMultiChainCurrencyRates from selectCurrencyRates', () => {
+ // Arrange
+ const customRates = {
+ ETH: { conversionRate: 3000, usdConversionRate: 3000 },
+ } as unknown as ReturnType;
+ mockSelectCurrencyRates.mockReturnValue(customRates);
+
+ // Act
+ renderHookWithProvider(() => useDisplayCurrencyValue('1', makeToken()), {
+ state: {},
+ });
+
+ // Assert
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledWith(
+ expect.objectContaining({
+ evmMultiChainCurrencyRates: customRates,
+ }),
+ );
+ });
+
+ it('forwards networkConfigurationsByChainId from selectNetworkConfigurations', () => {
+ // Arrange
+ const customNetworks = {
+ '0x89': { nativeCurrency: 'POL' },
+ } as unknown as ReturnType;
+ mockSelectNetworkConfigurations.mockReturnValue(customNetworks);
+
+ // Act
+ renderHookWithProvider(() => useDisplayCurrencyValue('1', makeToken()), {
+ state: {},
+ });
+
+ // Assert
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledWith(
+ expect.objectContaining({
+ networkConfigurationsByChainId: customNetworks,
+ }),
+ );
+ });
+
+ it('forwards currentCurrency from selectCurrentCurrency', () => {
+ // Arrange
+ mockSelectCurrentCurrency.mockReturnValue('EUR');
+
+ // Act
+ renderHookWithProvider(() => useDisplayCurrencyValue('1', makeToken()), {
+ state: {},
+ });
+
+ // Assert
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledWith(
+ expect.objectContaining({ currentCurrency: 'EUR' }),
+ );
+ });
+
+ it('forwards nonEvmMultichainAssetRates from selectMultichainAssetsRates', () => {
+ // Arrange
+ const customMultichainRates = {
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': { rate: '150' },
+ } as unknown as ReturnType;
+ mockSelectMultichainAssetsRates.mockReturnValue(customMultichainRates);
+
+ // Act
+ renderHookWithProvider(() => useDisplayCurrencyValue('1', makeToken()), {
+ state: {},
+ });
+
+ // Assert
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledWith(
+ expect.objectContaining({
+ nonEvmMultichainAssetRates: customMultichainRates,
+ }),
+ );
+ });
+ });
+
+ describe('different currencies', () => {
+ it('returns EUR-formatted value when current currency is EUR', () => {
+ // Arrange
+ mockSelectCurrentCurrency.mockReturnValue('EUR');
+ mockGetDisplayCurrencyValue.mockReturnValue('€42.00');
+
+ // Act
+ const { result } = renderHookWithProvider(
+ () => useDisplayCurrencyValue('42', makeToken()),
+ { state: {} },
+ );
+
+ // Assert
+ expect(result.current).toBe('€42.00');
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledWith(
+ expect.objectContaining({ currentCurrency: 'EUR' }),
+ );
+ });
+
+ it('returns GBP-formatted value when current currency is GBP', () => {
+ // Arrange
+ mockSelectCurrentCurrency.mockReturnValue('GBP');
+ mockGetDisplayCurrencyValue.mockReturnValue('£10.50');
+
+ // Act
+ const { result } = renderHookWithProvider(
+ () => useDisplayCurrencyValue('10', makeToken()),
+ { state: {} },
+ );
+
+ // Assert
+ expect(result.current).toBe('£10.50');
+ });
+
+ it('returns small-value threshold string', () => {
+ // Arrange
+ mockGetDisplayCurrencyValue.mockReturnValue('< $0.01');
+
+ // Act
+ const { result } = renderHookWithProvider(
+ () => useDisplayCurrencyValue('0.000001', makeToken()),
+ { state: {} },
+ );
+
+ // Assert
+ expect(result.current).toBe('< $0.01');
+ });
+ });
+
+ describe('non-EVM token', () => {
+ it('forwards a non-EVM token to getDisplayCurrencyValue', () => {
+ // Arrange
+ const solanaToken = makeToken({
+ chainId:
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as BridgeToken['chainId'],
+ address: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
+ symbol: 'SOL',
+ decimals: 9,
+ });
+ mockGetDisplayCurrencyValue.mockReturnValue('$15.00');
+
+ // Act
+ const { result } = renderHookWithProvider(
+ () => useDisplayCurrencyValue('0.1', solanaToken),
+ { state: {} },
+ );
+
+ // Assert
+ expect(result.current).toBe('$15.00');
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledWith(
+ expect.objectContaining({ token: solanaToken, amount: '0.1' }),
+ );
+ });
+ });
+});
diff --git a/app/components/UI/Bridge/hooks/useDisplayCurrencyValue/index.ts b/app/components/UI/Bridge/hooks/useDisplayCurrencyValue/index.ts
new file mode 100644
index 00000000000..954258b5fa4
--- /dev/null
+++ b/app/components/UI/Bridge/hooks/useDisplayCurrencyValue/index.ts
@@ -0,0 +1,41 @@
+import { useSelector } from 'react-redux';
+import { BridgeToken } from '../../types';
+import { getDisplayCurrencyValue } from '../../utils/exchange-rates';
+import { selectTokenMarketData } from '../../../../../selectors/tokenRatesController';
+import {
+ selectCurrencyRates,
+ selectCurrentCurrency,
+} from '../../../../../selectors/currencyRateController';
+import { selectNetworkConfigurations } from '../../../../../selectors/networkController';
+///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
+import { selectMultichainAssetsRates } from '../../../../../selectors/multichain';
+///: END:ONLY_INCLUDE_IF(keyring-snaps)
+
+export const useDisplayCurrencyValue = (
+ amount?: string,
+ token?: BridgeToken,
+) => {
+ const evmMultiChainMarketData = useSelector(selectTokenMarketData);
+ const evmMultiChainCurrencyRates = useSelector(selectCurrencyRates);
+ const networkConfigurationsByChainId = useSelector(
+ selectNetworkConfigurations,
+ );
+ const currentCurrency = useSelector(selectCurrentCurrency);
+
+ let nonEvmMultichainAssetRates = {};
+ ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
+ nonEvmMultichainAssetRates = useSelector(selectMultichainAssetsRates);
+ ///: END:ONLY_INCLUDE_IF(keyring-snaps)
+
+ const currencyValue = getDisplayCurrencyValue({
+ token,
+ amount,
+ evmMultiChainMarketData,
+ networkConfigurationsByChainId,
+ evmMultiChainCurrencyRates,
+ currentCurrency,
+ nonEvmMultichainAssetRates,
+ });
+
+ return currencyValue;
+};
diff --git a/app/components/UI/Bridge/hooks/useTokenInputAreaFormattedBalance/index.test.ts b/app/components/UI/Bridge/hooks/useFormattedBalanceWithThreshold/index.test.ts
similarity index 78%
rename from app/components/UI/Bridge/hooks/useTokenInputAreaFormattedBalance/index.test.ts
rename to app/components/UI/Bridge/hooks/useFormattedBalanceWithThreshold/index.test.ts
index 9ffaf4fb7dc..9d52402f158 100644
--- a/app/components/UI/Bridge/hooks/useTokenInputAreaFormattedBalance/index.test.ts
+++ b/app/components/UI/Bridge/hooks/useFormattedBalanceWithThreshold/index.test.ts
@@ -1,5 +1,5 @@
import { renderHook } from '@testing-library/react-hooks';
-import { useTokenInputAreaFormattedBalance } from '.';
+import { useFormattedBalanceWithThreshold } from '.';
import { BridgeToken } from '../../types';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import I18n from '../../../../../../locales/i18n';
@@ -18,7 +18,7 @@ const makeToken = (overrides?: Partial): BridgeToken => ({
...overrides,
});
-describe('useTokenInputAreaFormattedBalance', () => {
+describe('useFormattedBalanceWithThreshold', () => {
beforeEach(() => {
mockI18n.locale = 'en';
});
@@ -26,7 +26,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
describe('guard clauses', () => {
it('returns undefined when token is undefined', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('100', undefined),
+ useFormattedBalanceWithThreshold('100', undefined),
);
expect(result.current).toBeUndefined();
@@ -36,7 +36,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
const token = makeToken({ symbol: '' });
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('100', token),
+ useFormattedBalanceWithThreshold('100', token),
);
expect(result.current).toBeUndefined();
@@ -46,7 +46,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
const token = makeToken();
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance(undefined, token),
+ useFormattedBalanceWithThreshold(undefined, token),
);
expect(result.current).toBeUndefined();
@@ -56,7 +56,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
const token = makeToken();
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('', token),
+ useFormattedBalanceWithThreshold('', token),
);
expect(result.current).toBeUndefined();
@@ -64,7 +64,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('returns undefined when both token and tokenBalance are missing', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance(undefined, undefined),
+ useFormattedBalanceWithThreshold(undefined, undefined),
);
expect(result.current).toBeUndefined();
@@ -76,7 +76,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('returns "< 0.00001" for a value just below threshold', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('0.000009', token),
+ useFormattedBalanceWithThreshold('0.000009', token),
);
expect(result.current).toBe('< 0.00001');
@@ -84,7 +84,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('returns "< 0.00001" for extremely small positive values', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('0.000000000000000001', token),
+ useFormattedBalanceWithThreshold('0.000000000000000001', token),
);
expect(result.current).toBe('< 0.00001');
@@ -92,7 +92,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('does not trigger for value equal to threshold (0.00001)', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('0.00001', token),
+ useFormattedBalanceWithThreshold('0.00001', token),
);
expect(result.current).not.toBe('< 0.00001');
@@ -101,7 +101,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('does not trigger for value above threshold', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('0.0001', token),
+ useFormattedBalanceWithThreshold('0.0001', token),
);
expect(result.current).not.toBe('< 0.00001');
@@ -110,7 +110,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('does not trigger for zero', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('0', token),
+ useFormattedBalanceWithThreshold('0', token),
);
expect(result.current).toBe('0 USDC');
@@ -118,7 +118,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('does not trigger for negative values', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('-0.000001', token),
+ useFormattedBalanceWithThreshold('-0.000001', token),
);
// parseAmount can't parse negative numbers (regex doesn't match "-")
@@ -132,7 +132,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('formats a single digit', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('5', token),
+ useFormattedBalanceWithThreshold('5', token),
);
expect(result.current).toBe('5 USDC');
@@ -140,7 +140,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('formats a three-digit number without grouping', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('999', token),
+ useFormattedBalanceWithThreshold('999', token),
);
expect(result.current).toBe('999 USDC');
@@ -148,7 +148,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('formats thousands with grouping separator', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1000', token),
+ useFormattedBalanceWithThreshold('1000', token),
);
expect(result.current).toBe('1,000 USDC');
@@ -156,7 +156,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('formats millions with grouping separators', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1234567', token),
+ useFormattedBalanceWithThreshold('1234567', token),
);
expect(result.current).toBe('1,234,567 USDC');
@@ -164,7 +164,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('formats billions', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1000000000', token),
+ useFormattedBalanceWithThreshold('1000000000', token),
);
expect(result.current).toBe('1,000,000,000 USDC');
@@ -172,7 +172,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('strips leading zeros from integers', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('007', token),
+ useFormattedBalanceWithThreshold('007', token),
);
expect(result.current).toBe('7 USDC');
@@ -184,7 +184,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('preserves up to 5 decimal places', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1.12345', token),
+ useFormattedBalanceWithThreshold('1.12345', token),
);
expect(result.current).toBe('1.12345 USDC');
@@ -192,7 +192,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('truncates beyond 5 decimal places', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1.123456789', token),
+ useFormattedBalanceWithThreshold('1.123456789', token),
);
expect(result.current).toBe('1.12345 USDC');
@@ -200,7 +200,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('trims trailing zeros after truncation', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1.10000', token),
+ useFormattedBalanceWithThreshold('1.10000', token),
);
expect(result.current).toBe('1.1 USDC');
@@ -208,7 +208,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('formats decimal with thousands in integer part', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('12345.6789', token),
+ useFormattedBalanceWithThreshold('12345.6789', token),
);
expect(result.current).toBe('12,345.6789 USDC');
@@ -216,7 +216,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('handles value with only a fractional part', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('.5', token),
+ useFormattedBalanceWithThreshold('.5', token),
);
expect(result.current).toBe('0.5 USDC');
@@ -228,7 +228,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('formats a 12-digit integer', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('999999999999', token),
+ useFormattedBalanceWithThreshold('999999999999', token),
);
expect(result.current).toBe('999,999,999,999 USDC');
@@ -236,7 +236,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('formats a large number with decimals', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('123456789.12345', token),
+ useFormattedBalanceWithThreshold('123456789.12345', token),
);
expect(result.current).toBe('123,456,789.12345 USDC');
@@ -244,7 +244,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('handles numbers beyond safe integer range', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('99999999999999999999', token),
+ useFormattedBalanceWithThreshold('99999999999999999999', token),
);
expect(result.current).toMatch(/^[\d,]+ USDC$/);
@@ -256,7 +256,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('returns raw tokenBalance with symbol for strings with commas', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1,234.56', token),
+ useFormattedBalanceWithThreshold('1,234.56', token),
);
expect(result.current).toBe('1,234.56 USDC');
@@ -264,7 +264,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('returns raw tokenBalance with symbol for scientific notation', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1e3', token),
+ useFormattedBalanceWithThreshold('1e3', token),
);
expect(result.current).toBe('1e3 USDC');
@@ -272,7 +272,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
it('returns raw tokenBalance with symbol for non-numeric strings', () => {
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('abc', token),
+ useFormattedBalanceWithThreshold('abc', token),
);
expect(result.current).toBe('abc USDC');
@@ -284,7 +284,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
const token = makeToken({ symbol: 'ETH' });
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1.5', token),
+ useFormattedBalanceWithThreshold('1.5', token),
);
expect(result.current).toBe('1.5 ETH');
@@ -294,7 +294,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
const wbtc = makeToken({ symbol: 'WBTC' });
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('0.00123', wbtc),
+ useFormattedBalanceWithThreshold('0.00123', wbtc),
);
expect(result.current).toBe('0.00123 WBTC');
@@ -304,7 +304,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
const token = makeToken({ symbol: 'ETH' });
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('0.000001', token),
+ useFormattedBalanceWithThreshold('0.000001', token),
);
expect(result.current).toBe('< 0.00001');
@@ -315,7 +315,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
const token = makeToken({ symbol: 'DAI' });
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1e3', token),
+ useFormattedBalanceWithThreshold('1e3', token),
);
expect(result.current).toBe('1e3 DAI');
@@ -329,7 +329,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
mockI18n.locale = 'de-DE';
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1234567.89', token),
+ useFormattedBalanceWithThreshold('1234567.89', token),
);
expect(result.current).toMatch(/1\.234\.567/);
@@ -340,7 +340,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
mockI18n.locale = 'fr-FR';
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1234567.89', token),
+ useFormattedBalanceWithThreshold('1234567.89', token),
);
// French uses narrow no-break space (U+202F) or non-breaking space for grouping
@@ -352,7 +352,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
mockI18n.locale = 'de-DE';
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('50000', token),
+ useFormattedBalanceWithThreshold('50000', token),
);
expect(result.current).toBe('50.000 USDC');
@@ -362,7 +362,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
mockI18n.locale = 'ja-JP';
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1000000', token),
+ useFormattedBalanceWithThreshold('1000000', token),
);
expect(result.current).toBe('1,000,000 USDC');
@@ -372,7 +372,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
mockI18n.locale = 'de-DE';
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('999', token),
+ useFormattedBalanceWithThreshold('999', token),
);
expect(result.current).toBe('999 USDC');
@@ -382,7 +382,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
mockI18n.locale = 'de-DE';
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('0.000001', token),
+ useFormattedBalanceWithThreshold('0.000001', token),
);
expect(result.current).toBe('< 0.00001');
@@ -392,7 +392,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
mockI18n.locale = 'pt-BR';
const { result } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1234.56', token),
+ useFormattedBalanceWithThreshold('1234.56', token),
);
expect(result.current).toMatch(/1\.234/);
@@ -405,7 +405,7 @@ describe('useTokenInputAreaFormattedBalance', () => {
const token = makeToken();
const { result, rerender } = renderHook(() =>
- useTokenInputAreaFormattedBalance('1000', token),
+ useFormattedBalanceWithThreshold('1000', token),
);
const firstResult = result.current;
diff --git a/app/components/UI/Bridge/hooks/useTokenInputAreaFormattedBalance/index.ts b/app/components/UI/Bridge/hooks/useFormattedBalanceWithThreshold/index.ts
similarity index 95%
rename from app/components/UI/Bridge/hooks/useTokenInputAreaFormattedBalance/index.ts
rename to app/components/UI/Bridge/hooks/useFormattedBalanceWithThreshold/index.ts
index d0c0ef10880..7cc4259d6b8 100644
--- a/app/components/UI/Bridge/hooks/useTokenInputAreaFormattedBalance/index.ts
+++ b/app/components/UI/Bridge/hooks/useFormattedBalanceWithThreshold/index.ts
@@ -4,7 +4,7 @@ import { MINIMUM_DISPLAY_THRESHOLD } from '../../../../../util/number';
import { formatAmountWithLocaleSeparators } from '../../utils/formatAmountWithLocaleSeparators';
import parseAmount from '../../../../../util/parseAmount';
-export const useTokenInputAreaFormattedBalance = (
+export const useFormattedBalanceWithThreshold = (
tokenBalance?: string,
token?: BridgeToken,
) =>
diff --git a/app/components/UI/Bridge/hooks/useTrackAllQuotesSortedEvent/index.test.ts b/app/components/UI/Bridge/hooks/useTrackAllQuotesSortedEvent/index.test.ts
new file mode 100644
index 00000000000..67194901d4a
--- /dev/null
+++ b/app/components/UI/Bridge/hooks/useTrackAllQuotesSortedEvent/index.test.ts
@@ -0,0 +1,517 @@
+import { renderHook } from '@testing-library/react-native';
+import { useTrackAllQuotesSortedEvent } from './index';
+import Engine from '../../../../../core/Engine';
+import {
+ SortOrder,
+ UnifiedSwapBridgeEventName,
+ type Quote,
+} from '@metamask/bridge-controller';
+import { BigNumber } from 'ethers';
+import { useSelector } from 'react-redux';
+import {
+ selectSourceAmount,
+ selectSourceToken,
+ selectDestToken,
+ selectIsBridge,
+} from '../../../../../core/redux/slices/bridge';
+import { selectShouldUseSmartTransaction } from '../../../../../selectors/smartTransactionsController';
+
+// Mock Engine
+jest.mock('../../../../../core/Engine', () => ({
+ context: {
+ BridgeController: {
+ trackUnifiedSwapBridgeEvent: jest.fn(),
+ },
+ },
+}));
+
+// Mock useLatestBalance
+const mockUseLatestBalance = jest.fn();
+jest.mock('../useLatestBalance', () => ({
+ useLatestBalance: (params: unknown) => mockUseLatestBalance(params),
+}));
+
+// Mock useIsInsufficientBalance
+const mockUseIsInsufficientBalance = jest.fn();
+jest.mock('../useInsufficientBalance', () => ({
+ __esModule: true,
+ default: (params: unknown) => mockUseIsInsufficientBalance(params),
+}));
+
+// Mock Redux selectors - use a mutable object so we can change values in tests
+const mockSelectorValues = {
+ sourceToken: {
+ symbol: 'ETH',
+ chainId: '0x1',
+ address: '0x0000000000000000000000000000000000000000',
+ decimals: 18,
+ },
+ destToken: {
+ symbol: 'USDC',
+ chainId: '0x89',
+ address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
+ decimals: 6,
+ },
+ sourceAmount: '1.5',
+ smartTransactionsEnabled: true,
+ isBridge: true,
+};
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest.fn(),
+}));
+
+const mockUseSelector = useSelector as jest.Mock;
+
+// Mock getNativeAssetForChainId
+jest.mock('@metamask/bridge-controller', () => ({
+ ...jest.requireActual('@metamask/bridge-controller'),
+ getNativeAssetForChainId: jest.fn((chainId: string) => ({
+ symbol: chainId === '0x1' ? 'ETH' : 'MATIC',
+ })),
+ formatProviderLabel: jest.fn((quote: Quote) => quote.bridges[0]),
+}));
+
+describe('useTrackAllQuotesSortedEvent', () => {
+ const mockQuote = {
+ requestId: 'test-request-id',
+ srcChainId: 1,
+ destChainId: 137,
+ srcTokenAmount: '1500000000000000000',
+ destTokenAmount: '3000000000',
+ minDestTokenAmount: '2900000000',
+ bridgeId: 'lifi',
+ srcAsset: {
+ chainId: 1,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ decimals: 18,
+ icon: '',
+ assetId: 'eip155:1/slip44:60' as const,
+ },
+ destAsset: {
+ chainId: 137,
+ address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ decimals: 6,
+ icon: '',
+ assetId:
+ 'eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174' as const,
+ },
+ feeData: {
+ metabridge: {
+ amount: '0',
+ asset: {
+ chainId: 1,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ decimals: 18,
+ icon: '',
+ assetId: 'eip155:1/slip44:60' as const,
+ },
+ },
+ },
+ bridges: ['lifi'],
+ steps: [],
+ priceData: {
+ priceImpact: '0.05',
+ },
+ gasIncluded: false,
+ } as unknown as Quote;
+
+ const mockLatestBalance = {
+ displayBalance: '10',
+ atomicBalance: BigNumber.from('10000000000000000000'),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseIsInsufficientBalance.mockReturnValue(false);
+ (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mockClear();
+
+ // Reset mock values to defaults
+ mockSelectorValues.sourceToken = {
+ symbol: 'ETH',
+ chainId: '0x1',
+ address: '0x0000000000000000000000000000000000000000',
+ decimals: 18,
+ };
+ mockSelectorValues.destToken = {
+ symbol: 'USDC',
+ chainId: '0x89',
+ address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
+ decimals: 6,
+ };
+ mockSelectorValues.sourceAmount = '1.5';
+ mockSelectorValues.smartTransactionsEnabled = true;
+ mockSelectorValues.isBridge = true;
+
+ // Setup useSelector mock to read from mockSelectorValues
+ mockUseSelector.mockImplementation((selector) => {
+ if (selector === selectSourceAmount) {
+ return mockSelectorValues.sourceAmount;
+ }
+ if (selector === selectSourceToken) {
+ return mockSelectorValues.sourceToken;
+ }
+ if (selector === selectDestToken) {
+ return mockSelectorValues.destToken;
+ }
+ if (selector === selectShouldUseSmartTransaction) {
+ return mockSelectorValues.smartTransactionsEnabled;
+ }
+ if (selector === selectIsBridge) {
+ return mockSelectorValues.isBridge;
+ }
+ return null;
+ });
+ });
+
+ describe('return value', () => {
+ it('returns a function', () => {
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ expect(typeof result.current).toBe('function');
+ });
+ });
+
+ describe('event tracking for bridge transactions', () => {
+ it('calls trackUnifiedSwapBridgeEvent with correct parameters for bridge', () => {
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ expect(
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent,
+ ).toHaveBeenCalledWith(UnifiedSwapBridgeEventName.AllQuotesSorted, {
+ can_submit: true,
+ price_impact: 0.05,
+ gas_included: false,
+ gas_included_7702: false,
+ token_symbol_source: 'ETH',
+ token_symbol_destination: 'USDC',
+ stx_enabled: true,
+ sort_order: SortOrder.COST_ASC,
+ best_quote_provider: 'lifi',
+ });
+ });
+
+ it('includes sort_order and best_quote_provider for bridge transactions', () => {
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData).toHaveProperty('sort_order', SortOrder.COST_ASC);
+ expect(eventData).toHaveProperty('best_quote_provider', 'lifi');
+ });
+ });
+
+ describe('event tracking for swap transactions', () => {
+ it('excludes sort_order and best_quote_provider for swap transactions', () => {
+ mockSelectorValues.isBridge = false; // Swap transaction
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData).not.toHaveProperty('sort_order');
+ expect(eventData).not.toHaveProperty('best_quote_provider');
+ });
+ });
+
+ describe('insufficient balance handling', () => {
+ it('sets can_submit to false when balance is insufficient', () => {
+ mockUseIsInsufficientBalance.mockReturnValue(true);
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.can_submit).toBe(false);
+ });
+
+ it('sets can_submit to true when balance is sufficient', () => {
+ mockUseIsInsufficientBalance.mockReturnValue(false);
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.can_submit).toBe(true);
+ });
+ });
+
+ describe('quote data handling', () => {
+ it('handles missing priceData gracefully', () => {
+ const quoteWithoutPriceData = {
+ ...mockQuote,
+ priceData: undefined,
+ } as Quote;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(quoteWithoutPriceData);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.price_impact).toBe(0);
+ });
+
+ it('handles missing priceImpact in priceData', () => {
+ const quoteWithoutPriceImpact = {
+ ...mockQuote,
+ priceData: {},
+ } as Quote;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(quoteWithoutPriceImpact);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.price_impact).toBe(0);
+ });
+
+ it('converts priceImpact string to number', () => {
+ const quoteWithStringPriceImpact = {
+ ...mockQuote,
+ priceData: {
+ priceImpact: '0.123',
+ },
+ } as Quote;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(quoteWithStringPriceImpact);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.price_impact).toBe(0.123);
+ });
+ });
+
+ describe('gas included handling', () => {
+ it('sets gas_included to true when quote has gasIncluded', () => {
+ const quoteWithGasIncluded = {
+ ...mockQuote,
+ gasIncluded: true,
+ } as Quote;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(quoteWithGasIncluded);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.gas_included).toBe(true);
+ });
+
+ it('sets gas_included_7702 to true when quote has gasIncluded7702', () => {
+ const quoteWithGasIncluded7702 = {
+ ...mockQuote,
+ gasIncluded7702: true,
+ } as unknown as Quote;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(quoteWithGasIncluded7702);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.gas_included_7702).toBe(true);
+ });
+ });
+
+ describe('token symbol handling', () => {
+ it('uses sourceToken symbol when available', () => {
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.token_symbol_source).toBe('ETH');
+ });
+
+ it('falls back to native asset symbol when sourceToken has no symbol', () => {
+ mockSelectorValues.sourceToken = {
+ ...mockSelectorValues.sourceToken,
+ symbol: undefined as unknown as string,
+ };
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.token_symbol_source).toBe('ETH');
+ });
+
+ it('uses space string when sourceToken is undefined', () => {
+ mockSelectorValues.sourceToken =
+ undefined as unknown as typeof mockSelectorValues.sourceToken;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.token_symbol_source).toBe(' ');
+ });
+
+ it('uses destToken symbol when available', () => {
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.token_symbol_destination).toBe('USDC');
+ });
+
+ it('uses null when destToken is undefined', () => {
+ mockSelectorValues.destToken =
+ undefined as unknown as typeof mockSelectorValues.destToken;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.token_symbol_destination).toBe(null);
+ });
+ });
+
+ describe('smart transactions handling', () => {
+ it('sets stx_enabled to true when smart transactions are enabled', () => {
+ mockSelectorValues.smartTransactionsEnabled = true;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.stx_enabled).toBe(true);
+ });
+
+ it('sets stx_enabled to false when smart transactions are disabled', () => {
+ mockSelectorValues.smartTransactionsEnabled = false;
+
+ const { result } = renderHook(() =>
+ useTrackAllQuotesSortedEvent(mockLatestBalance),
+ );
+
+ result.current(mockQuote);
+
+ const eventData = (
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent as jest.Mock
+ ).mock.calls[0][1];
+
+ expect(eventData.stx_enabled).toBe(false);
+ });
+ });
+
+ describe('latestSourceBalance parameter', () => {
+ it('passes latestSourceBalance to useIsInsufficientBalance', () => {
+ renderHook(() => useTrackAllQuotesSortedEvent(mockLatestBalance));
+
+ expect(mockUseIsInsufficientBalance).toHaveBeenCalledWith({
+ amount: mockSelectorValues.sourceAmount,
+ token: mockSelectorValues.sourceToken,
+ latestAtomicBalance: mockLatestBalance.atomicBalance,
+ });
+ });
+
+ it('handles undefined latestSourceBalance', () => {
+ renderHook(() => useTrackAllQuotesSortedEvent(undefined));
+
+ expect(mockUseIsInsufficientBalance).toHaveBeenCalledWith({
+ amount: mockSelectorValues.sourceAmount,
+ token: mockSelectorValues.sourceToken,
+ latestAtomicBalance: undefined,
+ });
+ });
+ });
+});
diff --git a/app/components/UI/Bridge/hooks/useTrackAllQuotesSortedEvent/index.ts b/app/components/UI/Bridge/hooks/useTrackAllQuotesSortedEvent/index.ts
new file mode 100644
index 00000000000..ffb9106181f
--- /dev/null
+++ b/app/components/UI/Bridge/hooks/useTrackAllQuotesSortedEvent/index.ts
@@ -0,0 +1,58 @@
+import {
+ formatProviderLabel,
+ getNativeAssetForChainId,
+ Quote,
+ SortOrder,
+ UnifiedSwapBridgeEventName,
+} from '@metamask/bridge-controller';
+import Engine from '../../../../../core/Engine';
+import { useLatestBalance } from '../useLatestBalance';
+import { useSelector } from 'react-redux';
+import {
+ selectDestToken,
+ selectIsBridge,
+ selectSourceAmount,
+ selectSourceToken,
+} from '../../../../../core/redux/slices/bridge';
+import useIsInsufficientBalance from '../useInsufficientBalance';
+import { selectShouldUseSmartTransaction } from '../../../../../selectors/smartTransactionsController';
+
+export const useTrackAllQuotesSortedEvent = (
+ latestSourceBalance?: ReturnType,
+) => {
+ const sourceAmount = useSelector(selectSourceAmount);
+ const sourceToken = useSelector(selectSourceToken);
+ const destToken = useSelector(selectDestToken);
+ const smartTransactionsEnabled = useSelector(selectShouldUseSmartTransaction);
+ const isBridge = useSelector(selectIsBridge);
+
+ const hasInsufficientBalance = useIsInsufficientBalance({
+ amount: sourceAmount,
+ token: sourceToken,
+ latestAtomicBalance: latestSourceBalance?.atomicBalance,
+ });
+
+ return (quote: Quote) => {
+ Engine.context.BridgeController.trackUnifiedSwapBridgeEvent(
+ UnifiedSwapBridgeEventName.AllQuotesSorted,
+ {
+ can_submit: !hasInsufficientBalance,
+ price_impact: Number(quote?.priceData?.priceImpact ?? '0'),
+ gas_included: Boolean(quote?.gasIncluded),
+ // @ts-expect-error gas_included_7702 needs to be added to bridge-controller types
+ gas_included_7702: Boolean(quote?.gasIncluded7702),
+ token_symbol_source:
+ sourceToken?.symbol ??
+ (sourceToken
+ ? getNativeAssetForChainId(sourceToken.chainId).symbol
+ : ' '),
+ token_symbol_destination: destToken?.symbol ?? null,
+ stx_enabled: smartTransactionsEnabled,
+ ...(isBridge && {
+ sort_order: SortOrder.COST_ASC,
+ best_quote_provider: formatProviderLabel(quote),
+ }),
+ },
+ );
+ };
+};
diff --git a/app/components/UI/Bridge/routes.tsx b/app/components/UI/Bridge/routes.tsx
index 5a6daffdd06..163f44e9d3c 100644
--- a/app/components/UI/Bridge/routes.tsx
+++ b/app/components/UI/Bridge/routes.tsx
@@ -11,6 +11,7 @@ import MarketClosedBottomSheet from './components/MarketClosedBottomSheets/Marke
import { DefaultSlippageModal } from './components/SlippageModal/DefaultSlippageModal';
import { CustomSlippageModal } from './components/SlippageModal/CustomSlippageModal';
import NetworkListModal from './components/BridgeTokenSelector/NetworkListModal';
+import { QuoteSelectorView } from './components/QuoteSelectorView';
import { PriceImpactModal } from './components/PriceImpactModal';
const clearStackNavigatorOptions = {
@@ -39,6 +40,11 @@ export const BridgeScreenStack = () => (
component={BridgeTokenSelector}
options={{ title: '' }}
/>
+
);
diff --git a/app/components/UI/Bridge/utils/formatNetworkFee.test.ts b/app/components/UI/Bridge/utils/formatNetworkFee.test.ts
index 5bc13ae70d5..3994c36ed06 100644
--- a/app/components/UI/Bridge/utils/formatNetworkFee.test.ts
+++ b/app/components/UI/Bridge/utils/formatNetworkFee.test.ts
@@ -3,20 +3,26 @@ import { BigNumber } from 'bignumber.js';
import formatFiat from '../../../../util/formatFiat';
import { isNumberValue } from '../../../../util/number';
import { formatNetworkFee } from './formatNetworkFee';
+import { isGaslessQuote } from './isGaslessQuote';
jest.mock('../../../../util/formatFiat');
jest.mock('../../../../util/number');
+jest.mock('./isGaslessQuote');
const mockFormatFiat = formatFiat as jest.MockedFunction;
const mockIsNumberValue = isNumberValue as jest.MockedFunction<
typeof isNumberValue
>;
+const mockIsGaslessQuote = isGaslessQuote as jest.MockedFunction<
+ typeof isGaslessQuote
+>;
describe('formatNetworkFee', () => {
beforeEach(() => {
jest.clearAllMocks();
mockIsNumberValue.mockReset();
mockFormatFiat.mockReset();
+ mockIsGaslessQuote.mockReturnValue(false);
});
describe('when quote is null or undefined', () => {
@@ -31,58 +37,143 @@ describe('formatNetworkFee', () => {
});
});
- describe('when totalNetworkFee is missing', () => {
- it('returns "-" when totalNetworkFee is undefined', () => {
- const quote = {} as QuoteResponse & QuoteMetadata;
+ describe('gasless quotes', () => {
+ beforeEach(() => {
+ mockIsGaslessQuote.mockReturnValue(true);
+ });
+
+ it('returns formatted fiat when includedTxFees has valid amount and valueInCurrency', () => {
+ mockIsNumberValue.mockReturnValue(true);
+ mockFormatFiat.mockReturnValue('$5.00');
+
+ const quote = {
+ quote: { gasIncluded: true },
+ includedTxFees: {
+ amount: '0.002',
+ valueInCurrency: '5.00',
+ },
+ } as unknown as QuoteResponse & QuoteMetadata;
+
const result = formatNetworkFee('USD', quote);
+
+ expect(mockIsGaslessQuote).toHaveBeenCalledWith(quote.quote);
+ expect(mockFormatFiat).toHaveBeenCalledWith(new BigNumber('5.00'), 'USD');
+ expect(result).toBe('$5.00');
+ });
+
+ it('returns "-" when includedTxFees.valueInCurrency is null', () => {
+ const quote = {
+ quote: { gasIncluded: true },
+ includedTxFees: {
+ amount: '0.002',
+ valueInCurrency: null,
+ },
+ } as unknown as QuoteResponse & QuoteMetadata;
+
+ const result = formatNetworkFee('USD', quote);
+
expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
});
- it('returns "-" when totalNetworkFee is null', () => {
+ it('returns "-" when includedTxFees.amount is null', () => {
const quote = {
- totalNetworkFee: null,
+ quote: { gasIncluded: true },
+ includedTxFees: {
+ amount: null,
+ valueInCurrency: '5.00',
+ },
} as unknown as QuoteResponse & QuoteMetadata;
+
const result = formatNetworkFee('USD', quote);
+
expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
});
- });
- describe('when totalNetworkFee properties are invalid', () => {
- it('returns "-" when amount is null', () => {
+ it('returns "-" when includedTxFees.amount is not a valid number', () => {
mockIsNumberValue.mockReturnValueOnce(false);
const quote = {
- totalNetworkFee: {
- amount: null,
- valueInCurrency: '100',
+ quote: { gasIncluded: true },
+ includedTxFees: {
+ amount: 'invalid',
+ valueInCurrency: '5.00',
},
} as unknown as QuoteResponse & QuoteMetadata;
const result = formatNetworkFee('USD', quote);
+
expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
});
- it('returns "-" when valueInCurrency is null', () => {
+ it('returns "-" when includedTxFees.valueInCurrency is not a valid number', () => {
mockIsNumberValue.mockReturnValueOnce(true);
mockIsNumberValue.mockReturnValueOnce(false);
const quote = {
+ quote: { gasIncluded: true },
+ includedTxFees: {
+ amount: '0.002',
+ valueInCurrency: 'invalid',
+ },
+ } as unknown as QuoteResponse & QuoteMetadata;
+
+ const result = formatNetworkFee('USD', quote);
+
+ expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
+ });
+
+ it('returns "-" when includedTxFees is not set', () => {
+ const quote = {
+ quote: { gasIncluded: true },
+ } as unknown as QuoteResponse & QuoteMetadata;
+
+ const result = formatNetworkFee('USD', quote);
+
+ expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
+ });
+
+ it('does not fall through to totalNetworkFee when gasless', () => {
+ const quote = {
+ quote: { gasIncluded: true },
totalNetworkFee: {
amount: '0.01',
- valueInCurrency: null,
+ valueInCurrency: '10.00',
},
} as unknown as QuoteResponse & QuoteMetadata;
+ const result = formatNetworkFee('USD', quote);
+
+ expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('non-gasless quotes — totalNetworkFee path', () => {
+ it('returns "-" when totalNetworkFee is undefined', () => {
+ const quote = {} as QuoteResponse & QuoteMetadata;
+ const result = formatNetworkFee('USD', quote);
+ expect(result).toBe('-');
+ });
+
+ it('returns "-" when totalNetworkFee is null', () => {
+ const quote = {
+ totalNetworkFee: null,
+ } as unknown as QuoteResponse & QuoteMetadata;
const result = formatNetworkFee('USD', quote);
expect(result).toBe('-');
});
- it('returns "-" when amount is undefined', () => {
+ it('returns "-" when totalNetworkFee.amount is null', () => {
mockIsNumberValue.mockReturnValueOnce(false);
const quote = {
totalNetworkFee: {
- amount: undefined,
+ amount: null,
valueInCurrency: '100',
},
} as unknown as QuoteResponse & QuoteMetadata;
@@ -91,14 +182,14 @@ describe('formatNetworkFee', () => {
expect(result).toBe('-');
});
- it('returns "-" when valueInCurrency is undefined', () => {
+ it('returns "-" when totalNetworkFee.valueInCurrency is null', () => {
mockIsNumberValue.mockReturnValueOnce(true);
mockIsNumberValue.mockReturnValueOnce(false);
const quote = {
totalNetworkFee: {
amount: '0.01',
- valueInCurrency: undefined,
+ valueInCurrency: null,
},
} as unknown as QuoteResponse & QuoteMetadata;
@@ -106,7 +197,7 @@ describe('formatNetworkFee', () => {
expect(result).toBe('-');
});
- it('returns "-" when amount is not a valid number value', () => {
+ it('returns "-" when totalNetworkFee.amount is not a valid number', () => {
mockIsNumberValue.mockReturnValueOnce(false);
const quote = {
@@ -121,7 +212,7 @@ describe('formatNetworkFee', () => {
expect(isNumberValue).toHaveBeenCalledWith('invalid');
});
- it('returns "-" when valueInCurrency is not a valid number value', () => {
+ it('returns "-" when totalNetworkFee.valueInCurrency is not a valid number', () => {
mockIsNumberValue.mockReturnValueOnce(true);
mockIsNumberValue.mockReturnValueOnce(false);
@@ -137,10 +228,8 @@ describe('formatNetworkFee', () => {
expect(isNumberValue).toHaveBeenCalledWith('0.01');
expect(isNumberValue).toHaveBeenCalledWith('invalid');
});
- });
- describe('when totalNetworkFee is valid', () => {
- it('formats the network fee correctly with USD', () => {
+ it('formats fee with USD currency', () => {
mockIsNumberValue.mockImplementation(
(value) => value === '0.01' || value === '10.50',
);
@@ -161,7 +250,7 @@ describe('formatNetworkFee', () => {
expect(result).toBe('$10.50');
});
- it('formats the network fee correctly with EUR', () => {
+ it('formats fee with EUR currency', () => {
mockIsNumberValue.mockImplementation(
(value) => value === '0.02' || value === '25.00',
);
@@ -180,120 +269,148 @@ describe('formatNetworkFee', () => {
expect(result).toBe('€25.00');
});
- it('formats the network fee correctly with GBP', () => {
+ it('handles small network fees', () => {
mockIsNumberValue.mockImplementation(
- (value) => value === '0.005' || value === '5.25',
+ (value) => value === '0.000001' || value === '0.005',
);
- mockFormatFiat.mockReturnValue('£5.25');
+ mockFormatFiat.mockReturnValue('<$0.01');
const quote = {
totalNetworkFee: {
- amount: '0.005',
- valueInCurrency: '5.25',
+ amount: '0.000001',
+ valueInCurrency: '0.005',
},
} as unknown as QuoteResponse & QuoteMetadata;
- const result = formatNetworkFee('GBP', quote);
+ const result = formatNetworkFee('USD', quote);
- expect(formatFiat).toHaveBeenCalledWith(new BigNumber('5.25'), 'GBP');
- expect(result).toBe('£5.25');
+ expect(formatFiat).toHaveBeenCalledWith(new BigNumber('0.005'), 'USD');
+ expect(result).toBe('<$0.01');
});
- it('handles small network fees', () => {
- mockIsNumberValue.mockImplementation(
- (value) => value === '0.000001' || value === '0.005',
- );
- mockFormatFiat.mockReturnValue('<$0.01');
+ it('handles zero network fee', () => {
+ mockIsNumberValue.mockImplementation((value) => value === '0');
+ mockFormatFiat.mockReturnValue('$0');
const quote = {
totalNetworkFee: {
- amount: '0.000001',
- valueInCurrency: '0.005',
+ amount: '0',
+ valueInCurrency: '0',
},
} as unknown as QuoteResponse & QuoteMetadata;
const result = formatNetworkFee('USD', quote);
- expect(formatFiat).toHaveBeenCalledWith(new BigNumber('0.005'), 'USD');
- expect(result).toBe('<$0.01');
+ expect(formatFiat).toHaveBeenCalledWith(new BigNumber('0'), 'USD');
+ expect(result).toBe('$0');
});
+ });
- it('handles large network fees', () => {
+ describe('non-gasless quotes — gasFee.effective fallback path', () => {
+ it('returns formatted fiat from gasFee.effective when totalNetworkFee is missing', () => {
mockIsNumberValue.mockImplementation(
- (value) => value === '1.5' || value === '1234.56',
+ (value) => value === '0.005' || value === '8.00',
);
- mockFormatFiat.mockReturnValue('$1,234.56');
+ mockFormatFiat.mockReturnValue('$8.00');
const quote = {
- totalNetworkFee: {
- amount: '1.5',
- valueInCurrency: '1234.56',
+ gasFee: {
+ effective: {
+ amount: '0.005',
+ valueInCurrency: '8.00',
+ },
},
} as unknown as QuoteResponse & QuoteMetadata;
const result = formatNetworkFee('USD', quote);
- expect(formatFiat).toHaveBeenCalledWith(new BigNumber('1234.56'), 'USD');
- expect(result).toBe('$1,234.56');
+ expect(formatFiat).toHaveBeenCalledWith(new BigNumber('8.00'), 'USD');
+ expect(result).toBe('$8.00');
});
- it('handles zero network fee', () => {
- mockIsNumberValue.mockImplementation((value) => value === '0');
- mockFormatFiat.mockReturnValue('$0');
+ it('returns formatted fiat from gasFee.effective when totalNetworkFee values are invalid', () => {
+ mockIsNumberValue
+ .mockReturnValueOnce(false) // totalNetworkFee.amount invalid
+ .mockReturnValueOnce(true) // gasFee.effective.amount
+ .mockReturnValueOnce(true); // gasFee.effective.valueInCurrency
+ mockFormatFiat.mockReturnValue('$3.50');
const quote = {
totalNetworkFee: {
- amount: '0',
- valueInCurrency: '0',
+ amount: 'invalid',
+ valueInCurrency: '10.00',
+ },
+ gasFee: {
+ effective: {
+ amount: '0.002',
+ valueInCurrency: '3.50',
+ },
},
} as unknown as QuoteResponse & QuoteMetadata;
const result = formatNetworkFee('USD', quote);
- expect(formatFiat).toHaveBeenCalledWith(new BigNumber('0'), 'USD');
- expect(result).toBe('$0');
+ expect(formatFiat).toHaveBeenCalledWith(new BigNumber('3.50'), 'USD');
+ expect(result).toBe('$3.50');
});
- });
- describe('edge cases', () => {
- it('passes the currency parameter correctly to formatFiat', () => {
- mockIsNumberValue.mockImplementation(
- (value) => value === '1' || value === '100',
- );
- mockFormatFiat.mockReturnValue('100 XYZ');
+ it('returns "-" when gasFee.effective.valueInCurrency is null', () => {
+ const quote = {
+ gasFee: {
+ effective: {
+ amount: '0.002',
+ valueInCurrency: null,
+ },
+ },
+ } as unknown as QuoteResponse & QuoteMetadata;
+
+ const result = formatNetworkFee('USD', quote);
+
+ expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
+ });
+ it('returns "-" when gasFee.effective.amount is null', () => {
const quote = {
- totalNetworkFee: {
- amount: '1',
- valueInCurrency: '100',
+ gasFee: {
+ effective: {
+ amount: null,
+ valueInCurrency: '8.00',
+ },
},
} as unknown as QuoteResponse & QuoteMetadata;
- formatNetworkFee('XYZ', quote);
+ const result = formatNetworkFee('USD', quote);
- expect(formatFiat).toHaveBeenCalledWith(new BigNumber('100'), 'XYZ');
+ expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
});
- it('creates a new BigNumber instance with the valueInCurrency', () => {
- mockIsNumberValue.mockImplementation(
- (value) => value === '0.05' || value === '42.99',
- );
- mockFormatFiat.mockImplementation((value) => {
- expect(value).toBeInstanceOf(BigNumber);
- expect(value.toString()).toBe('42.99');
- return '$42.99';
- });
+ it('returns "-" when gasFee.effective has invalid number values', () => {
+ mockIsNumberValue.mockReturnValue(false);
const quote = {
- totalNetworkFee: {
- amount: '0.05',
- valueInCurrency: '42.99',
+ gasFee: {
+ effective: {
+ amount: 'bad',
+ valueInCurrency: 'bad',
+ },
},
} as unknown as QuoteResponse & QuoteMetadata;
- formatNetworkFee('USD', quote);
+ const result = formatNetworkFee('USD', quote);
+
+ expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
+ });
+
+ it('returns "-" when neither totalNetworkFee nor gasFee.effective is available', () => {
+ const quote = {} as QuoteResponse & QuoteMetadata;
+
+ const result = formatNetworkFee('USD', quote);
- expect(formatFiat).toHaveBeenCalled();
+ expect(result).toBe('-');
+ expect(mockFormatFiat).not.toHaveBeenCalled();
});
});
});
diff --git a/app/components/UI/Bridge/utils/formatNetworkFee.ts b/app/components/UI/Bridge/utils/formatNetworkFee.ts
index ab55146cd94..283ab86bed0 100644
--- a/app/components/UI/Bridge/utils/formatNetworkFee.ts
+++ b/app/components/UI/Bridge/utils/formatNetworkFee.ts
@@ -2,30 +2,55 @@ import { QuoteMetadata, QuoteResponse } from '@metamask/bridge-controller';
import { isNumberValue } from '../../../../util/number';
import formatFiat from '../../../../util/formatFiat';
import { BigNumber } from 'bignumber.js';
+import { isGaslessQuote } from './isGaslessQuote';
export const formatNetworkFee = (
currency: string,
quote?: (QuoteResponse & QuoteMetadata) | null,
) => {
- if (!quote?.totalNetworkFee) return '-';
-
- const { totalNetworkFee } = quote;
-
- const { amount, valueInCurrency } = totalNetworkFee;
+ if (!quote) return '-';
if (
- amount == null ||
- valueInCurrency == null ||
- !isNumberValue(amount) ||
- !isNumberValue(valueInCurrency)
+ isGaslessQuote(quote.quote) &&
+ quote.includedTxFees?.valueInCurrency != null &&
+ quote.includedTxFees?.amount != null &&
+ isNumberValue(quote.includedTxFees.amount) &&
+ isNumberValue(quote.includedTxFees.valueInCurrency)
) {
+ return formatFiat(
+ new BigNumber(quote.includedTxFees.valueInCurrency),
+ currency,
+ );
+ } else if (isGaslessQuote(quote.quote)) {
+ // Quote is gasless but includedTxFees is not set.
+ // Return "uknown" gas fee to keep the same behavior
+ // as the previous vesrions of this utility.
return '-';
}
- const formattedValueInCurrency = formatFiat(
- new BigNumber(valueInCurrency),
- currency,
- );
+ if (
+ quote.totalNetworkFee?.valueInCurrency != null &&
+ quote.totalNetworkFee?.amount != null &&
+ isNumberValue(quote.totalNetworkFee.amount) &&
+ isNumberValue(quote.totalNetworkFee.valueInCurrency)
+ ) {
+ return formatFiat(
+ new BigNumber(quote.totalNetworkFee.valueInCurrency),
+ currency,
+ );
+ }
+
+ if (
+ quote.gasFee?.effective?.valueInCurrency != null &&
+ quote.gasFee?.effective?.amount != null &&
+ isNumberValue(quote.gasFee.effective.amount) &&
+ isNumberValue(quote.gasFee.effective.valueInCurrency)
+ ) {
+ return formatFiat(
+ new BigNumber(quote.gasFee.effective.valueInCurrency),
+ currency,
+ );
+ }
- return formattedValueInCurrency;
+ return '-';
};
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 4421af0948d..599918d150d 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -273,6 +273,7 @@ const Routes = {
ROOT: 'Bridge',
BRIDGE_VIEW: 'BridgeView',
TOKEN_SELECTOR: 'BridgeTokenSelector',
+ QUOTE_SELECTOR_VIEW: 'QuoteSelectorView',
MODALS: {
ROOT: 'BridgeModals',
DEFAULT_SLIPPAGE_MODAL: 'DefaultSlippageModal',
diff --git a/app/core/redux/slices/bridge/index.test.ts b/app/core/redux/slices/bridge/index.test.ts
index bcf1851b52b..6b656cac604 100644
--- a/app/core/redux/slices/bridge/index.test.ts
+++ b/app/core/redux/slices/bridge/index.test.ts
@@ -18,6 +18,8 @@ import reducer, {
selectTokenSelectorNetworkFilter,
setVisiblePillChainIds,
selectVisiblePillChainIds,
+ setSelectedQuoteRequestId,
+ selectSelectedQuoteRequestId,
} from '.';
import {
BridgeToken,
@@ -71,6 +73,8 @@ describe('bridge slice', () => {
isDestTokenManuallySet: false,
tokenSelectorNetworkFilter: undefined,
visiblePillChainIds: undefined,
+ selectedQuoteRequestId: undefined,
+ abTestContext: undefined,
});
});
});
@@ -680,6 +684,77 @@ describe('bridge slice', () => {
});
});
+ describe('setSelectedQuoteRequestId', () => {
+ it('sets the selected quote request ID', () => {
+ const requestId = 'quote-request-123';
+ const action = setSelectedQuoteRequestId(requestId);
+ const state = reducer(initialState, action);
+
+ expect(state.selectedQuoteRequestId).toBe(requestId);
+ });
+
+ it('clears the selected quote request ID when set to undefined', () => {
+ const stateWithSelection = {
+ ...initialState,
+ selectedQuoteRequestId: 'quote-request-123',
+ };
+ const action = setSelectedQuoteRequestId(undefined);
+ const state = reducer(stateWithSelection, action);
+
+ expect(state.selectedQuoteRequestId).toBeUndefined();
+ });
+
+ it('updates the selected quote request ID from one to another', () => {
+ const stateWithSelection = {
+ ...initialState,
+ selectedQuoteRequestId: 'quote-request-123',
+ };
+ const action = setSelectedQuoteRequestId('quote-request-456');
+ const state = reducer(stateWithSelection, action);
+
+ expect(state.selectedQuoteRequestId).toBe('quote-request-456');
+ });
+ });
+
+ describe('selectSelectedQuoteRequestId', () => {
+ it('returns undefined when no quote is selected', () => {
+ const mockState = {
+ bridge: initialState,
+ } as RootState;
+
+ const result = selectSelectedQuoteRequestId(mockState);
+
+ expect(result).toBeUndefined();
+ });
+
+ it('returns the selected quote request ID', () => {
+ const mockState = {
+ bridge: {
+ ...initialState,
+ selectedQuoteRequestId: 'quote-request-789',
+ },
+ } as RootState;
+
+ const result = selectSelectedQuoteRequestId(mockState);
+
+ expect(result).toBe('quote-request-789');
+ });
+ });
+
+ describe('resetBridgeState with selectedQuoteRequestId', () => {
+ it('resets selectedQuoteRequestId when bridge state resets', () => {
+ const stateWithSelection = {
+ ...initialState,
+ selectedQuoteRequestId: 'quote-request-123',
+ sourceAmount: '1.5',
+ };
+
+ const newState = reducer(stateWithSelection, resetBridgeState());
+
+ expect(newState.selectedQuoteRequestId).toBeUndefined();
+ });
+ });
+
describe('selectIsBridgeEnabledSource - ALLOWED_BRIDGE_CHAIN_IDS filtering', () => {
it('returns false for a chain in chainRanking but not in ALLOWED_BRIDGE_CHAIN_IDS', () => {
const mockState = cloneDeep(mockRootState);
diff --git a/app/core/redux/slices/bridge/index.ts b/app/core/redux/slices/bridge/index.ts
index 541647e4367..92e6e6e8df8 100644
--- a/app/core/redux/slices/bridge/index.ts
+++ b/app/core/redux/slices/bridge/index.ts
@@ -76,6 +76,12 @@ export interface BridgeState {
* When undefined, defaults to the first N entries from chainRanking.
*/
visiblePillChainIds: CaipChainId[] | undefined;
+ /**
+ * The requestId of the quote manually selected by the user.
+ * When set, this quote becomes the active quote across all components.
+ * When undefined, the recommended quote (best quote) is used.
+ */
+ selectedQuoteRequestId: string | undefined;
}
export const initialState: BridgeState = {
@@ -98,6 +104,7 @@ export const initialState: BridgeState = {
abTestContext: undefined,
tokenSelectorNetworkFilter: undefined,
visiblePillChainIds: undefined,
+ selectedQuoteRequestId: undefined,
};
const name = 'bridge';
@@ -206,6 +213,16 @@ const slice = createSlice({
) => {
state.visiblePillChainIds = action.payload;
},
+ /**
+ * Sets the requestId of the manually selected quote.
+ * Pass undefined to reset to the recommended quote.
+ */
+ setSelectedQuoteRequestId: (
+ state,
+ action: PayloadAction,
+ ) => {
+ state.selectedQuoteRequestId = action.payload;
+ },
},
extraReducers: (builder) => {
builder.addCase(setSourceTokenExchangeRate.pending, (state) => {
@@ -418,6 +435,11 @@ export const selectDestAddress = createSelector(
(bridgeState) => bridgeState.destAddress,
);
+export const selectSelectedQuoteRequestId = createSelector(
+ selectBridgeState,
+ (bridgeState) => bridgeState.selectedQuoteRequestId,
+);
+
// Selectors for gas included STX/SendBundle support
export const selectIsGasIncludedSTXSendBundleSupported = (state: RootState) =>
state.bridge.isGasIncludedSTXSendBundleSupported;
@@ -442,11 +464,38 @@ const selectControllerFields = (state: RootState) => ({
export const selectBridgeQuotes = createSelector(
selectControllerFields,
- (requiredControllerFields) =>
- selectBridgeQuotesBase(requiredControllerFields, {
- sortOrder: SortOrder.COST_ASC, // TODO for v1 we don't allow user to select alternative quotes, hardcode for now
- selectedQuote: null, // TODO for v1 we don't allow user to select alternative quotes, pass in null for now
- }),
+ selectSelectedQuoteRequestId,
+ (
+ requiredControllerFields,
+ selectedQuoteRequestId,
+ ): ReturnType => {
+ // First get all quotes
+ const allQuotesResult = selectBridgeQuotesBase(requiredControllerFields, {
+ sortOrder: SortOrder.COST_ASC,
+ selectedQuote: null,
+ });
+
+ // If no selectedQuoteRequestId, return the default result
+ if (!selectedQuoteRequestId) {
+ return allQuotesResult;
+ }
+
+ // Find the quote with the matching requestId
+ const selectedQuote = allQuotesResult.sortedQuotes?.find(
+ (quote) => quote.quote.requestId === selectedQuoteRequestId,
+ );
+
+ // If found, recalculate with the selected quote
+ if (selectedQuote) {
+ return selectBridgeQuotesBase(requiredControllerFields, {
+ sortOrder: SortOrder.COST_ASC,
+ selectedQuote,
+ });
+ }
+
+ // If not found, return default result
+ return allQuotesResult;
+ },
);
export const selectIsSolanaSourced = createSelector(
@@ -650,4 +699,5 @@ export const {
setAbTestContext,
setTokenSelectorNetworkFilter,
setVisiblePillChainIds,
+ setSelectedQuoteRequestId,
} = actions;
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 1ef43c2e1fa..beda8aaef62 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -6604,6 +6604,10 @@
"exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%",
"custom": "Custom",
"invalid_recipient_address": "Invalid address",
+ "select_quote": "Select Quote",
+ "select_quote_info": "Quotes are sorted by the estimated total cost, which includes the exchange rate and network fee.",
+ "lowest_cost": "Lowest cost",
+ "total_cost": "Total Cost",
"got_it": "Got it"
},
"quote_expired_modal": {
From 96b70542ce04de06ea5a6144ee0fb2714cd67a2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luis=20Tani=C3=A7a?=
Date: Fri, 6 Mar 2026 13:15:52 -0700
Subject: [PATCH 08/11] fix(predict): fix query invalidation logic in
usePredictToastRegistrations (#27147)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Deposit and withdraw toast handlers were incorrectly invalidating
positions and activity queries, causing unnecessary refetches. This
change scopes those invalidations to only the `claim` type, where they
are actually relevant. Balance and unrealized PnL queries remain
invalidated for all types.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes:
## **Manual testing steps**
```gherkin
Feature: Predict query invalidations
Scenario: Deposit completes without refetching positions/activity
Given the user has a pending deposit transaction
When the deposit transaction is confirmed
Then balance and unrealized PnL queries are invalidated
And positions and activity queries are NOT invalidated
Scenario: Claim completes and refetches all relevant data
Given the user has a pending claim transaction
When the claim transaction is confirmed
Then balance, unrealized PnL, positions, and activity queries are all invalidated
```
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Low Risk**
> Low-risk change limited to React Query cache invalidation; it only
reduces unnecessary refetches after deposit/withdraw confirmations and
should not affect transaction execution.
>
> **Overview**
> Fixes `usePredictToastRegistrations` query invalidation so **on
`confirmed` transactions** it always invalidates `balance` and
`unrealizedPnL`, but only invalidates `positions` and `activity` when
the transaction `type` is `claim`.
>
> This prevents deposit/withdraw toast flows from triggering unnecessary
`positions`/`activity` refetches while keeping claim-related data
refresh behavior intact.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e157a2edcf2c6826cc09a382e548998e45795d76. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../hooks/usePredictToastRegistrations.tsx | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx b/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
index 1e31f256e3f..8be10f0f751 100644
--- a/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
+++ b/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
@@ -158,13 +158,20 @@ export const usePredictToastRegistrations = (): ToastRegistration[] => {
queryKey: predictQueries.balance.keys.all(),
});
- queryClient.invalidateQueries({
- queryKey: predictQueries.activity.keys.all(),
- });
-
queryClient.invalidateQueries({
queryKey: predictQueries.unrealizedPnL.keys.all(),
});
+
+ // Deposit/Withdraw should not invalidate positions/activity
+ if (type === 'claim') {
+ queryClient.invalidateQueries({
+ queryKey: predictQueries.positions.keys.all(),
+ });
+
+ queryClient.invalidateQueries({
+ queryKey: predictQueries.activity.keys.all(),
+ });
+ }
}
if (type === 'deposit') {
@@ -250,10 +257,6 @@ export const usePredictToastRegistrations = (): ToastRegistration[] => {
}
if (status === 'confirmed') {
- queryClient.invalidateQueries({
- queryKey: predictQueries.positions.keys.all(),
- });
-
showSuccessToast({
showToast,
title: strings('predict.deposit.account_ready'),
From b27775959f74011a2f762660b169e591c9fbdbeb Mon Sep 17 00:00:00 2001
From: cmd-ob
Date: Fri, 6 Mar 2026 20:21:50 +0000
Subject: [PATCH 09/11] feat: update dependencies and add token usage reporting
for AI providers (#27146)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Upgrades the smart E2E selector's AI provider stack and adds token usage
and cost reporting to make it easier to monitor and compare provider
costs.
### Provider changes
- **Default provider switched to OpenAI** (`openai → anthropic → google`
priority)
- **OpenAI model**: `gpt-5.2-chat-latest`
- **Anthropic model**: `claude-sonnet-4-6` (was
`claude-opus-4-5-20251101`)
### SDK upgrades
- `openai`: `^4.77.0` → `^6.25.0`
- `@anthropic-ai/sdk`: `^0.71.0` → `^0.78.0`
- Fixed breaking change from openai v6: `ChatCompletionMessageToolCall`
is now
a discriminated union — added `toolCall.type === 'function'` guard
before
accessing `.function`
### Error visibility
- Provider `isAvailable()` catch blocks now log the actual API error
instead
of silently returning `false`
- Availability check output now distinguishes between missing API key
and
failed API call
### Token cost tracking
- Added `LLMUsage` type to `LLMResponse` — all three providers now
return
`inputTokens` / `outputTokens` per API call
- `analyzeWithAgent` accumulates totals across iterations and prints a
cost
report on completion
- `MODEL_PRICING` table added to `config.ts` (keyed to the three active
models)
## **Changelog**
CHANGELOG entry:
## **Related issues**
Fixes:
## **Manual testing steps**
```gherkin
Feature: Smart E2E provider selection and cost tracking
Scenario: user runs analyzer with valid OpenAI key
Given E2E_OPENAI_API_KEY is set to a valid key
When user runs node -r esbuild-register tests/tools/e2e-ai-analyzer --pr
-p openai
Then the output shows "✅ OpenAI GPT is available"
And a "💰 Token Usage Report" is printed with input tokens, output
tokens, and total cost in
USD
Scenario: user runs analyzer with missing API key
Given E2E_OPENAI_API_KEY is not set
When user runs node -r esbuild-register tests/tools/e2e-ai-analyzer --pr
Then the output shows "❌ OpenAI GPT is not available — missing
E2E_OPENAI_API_KEY"
And the analyzer falls back to the next available provider
Scenario: user runs analyzer with an invalid API key
Given E2E_OPENAI_API_KEY is set to an invalid value
When user runs node -r esbuild-register tests/tools/e2e-ai-analyzer --pr
Then the output shows "⚠️ OpenAI API error: "
And the output shows "❌ OpenAI GPT is not available — API call failed
(see warning above)"
Scenario: user runs analyzer with only Anthropic key set
Given E2E_OPENAI_API_KEY is not set and E2E_CLAUDE_API_KEY is valid
When user runs node -r esbuild-register tests/tools/e2e-ai-analyzer --pr
Then Anthropic is used as the active provider
And the token report shows model "claude-sonnet-4-6" with cost in USD
```
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Medium Risk**
> Medium risk due to major `openai` SDK upgrade and provider/model
default changes that can alter analyzer behavior and costs, though the
impact is confined to test tooling.
>
> **Overview**
> Updates the `tests/tools/e2e-ai-analyzer` provider stack by upgrading
`openai` to v6 and `@anthropic-ai/sdk`, switching the default provider
priority to **OpenAI → Anthropic → Google**, and updating the default
OpenAI/Anthropic model IDs.
>
> Adds **token usage + estimated cost reporting**: introduces `LLMUsage`
on `LLMResponse`, populates usage from all three providers, accumulates
totals across agent iterations in `analyzeWithAgent`, and prints a final
report using a new `MODEL_PRICING` table.
>
> Improves provider diagnostics by logging underlying API errors during
`isAvailable()` checks and making availability output distinguish
between missing API keys and failed API calls; also adds an OpenAI v6
tool-call type guard (`toolCall.type === 'function'`) when decoding tool
uses.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8389bd8f25556b803e6df589a96ef16d3ad9c08c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
package.json | 4 +-
.../e2e-ai-analyzer/analysis/analyzer.ts | 49 ++++++++++++++++++-
tests/tools/e2e-ai-analyzer/config.ts | 19 +++++--
tests/tools/e2e-ai-analyzer/index.ts | 7 ++-
.../providers/anthropic-provider.ts | 8 ++-
.../providers/google-provider.ts | 11 ++++-
.../providers/openai-provider.ts | 24 ++++++---
.../tools/e2e-ai-analyzer/providers/types.ts | 9 ++++
yarn.lock | 31 +++++++++---
9 files changed, 139 insertions(+), 23 deletions(-)
diff --git a/package.json b/package.json
index fbcb3db6cbc..a838e7745f6 100644
--- a/package.json
+++ b/package.json
@@ -503,7 +503,7 @@
"vm-browserify": "1.1.2"
},
"devDependencies": {
- "@anthropic-ai/sdk": "^0.71.0",
+ "@anthropic-ai/sdk": "^0.78.0",
"@babel/core": "^7.25.2",
"@babel/eslint-parser": "^7.25.1",
"@babel/preset-env": "^7.25.3",
@@ -620,7 +620,7 @@
"metro-react-native-babel-preset": "~0.76.9",
"metro-react-native-babel-transformer": "~0.76.9",
"nyc": "^15.1.0",
- "openai": "^4.77.0",
+ "openai": "^6.25.0",
"patch-package": "^6.2.2",
"prettier": "^3.6.2",
"prettier-2": "npm:prettier@^2.8.8",
diff --git a/tests/tools/e2e-ai-analyzer/analysis/analyzer.ts b/tests/tools/e2e-ai-analyzer/analysis/analyzer.ts
index 3a5a212ced1..dbb21858ee0 100644
--- a/tests/tools/e2e-ai-analyzer/analysis/analyzer.ts
+++ b/tests/tools/e2e-ai-analyzer/analysis/analyzer.ts
@@ -13,7 +13,7 @@ import {
AnalysisContext,
type ModeConfig,
} from '../types';
-import { LLM_CONFIG } from '../config';
+import { LLM_CONFIG, MODEL_PRICING } from '../config';
import { getToolDefinitions } from '../ai-tools/tool-registry';
import { executeTool, ToolContext } from '../ai-tools/tool-executor';
import {
@@ -118,6 +118,43 @@ export async function analyzeWithAgent(
let currentMessage: LLMContentBlock[] | string = taskPrompt;
const conversationHistory: LLMMessage[] = [];
+ let totalInputTokens = 0;
+ let totalOutputTokens = 0;
+ const requestedModel = provider.getDefaultModel();
+ let resolvedModel: string | undefined;
+
+ function printTokenReport() {
+ if (totalInputTokens === 0 && totalOutputTokens === 0) return;
+ const pricing = MODEL_PRICING[requestedModel];
+ const inputCost = pricing
+ ? (totalInputTokens / 1_000_000) * pricing.inputPerM
+ : null;
+ const outputCost = pricing
+ ? (totalOutputTokens / 1_000_000) * pricing.outputPerM
+ : null;
+ const totalCost =
+ inputCost !== null && outputCost !== null ? inputCost + outputCost : null;
+
+ console.log('\n💰 Token Usage Report');
+ console.log('─────────────────────────────────────');
+ console.log(
+ ` Model: ${requestedModel}${resolvedModel && resolvedModel !== requestedModel ? ` (${resolvedModel})` : ''}`,
+ );
+ console.log(
+ ` Input tokens: ${totalInputTokens.toLocaleString()}${pricing ? ` ($${inputCost?.toFixed(4)})` : ''}`,
+ );
+ console.log(
+ ` Output tokens: ${totalOutputTokens.toLocaleString()}${pricing ? ` ($${outputCost?.toFixed(4)})` : ''}`,
+ );
+ if (totalCost !== null) {
+ console.log(` Total cost: $${totalCost.toFixed(4)}`);
+ }
+ if (!pricing) {
+ console.log(` ⚠️ No pricing data for model "${requestedModel}"`);
+ }
+ console.log('─────────────────────────────────────');
+ }
+
console.log(`🤖 Using provider: ${provider.displayName}`);
for (let iteration = 0; iteration < LLM_CONFIG.maxIterations; iteration++) {
@@ -135,6 +172,12 @@ export async function analyzeWithAgent(
],
});
+ if (response.usage) {
+ totalInputTokens += response.usage.inputTokens;
+ totalOutputTokens += response.usage.outputTokens;
+ resolvedModel = response.model;
+ }
+
// Handle tool uses
const toolUseBlocks = response.content.filter(
(block) => block.type === 'tool_use',
@@ -182,10 +225,12 @@ export async function analyzeWithAgent(
if (analysis) {
console.log(`✅ Analysis complete!`);
+ printTokenReport();
return analysis as ModeAnalysisResult;
}
console.log('⚠️ Failed to parse finalize_tag_selection');
+ printTokenReport();
return modeConfig.createConservativeResult() as ModeAnalysisResult;
}
@@ -221,6 +266,7 @@ export async function analyzeWithAgent(
);
if (analysis) {
console.log(`✅ Analysis complete!`);
+ printTokenReport();
return analysis as ModeAnalysisResult;
}
}
@@ -229,5 +275,6 @@ export async function analyzeWithAgent(
}
console.log('⚠️ Using fallback analysis');
+ printTokenReport();
return modeConfig.createConservativeResult() as ModeAnalysisResult;
}
diff --git a/tests/tools/e2e-ai-analyzer/config.ts b/tests/tools/e2e-ai-analyzer/config.ts
index ee235b50130..bc19c568898 100644
--- a/tests/tools/e2e-ai-analyzer/config.ts
+++ b/tests/tools/e2e-ai-analyzer/config.ts
@@ -6,6 +6,19 @@
import { ProviderType, ProviderConfig } from './providers/types';
+/**
+ * Pricing per million tokens (USD) for each model.
+ * Update when pricing changes: https://openai.com/api/pricing / https://anthropic.com/pricing
+ */
+export const MODEL_PRICING: Record<
+ string,
+ { inputPerM: number; outputPerM: number }
+> = {
+ 'gpt-5.2-chat-latest': { inputPerM: 1.75, outputPerM: 14.0 },
+ 'claude-sonnet-4-6': { inputPerM: 3.0, outputPerM: 15.0 },
+ 'gemini-2.0-flash': { inputPerM: 0.1, outputPerM: 0.4 },
+};
+
/**
* Multi-Provider LLM Configuration
*
@@ -16,18 +29,18 @@ export const LLM_CONFIG = {
* Provider priority order for automatic fallback
* The first available provider in this list will be used
*/
- providerPriority: ['anthropic', 'openai', 'google'] as ProviderType[],
+ providerPriority: ['openai', 'anthropic', 'google'] as ProviderType[],
/**
* Per-provider configuration
*/
providers: {
anthropic: {
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-sonnet-4-6',
envKey: 'E2E_CLAUDE_API_KEY',
} as ProviderConfig,
openai: {
- model: 'gpt-5',
+ model: 'gpt-5.2-chat-latest',
envKey: 'E2E_OPENAI_API_KEY',
} as ProviderConfig,
google: {
diff --git a/tests/tools/e2e-ai-analyzer/index.ts b/tests/tools/e2e-ai-analyzer/index.ts
index 7e1bbdb8d83..1b036a93e37 100644
--- a/tests/tools/e2e-ai-analyzer/index.ts
+++ b/tests/tools/e2e-ai-analyzer/index.ts
@@ -304,7 +304,12 @@ async function main() {
console.log(` ✅ ${provider.displayName} is available`);
availableProviders.push({ type: providerType, provider });
} else {
- console.log(` ❌ ${provider.displayName} is not available`);
+ const envKey = LLM_CONFIG.providers[providerType].envKey;
+ const hasKey = !!process.env[envKey];
+ const reason = hasKey
+ ? 'API call failed (see warning above)'
+ : `missing ${envKey}`;
+ console.log(` ❌ ${provider.displayName} is not available — ${reason}`);
}
}
diff --git a/tests/tools/e2e-ai-analyzer/providers/anthropic-provider.ts b/tests/tools/e2e-ai-analyzer/providers/anthropic-provider.ts
index 8b2490921e7..e7a8a2f9141 100644
--- a/tests/tools/e2e-ai-analyzer/providers/anthropic-provider.ts
+++ b/tests/tools/e2e-ai-analyzer/providers/anthropic-provider.ts
@@ -134,6 +134,10 @@ export class AnthropicProvider implements ILLMProvider {
content: fromAnthropicContent(response.content),
model: response.model,
stopReason: response.stop_reason || 'end_turn',
+ usage: {
+ inputTokens: response.usage.input_tokens,
+ outputTokens: response.usage.output_tokens,
+ },
};
}
@@ -150,7 +154,9 @@ export class AnthropicProvider implements ILLMProvider {
messages: [{ role: 'user', content: 'hi' }],
});
return true;
- } catch {
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.warn(` ⚠️ Anthropic API error: ${message}`);
return false;
}
}
diff --git a/tests/tools/e2e-ai-analyzer/providers/google-provider.ts b/tests/tools/e2e-ai-analyzer/providers/google-provider.ts
index 6f5a39e3ed4..01bd63afafa 100644
--- a/tests/tools/e2e-ai-analyzer/providers/google-provider.ts
+++ b/tests/tools/e2e-ai-analyzer/providers/google-provider.ts
@@ -296,10 +296,17 @@ export class GoogleProvider implements ILLMProvider {
const contents = toGoogleContents(request.messages);
const result = await model.generateContent({ contents });
+ const meta = result.response.usageMetadata;
return {
content: fromGoogleResponse(result),
model: request.model,
stopReason: mapFinishReason(result),
+ usage: meta
+ ? {
+ inputTokens: meta.promptTokenCount ?? 0,
+ outputTokens: meta.candidatesTokenCount ?? 0,
+ }
+ : undefined,
};
}
@@ -319,7 +326,9 @@ export class GoogleProvider implements ILLMProvider {
contents: [{ role: 'user', parts: [{ text: 'hi' }] }],
});
return true;
- } catch {
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.warn(` ⚠️ Google API error: ${message}`);
return false;
}
}
diff --git a/tests/tools/e2e-ai-analyzer/providers/openai-provider.ts b/tests/tools/e2e-ai-analyzer/providers/openai-provider.ts
index 2f6eb91e857..c2aa8bb5667 100644
--- a/tests/tools/e2e-ai-analyzer/providers/openai-provider.ts
+++ b/tests/tools/e2e-ai-analyzer/providers/openai-provider.ts
@@ -141,12 +141,14 @@ function fromOpenAIResponse(
// Add tool calls if present
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
- content.push({
- type: 'tool_use',
- id: toolCall.id,
- name: toolCall.function.name,
- input: JSON.parse(toolCall.function.arguments || '{}'),
- });
+ if (toolCall.type === 'function') {
+ content.push({
+ type: 'tool_use',
+ id: toolCall.id,
+ name: toolCall.function.name,
+ input: JSON.parse(toolCall.function.arguments || '{}'),
+ });
+ }
}
}
@@ -231,6 +233,12 @@ export class OpenAIProvider implements ILLMProvider {
content: fromOpenAIResponse(choice),
model: response.model,
stopReason: mapFinishReason(choice.finish_reason),
+ usage: response.usage
+ ? {
+ inputTokens: response.usage.prompt_tokens,
+ outputTokens: response.usage.completion_tokens,
+ }
+ : undefined,
};
}
@@ -248,7 +256,9 @@ export class OpenAIProvider implements ILLMProvider {
messages: [{ role: 'user', content: 'hi' }],
});
return true;
- } catch {
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.warn(` ⚠️ OpenAI API error: ${message}`);
return false;
}
}
diff --git a/tests/tools/e2e-ai-analyzer/providers/types.ts b/tests/tools/e2e-ai-analyzer/providers/types.ts
index ceb9b491412..a6827d74988 100644
--- a/tests/tools/e2e-ai-analyzer/providers/types.ts
+++ b/tests/tools/e2e-ai-analyzer/providers/types.ts
@@ -70,6 +70,14 @@ export interface LLMRequest {
messages: LLMMessage[];
}
+/**
+ * Token usage for a single API call
+ */
+export interface LLMUsage {
+ inputTokens: number;
+ outputTokens: number;
+}
+
/**
* Response from creating a message/completion
*/
@@ -77,6 +85,7 @@ export interface LLMResponse {
content: LLMContentBlock[];
model: string;
stopReason: string;
+ usage?: LLMUsage;
}
/**
diff --git a/yarn.lock b/yarn.lock
index 8713736bb64..28b2da4f431 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -48,9 +48,9 @@ __metadata:
languageName: node
linkType: hard
-"@anthropic-ai/sdk@npm:^0.71.0":
- version: 0.71.2
- resolution: "@anthropic-ai/sdk@npm:0.71.2"
+"@anthropic-ai/sdk@npm:^0.78.0":
+ version: 0.78.0
+ resolution: "@anthropic-ai/sdk@npm:0.78.0"
dependencies:
json-schema-to-ts: "npm:^3.1.1"
peerDependencies:
@@ -60,7 +60,7 @@ __metadata:
optional: true
bin:
anthropic-ai-sdk: bin/cli
- checksum: 10/a8190f9e860079dd97a544a95f36bd4b0b3a9a941610d7e067c431dc47febe03e3e761fc371166b261af9629d832533eeb3d8e72298e9f73dd52994a61881a2c
+ checksum: 10/7cb34e36d4fc766f0765b2581596825996073b03eec97a1193f07c6ca4ab48a021310dae9df630d61550ae2aa7fb3a6cf54236f7418932b25ea1a0e32624fdf1
languageName: node
linkType: hard
@@ -35396,7 +35396,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "metamask@workspace:."
dependencies:
- "@anthropic-ai/sdk": "npm:^0.71.0"
+ "@anthropic-ai/sdk": "npm:^0.78.0"
"@babel/core": "npm:^7.25.2"
"@babel/eslint-parser": "npm:^7.25.1"
"@babel/preset-env": "npm:^7.25.3"
@@ -35739,7 +35739,7 @@ __metadata:
multihashes: "npm:0.4.14"
number-to-bn: "npm:1.7.0"
nyc: "npm:^15.1.0"
- openai: "npm:^4.77.0"
+ openai: "npm:^6.25.0"
pako: "npm:^2.1.0"
patch-package: "npm:^6.2.2"
path: "npm:0.12.7"
@@ -37939,7 +37939,7 @@ __metadata:
languageName: node
linkType: hard
-"openai@npm:4.104.0, openai@npm:^4.77.0":
+"openai@npm:4.104.0":
version: 4.104.0
resolution: "openai@npm:4.104.0"
dependencies:
@@ -37989,6 +37989,23 @@ __metadata:
languageName: node
linkType: hard
+"openai@npm:^6.25.0":
+ version: 6.25.0
+ resolution: "openai@npm:6.25.0"
+ peerDependencies:
+ ws: ^8.18.0
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ ws:
+ optional: true
+ zod:
+ optional: true
+ bin:
+ openai: bin/cli
+ checksum: 10/c9243b5bb769463c794d7f76e4cd0deee1d3d16e752d0119aad677144f414c5d3f2aaed3f50ab8e8260f7f114140b751563aa48c6d61847c90752f7f39a0cfa2
+ languageName: node
+ linkType: hard
+
"openapi-fetch@npm:^0.13.5":
version: 0.13.7
resolution: "openapi-fetch@npm:0.13.7"
From 0dfec6d97bc2d1327432adcda2f3c64797058903 Mon Sep 17 00:00:00 2001
From: George Marshall
Date: Fri, 6 Mar 2026 13:16:12 -0800
Subject: [PATCH 10/11] test: color-no-hex perps (#26963)
## **Description**
This PR splits out the `app/components/UI/Perps` portion of the
color-no-hex lint migration from #26651 into its own reviewable batch.
The change applies the same test-only updates from the original PR for
Perps files, including replacing hardcoded hex usage in tests and
aligning theme mocks to shared `mockTheme` patterns where applicable.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes:
- Part of #26651
## **Manual testing steps**
```gherkin
Feature: Perps color-no-hex lint batch split
Scenario: validate updated Perps test files
Given the branch `chore/color-no-hex-perps-batch-3`
When running targeted test suites for updated files
Then all targeted suites pass
When running eslint on Perps files in this local workspace
Then lint execution is blocked by local environment config (`@typescript-eslint/no-parameter-properties` rule missing)
```
Commands used:
- `yarn jest --watchman=false
app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx
app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.test.tsx
app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx
app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx
--runInBand`
- `yarn jest --watchman=false
app/components/UI/Perps/utils/transactionDetailStyles.test.ts
--runInBand`
- `yarn eslint app/components/UI/Perps --max-warnings=0` (blocked
locally by missing rule definition)
## **Screenshots/Recordings**
### **Before**
N/A (test-only changes)
### **After**
N/A (test-only changes)
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Low Risk**
> Primarily test refactors plus an ESLint override to enforce
`color-no-hex` in `app/components/UI/Perps`; main risk is new lint
enforcement causing CI/local lint failures if any remaining hex usage
was missed.
>
> **Overview**
> Adds `app/components/UI/Perps/**/*` to the ESLint override that
*errors* on `@metamask/design-tokens/color-no-hex`.
>
> Updates Perps unit/view tests to comply by removing hardcoded hex
colors and `#`-prefixed bug/PR strings, standardizing theme/style mocks
to use the shared `mockTheme` (and in a few cases calling real style
functions) so assertions and mocked styles reference theme tokens
instead of literal hex values.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
bdc53b5eb909ff8a6203c01e3732f176336bb9dd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.eslintrc.js | 2 +
.../PerpsAdjustMarginView.test.tsx | 13 ++-
.../PerpsCancelAllOrdersView.test.tsx | 16 ++-
.../PerpsCloseAllPositionsView.test.tsx | 16 ++-
.../PerpsHeroCardView.test.tsx | 98 +++----------------
.../PerpsHomeView/PerpsHomeView.test.tsx | 8 +-
.../PerpsMarketDetailsView.view.test.tsx | 2 +-
.../PerpsMarketListView.view.test.tsx | 2 +-
.../PerpsOrderBookView.test.tsx | 47 ++++-----
.../PerpsOrderDetailsView.test.tsx | 15 ++-
.../PerpsTPSLView/PerpsTPSLView.test.tsx | 15 +--
.../Perps/components/FoxIcon/FoxIcon.test.tsx | 31 +++---
.../PerpsAdjustMarginActionSheet.test.tsx | 7 +-
.../PerpsAmountDisplay.test.tsx | 23 ++---
.../PerpsCloseSummary.test.tsx | 9 +-
.../PerpsCompactOrderRow.test.tsx | 8 +-
...erpsCrossMarginWarningBottomSheet.test.tsx | 16 ++-
.../PerpsFlipPositionConfirmSheet.test.tsx | 17 ++--
.../PerpsGTMModal/PerpsGTMModal.test.tsx | 24 ++---
.../PerpsHomeHeader/PerpsHomeHeader.test.tsx | 16 ++-
.../PerpsLeverageBottomSheet.test.tsx | 27 +++--
.../PerpsLimitPriceBottomSheet.test.tsx | 66 +++----------
.../PerpsLoader/PerpsLoader.test.tsx | 11 +--
.../PerpsLoadingSkeleton.test.tsx | 16 ++-
.../PerpsMarketListHeader.test.tsx | 16 ++-
.../PerpsMarketSortFieldBottomSheet.test.tsx | 17 +---
.../PerpsMarketStatisticsCard.test.tsx | 3 +-
.../PerpsModifyActionSheet.test.tsx | 3 +-
.../PerpsOrderBookDepthChart.test.tsx | 24 +----
.../PerpsOrderBookTable.test.tsx | 17 ++--
.../PerpsOrderHeader.test.tsx | 17 ++--
.../PerpsOrderTypeBottomSheet.test.tsx | 25 +++--
.../PerpsPositionCard.test.tsx | 25 +++--
.../PerpsQuoteDetailsCard.test.tsx | 9 +-
.../PerpsSlider/PerpsSlider.test.tsx | 82 ++--------------
.../PerpsTokenLogo/PerpsTokenLogo.test.tsx | 15 ++-
.../PerpsTransactionDetailAssetHero.test.tsx | 7 +-
.../PerpsTransactionItem.test.tsx | 12 +--
.../TradingViewChart.test.tsx | 29 +-----
.../UI/Perps/constants/chartConfig.test.ts | 15 +--
.../hooks/useColorPulseAnimation.test.ts | 12 +--
.../UI/Perps/hooks/usePerpsToasts.test.tsx | 19 ++--
.../services/PerpsConnectionManager.test.ts | 4 +-
.../utils/transactionDetailStyles.test.ts | 16 +--
44 files changed, 251 insertions(+), 621 deletions(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index db10cce9e90..cce30f950b1 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -122,11 +122,13 @@ module.exports = {
'app/components/Snaps/**/*.{js,jsx,ts,tsx}',
'app/components/UI/Predict/**/*.{js,jsx,ts,tsx}',
'app/components/UI/Rewards/**/*.{js,jsx,ts,tsx}',
+ 'app/components/UI/Perps/**/*.{js,jsx,ts,tsx}',
],
rules: {
'@metamask/design-tokens/color-no-hex': 'error',
},
},
+
{
files: [
'app/components/UI/Name/**/*.{js,ts,tsx}',
diff --git a/app/components/UI/Perps/Views/PerpsAdjustMarginView/PerpsAdjustMarginView.test.tsx b/app/components/UI/Perps/Views/PerpsAdjustMarginView/PerpsAdjustMarginView.test.tsx
index e02d584884e..2d2b9e1659c 100644
--- a/app/components/UI/Perps/Views/PerpsAdjustMarginView/PerpsAdjustMarginView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsAdjustMarginView/PerpsAdjustMarginView.test.tsx
@@ -103,13 +103,12 @@ jest.mock('./PerpsAdjustMarginView.styles', () => ({
}),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({
- colors: {
- icon: { alternative: '#888' },
- },
- }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../../../../../locales/i18n', () => ({
strings: jest.fn((key) => key),
diff --git a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.test.tsx b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.test.tsx
index fd26d927a81..9da2c1efae7 100644
--- a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.test.tsx
@@ -22,16 +22,12 @@ jest.mock('../../hooks/usePerpsToasts', () => ({
default: jest.fn(() => ({ showToast: jest.fn() })),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: jest.fn(() => ({
- colors: {
- accent03: { normal: '#00ff00', dark: '#008800' },
- accent01: { light: '#ffcccc', dark: '#cc0000' },
- primary: { default: '#0000ff' },
- background: { default: '#ffffff' },
- },
- })),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../hooks/usePerpsEventTracking', () => ({
usePerpsEventTracking: jest.fn(),
diff --git a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx
index fcbe9008403..89807a5f46c 100644
--- a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx
@@ -34,16 +34,12 @@ jest.mock('../../hooks/usePerpsToasts', () => ({
default: jest.fn(() => ({ showToast: jest.fn() })),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: jest.fn(() => ({
- colors: {
- accent03: { normal: '#00ff00', dark: '#008800' },
- accent01: { light: '#ffcccc', dark: '#cc0000' },
- primary: { default: '#0000ff' },
- background: { default: '#ffffff' },
- },
- })),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../hooks/usePerpsEventTracking', () => ({
usePerpsEventTracking: jest.fn(),
diff --git a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx
index 3b1250cf732..f8260921051 100644
--- a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx
@@ -24,24 +24,12 @@ const mockGoBack = jest.fn();
const mockShowToast = jest.fn();
const mockTrack = jest.fn();
-jest.mock('../../../../../util/theme', () => ({
- useAppThemeFromContext: jest.fn(() => ({
- colors: {
- text: { default: '#000000', alternative: '#000000' },
- primary: { inverse: '#FFFFFF', default: '#037DD6' },
- background: { default: '#FFFFFF', alternative: '#F2F4F6' },
- border: { default: '#BBC0C5', muted: '#D6D9DC' },
- icon: { default: '#24272A', alternative: '#6A737D' },
- overlay: { default: '#00000099' },
- shadow: { default: '#00000026' },
- error: { default: '#D73A49', muted: '#F97583' },
- warning: { default: '#F66A0A', muted: '#F8AA4B' },
- success: { default: '#28A745', muted: '#85E29D' },
- info: { default: '#037DD6', muted: '#66CAFF' },
- },
- themeAppearance: 'light',
- })),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useAppThemeFromContext: jest.fn(() => mockTheme),
+ };
+});
jest.mock('@react-navigation/native');
jest.mock('react-native-safe-area-context', () => ({
SafeAreaView: ({ children }: { children: React.ReactNode }) => children,
@@ -96,71 +84,15 @@ jest.mock('../../../Rewards/hooks/useReferralDetails', () => ({
jest.mock('../../../Rewards/hooks/useSeasonStatus', () => ({
useSeasonStatus: jest.fn(),
}));
-jest.mock('@metamask/design-tokens', () => ({
- brandColor: {
- black: '#000000',
- white: '#FFFFFF',
- },
- darkTheme: {
- colors: {
- background: {
- mutedHover: '#color1',
- },
- accent04: {
- light: '#color2',
- },
- },
- },
-}));
-jest.mock('../../../../../component-library/hooks', () => ({
- useStyles: jest.fn(() => ({
- styles: {
- safeAreaContainer: {},
- header: {},
- closeButton: {},
- headerTitle: {},
- carouselWrapper: {},
- carousel: {},
- cardContainer: {},
- backgroundImage: {},
- heroCardTopRow: {},
- metamaskLogo: {},
- heroCardAssetRow: {},
- assetIcon: {},
- assetName: {},
- directionBadge: {},
- directionBadgeText: {},
- pnlText: {},
- pnlPositive: {},
- pnlNegative: {},
- priceRowsContainer: {},
- priceRow: {},
- priceLabel: {},
- priceValue: {},
- qrCodeContainer: {},
- carouselDotIndicator: {},
- progressDot: {},
- progressDotActive: {},
- footerButtonContainer: {},
- },
- theme: {
- colors: {
- text: { default: '#000000', alternative: '#000000' },
- primary: { inverse: '#FFFFFF', default: '#037DD6' },
- background: { default: '#FFFFFF', alternative: '#F2F4F6' },
- border: { default: '#BBC0C5', muted: '#D6D9DC' },
- icon: { default: '#24272A', alternative: '#6A737D' },
- overlay: { default: '#00000099' },
- shadow: { default: '#00000026' },
- error: { default: '#D73A49', muted: '#F97583' },
- warning: { default: '#F66A0A', muted: '#F8AA4B' },
- success: { default: '#28A745', muted: '#85E29D' },
- info: { default: '#037DD6', muted: '#66CAFF' },
- },
- themeAppearance: 'light',
- },
- })),
-}));
+jest.mock('../../../../../component-library/hooks', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useStyles: jest.fn((styleFn, vars) => ({
+ styles: styleFn({ theme: mockTheme, vars }),
+ theme: mockTheme,
+ })),
+ };
+});
jest.mock('../../components/PerpsTokenLogo', () => 'PerpsTokenLogo');
jest.mock(
'../../../Rewards/components/RewardsReferralCodeTag',
diff --git a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.test.tsx b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.test.tsx
index 87b5af4f31c..e89f77d9c63 100644
--- a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.test.tsx
@@ -3,6 +3,7 @@ import { render, fireEvent } from '@testing-library/react-native';
import PerpsHomeView from './PerpsHomeView';
import { PERPS_EVENT_VALUE } from '@metamask/perps-controller';
import { selectPerpsFeedbackEnabledFlag } from '../../selectors/featureFlags';
+import { mockTheme } from '../../../../../util/theme';
// Mock navigation
const mockNavigate = jest.fn();
@@ -193,12 +194,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
bottomSpacer: {},
tabBarContainer: {},
},
- theme: {
- colors: {
- primary: { default: '#0000ff' },
- icon: { default: '#000000' },
- },
- },
+ theme: mockTheme,
}),
}));
diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.view.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.view.test.tsx
index ddc4a99c0ab..8462b746e9d 100644
--- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.view.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.view.test.tsx
@@ -136,7 +136,7 @@ describe('PerpsMarketDetailsView', () => {
).not.toBeOnTheScreen();
});
- describe('Bug #25315: Geo-restriction for Close and Modify actions', () => {
+ describe('Bug 25315: Geo-restriction for Close and Modify actions', () => {
it('shows geo block bottom sheet when Close is pressed (geo-restricted user)', async () => {
renderPerpsMarketDetailsView();
diff --git a/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.view.test.tsx b/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.view.test.tsx
index a98a4d76ac7..bf5c5856706 100644
--- a/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.view.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.view.test.tsx
@@ -38,7 +38,7 @@ const commodityMarket: PerpsMarketData = {
const marketDataWithCategories = [cryptoMarket, commodityMarket];
describe('PerpsMarketListView', () => {
- describe('Bug regression: #25571', () => {
+ describe('Bug regression: 25571', () => {
it('renders market list header and list with default state (no category filtering)', async () => {
renderPerpsMarketListView();
diff --git a/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.test.tsx b/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.test.tsx
index 37e6c063e7f..114ae9a24e7 100644
--- a/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.test.tsx
@@ -5,6 +5,7 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../util/test/initial-root-state';
import { PerpsOrderBookViewSelectorsIDs } from '../../Perps.testIds';
import type { OrderBookData } from '../../hooks/stream/usePerpsLiveOrderBook';
+import { mockTheme } from '../../../../../util/theme';
// Mock navigation
const mockNavigate = jest.fn();
@@ -48,35 +49,23 @@ jest.mock('../../../../../../locales/i18n', () => ({
// Mock useStyles
jest.mock('../../../../../component-library/hooks', () => ({
- useStyles: jest.fn(() => ({
- styles: {
- container: { flex: 1 },
- header: { flexDirection: 'row', padding: 16 },
- headerBackButton: { marginRight: 12 },
- headerTitleContainer: { flex: 1 },
- headerUnitToggle: { flexDirection: 'row' },
- headerUnitButton: { padding: 8 },
- headerUnitButtonActive: { backgroundColor: '#000' },
- depthBandButton: { padding: 8 },
- depthBandButtonPressed: { opacity: 0.7 },
- scrollView: { flex: 1 },
- scrollContent: { padding: 16 },
- section: { marginBottom: 16 },
- depthChartSection: { paddingTop: 16 },
- tableSection: { flex: 1 },
- footer: { padding: 16 },
- actionsContainer: { flexDirection: 'row', gap: 12 },
- actionButtonWrapper: { flex: 1 },
- errorContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- depthBandSheetContent: { padding: 16 },
- depthBandOption: { padding: 16 },
- depthBandOptionSelected: { backgroundColor: '#eee' },
- },
- })),
+ useStyles: jest.fn((styleSheet) => {
+ const perpsOrderBookViewStyleSheet = jest.requireActual(
+ './PerpsOrderBookView.styles',
+ ).default;
+
+ if (styleSheet === perpsOrderBookViewStyleSheet) {
+ return {
+ styles: styleSheet({ theme: mockTheme }),
+ theme: mockTheme,
+ };
+ }
+
+ return {
+ styles: {},
+ theme: mockTheme,
+ };
+ }),
}));
// Mock usePerpsLiveOrderBook
diff --git a/app/components/UI/Perps/Views/PerpsOrderDetailsView/PerpsOrderDetailsView.test.tsx b/app/components/UI/Perps/Views/PerpsOrderDetailsView/PerpsOrderDetailsView.test.tsx
index 52b16a28a6c..0b11274e34e 100644
--- a/app/components/UI/Perps/Views/PerpsOrderDetailsView/PerpsOrderDetailsView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsOrderDetailsView/PerpsOrderDetailsView.test.tsx
@@ -93,15 +93,12 @@ jest.mock('react-redux', () => ({
useSelector: () => ({ address: '0x1234' }),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({
- colors: {
- success: { default: '#00FF00' },
- error: { default: '#FF0000' },
- border: { muted: '#CCCCCC' },
- },
- }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../../../../../locales/i18n', () => ({
strings: jest.fn((key) => key),
diff --git a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx
index b5abe410ae1..5d2c5fa7493 100644
--- a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx
@@ -48,6 +48,9 @@ const mockUseTheme = jest.fn();
jest.mock('../../../../../util/theme', () => ({
useTheme: mockUseTheme,
}));
+const { mockTheme: baseMockTheme } = jest.requireActual(
+ '../../../../../util/theme',
+);
jest.mock('../../hooks/stream', () => ({
usePerpsLivePrices: jest.fn(() => ({})),
@@ -131,16 +134,6 @@ jest.mock('../../../../../../locales/i18n', () => ({
}));
describe('PerpsTPSLView', () => {
- const mockTheme = {
- colors: {
- background: { alternative: '#f0f0f0' },
- text: { default: '#000', muted: '#666', alternative: '#888' },
- border: { muted: '#e1e1e1' },
- primary: { default: '#0376c9' },
- error: { default: '#d73847' },
- },
- };
-
const defaultMockReturn = {
formState: {
takeProfitPrice: '',
@@ -200,7 +193,7 @@ describe('PerpsTPSLView', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockUseTheme.mockReturnValue(mockTheme);
+ mockUseTheme.mockReturnValue(baseMockTheme);
mockUsePerpsTPSLForm.mockReturnValue(defaultMockReturn);
mockRouteParams = { ...defaultRouteParams };
});
diff --git a/app/components/UI/Perps/components/FoxIcon/FoxIcon.test.tsx b/app/components/UI/Perps/components/FoxIcon/FoxIcon.test.tsx
index 655e94494ff..5e60960fd5d 100644
--- a/app/components/UI/Perps/components/FoxIcon/FoxIcon.test.tsx
+++ b/app/components/UI/Perps/components/FoxIcon/FoxIcon.test.tsx
@@ -3,22 +3,13 @@ import { render } from '@testing-library/react-native';
import FoxIcon from './FoxIcon';
import { IconColor } from '../../../../../component-library/components/Icons/Icon';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
+
// Mock the styles hook
jest.mock('../../../../../component-library/hooks/useStyles', () => ({
useStyles: jest.fn(() => ({
styles: {},
- theme: {
- colors: {
- icon: {
- alternative: '#9B9B9B',
- muted: '#6A737D',
- default: '#24292E',
- },
- primary: {
- default: '#037DD6',
- },
- },
- },
+ theme: mockTheme,
})),
}));
@@ -61,21 +52,21 @@ describe('FoxIcon', () => {
const { getByTestId } = render();
const xmlContent = getByTestId('fox-icon-xml').props.children;
- expect(xmlContent).toContain('fill="#9B9B9B"');
+ expect(xmlContent).toContain(`fill="${mockTheme.colors.icon.alternative}"`);
});
it('uses muted icon color when specified', () => {
const { getByTestId } = render();
const xmlContent = getByTestId('fox-icon-xml').props.children;
- expect(xmlContent).toContain('fill="#6A737D"');
+ expect(xmlContent).toContain(`fill="${mockTheme.colors.icon.muted}"`);
});
it('uses default icon color when specified', () => {
const { getByTestId } = render();
const xmlContent = getByTestId('fox-icon-xml').props.children;
- expect(xmlContent).toContain('fill="#24292E"');
+ expect(xmlContent).toContain(`fill="${mockTheme.colors.icon.default}"`);
});
it('contains correct SVG path for fox icon', () => {
@@ -129,14 +120,18 @@ describe('FoxIcon', () => {
// Act & Assert - Should fallback to alternative color
const xmlContent = getByTestId('fox-icon-xml').props.children;
- expect(xmlContent).toContain('fill="#9B9B9B"'); // Should fallback to alternative color
+ expect(xmlContent).toContain(
+ `fill="${mockTheme.colors.icon.alternative}"`,
+ ); // Should fallback to alternative color
});
it('handles Primary icon color', () => {
const { getByTestId } = render();
const xmlContent = getByTestId('fox-icon-xml').props.children;
- expect(xmlContent).toContain('fill="#037DD6"'); // Primary color from mock
+ expect(xmlContent).toContain(
+ `fill="${mockTheme.colors.primary.default}"`,
+ ); // Primary color from mock
});
it('memoizes SVG XML to prevent unnecessary regeneration', () => {
@@ -172,7 +167,7 @@ describe('FoxIcon', () => {
expect(updatedXml).not.toBe(initialXml);
expect(updatedXml).toContain('width="20"');
expect(updatedXml).toContain('height="20"');
- expect(updatedXml).toContain('fill="#6A737D"'); // Muted color
+ expect(updatedXml).toContain(`fill="${mockTheme.colors.icon.muted}"`); // Muted color
});
});
});
diff --git a/app/components/UI/Perps/components/PerpsAdjustMarginActionSheet/PerpsAdjustMarginActionSheet.test.tsx b/app/components/UI/Perps/components/PerpsAdjustMarginActionSheet/PerpsAdjustMarginActionSheet.test.tsx
index a47b888005d..e6f2ddc9fa9 100644
--- a/app/components/UI/Perps/components/PerpsAdjustMarginActionSheet/PerpsAdjustMarginActionSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsAdjustMarginActionSheet/PerpsAdjustMarginActionSheet.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import PerpsAdjustMarginActionSheet from './PerpsAdjustMarginActionSheet';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock dependencies
jest.mock('../../../../../component-library/hooks', () => ({
@@ -11,11 +12,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
actionContent: {},
separator: {},
},
- theme: {
- colors: {
- border: { muted: '#CCCCCC' },
- },
- },
+ theme: mockTheme,
}),
}));
diff --git a/app/components/UI/Perps/components/PerpsAmountDisplay/PerpsAmountDisplay.test.tsx b/app/components/UI/Perps/components/PerpsAmountDisplay/PerpsAmountDisplay.test.tsx
index 321e5e4bf05..9b67c1433fd 100644
--- a/app/components/UI/Perps/components/PerpsAmountDisplay/PerpsAmountDisplay.test.tsx
+++ b/app/components/UI/Perps/components/PerpsAmountDisplay/PerpsAmountDisplay.test.tsx
@@ -4,23 +4,12 @@ import { PerpsAmountDisplaySelectorsIDs } from '../../Perps.testIds';
import PerpsAmountDisplay from './PerpsAmountDisplay';
import { formatPositionSize } from '../../utils/formatUtils';
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({
- colors: {
- text: {
- default: '#141618',
- alternative: '#9fa6ae',
- },
- primary: {
- default: '#037DD6',
- },
- warning: {
- default: '#ffd33d',
- },
- },
- themeAppearance: 'light',
- }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../utils/formatUtils', () => {
const actual = jest.requireActual('../../utils/formatUtils');
diff --git a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx
index a1194ea30a7..0bab0c54e42 100644
--- a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx
+++ b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx
@@ -3,6 +3,7 @@ import { render, fireEvent } from '@testing-library/react-native';
import PerpsCloseSummary from './PerpsCloseSummary';
import { strings } from '../../../../../../locales/i18n';
import type { InternalAccount } from '@metamask/keyring-internal-api';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock dependencies
jest.mock('../../../../../../locales/i18n', () => ({
@@ -31,13 +32,7 @@ jest.mock('../../../../hooks/useStyles', () => ({
rewardsContent: {},
loadingContainer: {},
},
- theme: {
- colors: {
- icon: {
- alternative: '#CCCCCC',
- },
- },
- },
+ theme: mockTheme,
})),
}));
diff --git a/app/components/UI/Perps/components/PerpsCompactOrderRow/PerpsCompactOrderRow.test.tsx b/app/components/UI/Perps/components/PerpsCompactOrderRow/PerpsCompactOrderRow.test.tsx
index cf2014c9e73..6e5fa79e887 100644
--- a/app/components/UI/Perps/components/PerpsCompactOrderRow/PerpsCompactOrderRow.test.tsx
+++ b/app/components/UI/Perps/components/PerpsCompactOrderRow/PerpsCompactOrderRow.test.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import PerpsCompactOrderRow from './PerpsCompactOrderRow';
import { type Order } from '@metamask/perps-controller';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock dependencies
jest.mock('../../../../../component-library/hooks', () => ({
@@ -14,12 +15,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
priceText: {},
labelText: {},
},
- theme: {
- colors: {
- success: { default: '#00FF00' },
- error: { default: '#FF0000' },
- },
- },
+ theme: mockTheme,
}),
}));
diff --git a/app/components/UI/Perps/components/PerpsCrossMarginWarningBottomSheet/PerpsCrossMarginWarningBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsCrossMarginWarningBottomSheet/PerpsCrossMarginWarningBottomSheet.test.tsx
index 8cad5ede861..7cd15912837 100644
--- a/app/components/UI/Perps/components/PerpsCrossMarginWarningBottomSheet/PerpsCrossMarginWarningBottomSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsCrossMarginWarningBottomSheet/PerpsCrossMarginWarningBottomSheet.test.tsx
@@ -13,16 +13,12 @@ jest.mock('react-native-safe-area-context', () => ({
useSafeAreaFrame: () => ({ x: 0, y: 0, width: 390, height: 844 }),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: jest.fn(() => ({
- colors: {
- background: { alternative: '#f0f0f0' },
- text: { default: '#000000', muted: '#666666' },
- border: { muted: '#e1e1e1' },
- primary: { default: '#0066cc', muted: '#cce0ff' },
- },
- })),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('./PerpsCrossMarginWarningBottomSheet.styles', () => ({
createStyles: () => ({
diff --git a/app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.test.tsx b/app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.test.tsx
index c06c71208e2..cb509b27819 100644
--- a/app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.test.tsx
@@ -12,17 +12,12 @@ const mockHandleFlipPosition = jest.fn();
let mockIsFlipping = false;
// Mock dependencies
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({
- colors: {
- primary: { default: '#0376C9' },
- success: { default: '#00FF00' },
- error: { default: '#FF0000' },
- border: { muted: '#CCCCCC' },
- background: { alternative: '#F5F5F5' },
- },
- }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('./PerpsFlipPositionConfirmSheet.styles', () => () => ({
contentContainer: {},
diff --git a/app/components/UI/Perps/components/PerpsGTMModal/PerpsGTMModal.test.tsx b/app/components/UI/Perps/components/PerpsGTMModal/PerpsGTMModal.test.tsx
index 47cb5486e2c..e3e29f0651f 100644
--- a/app/components/UI/Perps/components/PerpsGTMModal/PerpsGTMModal.test.tsx
+++ b/app/components/UI/Perps/components/PerpsGTMModal/PerpsGTMModal.test.tsx
@@ -9,24 +9,12 @@ import { PERPS_GTM_MODAL_SHOWN } from '../../../../../constants/storage';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../util/test/initial-root-state';
-const mockTheme = {
- colors: {
- background: {
- default: '#ffffff',
- alternative: '#f2f4f6',
- },
- text: {
- default: '#24272a',
- },
- shadow: {
- default: '#000000',
- },
- },
-};
-
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({ theme: mockTheme }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../../../../../locales/i18n', () => ({
strings: (key: string) => key,
diff --git a/app/components/UI/Perps/components/PerpsHomeHeader/PerpsHomeHeader.test.tsx b/app/components/UI/Perps/components/PerpsHomeHeader/PerpsHomeHeader.test.tsx
index 91f139fd8ea..7d51092fd43 100644
--- a/app/components/UI/Perps/components/PerpsHomeHeader/PerpsHomeHeader.test.tsx
+++ b/app/components/UI/Perps/components/PerpsHomeHeader/PerpsHomeHeader.test.tsx
@@ -60,16 +60,12 @@ jest.mock('@metamask/design-system-twrnc-preset', () => ({
}),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({
- colors: {
- text: {
- muted: '#999',
- default: '#000',
- },
- },
- }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../../../../component-library/components/Icons/Icon', () => {
const { View } = jest.requireActual('react-native');
diff --git a/app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.test.tsx
index 6b9236f9779..45326b016f8 100644
--- a/app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.test.tsx
@@ -69,9 +69,16 @@ jest.mock('react-native-safe-area-context', () => {
// Mock theme
const mockUseTheme = jest.fn();
-jest.mock('../../../../../util/theme', () => ({
- useTheme: mockUseTheme,
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: mockUseTheme,
+ mockTheme,
+ };
+});
+const { mockTheme: baseMockTheme } = jest.requireActual(
+ '../../../../../util/theme',
+);
// Mock strings
jest.mock('../../../../../../locales/i18n', () => ({
@@ -297,7 +304,7 @@ jest.mock('./PerpsLeverageBottomSheet.styles', () => ({
emptyPriceInfo: { textAlign: 'center' },
sliderContainer: { marginVertical: 24 },
leverageSliderContainer: { height: 40 },
- leverageTrack: { height: 8, backgroundColor: '#e0e0e0' },
+ leverageTrack: { height: 8, backgroundColor: 'rgb(224, 224, 224)' },
progressContainer: { height: '100%', overflow: 'hidden' },
gradientStyle: { height: '100%' },
tickMark: { position: 'absolute', height: 12, width: 2 },
@@ -314,16 +321,6 @@ jest.mock('./PerpsLeverageBottomSheet.styles', () => ({
}));
describe('PerpsLeverageBottomSheet', () => {
- const mockTheme = {
- colors: {
- background: { alternative: '#f0f0f0' },
- text: { default: '#000000', muted: '#666666' },
- primary: { default: '#0066cc' },
- warning: { default: '#ff9800' },
- error: { default: '#ff0000' },
- },
- };
-
const defaultProps = {
isVisible: true,
onClose: jest.fn(),
@@ -338,7 +335,7 @@ describe('PerpsLeverageBottomSheet', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockUseTheme.mockReturnValue(mockTheme);
+ mockUseTheme.mockReturnValue(baseMockTheme);
// Default mock for usePerpsLivePrices - returns price of 3000
mockUsePerpsLivePrices.mockReturnValue({
'BTC-USD': { price: '3000' },
diff --git a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx
index 2b547213573..44e9f158912 100644
--- a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx
@@ -65,19 +65,16 @@ jest.mock('react-native-safe-area-context', () => {
// Mock theme
const mockUseTheme = jest.fn();
-jest.mock('../../../../../util/theme', () => ({
- useTheme: mockUseTheme,
- mockTheme: {
- colors: {
- background: { default: '#FFFFFF', alternative: '#f0f0f0' },
- text: { default: '#000000', alternative: '#666666', muted: '#999999' },
- border: { muted: '#CCCCCC' },
- success: { default: '#00FF00' },
- primary: { default: '#0066cc' },
- error: { default: '#ff0000' },
- },
- },
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: mockUseTheme,
+ mockTheme,
+ };
+});
+const { mockTheme: baseMockTheme } = jest.requireActual(
+ '../../../../../util/theme',
+);
// Mock useTailwind
jest.mock('@metamask/design-system-twrnc-preset', () => ({
@@ -304,48 +301,7 @@ jest.mock('../../../../../component-library/components/Buttons/Button', () => ({
},
}));
-// Mock styles
-jest.mock('./PerpsLimitPriceBottomSheet.styles', () => ({
- createStyles: () => ({
- container: { paddingHorizontal: 16 },
- priceInfo: { marginTop: 8, marginBottom: 16 },
- priceRow: { flexDirection: 'row', justifyContent: 'space-between' },
- priceLabel: { fontSize: 14, color: '#666' },
- priceValue: { fontSize: 16, fontWeight: '500' },
- limitPriceDisplay: {
- backgroundColor: '#f0f0f0',
- borderRadius: 12,
- padding: 16,
- marginBottom: 16,
- flexDirection: 'row',
- justifyContent: 'space-between',
- },
- limitPriceValue: { fontSize: 32, fontWeight: '600' },
- limitPriceCurrency: { fontSize: 18, color: '#666' },
- percentageButtonsRow: { flexDirection: 'row', marginBottom: 10, gap: 8 },
- percentageButton: {
- flex: 1,
- backgroundColor: '#fff',
- borderRadius: 8,
- paddingVertical: 12,
- alignItems: 'center',
- },
- keypadContainer: { marginBottom: 16, padding: 0 },
- footerContainer: { paddingHorizontal: 16, paddingBottom: 24 },
- }),
-}));
-
describe('PerpsLimitPriceBottomSheet', () => {
- const mockTheme = {
- colors: {
- background: { alternative: '#f0f0f0', default: '#ffffff' },
- text: { default: '#000000', muted: '#666666', alternative: '#999999' },
- border: { muted: '#e1e1e1' },
- primary: { default: '#0066cc' },
- error: { default: '#ff0000' },
- },
- };
-
const defaultProps = {
isVisible: true,
onClose: jest.fn(),
@@ -357,7 +313,7 @@ describe('PerpsLimitPriceBottomSheet', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockUseTheme.mockReturnValue(mockTheme);
+ mockUseTheme.mockReturnValue(baseMockTheme);
// Mock stream hooks
const { usePerpsLivePrices, usePerpsTopOfBook } =
diff --git a/app/components/UI/Perps/components/PerpsLoader/PerpsLoader.test.tsx b/app/components/UI/Perps/components/PerpsLoader/PerpsLoader.test.tsx
index 8cc331d6a0d..85d8b3a96d1 100644
--- a/app/components/UI/Perps/components/PerpsLoader/PerpsLoader.test.tsx
+++ b/app/components/UI/Perps/components/PerpsLoader/PerpsLoader.test.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import PerpsLoader from './PerpsLoader';
import { PerpsLoaderSelectorsIDs } from '../../Perps.testIds';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock useStyles
jest.mock('../../../../../component-library/hooks', () => ({
@@ -12,13 +13,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
spinner: {},
loadingText: {},
},
- theme: {
- colors: {
- primary: {
- default: '#0376C9',
- },
- },
- },
+ theme: mockTheme,
})),
}));
@@ -61,7 +56,7 @@ describe('PerpsLoader', () => {
const spinner = getByTestId(PerpsLoaderSelectorsIDs.SPINNER);
expect(spinner).toBeTruthy();
expect(spinner.props.size).toBe('large');
- expect(spinner.props.color).toBe('#0376C9');
+ expect(spinner.props.color).toBe(mockTheme.colors.primary.default);
});
it('should apply correct styles for inline mode', () => {
diff --git a/app/components/UI/Perps/components/PerpsLoadingSkeleton/PerpsLoadingSkeleton.test.tsx b/app/components/UI/Perps/components/PerpsLoadingSkeleton/PerpsLoadingSkeleton.test.tsx
index d5497a685e1..fbf3d1542f7 100644
--- a/app/components/UI/Perps/components/PerpsLoadingSkeleton/PerpsLoadingSkeleton.test.tsx
+++ b/app/components/UI/Perps/components/PerpsLoadingSkeleton/PerpsLoadingSkeleton.test.tsx
@@ -4,16 +4,12 @@ import { render, act, fireEvent } from '@testing-library/react-native';
import PerpsLoadingSkeleton from './PerpsLoadingSkeleton';
// Mock the theme hook
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({
- colors: {
- text: {
- muted: '#6B7280',
- alternative: '#6B7280',
- },
- },
- }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
// Mock the tailwind hook
jest.mock('@metamask/design-system-twrnc-preset', () => ({
diff --git a/app/components/UI/Perps/components/PerpsMarketListHeader/PerpsMarketListHeader.test.tsx b/app/components/UI/Perps/components/PerpsMarketListHeader/PerpsMarketListHeader.test.tsx
index 4dc06c07e63..09c629bd4f4 100644
--- a/app/components/UI/Perps/components/PerpsMarketListHeader/PerpsMarketListHeader.test.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketListHeader/PerpsMarketListHeader.test.tsx
@@ -36,16 +36,12 @@ jest.mock('@metamask/design-system-twrnc-preset', () => ({
}),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({
- colors: {
- text: {
- muted: '#999',
- default: '#000',
- },
- },
- }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../../../../component-library/components/Icons/Icon', () => {
const { View } = jest.requireActual('react-native');
diff --git a/app/components/UI/Perps/components/PerpsMarketSortFieldBottomSheet/PerpsMarketSortFieldBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsMarketSortFieldBottomSheet/PerpsMarketSortFieldBottomSheet.test.tsx
index 5998cc9735c..b02a67b53b9 100644
--- a/app/components/UI/Perps/components/PerpsMarketSortFieldBottomSheet/PerpsMarketSortFieldBottomSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketSortFieldBottomSheet/PerpsMarketSortFieldBottomSheet.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import PerpsMarketSortFieldBottomSheet from './PerpsMarketSortFieldBottomSheet';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock dependencies
jest.mock('../../../../../component-library/hooks', () => ({
@@ -13,21 +14,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
applyButton: {},
applyButtonText: {},
},
- theme: {
- colors: {
- background: {
- alternative: '#E5E5E5',
- muted: '#F0F0F0',
- },
- icon: {
- default: '#000000',
- inverse: '#FFFFFF',
- },
- border: {
- muted: '#D6D6D6',
- },
- },
- },
+ theme: mockTheme,
}),
}));
diff --git a/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.test.tsx b/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.test.tsx
index 7b09fe243df..12a72d81801 100644
--- a/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.test.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.test.tsx
@@ -3,6 +3,7 @@ import { render, fireEvent } from '@testing-library/react-native';
import PerpsMarketStatisticsCard from './PerpsMarketStatisticsCard';
import type { PerpsMarketStatisticsCardProps } from './PerpsMarketStatisticsCard.types';
import { FUNDING_RATE_CONFIG } from '../../constants/perpsConfig';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Navigation mock functions
const mockNavigate = jest.fn();
@@ -39,7 +40,7 @@ jest.mock('../../../../hooks/useStyles', () => ({
},
statisticsItem: {
flex: 1,
- backgroundColor: '#f0f0f0',
+ backgroundColor: mockTheme.colors.background.alternative,
padding: 16,
borderRadius: 8,
},
diff --git a/app/components/UI/Perps/components/PerpsModifyActionSheet/PerpsModifyActionSheet.test.tsx b/app/components/UI/Perps/components/PerpsModifyActionSheet/PerpsModifyActionSheet.test.tsx
index 061abbe784e..d63d92e495e 100644
--- a/app/components/UI/Perps/components/PerpsModifyActionSheet/PerpsModifyActionSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsModifyActionSheet/PerpsModifyActionSheet.test.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import PerpsModifyActionSheet from './PerpsModifyActionSheet';
import { type Position } from '@metamask/perps-controller';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock dependencies
jest.mock('../../../../../component-library/hooks', () => ({
@@ -12,7 +13,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
actionItemBorder: {},
actionIconContainer: {},
actionTextContainer: {},
- iconColor: { color: '#000000' },
+ iconColor: { color: mockTheme.colors.text.default },
},
}),
}));
diff --git a/app/components/UI/Perps/components/PerpsOrderBookDepthChart/PerpsOrderBookDepthChart.test.tsx b/app/components/UI/Perps/components/PerpsOrderBookDepthChart/PerpsOrderBookDepthChart.test.tsx
index 8a1f67d77b4..4f513d4ccf1 100644
--- a/app/components/UI/Perps/components/PerpsOrderBookDepthChart/PerpsOrderBookDepthChart.test.tsx
+++ b/app/components/UI/Perps/components/PerpsOrderBookDepthChart/PerpsOrderBookDepthChart.test.tsx
@@ -3,6 +3,7 @@ import { render } from '@testing-library/react-native';
import PerpsOrderBookDepthChart from './PerpsOrderBookDepthChart';
import type { OrderBookData } from '../../hooks/stream/usePerpsLiveOrderBook';
import { PerpsOrderBookDepthChartSelectorsIDs } from '../../Perps.testIds';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock the strings function
jest.mock('../../../../../../locales/i18n', () => ({
@@ -21,7 +22,7 @@ jest.mock('../../../../hooks/useStyles', () => ({
styles: {
container: {
width: '100%',
- backgroundColor: '#ffffff',
+ backgroundColor: mockTheme.colors.background.default,
borderRadius: 8,
overflow: 'hidden',
},
@@ -51,28 +52,13 @@ jest.mock('../../../../hooks/useStyles', () => ({
borderRadius: 4,
},
bidDot: {
- backgroundColor: '#28a745',
+ backgroundColor: mockTheme.colors.success.default,
},
askDot: {
- backgroundColor: '#dc3545',
- },
- },
- theme: {
- colors: {
- background: {
- default: '#ffffff',
- },
- border: {
- default: '#e0e0e0',
- },
- success: {
- default: '#28a745',
- },
- error: {
- default: '#dc3545',
- },
+ backgroundColor: mockTheme.colors.error.default,
},
},
+ theme: mockTheme,
})),
}));
diff --git a/app/components/UI/Perps/components/PerpsOrderBookTable/PerpsOrderBookTable.test.tsx b/app/components/UI/Perps/components/PerpsOrderBookTable/PerpsOrderBookTable.test.tsx
index e91f0e6b4e3..36311234392 100644
--- a/app/components/UI/Perps/components/PerpsOrderBookTable/PerpsOrderBookTable.test.tsx
+++ b/app/components/UI/Perps/components/PerpsOrderBookTable/PerpsOrderBookTable.test.tsx
@@ -3,6 +3,7 @@ import { render } from '@testing-library/react-native';
import PerpsOrderBookTable from './PerpsOrderBookTable';
import type { OrderBookData } from '../../hooks/stream/usePerpsLiveOrderBook';
import { PerpsOrderBookTableSelectorsIDs } from '../../Perps.testIds';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock the strings function
jest.mock('../../../../../../locales/i18n', () => ({
@@ -47,8 +48,11 @@ jest.mock('../../../../hooks/useStyles', () => ({
bottom: 0,
opacity: 0.15,
},
- bidDepthBar: { right: 0, backgroundColor: '#28a745' },
- askDepthBar: { left: 0, backgroundColor: '#dc3545' },
+ bidDepthBar: {
+ right: 0,
+ backgroundColor: mockTheme.colors.success.default,
+ },
+ askDepthBar: { left: 0, backgroundColor: mockTheme.colors.error.default },
totalColumn: { flex: 1, zIndex: 1 },
totalColumnRight: { flex: 1, alignItems: 'flex-end', zIndex: 1 },
priceColumnBid: {
@@ -78,14 +82,7 @@ jest.mock('../../../../hooks/useStyles', () => ({
paddingVertical: 48,
},
},
- theme: {
- colors: {
- background: { default: '#ffffff' },
- border: { muted: '#e0e0e0' },
- success: { default: '#28a745' },
- error: { default: '#dc3545' },
- },
- },
+ theme: mockTheme,
})),
}));
diff --git a/app/components/UI/Perps/components/PerpsOrderHeader/PerpsOrderHeader.test.tsx b/app/components/UI/Perps/components/PerpsOrderHeader/PerpsOrderHeader.test.tsx
index bf6cadf0776..fe3ceba9a54 100644
--- a/app/components/UI/Perps/components/PerpsOrderHeader/PerpsOrderHeader.test.tsx
+++ b/app/components/UI/Perps/components/PerpsOrderHeader/PerpsOrderHeader.test.tsx
@@ -8,17 +8,12 @@ jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: jest.fn(() => ({
- colors: {
- background: { alternative: '#F2F4F6' },
- border: { muted: '#D6D9DC' },
- text: { default: '#000000' },
- success: { default: '#00C781' },
- error: { default: '#D73A49' },
- },
- })),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('../../../../Base/TokenIcon', () => 'TokenIcon');
diff --git a/app/components/UI/Perps/components/PerpsOrderTypeBottomSheet/PerpsOrderTypeBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsOrderTypeBottomSheet/PerpsOrderTypeBottomSheet.test.tsx
index 4d15f621079..6674a19f57f 100644
--- a/app/components/UI/Perps/components/PerpsOrderTypeBottomSheet/PerpsOrderTypeBottomSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsOrderTypeBottomSheet/PerpsOrderTypeBottomSheet.test.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import PerpsOrderTypeBottomSheet from './PerpsOrderTypeBottomSheet';
import { type OrderType } from '@metamask/perps-controller';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
jest.mock('react-native-safe-area-context', () => ({
SafeAreaProvider: ({ children }: { children: React.ReactNode }) => children,
@@ -14,16 +15,12 @@ jest.mock('react-native-safe-area-context', () => ({
useSafeAreaFrame: () => ({ x: 0, y: 0, width: 390, height: 844 }),
}));
-jest.mock('../../../../../util/theme', () => ({
- useTheme: jest.fn(() => ({
- colors: {
- background: { alternative: '#f0f0f0' },
- text: { default: '#000000', muted: '#666666' },
- border: { muted: '#e1e1e1' },
- primary: { default: '#0066cc', muted: '#cce0ff' },
- },
- })),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
jest.mock('./PerpsOrderTypeBottomSheet.styles', () => ({
createStyles: jest.fn(() => ({
@@ -36,13 +33,13 @@ jest.mock('./PerpsOrderTypeBottomSheet.styles', () => ({
paddingHorizontal: 16,
borderRadius: 12,
marginBottom: 16,
- backgroundColor: '#f0f0f0',
+ backgroundColor: mockTheme.colors.background.alternative,
borderWidth: 1,
- borderColor: '#e1e1e1',
+ borderColor: mockTheme.colors.border.muted,
},
optionSelected: {
- backgroundColor: '#cce0ff',
- borderColor: '#0066cc',
+ backgroundColor: 'rgb(204, 224, 255)',
+ borderColor: mockTheme.colors.primary.default,
},
optionHeader: {
flexDirection: 'row',
diff --git a/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx b/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx
index e5466115309..18bdbcc47e2 100644
--- a/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx
+++ b/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx
@@ -43,9 +43,16 @@ jest.mock('../../../../../../locales/i18n', () => ({
}));
const mockUseTheme = jest.fn();
-jest.mock('../../../../../util/theme', () => ({
- useTheme: mockUseTheme,
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: mockUseTheme,
+ mockTheme,
+ };
+});
+const { mockTheme: baseMockTheme } = jest.requireActual(
+ '../../../../../util/theme',
+);
// Mock PnL calculations
jest.mock('../../utils/pnlCalculations', () => ({
@@ -195,19 +202,9 @@ describe('PerpsPositionCard', () => {
stopLossCount: 0,
};
- const mockTheme = {
- colors: {
- background: { section: '#ffffff' },
- text: { default: '#000000', muted: '#666666' },
- border: { muted: '#e1e1e1' },
- success: { default: '#00ff00', muted: '#ccffcc' },
- error: { default: '#ff0000', muted: '#ffcccc' },
- },
- };
-
beforeEach(() => {
jest.clearAllMocks();
- mockUseTheme.mockReturnValue(mockTheme);
+ mockUseTheme.mockReturnValue(baseMockTheme);
// Reset the PnL calculation mock to default value
const { calculatePnLPercentageFromUnrealized } = jest.requireMock(
'../../utils/pnlCalculations',
diff --git a/app/components/UI/Perps/components/PerpsQuoteDetailsCard/PerpsQuoteDetailsCard.test.tsx b/app/components/UI/Perps/components/PerpsQuoteDetailsCard/PerpsQuoteDetailsCard.test.tsx
index d72b0088adb..d5f6e22fa4c 100644
--- a/app/components/UI/Perps/components/PerpsQuoteDetailsCard/PerpsQuoteDetailsCard.test.tsx
+++ b/app/components/UI/Perps/components/PerpsQuoteDetailsCard/PerpsQuoteDetailsCard.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import PerpsQuoteDetailsCard from './PerpsQuoteDetailsCard';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
@@ -20,13 +21,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
quoteRow: {},
slippageButton: {},
},
- theme: {
- colors: {
- primary: {
- default: '#0376C9',
- },
- },
- },
+ theme: mockTheme,
})),
}));
diff --git a/app/components/UI/Perps/components/PerpsSlider/PerpsSlider.test.tsx b/app/components/UI/Perps/components/PerpsSlider/PerpsSlider.test.tsx
index 7cfcfa5887d..f74dec407eb 100644
--- a/app/components/UI/Perps/components/PerpsSlider/PerpsSlider.test.tsx
+++ b/app/components/UI/Perps/components/PerpsSlider/PerpsSlider.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import PerpsSlider from './PerpsSlider';
+import { mockTheme } from '../../../../../util/theme';
// Mock dependencies - only what's absolutely necessary
jest.mock('react-native-reanimated', () => {
@@ -72,42 +73,6 @@ jest.mock('../../../../../component-library/components/Texts/Text', () => {
}) => {props.children};
});
-// Mock styles
-jest.mock('./PerpsSlider.styles', () => () => ({
- container: { paddingVertical: 8 },
- sliderContainer: { flexDirection: 'row', alignItems: 'center' },
- trackContainer: { flex: 1, position: 'relative', paddingBottom: 30 },
- track: { height: 6, backgroundColor: '#e1e1e1', borderRadius: 3 },
- progress: { height: 6, backgroundColor: '#0066cc', borderRadius: 3 },
- thumb: {
- width: 21,
- height: 21,
- backgroundColor: '#ffffff',
- borderRadius: 10.5,
- position: 'absolute',
- },
- percentageWrapper: { position: 'absolute', top: 14, alignItems: 'center' },
- percentageWrapper0: { left: 0 },
- percentageWrapper25: { left: '25%' },
- percentageWrapper50: { left: '50%' },
- percentageWrapper75: { left: '75%' },
- percentageWrapper100: { right: 0, left: 'auto' },
- percentageDot: {
- width: 5,
- height: 5,
- backgroundColor: '#666',
- borderRadius: 2.5,
- position: 'absolute',
- },
- percentageDot25: { left: '25%' },
- percentageDot50: { left: '50%' },
- percentageDot75: { left: '75%' },
- percentageText: { color: '#666', fontSize: 12, fontWeight: '500' },
- quickValuesRow: { flexDirection: 'row', justifyContent: 'space-around' },
- quickValueButton: { padding: 8, backgroundColor: '#f0f0f0' },
- gradientProgress: { flex: 1, borderRadius: 3 },
-}));
-
describe('PerpsSlider', () => {
const defaultProps = {
value: 50,
@@ -116,47 +81,20 @@ describe('PerpsSlider', () => {
maximumValue: 100,
};
- const mockStyles = {
- container: { paddingVertical: 8 },
- sliderContainer: { flexDirection: 'row', alignItems: 'center' },
- trackContainer: { flex: 1, position: 'relative', paddingBottom: 30 },
- track: { height: 6, backgroundColor: '#e1e1e1', borderRadius: 3 },
- progress: { height: 6, backgroundColor: '#0066cc', borderRadius: 3 },
- thumb: {
- width: 21,
- height: 21,
- backgroundColor: '#ffffff',
- borderRadius: 10.5,
- position: 'absolute',
- },
- percentageWrapper: { position: 'absolute', top: 14, alignItems: 'center' },
- percentageWrapper0: { left: 0 },
- percentageWrapper25: { left: '25%' },
- percentageWrapper50: { left: '50%' },
- percentageWrapper75: { left: '75%' },
- percentageWrapper100: { right: 0, left: 'auto' },
- percentageDot: {
- width: 5,
- height: 5,
- backgroundColor: '#666',
- borderRadius: 2.5,
- position: 'absolute',
- },
- percentageDot25: { left: '25%' },
- percentageDot50: { left: '50%' },
- percentageDot75: { left: '75%' },
- percentageText: { color: '#666', fontSize: 12, fontWeight: '500' },
- quickValuesRow: { flexDirection: 'row', justifyContent: 'space-around' },
- quickValueButton: { padding: 8, backgroundColor: '#f0f0f0' },
- gradientProgress: { flex: 1, borderRadius: 3 },
- };
-
beforeEach(() => {
jest.clearAllMocks();
const { useStyles } = jest.requireMock(
'../../../../../component-library/hooks',
);
- useStyles.mockReturnValue({ styles: mockStyles });
+ useStyles.mockImplementation(
+ (
+ styleSheet: (params: {
+ theme: typeof mockTheme;
+ }) => Record,
+ ) => ({
+ styles: styleSheet({ theme: mockTheme }),
+ }),
+ );
});
describe('Component Rendering', () => {
diff --git a/app/components/UI/Perps/components/PerpsTokenLogo/PerpsTokenLogo.test.tsx b/app/components/UI/Perps/components/PerpsTokenLogo/PerpsTokenLogo.test.tsx
index a3549cccca9..c9bcc4dbb7a 100644
--- a/app/components/UI/Perps/components/PerpsTokenLogo/PerpsTokenLogo.test.tsx
+++ b/app/components/UI/Perps/components/PerpsTokenLogo/PerpsTokenLogo.test.tsx
@@ -3,15 +3,12 @@ import { render, act } from '@testing-library/react-native';
import { Image } from 'expo-image';
import PerpsTokenLogo from './PerpsTokenLogo';
-jest.mock('../../../../../util/theme', () => ({
- useTheme: () => ({
- colors: {
- background: {
- default: '#FFFFFF',
- },
- },
- }),
-}));
+jest.mock('../../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../../util/theme');
+ return {
+ useTheme: jest.fn(() => mockTheme),
+ };
+});
// Note: Avatar component is no longer used in PerpsTokenLogo
// The component now uses a simple text-based fallback instead
diff --git a/app/components/UI/Perps/components/PerpsTransactionDetailAssetHero/PerpsTransactionDetailAssetHero.test.tsx b/app/components/UI/Perps/components/PerpsTransactionDetailAssetHero/PerpsTransactionDetailAssetHero.test.tsx
index 3bba5c5f2c9..cf0a2098bfd 100644
--- a/app/components/UI/Perps/components/PerpsTransactionDetailAssetHero/PerpsTransactionDetailAssetHero.test.tsx
+++ b/app/components/UI/Perps/components/PerpsTransactionDetailAssetHero/PerpsTransactionDetailAssetHero.test.tsx
@@ -7,6 +7,7 @@ import renderWithProvider, {
} from '../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../util/test/initial-root-state';
import { RootState } from '../../../../../reducers';
+import { mockTheme } from '../../../../../util/theme';
import { PerpsTransactionSelectorsIDs } from '../../Perps.testIds';
import { FillType } from '../PerpsTransactionItem/PerpsTransactionItem';
@@ -31,10 +32,6 @@ const mockInitialState: DeepPartial = {
},
},
};
-const mockColors = {
- black: '#000000',
-};
-
const mockStyles = StyleSheet.create({
assetContainer: {
alignItems: 'center',
@@ -54,7 +51,7 @@ const mockStyles = StyleSheet.create({
},
assetAmount: {
fontWeight: '700',
- color: mockColors.black,
+ color: mockTheme.colors.text.default,
},
});
diff --git a/app/components/UI/Perps/components/PerpsTransactionItem/PerpsTransactionItem.test.tsx b/app/components/UI/Perps/components/PerpsTransactionItem/PerpsTransactionItem.test.tsx
index e043c9138b9..561ea86ae71 100644
--- a/app/components/UI/Perps/components/PerpsTransactionItem/PerpsTransactionItem.test.tsx
+++ b/app/components/UI/Perps/components/PerpsTransactionItem/PerpsTransactionItem.test.tsx
@@ -13,6 +13,7 @@ import {
PERPS_EVENT_VALUE,
} from '@metamask/perps-controller';
import { PERPS_SUPPORT_ARTICLES_URLS } from '../../constants/perpsConfig';
+import { mockTheme } from '../../../../../util/theme';
// Mock Redux selector
jest.mock('react-redux', () => ({
@@ -77,11 +78,6 @@ jest.mock('../../hooks', () => ({
usePerpsEventTracking: jest.fn(),
}));
-const mockColors = {
- black: '#000000',
- gray: '#666666',
-};
-
const mockStyles = StyleSheet.create({
transactionItem: {
flexDirection: 'row',
@@ -109,18 +105,18 @@ const mockStyles = StyleSheet.create({
transactionTitle: {
fontSize: 16,
fontWeight: '400',
- color: mockColors.black,
+ color: mockTheme.colors.text.default,
marginBottom: 4,
},
transactionTitleCentered: {
fontSize: 16,
fontWeight: '400',
- color: mockColors.black,
+ color: mockTheme.colors.text.default,
marginBottom: 0,
},
transactionSubtitle: {
fontSize: 14,
- color: mockColors.gray,
+ color: mockTheme.colors.text.alternative,
},
rightContent: {
alignItems: 'flex-end',
diff --git a/app/components/UI/Perps/components/TradingViewChart/TradingViewChart.test.tsx b/app/components/UI/Perps/components/TradingViewChart/TradingViewChart.test.tsx
index 2951e5b6591..3314f2bcc5f 100644
--- a/app/components/UI/Perps/components/TradingViewChart/TradingViewChart.test.tsx
+++ b/app/components/UI/Perps/components/TradingViewChart/TradingViewChart.test.tsx
@@ -6,6 +6,7 @@ import {
type CandleData,
} from '@metamask/perps-controller';
import TradingViewChart, { TPSLLines } from './TradingViewChart';
+const { mockTheme } = jest.requireActual('../../../../../util/theme');
// Mock WebView - using a string name to avoid out-of-scope issues
jest.mock('@metamask/react-native-webview', () => ({
@@ -18,33 +19,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
styles: {
webView: { flex: 1 },
},
- theme: {
- colors: {
- background: { default: '#FFFFFF' },
- border: {
- muted: '#E5E5E5',
- default: '#D1D5DB',
- },
- text: {
- muted: '#6B7280',
- default: '#111827',
- alternative: '#374151',
- },
- error: {
- muted: '#FEF2F2',
- default: '#EF4444',
- alternative: '#DC2626',
- },
- success: {
- muted: '#F0FDF4',
- default: '#22C55E',
- alternative: '#16A34A',
- },
- icon: {
- alternative: '#6B7280',
- },
- },
- },
+ theme: mockTheme,
}),
}));
diff --git a/app/components/UI/Perps/constants/chartConfig.test.ts b/app/components/UI/Perps/constants/chartConfig.test.ts
index fcc2b0e7b76..3346ae3c8a2 100644
--- a/app/components/UI/Perps/constants/chartConfig.test.ts
+++ b/app/components/UI/Perps/constants/chartConfig.test.ts
@@ -6,6 +6,7 @@ import {
calculateCandleCount,
} from '@metamask/perps-controller';
import { getCandlestickColors } from './chartConfig';
+import { mockTheme } from '../../../../util/theme';
describe('chartConfig', () => {
describe('getCandlePeriodsForDuration', () => {
@@ -97,17 +98,19 @@ describe('chartConfig', () => {
describe('getCandlestickColors', () => {
it('returns colors object with positive and negative properties', () => {
// Arrange
- const mockColors = {
- success: { default: '#00ff00' },
- error: { default: '#ff0000' },
- } as Parameters[0];
+ const mockColors = mockTheme.colors as Parameters<
+ typeof getCandlestickColors
+ >[0];
// Act
const colors = getCandlestickColors(mockColors);
// Assert
- expect(colors).toHaveProperty('positive', '#00ff00');
- expect(colors).toHaveProperty('negative', '#ff0000');
+ expect(colors).toHaveProperty(
+ 'positive',
+ mockTheme.colors.success.default,
+ );
+ expect(colors).toHaveProperty('negative', mockTheme.colors.error.default);
});
});
});
diff --git a/app/components/UI/Perps/hooks/useColorPulseAnimation.test.ts b/app/components/UI/Perps/hooks/useColorPulseAnimation.test.ts
index 2810af349af..1299bd3c314 100644
--- a/app/components/UI/Perps/hooks/useColorPulseAnimation.test.ts
+++ b/app/components/UI/Perps/hooks/useColorPulseAnimation.test.ts
@@ -3,15 +3,11 @@ import {
useColorPulseAnimation,
type PulseColor,
} from './useColorPulseAnimation';
+import { mockTheme } from '../../../../util/theme';
jest.mock('../../../../component-library/hooks', () => ({
useStyles: jest.fn(() => ({
- theme: {
- colors: {
- success: { default: '#00ff00' },
- error: { default: '#ff0000' },
- },
- },
+ theme: mockTheme,
})),
}));
@@ -321,8 +317,8 @@ describe('useColorPulseAnimation', () => {
colorDuration: 200,
minOpacity: 0.6,
colors: {
- increase: '#00ff00',
- decrease: '#ff0000',
+ increase: mockTheme.colors.success.default,
+ decrease: mockTheme.colors.error.default,
same: 'transparent',
},
}),
diff --git a/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx b/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
index d691f8ca3f6..6cb883c9bcc 100644
--- a/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
+++ b/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
@@ -29,19 +29,12 @@ jest.mock('expo-haptics', () => ({
},
}));
-jest.mock('../../../../util/theme', () => ({
- useAppThemeFromContext: () => ({
- colors: {
- icon: { default: '#000000' },
- primary: { default: '#0376C9' },
- background: { default: '#FFFFFF' },
- error: { default: '#D73A49' },
- accent03: { dark: '#000000', normal: '#FFFFFF' },
- accent04: { dark: '#000000', normal: '#FFFFFF' },
- accent01: { dark: '#000000', light: '#FFFFFF' },
- },
- }),
-}));
+jest.mock('../../../../util/theme', () => {
+ const { mockTheme } = jest.requireActual('../../../../util/theme');
+ return {
+ useAppThemeFromContext: jest.fn(() => mockTheme),
+ };
+});
jest.mock('@metamask/design-system-react-native', () => ({
IconSize: {
diff --git a/app/components/UI/Perps/services/PerpsConnectionManager.test.ts b/app/components/UI/Perps/services/PerpsConnectionManager.test.ts
index 12d356325d8..f41f1148628 100644
--- a/app/components/UI/Perps/services/PerpsConnectionManager.test.ts
+++ b/app/components/UI/Perps/services/PerpsConnectionManager.test.ts
@@ -867,7 +867,7 @@ describe('PerpsConnectionManager', () => {
});
});
- describe('DEX Abstraction Cache Clearing (PR #25334)', () => {
+ describe('DEX Abstraction Cache Clearing (PR 25334)', () => {
beforeEach(() => {
jest.clearAllMocks();
});
@@ -972,7 +972,7 @@ describe('PerpsConnectionManager', () => {
describe('foreground reconnection — single reconnection flow', () => {
it('PerpsConnectionManager has no AppState listener — only the hook triggers foreground reconnect', () => {
- // This test documents the fix for the race condition introduced by PR #26780.
+ // This test documents the fix for the race condition introduced by PR 26780.
// Previously, PerpsConnectionManager registered its own AppState listener in
// setupStateMonitoring(), which competed with usePerpsConnectionLifecycle hook.
//
diff --git a/app/components/UI/Perps/utils/transactionDetailStyles.test.ts b/app/components/UI/Perps/utils/transactionDetailStyles.test.ts
index b356967217d..694e3f104c3 100644
--- a/app/components/UI/Perps/utils/transactionDetailStyles.test.ts
+++ b/app/components/UI/Perps/utils/transactionDetailStyles.test.ts
@@ -1,13 +1,5 @@
import { createTransactionDetailStyles } from './transactionDetailStyles';
-import { lightTheme, brandColor } from '@metamask/design-tokens';
-import { AppThemeKey } from '../../../../util/theme/models';
-
-// Mock theme object using the actual light theme
-const mockTheme = {
- ...lightTheme,
- themeAppearance: AppThemeKey.light as const,
- brandColors: brandColor,
-};
+import { mockTheme } from '../../../../util/theme';
describe('createTransactionDetailStyles', () => {
it('should create styles object with all required properties', () => {
@@ -121,9 +113,9 @@ describe('createTransactionDetailStyles', () => {
// Arrange
const incompleteTheme = {
colors: {
- background: { default: '#FFFFFF' },
- text: { default: '#000000' },
- success: { default: '#28A745' },
+ background: { default: mockTheme.colors.background.default },
+ text: { default: mockTheme.colors.text.default },
+ success: { default: mockTheme.colors.success.default },
border: {},
},
};
From c6e430297e2316d91965f3e44308ad2901eea4ef Mon Sep 17 00:00:00 2001
From: Pedro Pablo Aste Kompen
Date: Fri, 6 Mar 2026 18:59:27 -0300
Subject: [PATCH 11/11] refactor: replace custom prediction hooks with team
React Query hooks (#27148)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Replaces the custom controller-level implementations in
`usePredictPositionsForHomepage` and `usePredictMarketsForHomepage` with
thin wrappers around the Predict team's existing React Query-backed
hooks (`usePredictPositions`, `usePredictMarketData`). This removes ~300
lines of manual caching (Map + TTL), staleness guards (`requestIdRef`),
unmount protection (`isMountedRef`), and request deduplication that
React Query handles automatically.
Key changes:
- `usePredictPositionsForHomepage` now delegates to
`usePredictPositions`, accepts an options object `{ maxPositions?,
claimable? }`, and returns `totalClaimableValue`
- `usePredictMarketsForHomepage` now delegates to `usePredictMarketData`
- `PredictionsSection` no longer manually computes `totalClaimable` or
calls `refreshClaimable` after claiming (React Query handles cache
invalidation)
- All tests updated to mock team hooks instead of
`Engine.context.PredictController`
Implementation Plan
# Refactor Predictions Homepage Hooks to Use Team Hooks
## Problem
`usePredictPositionsForHomepage` directly calls
`Engine.context.PredictController.getPositions()` and implements its own
module-level caching (Map + TTL), staleness guards (`requestIdRef`), and
unmount protection (`isMountedRef`). The Predict team already has
`usePredictPositions` which wraps the same controller call with React
Query, providing automatic caching (5s stale time), deduplication, error
handling, and optimistic polling -- all for free.
The same applies to `usePredictMarketsForHomepage` which directly calls
`PredictController.getMarkets()` with its own cache, while the team has
`usePredictMarketData` with pagination support.
## Current vs Team Hook Comparison
### Positions
| Aspect | `usePredictPositionsForHomepage` (current) |
`usePredictPositions` (team) |
|--------|-------------------------------------------|------------------------------|
| Data source | `Engine.context.PredictController.getPositions()` |
Same, via React Query `queryFn` |
| Caching | Manual Map + 60s TTL | React Query, 5s stale time |
| Staleness guard | `requestIdRef` counter | React Query built-in |
| Unmount safety | `isMountedRef` | React Query built-in |
| Filtering | `maxPositions` slice, `claimable` param | `claimable`
select filter, `marketId` filter |
| Account tracking | `selectSelectedInternalAccountFormattedAddress` |
`getEvmAccountFromSelectedAccountGroup()` |
| Return shape | `{ positions, isLoading, error, refresh }` | React
Query result (`{ data, isLoading, error, refetch }`) |
### Markets
`usePredictMarketsForHomepage` directly calls
`PredictController.getMarkets()` with manual caching, while
`usePredictMarketData` provides the same with pagination and error
handling.
## Changes
### 1. `usePredictPositionsForHomepage` -- delegate to
`usePredictPositions`
- Change signature to options object `({ maxPositions?, claimable? })`
- Remove manual state, caching, refs, and `useEffect`
- Add `totalClaimableValue` to return type
### 2. `usePredictMarketsForHomepage` -- delegate to
`usePredictMarketData`
- Remove module-level cache, manual state, refs
- Map `marketData` to `markets`, `isFetching` to `isLoading`
### 3. `PredictionsSection` -- update call sites
- Use options object for claimable call
- Remove local `totalClaimable` reduce
- Remove `refreshClaimable()` from `handleClaim` (React Query handles
it)
- Remove `refreshClaimable` from `refresh` callback
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Refs: Feedback from Predict team (Luis) about using their existing hooks
## **Manual testing steps**
```gherkin
Feature: Predictions section uses team hooks
Scenario: Positions load correctly
Given user has prediction positions
When user views the homepage Predictions section
Then positions are displayed correctly
Scenario: Trending markets load correctly
Given user has no prediction positions
When user views the homepage Predictions section
Then trending market cards are displayed in the carousel
Scenario: Claim button works
Given user has claimable positions
When user taps the Claim button
Then the claim is processed and positions update automatically
```
## **Screenshots/Recordings**
N/A -- internal refactor with no UI changes.
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> **Medium Risk**
> Refactors data-fetching for the homepage Predictions section to rely
on shared React Query hooks, which may subtly change caching/refresh
behavior and when claimable amounts update. UI logic is mostly unchanged
but depends on new hook return shapes (`refetch`,
`totalClaimableValue`).
>
> **Overview**
> **Refactors the homepage Predictions section to use the Predict team’s
React Query hooks instead of controller calls + manual caching.**
`usePredictMarketsForHomepage` now wraps `usePredictMarketData` and
`usePredictPositionsForHomepage` wraps `usePredictPositions`, replacing
the old `refresh` APIs with `refetch` and adding `totalClaimableValue`
for claimable positions.
>
> `PredictionsSection` is updated to use the new hook signatures, show
the claim button based on `totalClaimableValue`, and simplify
pull-to-refresh to always `refetch` positions + markets (no separate
claimable refresh; claim no longer triggers a manual refresh).
>
> Tests are updated accordingly, including mocking
`usePredictPositions`/`usePredictMarketData` in `Homepage.test.tsx` and
adjusting section/hook tests to the new `refetch`/options-object APIs.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
fa612f298b42079c0ef2597bef427dd5c4e881fb. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../Views/Homepage/Homepage.test.tsx | 21 ++
.../Predictions/PredictionsSection.test.tsx | 134 ++++----
.../Predictions/PredictionsSection.tsx | 40 +--
.../usePredictMarketsForHomepage.test.ts | 217 +++----------
.../hooks/usePredictMarketsForHomepage.ts | 215 +------------
.../usePredictPositionsForHomepage.test.ts | 295 ++++++------------
.../hooks/usePredictPositionsForHomepage.ts | 214 +++----------
7 files changed, 285 insertions(+), 851 deletions(-)
diff --git a/app/components/Views/Homepage/Homepage.test.tsx b/app/components/Views/Homepage/Homepage.test.tsx
index 95bf0edfafb..54fdccab7f8 100644
--- a/app/components/Views/Homepage/Homepage.test.tsx
+++ b/app/components/Views/Homepage/Homepage.test.tsx
@@ -106,6 +106,27 @@ jest.mock('../../UI/Predict/selectors/featureFlags', () => ({
selectPredictEnabledFlag: jest.fn(() => true),
}));
+jest.mock('../../UI/Predict/hooks/usePredictPositions', () => ({
+ usePredictPositions: () => ({
+ data: [],
+ isLoading: false,
+ error: null,
+ refetch: jest.fn().mockResolvedValue(undefined),
+ }),
+}));
+
+jest.mock('../../UI/Predict/hooks/usePredictMarketData', () => ({
+ usePredictMarketData: () => ({
+ marketData: [],
+ isFetching: false,
+ isFetchingMore: false,
+ error: null,
+ hasMore: false,
+ refetch: jest.fn().mockResolvedValue(undefined),
+ fetchMore: jest.fn().mockResolvedValue(undefined),
+ }),
+}));
+
jest.mock(
'../../../selectors/featureFlagController/assetsDefiPositions',
() => ({
diff --git a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.test.tsx b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.test.tsx
index cb28a5ba4ea..693b1357a40 100644
--- a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.test.tsx
+++ b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.test.tsx
@@ -6,7 +6,6 @@ import Routes from '../../../../../constants/navigation/Routes';
const mockNavigate = jest.fn();
const mockClaim = jest.fn();
-const mockRefreshClaimable = jest.fn();
jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
@@ -51,13 +50,13 @@ jest.mock('./hooks', () => ({
markets: [],
isLoading: false,
error: null,
- refresh: jest.fn(),
+ refetch: jest.fn(),
})),
usePredictPositionsForHomepage: jest.fn(() => ({
positions: [],
isLoading: false,
error: null,
- refresh: jest.fn(),
+ refetch: jest.fn(),
})),
}));
@@ -140,7 +139,6 @@ describe('PredictionsSection', () => {
beforeEach(() => {
jest.clearAllMocks();
mockClaim.mockResolvedValue(undefined);
- mockRefreshClaimable.mockResolvedValue(undefined);
// Reset mock return value to default (true) to ensure test isolation
jest
@@ -165,16 +163,16 @@ describe('PredictionsSection', () => {
],
isLoading: false,
error: null,
- refresh: jest.fn(),
+ refetch: jest.fn(),
});
- // Differentiate active vs claimable positions calls by second arg
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ (_options: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: [],
isLoading: false,
error: null,
- refresh: claimable ? mockRefreshClaimable : jest.fn(),
+ totalClaimableValue: 0,
+ refetch: jest.fn(),
}),
);
});
@@ -214,11 +212,14 @@ describe('PredictionsSection', () => {
describe('when user has positions', () => {
beforeEach(() => {
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ ({
+ claimable = false,
+ }: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: claimable ? [] : mockActivePositions,
isLoading: false,
error: null,
- refresh: claimable ? mockRefreshClaimable : jest.fn(),
+ totalClaimableValue: 0,
+ refetch: jest.fn(),
}),
);
});
@@ -236,11 +237,14 @@ describe('PredictionsSection', () => {
it('shows position skeletons when loading positions', () => {
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ ({
+ claimable = false,
+ }: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: [],
isLoading: !claimable, // only active positions loading
error: null,
- refresh: claimable ? mockRefreshClaimable : jest.fn(),
+ totalClaimableValue: 0,
+ refetch: jest.fn(),
}),
);
@@ -280,7 +284,7 @@ describe('PredictionsSection', () => {
markets: mockMarkets,
isLoading: false,
error: null,
- refresh: jest.fn(),
+ refetch: jest.fn(),
});
renderWithProvider(
@@ -297,7 +301,7 @@ describe('PredictionsSection', () => {
markets: [],
isLoading: true,
error: null,
- refresh: jest.fn(),
+ refetch: jest.fn(),
});
renderWithProvider(
@@ -313,7 +317,7 @@ describe('PredictionsSection', () => {
markets: [],
isLoading: false,
error: null,
- refresh: jest.fn(),
+ refetch: jest.fn(),
});
const { toJSON } = renderWithProvider(
@@ -330,7 +334,7 @@ describe('PredictionsSection', () => {
markets: [],
isLoading: false,
error: 'Network error',
- refresh: jest.fn(),
+ refetch: jest.fn(),
});
renderWithProvider(
@@ -346,7 +350,7 @@ describe('PredictionsSection', () => {
markets: [],
isLoading: true,
error: null,
- refresh: jest.fn(),
+ refetch: jest.fn(),
});
renderWithProvider(
@@ -363,11 +367,14 @@ describe('PredictionsSection', () => {
beforeEach(() => {
// Show positions so the positions branch renders
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ ({
+ claimable = false,
+ }: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: claimable ? [] : mockActivePositions,
isLoading: false,
error: null,
- refresh: claimable ? mockRefreshClaimable : jest.fn(),
+ totalClaimableValue: 0,
+ refetch: jest.fn(),
}),
);
});
@@ -381,13 +388,15 @@ describe('PredictionsSection', () => {
});
it('shows claim button with total amount when claimable positions exist', async () => {
- // totalClaimable = 75 + 125 = 200
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ ({
+ claimable = false,
+ }: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: claimable ? mockClaimablePositions : mockActivePositions,
isLoading: false,
error: null,
- refresh: claimable ? mockRefreshClaimable : jest.fn(),
+ totalClaimableValue: claimable ? 200 : 0,
+ refetch: jest.fn(),
}),
);
@@ -402,11 +411,14 @@ describe('PredictionsSection', () => {
it('does not show claim button while claimable positions are loading', () => {
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ ({
+ claimable = false,
+ }: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: [],
isLoading: claimable, // claimable fetch still loading
error: null,
- refresh: claimable ? mockRefreshClaimable : jest.fn(),
+ totalClaimableValue: 0,
+ refetch: jest.fn(),
}),
);
@@ -419,11 +431,14 @@ describe('PredictionsSection', () => {
it('does not show claim button while active positions are loading', () => {
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ ({
+ claimable = false,
+ }: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: [],
isLoading: !claimable, // active fetch still loading
error: null,
- refresh: claimable ? mockRefreshClaimable : jest.fn(),
+ totalClaimableValue: 0,
+ refetch: jest.fn(),
}),
);
@@ -434,13 +449,16 @@ describe('PredictionsSection', () => {
expect(screen.queryByText(/Claim \$/)).not.toBeOnTheScreen();
});
- it('calls claim and then refreshes claimable positions on press', async () => {
+ it('calls claim on press without manual refresh', async () => {
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ ({
+ claimable = false,
+ }: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: claimable ? mockClaimablePositions : mockActivePositions,
isLoading: false,
error: null,
- refresh: claimable ? mockRefreshClaimable : jest.fn(),
+ totalClaimableValue: claimable ? 200 : 0,
+ refetch: jest.fn(),
}),
);
@@ -456,31 +474,29 @@ describe('PredictionsSection', () => {
await waitFor(() => {
expect(mockClaim).toHaveBeenCalledTimes(1);
- expect(mockRefreshClaimable).toHaveBeenCalledTimes(1);
});
});
});
describe('refresh functionality', () => {
- it('refreshes markets and claimable when user has no positions', async () => {
- const mockRefreshActivePositions = jest.fn().mockResolvedValue(undefined);
- const mockRefreshMarkets = jest.fn().mockResolvedValue(undefined);
+ it('refreshes both positions and markets on pull-to-refresh', async () => {
+ const mockRefetchPositions = jest.fn().mockResolvedValue(undefined);
+ const mockRefetchMarkets = jest.fn().mockResolvedValue(undefined);
mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
+ (_options: { maxPositions?: number; claimable?: boolean } = {}) => ({
positions: [],
isLoading: false,
error: null,
- refresh: claimable
- ? mockRefreshClaimable
- : mockRefreshActivePositions,
+ totalClaimableValue: 0,
+ refetch: mockRefetchPositions,
}),
);
mockUsePredictMarketsForHomepage.mockReturnValue({
markets: [],
isLoading: false,
error: null,
- refresh: mockRefreshMarkets,
+ refetch: mockRefetchMarkets,
});
const ref = React.createRef<{ refresh: () => Promise }>();
@@ -494,46 +510,8 @@ describe('PredictionsSection', () => {
await ref.current?.refresh();
- expect(mockRefreshActivePositions).not.toHaveBeenCalled();
- expect(mockRefreshMarkets).toHaveBeenCalled();
- expect(mockRefreshClaimable).toHaveBeenCalled();
- });
-
- it('refreshes positions, markets, and claimable when user has positions', async () => {
- const mockRefreshActivePositions = jest.fn().mockResolvedValue(undefined);
- const mockRefreshMarkets = jest.fn().mockResolvedValue(undefined);
-
- mockUsePredictPositionsForHomepage.mockImplementation(
- (_maxPositions?: number, claimable = false) => ({
- positions: claimable ? [] : mockActivePositions,
- isLoading: false,
- error: null,
- refresh: claimable
- ? mockRefreshClaimable
- : mockRefreshActivePositions,
- }),
- );
- mockUsePredictMarketsForHomepage.mockReturnValue({
- markets: [],
- isLoading: false,
- error: null,
- refresh: mockRefreshMarkets,
- });
-
- const ref = React.createRef<{ refresh: () => Promise }>();
- renderWithProvider(
- ,
- );
-
- await ref.current?.refresh();
-
- expect(mockRefreshActivePositions).toHaveBeenCalled();
- expect(mockRefreshMarkets).toHaveBeenCalled();
- expect(mockRefreshClaimable).toHaveBeenCalled();
+ expect(mockRefetchPositions).toHaveBeenCalled();
+ expect(mockRefetchMarkets).toHaveBeenCalled();
});
});
});
diff --git a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
index f13926e0516..8a73deeff9d 100644
--- a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
+++ b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
@@ -87,39 +87,26 @@ const PredictionsSection = forwardRef<
positions,
isLoading: isLoadingPositions,
error: positionsError,
- refresh: refreshPositions,
+ refetch: refetchPositions,
} = usePredictPositionsForHomepage();
const {
markets,
isLoading: isLoadingMarkets,
error: marketsError,
- refresh: refreshMarkets,
+ refetch: refetchMarkets,
} = usePredictMarketsForHomepage(MAX_MARKETS_DISPLAYED);
- const {
- positions: claimablePositions,
- isLoading: isLoadingClaimable,
- refresh: refreshClaimable,
- } = usePredictPositionsForHomepage(undefined, true);
+ const { totalClaimableValue, isLoading: isLoadingClaimable } =
+ usePredictPositionsForHomepage({ claimable: true });
const handleClaim = useCallback(async () => {
await claim();
- await refreshClaimable();
- }, [claim, refreshClaimable]);
-
- const totalClaimable = claimablePositions.reduce(
- (sum, p) => sum + (p.currentValue ?? 0),
- 0,
- );
+ }, [claim]);
// Determine if user has positions
const hasPositions = positions.length > 0;
- // Use ref so refresh always reads the latest value without stale closures
- const hasPositionsRef = useRef(hasPositions);
- hasPositionsRef.current = hasPositions;
-
const isLoading = isLoadingPositions || isLoadingMarkets;
const hasError =
@@ -152,18 +139,9 @@ const PredictionsSection = forwardRef<
itemCount,
});
- // Refresh: only refresh positions if user has them, always refresh markets + claimable
const refresh = useCallback(async () => {
- if (hasPositionsRef.current) {
- await Promise.all([
- refreshPositions(),
- refreshMarkets(),
- refreshClaimable(),
- ]);
- } else {
- await Promise.all([refreshMarkets(), refreshClaimable()]);
- }
- }, [refreshPositions, refreshMarkets, refreshClaimable]);
+ await Promise.all([refetchPositions(), refetchMarkets()]);
+ }, [refetchPositions, refetchMarkets]);
useImperativeHandle(ref, () => ({ refresh }), [refresh]);
@@ -231,10 +209,10 @@ const PredictionsSection = forwardRef<
)}
{!isLoadingPositions &&
!isLoadingClaimable &&
- totalClaimable > 0 && (
+ totalClaimableValue > 0 && (
diff --git a/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictMarketsForHomepage.test.ts b/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictMarketsForHomepage.test.ts
index 99af368106c..0c5c81971f0 100644
--- a/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictMarketsForHomepage.test.ts
+++ b/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictMarketsForHomepage.test.ts
@@ -1,37 +1,28 @@
-import { renderHook, act } from '@testing-library/react-hooks';
-import {
- usePredictMarketsForHomepage,
- _clearMarketsCache,
-} from './usePredictMarketsForHomepage';
+import { renderHook } from '@testing-library/react-native';
+import { usePredictMarketsForHomepage } from './usePredictMarketsForHomepage';
import type { PredictMarket } from '../../../../../UI/Predict/types';
-const mockGetMarkets = jest.fn();
-
-jest.mock('../../../../../../core/Engine', () => ({
- context: {
- PredictController: {
- getMarkets: (...args: unknown[]) => mockGetMarkets(...args),
- },
- },
-}));
-
-let mockIsPredictEnabled = true;
-
-jest.mock('react-redux', () => ({
- ...jest.requireActual('react-redux'),
- useSelector: (selector: (...args: unknown[]) => unknown) => {
- if (
- selector ===
- jest.requireMock('../../../../../UI/Predict').selectPredictEnabledFlag
- ) {
- return mockIsPredictEnabled;
- }
- return undefined;
- },
-}));
-
-jest.mock('../../../../../UI/Predict', () => ({
- selectPredictEnabledFlag: jest.fn(),
+const mockRefetch = jest.fn().mockResolvedValue(undefined);
+let mockUsePredictMarketDataReturn: {
+ marketData: PredictMarket[];
+ isFetching: boolean;
+ isFetchingMore: boolean;
+ error: string | null;
+ hasMore: boolean;
+ refetch: jest.Mock;
+ fetchMore: jest.Mock;
+} = {
+ marketData: [],
+ isFetching: false,
+ isFetchingMore: false,
+ error: null,
+ hasMore: false,
+ refetch: mockRefetch,
+ fetchMore: jest.fn(),
+};
+
+jest.mock('../../../../../UI/Predict/hooks/usePredictMarketData', () => ({
+ usePredictMarketData: () => mockUsePredictMarketDataReturn,
}));
const createMockMarket = (id: string): PredictMarket =>
@@ -51,166 +42,56 @@ const createMockMarket = (id: string): PredictMarket =>
describe('usePredictMarketsForHomepage', () => {
beforeEach(() => {
jest.clearAllMocks();
- _clearMarketsCache();
- mockIsPredictEnabled = true;
-
- mockGetMarkets.mockResolvedValue([
- createMockMarket('1'),
- createMockMarket('2'),
- createMockMarket('3'),
- ]);
+ mockUsePredictMarketDataReturn = {
+ marketData: [
+ createMockMarket('1'),
+ createMockMarket('2'),
+ createMockMarket('3'),
+ ],
+ isFetching: false,
+ isFetchingMore: false,
+ error: null,
+ hasMore: false,
+ refetch: mockRefetch,
+ fetchMore: jest.fn(),
+ };
});
- it('fetches markets on mount when predict is enabled', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictMarketsForHomepage(5),
- );
-
- await waitForNextUpdate();
+ it('returns markets from usePredictMarketData', () => {
+ const { result } = renderHook(() => usePredictMarketsForHomepage(5));
expect(result.current.markets).toHaveLength(3);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
- expect(mockGetMarkets).toHaveBeenCalledWith({
- category: 'trending',
- limit: 5,
- });
});
- it('returns empty markets when predict is disabled', () => {
- mockIsPredictEnabled = false;
+ it('forwards isFetching as isLoading', () => {
+ mockUsePredictMarketDataReturn.isFetching = true;
const { result } = renderHook(() => usePredictMarketsForHomepage(5));
- expect(result.current.markets).toHaveLength(0);
- expect(result.current.isLoading).toBe(false);
- expect(mockGetMarkets).not.toHaveBeenCalled();
- });
-
- it('limits markets to the specified limit', async () => {
- mockGetMarkets.mockResolvedValue([
- createMockMarket('1'),
- createMockMarket('2'),
- createMockMarket('3'),
- createMockMarket('4'),
- createMockMarket('5'),
- ]);
-
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictMarketsForHomepage(3),
- );
-
- await waitForNextUpdate();
-
- expect(result.current.markets).toHaveLength(3);
+ expect(result.current.isLoading).toBe(true);
});
- it('sets error state when fetch fails', async () => {
- mockGetMarkets.mockRejectedValue(new Error('Network error'));
-
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictMarketsForHomepage(5),
- );
+ it('forwards error from usePredictMarketData', () => {
+ mockUsePredictMarketDataReturn.error = 'Network error';
- await waitForNextUpdate();
+ const { result } = renderHook(() => usePredictMarketsForHomepage(5));
expect(result.current.error).toBe('Network error');
- expect(result.current.markets).toHaveLength(0);
- expect(result.current.isLoading).toBe(false);
- });
-
- it('sets fallback error for non-Error throws', async () => {
- mockGetMarkets.mockRejectedValue('string error');
-
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictMarketsForHomepage(5),
- );
-
- await waitForNextUpdate();
-
- expect(result.current.error).toBe('Failed to fetch prediction markets');
- });
-
- it('handles null response from getMarkets', async () => {
- mockGetMarkets.mockResolvedValue(null);
-
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictMarketsForHomepage(5),
- );
-
- await waitForNextUpdate();
-
- expect(result.current.markets).toHaveLength(0);
- expect(result.current.error).toBeNull();
- });
-
- it('uses cached data on subsequent renders within TTL', async () => {
- const { waitForNextUpdate, unmount } = renderHook(() =>
- usePredictMarketsForHomepage(5),
- );
-
- await waitForNextUpdate();
-
- expect(mockGetMarkets).toHaveBeenCalledTimes(1);
- unmount();
-
- const { result: result2 } = renderHook(() =>
- usePredictMarketsForHomepage(5),
- );
-
- expect(result2.current.markets).toHaveLength(3);
- expect(result2.current.isLoading).toBe(false);
- expect(mockGetMarkets).toHaveBeenCalledTimes(1);
- });
-
- it('clears cache and refetches on refresh', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictMarketsForHomepage(5),
- );
-
- await waitForNextUpdate();
-
- expect(mockGetMarkets).toHaveBeenCalledTimes(1);
-
- await act(async () => {
- await result.current.refresh();
- });
-
- expect(mockGetMarkets).toHaveBeenCalledTimes(2);
});
- it('returns error when Engine context is null', async () => {
- const engineMock = jest.requireMock('../../../../../../core/Engine');
- const savedContext = engineMock.context;
- engineMock.context = null;
-
+ it('returns null error when no error', () => {
const { result } = renderHook(() => usePredictMarketsForHomepage(5));
- // Allow the async fetchMarkets to settle
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 0));
- });
-
- expect(result.current.error).toBe('Engine not initialized');
- expect(result.current.isLoading).toBe(false);
-
- engineMock.context = savedContext;
+ expect(result.current.error).toBeNull();
});
- it('returns error when PredictController is null', async () => {
- const engineMock = jest.requireMock('../../../../../../core/Engine');
- const savedController = engineMock.context.PredictController;
- engineMock.context.PredictController = null;
-
+ it('exposes refetch from usePredictMarketData', async () => {
const { result } = renderHook(() => usePredictMarketsForHomepage(5));
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 0));
- });
-
- expect(result.current.error).toBe('Predict controller not available');
- expect(result.current.isLoading).toBe(false);
+ await result.current.refetch();
- engineMock.context.PredictController = savedController;
+ expect(mockRefetch).toHaveBeenCalledTimes(1);
});
});
diff --git a/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictMarketsForHomepage.ts b/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictMarketsForHomepage.ts
index 7c5de6e18b3..8825cd2b436 100644
--- a/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictMarketsForHomepage.ts
+++ b/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictMarketsForHomepage.ts
@@ -1,7 +1,4 @@
-import { useState, useEffect, useCallback, useRef } from 'react';
-import { useSelector } from 'react-redux';
-import Engine from '../../../../../../core/Engine';
-import { selectPredictEnabledFlag } from '../../../../../UI/Predict';
+import { usePredictMarketData } from '../../../../../UI/Predict/hooks/usePredictMarketData';
import type { PredictMarket } from '../../../../../UI/Predict/types';
/**
@@ -14,214 +11,34 @@ export interface UsePredictMarketsForHomepageResult {
isLoading: boolean;
/** Error message if fetch failed */
error: string | null;
- /** Refresh function to manually refetch data */
- refresh: () => Promise;
+ /** Refetch function to manually refetch data */
+ refetch: () => Promise;
}
-// Module-level cache for markets
-// Persists across component mounts/unmounts for efficient re-use
-interface CacheEntry {
- markets: PredictMarket[];
- timestamp: number;
- limit: number;
-}
-
-let marketsCache: CacheEntry | null = null;
-
-// Cache TTL: 5 minutes
-const CACHE_TTL_MS = 5 * 60 * 1000;
-
-/**
- * Clear the cache - exported for testing purposes only
- * @internal
- */
-export const _clearMarketsCache = (): void => {
- marketsCache = null;
-};
-
/**
- * usePredictMarketsForHomepage Hook
+ * Lightweight wrapper around the Predict team's usePredictMarketData hook,
+ * adapted for homepage display with trending markets.
*
- * Fetches trending prediction markets for homepage display.
- * Designed for homepage/discovery use cases with module-level caching.
- *
- * Key Features:
- * - Module-level caching (5 min TTL) to avoid repeated API calls
- * - Uses PredictController.getMarkets() with category: 'trending'
- * - Returns limited results for carousel display
+ * The feature flag check is handled at the UI level (Homepage conditionally
+ * renders the Predictions section), so this hook assumes it is only called
+ * when predictions are enabled.
*
* @param limit - Maximum number of markets to return (default: 5)
- * @returns Object with markets, isLoading, error, refresh
- *
- * @example
- * ```tsx
- * const { markets, isLoading, error, refresh } = usePredictMarketsForHomepage(5);
- * ```
+ * @returns Object with markets, isLoading, error, refetch
*/
export const usePredictMarketsForHomepage = (
limit = 5,
): UsePredictMarketsForHomepageResult => {
- const isPredictEnabled = useSelector(selectPredictEnabledFlag);
-
- // Track if component is still mounted
- const isMountedRef = useRef(true);
-
- // Track current request to prevent stale responses
- const requestIdRef = useRef(0);
-
- // Use ref for limit to keep fetchMarkets callback stable
- const limitRef = useRef(limit);
- limitRef.current = limit;
-
- const [state, setState] = useState<{
- markets: PredictMarket[];
- isLoading: boolean;
- error: string | null;
- }>(() => {
- // Initialize from cache if available and limit is satisfied
- if (
- marketsCache &&
- Date.now() - marketsCache.timestamp < CACHE_TTL_MS &&
- marketsCache.limit >= limit
- ) {
- return {
- markets: marketsCache.markets.slice(0, limit),
- isLoading: false,
- error: null,
- };
- }
-
- return {
- markets: [],
- isLoading: isPredictEnabled,
- error: null,
- };
+ const { marketData, isFetching, error, refetch } = usePredictMarketData({
+ category: 'trending',
+ pageSize: limit,
});
- const fetchMarkets = useCallback(async () => {
- const currentLimit = limitRef.current;
-
- if (!isPredictEnabled) {
- setState({
- markets: [],
- isLoading: false,
- error: null,
- });
- return;
- }
-
- // Capture current request ID to detect stale responses
- const currentRequestId = ++requestIdRef.current;
-
- // Check cache first (only valid if limit is satisfied)
- if (
- marketsCache &&
- Date.now() - marketsCache.timestamp < CACHE_TTL_MS &&
- marketsCache.limit >= currentLimit
- ) {
- if (isMountedRef.current) {
- setState({
- markets: marketsCache.markets.slice(0, currentLimit),
- isLoading: false,
- error: null,
- });
- }
- return;
- }
-
- try {
- setState((prev) => ({ ...prev, isLoading: true, error: null }));
-
- if (!Engine || !Engine.context) {
- throw new Error('Engine not initialized');
- }
-
- const controller = Engine.context.PredictController;
- if (!controller) {
- throw new Error('Predict controller not available');
- }
-
- const markets = await controller.getMarkets({
- category: 'trending',
- limit: currentLimit,
- });
-
- // Verify this response matches current request
- if (requestIdRef.current !== currentRequestId || !isMountedRef.current) {
- return;
- }
-
- if (!markets || !Array.isArray(markets)) {
- setState({
- markets: [],
- isLoading: false,
- error: null,
- });
- return;
- }
-
- // Cache the result with the limit used for the API call
- marketsCache = {
- markets,
- timestamp: Date.now(),
- limit: currentLimit,
- };
-
- setState({
- markets: markets.slice(0, currentLimit),
- isLoading: false,
- error: null,
- });
- } catch (err) {
- // Verify this error is for current request
- if (requestIdRef.current !== currentRequestId || !isMountedRef.current) {
- return;
- }
-
- setState({
- markets: [],
- isLoading: false,
- error:
- err instanceof Error
- ? err.message
- : 'Failed to fetch prediction markets',
- });
- }
- }, [isPredictEnabled]);
-
- // Refresh function that bypasses cache
- const refresh = useCallback(async () => {
- marketsCache = null; // Clear cache to force refetch
- await fetchMarkets();
- }, [fetchMarkets]);
-
- // Effect to fetch markets on mount
- useEffect(() => {
- isMountedRef.current = true;
-
- if (!isPredictEnabled) {
- setState({
- markets: [],
- isLoading: false,
- error: null,
- });
- return () => {
- isMountedRef.current = false;
- };
- }
-
- fetchMarkets();
-
- return () => {
- isMountedRef.current = false;
- };
- }, [isPredictEnabled, fetchMarkets]);
-
return {
- markets: state.markets,
- isLoading: state.isLoading,
- error: state.error,
- refresh,
+ markets: marketData,
+ isLoading: isFetching,
+ error,
+ refetch,
};
};
diff --git a/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage.test.ts b/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage.test.ts
index 321a59c92c8..fecd76145cb 100644
--- a/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage.test.ts
+++ b/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage.test.ts
@@ -1,52 +1,25 @@
-import { renderHook, act } from '@testing-library/react-hooks';
-import {
- usePredictPositionsForHomepage,
- _clearPositionsCache,
-} from './usePredictPositionsForHomepage';
+import { renderHook } from '@testing-library/react-native';
+import { usePredictPositionsForHomepage } from './usePredictPositionsForHomepage';
import type { PredictPosition } from '../../../../../UI/Predict/types';
-const mockGetPositions = jest.fn();
-
-jest.mock('../../../../../../core/Engine', () => ({
- context: {
- PredictController: {
- getPositions: (...args: unknown[]) => mockGetPositions(...args),
- },
- },
-}));
-
-let mockIsPredictEnabled = true;
-let mockUserAddress: string | null = '0xuser123';
-
-jest.mock('react-redux', () => ({
- ...jest.requireActual('react-redux'),
- useSelector: (selector: (...args: unknown[]) => unknown) => {
- if (
- selector ===
- jest.requireMock('../../../../../UI/Predict').selectPredictEnabledFlag
- ) {
- return mockIsPredictEnabled;
- }
- if (
- selector ===
- jest.requireMock('../../../../../../selectors/accountsController')
- .selectSelectedInternalAccountFormattedAddress
- ) {
- return mockUserAddress;
- }
- return undefined;
- },
-}));
-
-jest.mock('../../../../../UI/Predict', () => ({
- selectPredictEnabledFlag: jest.fn(),
-}));
-
-jest.mock('../../../../../../selectors/accountsController', () => ({
- selectSelectedInternalAccountFormattedAddress: jest.fn(),
+const mockRefetch = jest.fn().mockResolvedValue(undefined);
+let mockUsePredictPositionsReturn: {
+ data: PredictPosition[] | undefined;
+ isLoading: boolean;
+ error: Error | null;
+ refetch: jest.Mock;
+} = {
+ data: undefined,
+ isLoading: false,
+ error: null,
+ refetch: mockRefetch,
+};
+
+jest.mock('../../../../../UI/Predict/hooks/usePredictPositions', () => ({
+ usePredictPositions: () => mockUsePredictPositionsReturn,
}));
-const createMockPosition = (id: string): PredictPosition =>
+const createMockPosition = (id: string, currentValue = 12): PredictPosition =>
({
outcomeId: `outcome-${id}`,
outcomeIndex: 0,
@@ -55,7 +28,7 @@ const createMockPosition = (id: string): PredictPosition =>
outcome: 'Yes',
icon: `https://example.com/icon-${id}.png`,
initialValue: 10,
- currentValue: 12,
+ currentValue,
size: 15,
percentPnl: 20,
}) as unknown as PredictPosition;
@@ -63,222 +36,140 @@ const createMockPosition = (id: string): PredictPosition =>
describe('usePredictPositionsForHomepage', () => {
beforeEach(() => {
jest.clearAllMocks();
- _clearPositionsCache();
- mockIsPredictEnabled = true;
- mockUserAddress = '0xuser123';
-
- mockGetPositions.mockResolvedValue([
- createMockPosition('1'),
- createMockPosition('2'),
- createMockPosition('3'),
- ]);
+ mockUsePredictPositionsReturn = {
+ data: [
+ createMockPosition('1'),
+ createMockPosition('2'),
+ createMockPosition('3'),
+ ],
+ isLoading: false,
+ error: null,
+ refetch: mockRefetch,
+ };
});
- it('fetches positions on mount when predict is enabled', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictPositionsForHomepage(5),
- );
-
- await waitForNextUpdate();
+ it('returns positions from usePredictPositions', () => {
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
expect(result.current.positions).toHaveLength(3);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
- expect(mockGetPositions).toHaveBeenCalledWith({
- address: '0xuser123',
- claimable: false,
- });
- });
-
- it('returns empty positions when predict is disabled', () => {
- mockIsPredictEnabled = false;
-
- const { result } = renderHook(() => usePredictPositionsForHomepage(5));
-
- expect(result.current.positions).toHaveLength(0);
- expect(result.current.isLoading).toBe(false);
- expect(mockGetPositions).not.toHaveBeenCalled();
});
- it('returns empty positions when user address is null', () => {
- mockUserAddress = null;
+ it('returns empty positions when data is undefined', () => {
+ mockUsePredictPositionsReturn.data = undefined;
- const { result } = renderHook(() => usePredictPositionsForHomepage(5));
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
expect(result.current.positions).toHaveLength(0);
- expect(result.current.isLoading).toBe(false);
- expect(mockGetPositions).not.toHaveBeenCalled();
});
- it('limits positions to maxPositions parameter', async () => {
- mockGetPositions.mockResolvedValue([
+ it('slices positions to maxPositions', () => {
+ mockUsePredictPositionsReturn.data = [
createMockPosition('1'),
createMockPosition('2'),
createMockPosition('3'),
createMockPosition('4'),
createMockPosition('5'),
- ]);
+ ];
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictPositionsForHomepage(2),
+ const { result } = renderHook(() =>
+ usePredictPositionsForHomepage({ maxPositions: 2 }),
);
- await waitForNextUpdate();
-
expect(result.current.positions).toHaveLength(2);
+ expect(result.current.positions[0].outcomeId).toBe('outcome-1');
+ expect(result.current.positions[1].outcomeId).toBe('outcome-2');
});
- it('sets error state when fetch fails', async () => {
- mockGetPositions.mockRejectedValue(new Error('API error'));
+ it('returns all positions when maxPositions is omitted', () => {
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictPositionsForHomepage(5),
- );
+ expect(result.current.positions).toHaveLength(3);
+ });
+
+ it('maps Error to error string', () => {
+ mockUsePredictPositionsReturn.error = new Error('API error');
- await waitForNextUpdate();
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
expect(result.current.error).toBe('API error');
- expect(result.current.positions).toHaveLength(0);
- expect(result.current.isLoading).toBe(false);
});
- it('sets fallback error for non-Error throws', async () => {
- mockGetPositions.mockRejectedValue('string error');
-
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictPositionsForHomepage(5),
- );
+ it('maps non-Error to error string', () => {
+ mockUsePredictPositionsReturn.error = 'string error' as unknown as Error;
- await waitForNextUpdate();
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
- expect(result.current.error).toBe('Failed to fetch positions');
+ expect(result.current.error).toBe('string error');
});
- it('handles null response from getPositions', async () => {
- mockGetPositions.mockResolvedValue(null);
-
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictPositionsForHomepage(5),
- );
-
- await waitForNextUpdate();
+ it('returns null error when no error', () => {
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
- expect(result.current.positions).toHaveLength(0);
expect(result.current.error).toBeNull();
});
- it('uses cached data on subsequent renders within TTL', async () => {
- const { waitForNextUpdate, unmount } = renderHook(() =>
- usePredictPositionsForHomepage(5),
- );
-
- await waitForNextUpdate();
+ it('forwards isLoading from usePredictPositions', () => {
+ mockUsePredictPositionsReturn.isLoading = true;
- expect(mockGetPositions).toHaveBeenCalledTimes(1);
- unmount();
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
- const { result: result2 } = renderHook(() =>
- usePredictPositionsForHomepage(5),
- );
-
- expect(result2.current.positions).toHaveLength(3);
- expect(result2.current.isLoading).toBe(false);
- expect(mockGetPositions).toHaveBeenCalledTimes(1);
+ expect(result.current.isLoading).toBe(true);
});
- it('clears cache and refetches on refresh', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- usePredictPositionsForHomepage(5),
- );
-
- await waitForNextUpdate();
+ it('exposes refetch from usePredictPositions', async () => {
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
- expect(mockGetPositions).toHaveBeenCalledTimes(1);
+ await result.current.refetch();
- await act(async () => {
- await result.current.refresh();
- });
-
- expect(mockGetPositions).toHaveBeenCalledTimes(2);
+ expect(mockRefetch).toHaveBeenCalledTimes(1);
});
- describe('claimable parameter', () => {
- it('passes claimable: false to getPositions by default', async () => {
- const { waitForNextUpdate } = renderHook(() =>
- usePredictPositionsForHomepage(5),
- );
-
- await waitForNextUpdate();
+ describe('claimable option', () => {
+ it('defaults claimable to false', () => {
+ const { result } = renderHook(() => usePredictPositionsForHomepage());
- expect(mockGetPositions).toHaveBeenCalledWith({
- address: '0xuser123',
- claimable: false,
- });
+ expect(result.current.totalClaimableValue).toBe(0);
});
- it('passes claimable: true to getPositions when specified', async () => {
- const { waitForNextUpdate } = renderHook(() =>
- usePredictPositionsForHomepage(undefined, true),
- );
+ it('computes totalClaimableValue as sum of currentValue when claimable is true', () => {
+ mockUsePredictPositionsReturn.data = [
+ createMockPosition('c1', 5),
+ createMockPosition('c2', 10),
+ createMockPosition('c3', 3),
+ ];
- await waitForNextUpdate();
+ const { result } = renderHook(() =>
+ usePredictPositionsForHomepage({ claimable: true }),
+ );
- expect(mockGetPositions).toHaveBeenCalledWith({
- address: '0xuser123',
- claimable: true,
- });
+ expect(result.current.totalClaimableValue).toBe(18);
});
- it('uses separate cache keys for claimable and non-claimable fetches', async () => {
- // Fetch non-claimable
- const { waitForNextUpdate: wait1, unmount: unmount1 } = renderHook(() =>
- usePredictPositionsForHomepage(5, false),
- );
- await wait1();
- unmount1();
-
- expect(mockGetPositions).toHaveBeenCalledTimes(1);
+ it('returns totalClaimableValue 0 when claimable is false', () => {
+ mockUsePredictPositionsReturn.data = [createMockPosition('1', 100)];
- // Fetch claimable — must hit the API again (different cache key)
- const { waitForNextUpdate: wait2 } = renderHook(() =>
- usePredictPositionsForHomepage(5, true),
+ const { result } = renderHook(() =>
+ usePredictPositionsForHomepage({ claimable: false }),
);
- await wait2();
- expect(mockGetPositions).toHaveBeenCalledTimes(2);
+ expect(result.current.totalClaimableValue).toBe(0);
});
- it('serves claimable positions from cache independently of non-claimable', async () => {
- const claimablePositions = [createMockPosition('c1')];
- const nonClaimablePositions = [
- createMockPosition('1'),
- createMockPosition('2'),
+ it('treats undefined currentValue as 0 in totalClaimableValue sum', () => {
+ mockUsePredictPositionsReturn.data = [
+ {
+ ...createMockPosition('c1', 5),
+ currentValue: undefined,
+ } as unknown as PredictPosition,
];
- mockGetPositions
- .mockResolvedValueOnce(nonClaimablePositions)
- .mockResolvedValueOnce(claimablePositions);
-
- // Populate both caches
- const {
- result: r1,
- waitForNextUpdate: w1,
- unmount: u1,
- } = renderHook(() => usePredictPositionsForHomepage(undefined, false));
- await w1();
- u1();
-
- const {
- result: r2,
- waitForNextUpdate: w2,
- unmount: u2,
- } = renderHook(() => usePredictPositionsForHomepage(undefined, true));
- await w2();
- u2();
-
- // Each set has its own cached value
- expect(r1.current.positions).toHaveLength(2);
- expect(r2.current.positions).toHaveLength(1);
+ const { result } = renderHook(() =>
+ usePredictPositionsForHomepage({ claimable: true }),
+ );
+
+ expect(result.current.totalClaimableValue).toBe(0);
});
});
});
diff --git a/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage.ts b/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage.ts
index f2e07327c1a..4e3002a20a3 100644
--- a/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage.ts
+++ b/app/components/Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage.ts
@@ -1,198 +1,66 @@
-import { useState, useEffect, useCallback, useRef } from 'react';
-import { useSelector } from 'react-redux';
-import Engine from '../../../../../../core/Engine';
-import { selectSelectedInternalAccountFormattedAddress } from '../../../../../../selectors/accountsController';
-import { selectPredictEnabledFlag } from '../../../../../UI/Predict';
+import { useMemo } from 'react';
+import { usePredictPositions } from '../../../../../UI/Predict/hooks/usePredictPositions';
import type { PredictPosition } from '../../../../../UI/Predict/types';
export interface UsePredictPositionsForHomepageResult {
positions: PredictPosition[];
+ /** Sum of currentValue across all claimable positions (only meaningful when claimable: true) */
+ totalClaimableValue: number;
isLoading: boolean;
error: string | null;
- refresh: () => Promise;
+ refetch: () => Promise;
}
-interface CacheEntry {
- positions: PredictPosition[];
- timestamp: number;
+interface UsePredictPositionsForHomepageOptions {
+ maxPositions?: number;
+ claimable?: boolean;
}
-// Module-level cache for positions data
-const positionsCache = new Map();
-const CACHE_TTL_MS = 60 * 1000; // 1 minute TTL for positions (shorter than markets)
-
/**
- * Clean expired entries from cache
- */
-const cleanExpiredCache = () => {
- const now = Date.now();
- for (const [key, value] of positionsCache.entries()) {
- if (now - value.timestamp > CACHE_TTL_MS) {
- positionsCache.delete(key);
- }
- }
-};
-
-/**
- * Clear the positions cache (useful for testing or forced refresh)
- */
-export const _clearPositionsCache = (): void => {
- positionsCache.clear();
-};
-
-/**
- * Lightweight hook for fetching user prediction positions for the homepage.
- *
- * Uses module-level caching to avoid redundant API calls.
+ * Lightweight wrapper around the Predict team's usePredictPositions hook,
+ * adapted for homepage display with optional slicing and claimable value sum.
*
- * @param maxPositions - Maximum number of positions to return (all if omitted)
- * @param claimable - When true, returns only claimable positions; when false (default), returns only active positions
- * @returns Positions data, loading state, and refresh function
+ * The feature flag check is handled at the UI level (Homepage conditionally
+ * renders the Predictions section), so this hook assumes it is only called
+ * when predictions are enabled.
*/
export const usePredictPositionsForHomepage = (
- maxPositions?: number,
- claimable = false,
+ options: UsePredictPositionsForHomepageOptions = {},
): UsePredictPositionsForHomepageResult => {
- const isPredictEnabled = useSelector(selectPredictEnabledFlag);
- const userAddress = useSelector(
- selectSelectedInternalAccountFormattedAddress,
- );
-
- const isMountedRef = useRef(true);
- const requestIdRef = useRef(0);
-
- // Use ref for maxPositions to keep fetchPositions callback stable
- const maxPositionsRef = useRef(maxPositions);
- maxPositionsRef.current = maxPositions;
-
- const cacheKey = userAddress
- ? `predict_positions_${userAddress}_${claimable}`
- : null;
-
- const [state, setState] = useState<{
- positions: PredictPosition[];
- isLoading: boolean;
- error: string | null;
- }>(() => {
- // Check cache on initial render
- if (!cacheKey || !isPredictEnabled) {
- return { positions: [], isLoading: false, error: null };
- }
-
- const cached = positionsCache.get(cacheKey);
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
- return {
- positions: maxPositions
- ? cached.positions.slice(0, maxPositions)
- : cached.positions,
- isLoading: false,
- error: null,
- };
- }
+ const { maxPositions, claimable = false } = options;
- return { positions: [], isLoading: true, error: null };
+ const { data, isLoading, error, refetch } = usePredictPositions({
+ claimable,
});
- const fetchPositions = useCallback(async () => {
- const currentMaxPositions = maxPositionsRef.current;
+ const allPositions = useMemo(() => data ?? [], [data]);
- if (!isPredictEnabled || !cacheKey || !userAddress) {
- setState({ positions: [], isLoading: false, error: null });
- return;
- }
-
- const currentRequestId = ++requestIdRef.current;
-
- // Check cache first
- const cached = positionsCache.get(cacheKey);
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
- if (isMountedRef.current) {
- setState({
- positions: currentMaxPositions
- ? cached.positions.slice(0, currentMaxPositions)
- : cached.positions,
- isLoading: false,
- error: null,
- });
- }
- return;
- }
-
- try {
- setState((prev) => ({ ...prev, isLoading: true, error: null }));
-
- const controller = Engine.context.PredictController;
- const positionsData = await controller.getPositions({
- address: userAddress,
- claimable,
- });
-
- // Check if this request is still valid
- if (requestIdRef.current !== currentRequestId || !isMountedRef.current) {
- return;
- }
-
- const validPositions = Array.isArray(positionsData) ? positionsData : [];
-
- // Update cache
- positionsCache.set(cacheKey, {
- positions: validPositions,
- timestamp: Date.now(),
- });
-
- cleanExpiredCache();
-
- setState({
- positions: currentMaxPositions
- ? validPositions.slice(0, currentMaxPositions)
- : validPositions,
- isLoading: false,
- error: null,
- });
- } catch (err) {
- if (requestIdRef.current !== currentRequestId || !isMountedRef.current) {
- return;
- }
-
- setState({
- positions: [],
- isLoading: false,
- error: err instanceof Error ? err.message : 'Failed to fetch positions',
- });
- }
- }, [isPredictEnabled, cacheKey, userAddress, claimable]);
-
- const refresh = useCallback(async () => {
- // Clear cache and refetch
- if (cacheKey) {
- positionsCache.delete(cacheKey);
- }
- await fetchPositions();
- }, [cacheKey, fetchPositions]);
-
- // Initial fetch
- useEffect(() => {
- isMountedRef.current = true;
-
- if (!isPredictEnabled || !userAddress) {
- setState({ positions: [], isLoading: false, error: null });
- return () => {
- isMountedRef.current = false;
- };
- }
-
- fetchPositions();
+ const positions = useMemo(
+ () =>
+ maxPositions !== undefined
+ ? allPositions.slice(0, maxPositions)
+ : allPositions,
+ [allPositions, maxPositions],
+ );
- return () => {
- isMountedRef.current = false;
- };
- }, [isPredictEnabled, userAddress, fetchPositions]);
+ const totalClaimableValue = useMemo(
+ () =>
+ claimable
+ ? allPositions.reduce((sum, p) => sum + (p.currentValue ?? 0), 0)
+ : 0,
+ [claimable, allPositions],
+ );
return {
- positions: state.positions,
- isLoading: state.isLoading,
- error: state.error,
- refresh,
+ positions,
+ totalClaimableValue,
+ isLoading,
+ error: error
+ ? error instanceof Error
+ ? error.message
+ : String(error)
+ : null,
+ refetch,
};
};