diff --git a/e2e/guide.spec.ts b/e2e/guide.spec.ts
index a4de376f4..6db7c9639 100644
--- a/e2e/guide.spec.ts
+++ b/e2e/guide.spec.ts
@@ -63,7 +63,7 @@ test.describe('Guide 页面', () => {
await page.click('[data-testid="get-started-button"]', { force: true })
// 应该导航到创建钱包页
- await expect(page).toHaveURL(/\/#\/wallet\/create\/?/)
+ await expect(page).toHaveURL(/(\/#\/wallet\/create)|(\/#\/onboarding\/create)\/?/)
})
test('我有钱包按钮导航到导入钱包页', async ({ page }) => {
@@ -79,6 +79,6 @@ test.describe('Guide 页面', () => {
await page.click('[data-testid="have-wallet-button"]', { force: true })
// 应该导航到导入钱包页
- await expect(page).toHaveURL(/\/#\/wallet\/import\/?/)
+ await expect(page).toHaveURL(/(\/#\/wallet\/import)|(\/#\/onboarding\/recover)\/?/)
})
})
diff --git a/playwright.config.ts b/playwright.config.ts
index df569f833..ccca30610 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -85,6 +85,12 @@ export default defineConfig({
viewport: { width: 1280, height: 720 },
},
},
+ {
+ name: 'Mobile Safari',
+ use: {
+ ...devices['iPhone 12'],
+ },
+ },
],
// 使用标准 dev 服务器(端口 11173,避免与 vite 默认 517x 冲突)
diff --git a/src/components/asset/asset-item.test.tsx b/src/components/asset/asset-item.test.tsx
index 10473cf09..6dbe19e12 100644
--- a/src/components/asset/asset-item.test.tsx
+++ b/src/components/asset/asset-item.test.tsx
@@ -21,7 +21,8 @@ describe('AssetItem', () => {
it('renders formatted balance', () => {
render()
- expect(screen.getByText('1.5')).toBeInTheDocument()
+ // AmountDisplay uses aria-label due to NumberFlow dual-span structure
+ expect(screen.getByLabelText('1.5')).toBeInTheDocument()
})
it('uses assetType as name when name is not provided', () => {
diff --git a/src/components/asset/asset-list.test.tsx b/src/components/asset/asset-list.test.tsx
index 6b7ef40c4..f6edf917e 100644
--- a/src/components/asset/asset-list.test.tsx
+++ b/src/components/asset/asset-list.test.tsx
@@ -62,9 +62,10 @@ describe('AssetList', () => {
it('renders asset balances', () => {
renderWithI18n()
- expect(screen.getByText('1.5')).toBeInTheDocument() // ETH
- expect(screen.getByText('100')).toBeInTheDocument() // USDT
- expect(screen.getByText('0.5')).toBeInTheDocument() // BTC
+ // AmountDisplay uses aria-label due to NumberFlow dual-span structure
+ expect(screen.getByLabelText('1.5')).toBeInTheDocument()
+ expect(screen.getByLabelText('100')).toBeInTheDocument()
+ expect(screen.getByLabelText('0.5')).toBeInTheDocument()
})
it('applies custom className', () => {
diff --git a/src/components/asset/asset-selector.test.tsx b/src/components/asset/asset-selector.test.tsx
index 878dc34e5..3575ac0e6 100644
--- a/src/components/asset/asset-selector.test.tsx
+++ b/src/components/asset/asset-selector.test.tsx
@@ -1,5 +1,6 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
import { AssetSelector } from './asset-selector'
import type { TokenInfo } from '@/components/token/token-item'
@@ -78,7 +79,8 @@ describe('AssetSelector', () => {
expect(screen.getByText('Tether USD')).toBeInTheDocument()
})
- it('calls onSelect when asset is clicked', () => {
+ it('calls onSelect when asset is clicked', async () => {
+ const user = userEvent.setup()
const onSelect = vi.fn()
render(
{
)
// Open dropdown
- fireEvent.click(screen.getByTestId('asset-selector'))
+ await user.click(screen.getByTestId('asset-selector'))
- // Click on USDT
- fireEvent.click(screen.getByText('Tether USD'))
+ // Click on USDT option (Base UI Select uses role="option")
+ const options = screen.getAllByRole('option')
+ const usdtOption = options.find(opt => opt.textContent?.includes('Tether USD'))
+ expect(usdtOption).toBeDefined()
+ await user.click(usdtOption!)
expect(onSelect).toHaveBeenCalledWith(mockAssets[1])
})
diff --git a/src/components/common/amount-display.test.tsx b/src/components/common/amount-display.test.tsx
index d9b74b255..6a56af173 100644
--- a/src/components/common/amount-display.test.tsx
+++ b/src/components/common/amount-display.test.tsx
@@ -50,79 +50,85 @@ describe('formatAmount', () => {
})
describe('AmountDisplay', () => {
+ // Helper to get amount element (uses aria-label due to NumberFlow dual-span structure)
+ const getAmount = (value: string) => screen.getByLabelText(value)
+ const queryAmount = (value: string) => screen.queryByLabelText(value)
+
it('renders value', () => {
render()
- expect(screen.getByText('100')).toBeInTheDocument()
+ expect(getAmount('100')).toBeInTheDocument()
})
it('renders with symbol', () => {
render()
- expect(screen.getByText('100')).toBeInTheDocument()
- expect(screen.getByText('USDT')).toBeInTheDocument()
+ expect(getAmount('100 USDT')).toBeInTheDocument()
})
it('shows hidden state', () => {
render()
expect(screen.getByText('••••••')).toBeInTheDocument()
- expect(screen.queryByText('100')).not.toBeInTheDocument()
+ expect(queryAmount('100')).not.toBeInTheDocument()
})
it('shows + sign when sign=always for positive', () => {
render()
- expect(screen.getByText('+100')).toBeInTheDocument()
+ expect(getAmount('+100')).toBeInTheDocument()
})
it('shows - sign when sign=always for negative', () => {
render()
- expect(screen.getByText('-100')).toBeInTheDocument()
+ expect(getAmount('-100')).toBeInTheDocument()
})
it('shows - sign when sign=auto for negative', () => {
render()
- expect(screen.getByText('-100')).toBeInTheDocument()
+ expect(getAmount('-100')).toBeInTheDocument()
})
it('applies positive color when color=auto', () => {
render()
- expect(screen.getByText('100')).toHaveClass('text-green-500')
+ expect(getAmount('100')).toHaveClass('text-green-500')
})
it('applies negative color when color=auto', () => {
render()
- expect(screen.getByText('100')).toHaveClass('text-destructive')
+ expect(getAmount('100')).toHaveClass('text-destructive')
})
it('applies size classes', () => {
render()
- expect(screen.getByText('100')).toHaveClass('text-lg')
+ expect(getAmount('100')).toHaveClass('text-lg')
})
it('applies weight classes', () => {
render()
- expect(screen.getByText('100')).toHaveClass('font-bold')
+ expect(getAmount('100')).toHaveClass('font-bold')
})
it('applies mono font', () => {
render()
- expect(screen.getByText('100')).toHaveClass('font-mono')
+ expect(getAmount('100')).toHaveClass('font-mono')
})
it('uses compact format', () => {
render()
- expect(screen.getByText('1.5M')).toBeInTheDocument()
+ expect(getAmount('1.5M')).toBeInTheDocument()
})
})
describe('AmountWithFiat', () => {
+ // Helper to get amount element (uses aria-label due to NumberFlow dual-span structure)
+ const getAmount = (value: string) => screen.getByLabelText(value)
+
it('renders amount and fiat value vertically', () => {
render()
- expect(screen.getByText('100')).toBeInTheDocument()
+ expect(getAmount('100 USDT')).toBeInTheDocument()
expect(screen.getByText('≈ $100')).toBeInTheDocument()
})
it('renders amount and fiat value horizontally', () => {
render()
- expect(screen.getByText('100')).toBeInTheDocument()
+ expect(getAmount('100')).toBeInTheDocument()
expect(screen.getByText('≈ $100')).toBeInTheDocument()
})
@@ -133,7 +139,7 @@ describe('AmountWithFiat', () => {
it('renders without fiat when not provided', () => {
render()
- expect(screen.getByText('100')).toBeInTheDocument()
+ expect(getAmount('100')).toBeInTheDocument()
expect(screen.queryByText(/≈/)).not.toBeInTheDocument()
})
})
diff --git a/src/components/wallet/token-icon.test.tsx b/src/components/wallet/token-icon.test.tsx
index 21506114f..fdccbfcd0 100644
--- a/src/components/wallet/token-icon.test.tsx
+++ b/src/components/wallet/token-icon.test.tsx
@@ -3,9 +3,10 @@ import { render, screen, fireEvent } from '@testing-library/react'
import { TokenIcon, TokenBadge, TokenIconProvider } from './token-icon'
const mockGetTokenIconBases = (chainId: string) => {
+ // Templates use $symbol (lowercase) or $SYMBOL (uppercase) placeholders
const bases: Record = {
- ethereum: ['/icons/ethereum/tokens', 'https://cdn.example.com/eth'],
- bfmeta: ['/icons/bfmeta/tokens', 'https://cdn.example.com/bfm'],
+ ethereum: ['/icons/ethereum/tokens/$symbol.svg', 'https://cdn.example.com/eth/$symbol.png'],
+ bfmeta: ['/icons/bfmeta/tokens/$symbol.svg', 'https://cdn.example.com/bfm/$symbol.png'],
}
return bases[chainId] ?? []
}
diff --git a/src/i18n/locales/ar/common.json b/src/i18n/locales/ar/common.json
index d646fdf59..18ad684df 100644
--- a/src/i18n/locales/ar/common.json
+++ b/src/i18n/locales/ar/common.json
@@ -1,7 +1,7 @@
{
"a11y": {
- "addWallet": "إضافة محفظة",
"addContact": "إضافة جهة اتصال",
+ "addWallet": "إضافة محفظة",
"appInfo": "معلومات التطبيق",
"back": "رجوع",
"chainSelector": "اختر شبكة البلوكتشين",
@@ -44,24 +44,42 @@
"acquisitionTime": "Acquisition time",
"add": "Add",
"addressBook": {
+ "addContact": "إضافة جهة اتصال",
+ "addresses": "العناوين",
+ "changeAvatar": "انقر لتغيير الصورة",
"delete": "حذف",
"deleteConfirm": "هل أنت متأكد من حذف جهة الاتصال \"{{name}}\"؟",
"deleteTitle": "حذف جهة الاتصال",
"edit": "تعديل",
+ "memo": "ملاحظة",
+ "memoPlaceholder": "أضف ملاحظة (اختياري)",
+ "namePlaceholder": "أدخل اسم جهة الاتصال",
"noContacts": "لا توجد جهات اتصال بعد",
"noResults": "لم يتم العثور على جهات اتصال",
"passwordError": "كلمة المرور غير صحيحة",
+ "scanToAdd": "امسح لإضافة جهة اتصال",
"searchPlaceholder": "البحث عن جهات الاتصال",
- "title": "دفتر العناوين",
- "verifyFailed": "فشل التحقق",
- "addContact": "إضافة جهة اتصال",
- "namePlaceholder": "أدخل اسم جهة الاتصال",
- "addresses": "العناوين",
- "memo": "ملاحظة",
- "memoPlaceholder": "أضف ملاحظة (اختياري)",
"shareContact": "مشاركة البطاقة",
- "scanToAdd": "امسح لإضافة جهة اتصال",
- "changeAvatar": "انقر لتغيير الصورة"
+ "title": "دفتر العناوين",
+ "verifyFailed": "فشل التحقق"
+ },
+ "addressLookup": {
+ "address": "Address",
+ "addressOrHash": "Address or Transaction Hash",
+ "addressOrHashPlaceholder": "Enter address or tx hash",
+ "addressPlaceholder": "Enter wallet address",
+ "balanceTitle": "Address Balance",
+ "chain": "Chain",
+ "error": "Query Failed",
+ "explorerHint": "Transaction history requires block explorer",
+ "noTransactions": "No transactions found",
+ "onChain": "on {{chain}}",
+ "openExplorer": "Open {{name}} Explorer",
+ "otherChains": "Other Chains",
+ "queryError": "Failed to query transactions, please try again",
+ "transactionsTitle": "Address Transactions",
+ "useExplorerHint": "This chain does not support direct history query, please use the explorer",
+ "viewOnExplorer": "View on {{name}}"
},
"addressPlaceholder": "أدخل أو الصق العنوان",
"advancedEncryptionTechnologyDigitalWealthIsMoreSecure": "Advanced encryption technology <br /> Digital wealth is more secure",
@@ -86,6 +104,7 @@
"cancel": "Cancel",
"cardWorks": "[MISSING:ar] card works!",
"certificate": "Certificate",
+ "clickToCopy": "[MISSING:ar] 点击复制",
"clickToShowTheQr": "[MISSING:ar] 点开显示二维码",
"clickToShowTheQrCodeOfThePrivateKey": "Click to show the QR code of the private key",
"close": "Close",
@@ -99,9 +118,13 @@
"confirmToTurnOffVerification": "[MISSING:ar] 确认要关闭验证吗?",
"contact": {
"add": "إضافة",
+ "addAddress": "إضافة عنوان",
"addTitle": "إضافة جهة اتصال",
"address": "العنوان",
+ "addressLabel": "التسمية",
+ "addressLabelPlaceholder": "مثل: الحساب الرئيسي",
"addressPlaceholder": "أدخل عنوان المحفظة",
+ "addresses": "العناوين",
"cancel": "إلغاء",
"editTitle": "تعديل جهة اتصال",
"memoOptional": "ملاحظة (اختياري)",
@@ -113,11 +136,7 @@
"save": "حفظ",
"selectAddress": "اختر العنوان",
"selectContact": "اختر جهة اتصال",
- "viewAll": "عرض جميع جهات الاتصال",
- "addresses": "العناوين",
- "addressLabel": "التسمية",
- "addressLabelPlaceholder": "مثل: الحساب الرئيسي",
- "addAddress": "إضافة عنوان"
+ "viewAll": "عرض جميع جهات الاتصال"
},
"continue": "Continue",
"contractMethods": "Contract methods",
@@ -134,6 +153,7 @@
"delete": "Delete",
"destroy": "Destroy",
"done": "Done",
+ "download": "تحميل",
"dpInTab": "[MISSING:ar] dp",
"dpName": "DP Name",
"dpType": "Type",
@@ -250,10 +270,10 @@
"resubmit": "Resubmit",
"retry": "Retry",
"save": "Save",
- "saving": "جارٍ الحفظ...",
- "saved": "تم الحفظ",
"saveQrCode": "Save QR code",
"saveSuccessful": "Saved Successfully",
+ "saved": "تم الحفظ",
+ "saving": "جارٍ الحفظ...",
"scan": "Scan",
"scanAgain": "Scan again",
"scanner": "Scanner",
@@ -261,7 +281,6 @@
"selectNetwork": "Select Network",
"selectionNetwork": "Selection network",
"share": "Share",
- "download": "تحميل",
"shareCancel": "[MISSING:ar] 分享取消",
"shareCanceled": "Share canceled",
"shareSuccessful": "Shared Successfully",
@@ -329,23 +348,5 @@
"{{item}}Words": "words",
"{{length}}Words": "words",
"中文(简体)": "中文(简体)",
- "中文(繁體)": "中文(繁體)",
- "addressLookup": {
- "balanceTitle": "Address Balance",
- "transactionsTitle": "Address Transactions",
- "chain": "Chain",
- "address": "Address",
- "addressPlaceholder": "Enter wallet address",
- "addressOrHash": "Address or Transaction Hash",
- "addressOrHashPlaceholder": "Enter address or tx hash",
- "otherChains": "Other Chains",
- "error": "Query Failed",
- "queryError": "Failed to query transactions, please try again",
- "noTransactions": "No transactions found",
- "onChain": "on {{chain}}",
- "explorerHint": "Transaction history requires block explorer",
- "useExplorerHint": "This chain does not support direct history query, please use the explorer",
- "openExplorer": "Open {{name}} Explorer",
- "viewOnExplorer": "View on {{name}}"
- }
+ "中文(繁體)": "中文(繁體)"
}
diff --git a/src/i18n/locales/ar/ecosystem.json b/src/i18n/locales/ar/ecosystem.json
new file mode 100644
index 000000000..d479ee776
--- /dev/null
+++ b/src/i18n/locales/ar/ecosystem.json
@@ -0,0 +1,24 @@
+{
+ "title": "[MISSING:ar] 生态",
+ "empty": "[MISSING:ar] 暂无小程序",
+ "loading": "[MISSING:ar] 加载中...",
+ "notFound": "[MISSING:ar] 小程序不存在",
+ "refresh": "[MISSING:ar] 刷新",
+ "sources": {
+ "title": "[MISSING:ar] 可信源管理",
+ "add": "[MISSING:ar] 添加订阅",
+ "remove": "[MISSING:ar] 移除",
+ "empty": "[MISSING:ar] 暂无订阅源"
+ },
+ "permissions": {
+ "title": "[MISSING:ar] 权限请求",
+ "grant": "[MISSING:ar] 授权",
+ "deny": "[MISSING:ar] 拒绝",
+ "bio_requestAccounts": "[MISSING:ar] 访问钱包地址",
+ "bio_selectAccount": "[MISSING:ar] 选择账户",
+ "bio_pickWallet": "[MISSING:ar] 选择其他钱包",
+ "bio_signMessage": "[MISSING:ar] 签名消息",
+ "bio_signTypedData": "[MISSING:ar] 签名数据",
+ "bio_sendTransaction": "[MISSING:ar] 发起交易"
+ }
+}
diff --git a/src/i18n/locales/ar/home.json b/src/i18n/locales/ar/home.json
index 95a8e4eec..f6cdc38b3 100644
--- a/src/i18n/locales/ar/home.json
+++ b/src/i18n/locales/ar/home.json
@@ -1,17 +1,20 @@
{
- "welcome": {
- "title": "مرحبًا بك في BFM Pay",
- "subtitle": "إنشاء أو استيراد محفظة للبدء",
- "createWallet": "إنشاء محفظة جديدة",
- "importWallet": "استيراد محفظة موجودة"
- },
"wallet": {
- "send": "إرسال",
- "receive": "استلام",
+ "addressCopied": "تم نسخ العنوان",
"assets": "الأصول",
+ "destroy": "[MISSING:ar] 销毁",
"noAssets": "لا توجد أصول",
"noAssetsOnChain": "لا توجد رموز على {{chain}}",
+ "receive": "استلام",
"selectNetwork": "اختر الشبكة",
- "addressCopied": "تم نسخ العنوان"
+ "send": "إرسال",
+ "tokenActions": "[MISSING:ar] 资产操作",
+ "transfer": "[MISSING:ar] 转账"
+ },
+ "welcome": {
+ "createWallet": "إنشاء محفظة جديدة",
+ "importWallet": "استيراد محفظة موجودة",
+ "subtitle": "إنشاء أو استيراد محفظة للبدء",
+ "title": "مرحبًا بك في BFM Pay"
}
}
diff --git a/src/i18n/locales/ar/settings.json b/src/i18n/locales/ar/settings.json
index 111caa596..22f683d74 100644
--- a/src/i18n/locales/ar/settings.json
+++ b/src/i18n/locales/ar/settings.json
@@ -1,146 +1,149 @@
{
- "language": "اللغة",
- "settings": "الإعدادات",
- "title": "الإعدادات",
- "sections": {
- "walletManagement": "إدارة المحفظة",
- "security": "الأمان",
- "preferences": "التفضيلات",
- "about": "حول"
- },
- "items": {
- "walletManagement": "إدارة المحفظة",
- "walletChains": "شبكات المحفظة",
- "addressBook": "دفتر العناوين",
- "appLock": "قفل التطبيق",
- "viewMnemonic": "عرض العبارة السرية",
- "changeWalletLock": "تغيير كلمة المرور",
- "language": "اللغة",
- "currency": "العملة",
- "chainConfig": "تكوين السلسلة",
- "appearance": "المظهر",
- "aboutApp": "حول BFM Pay",
- "clearData": "مسح بيانات التطبيق",
- "storage": "التخزين"
- },
"appearance": {
- "title": "المظهر",
- "system": "اتباع النظام",
+ "dark": "الوضع الداكن",
"light": "الوضع الفاتح",
- "dark": "الوضع الداكن"
- },
- "languagePage": {
- "title": "اللغة",
- "hint": "بعد اختيار لغة الواجهة، سيتحول التطبيق فوراً إلى اللغة المحددة."
- },
- "currency": {
- "title": "العملة",
- "hint": "بعد اختيار العملة، ستُعرض قيم الأصول بالعملة المحددة.",
- "names": {
- "USD": "الدولار الأمريكي",
- "CNY": "اليوان الصيني",
- "EUR": "اليورو",
- "JPY": "الين الياباني",
- "KRW": "الوون الكوري"
- }
- },
- "viewMnemonic": {
- "titleMnemonic": "عرض العبارة السرية",
- "titleKey": "عرض المفتاح",
- "noWallet": "الرجاء إنشاء أو استيراد محفظة أولاً",
- "walletIncomplete": "بيانات المحفظة غير مكتملة",
- "patternError": "النمط غير صحيح",
- "securityTip": "نصيحة أمان",
- "securityWarningMnemonic": "تأكد من عدم وجود أحد يراقب. أصولك معرضة للخطر إذا تم كشف العبارة السرية.",
- "securityWarningKey": "تأكد من عدم وجود أحد يراقب. أصولك معرضة للخطر إذا تم كشف المفتاح.",
- "charCount": "{{count}} حرف",
- "wordCount": "عبارة سرية من {{count}} كلمة",
- "show": "إظهار",
- "hide": "إخفاء",
- "autoHideNote": "سيتم الإخفاء تلقائياً بعد 30 ثانية",
- "verifyTitle": "تحقق من كلمة المرور",
- "verifyDescMnemonic": "أدخل كلمة مرور المحفظة لعرض العبارة السرية",
- "verifyDescKey": "أدخل كلمة مرور المحفظة لعرض المفتاح"
- },
- "changeWalletLock": {
- "title": "تغيير قفل المحفظة",
- "noWallet": "الرجاء إنشاء أو استيراد محفظة أولاً",
- "walletIncomplete": "بيانات المحفظة غير مكتملة",
- "success": "تم تغيير قفل المحفظة بنجاح",
- "returning": "جاري العودة إلى الإعدادات...",
- "verifyTitle": "تحقق من النمط الحالي",
- "verifyDesc": "ارسم نمط قفل المحفظة الحالي للمتابعة",
- "forgotPattern": "نسيت النمط؟ إعادة تعيين باستخدام عبارة الاسترداد",
- "mnemonicVerifyTitle": "التحقق بعبارة الاسترداد",
- "mnemonicVerifyDesc": "أدخل عبارة الاسترداد التي احتفظت بها عند إنشاء المحفظة",
- "verifyMnemonic": "تحقق من عبارة الاسترداد",
- "verifying": "جاري التحقق...",
- "usePattern": "العودة للتحقق بالنمط",
- "invalidMnemonic": "تنسيق عبارة الاسترداد غير صالح",
- "mnemonicVerifyFailed": "فشل التحقق من عبارة الاسترداد، يرجى التحقق من صحتها"
+ "system": "اتباع النظام",
+ "title": "المظهر"
},
"chainAddressCount": "{{count}} عناوين سلاسل",
- "notSupported": "غير مدعوم",
"chainConfig": {
- "title": "تهيئة السلاسل",
- "subscription": {
- "title": "رابط الاشتراك",
- "description": "أدخل default لاستخدام تهيئة السلاسل المضمنة.",
- "refreshAriaLabel": "تحديث الاشتراك",
- "save": "حفظ",
- "placeholder": "default أو https://example.com/chains.json",
- "lastUpdated": "آخر تحديث: {{value}}"
+ "hint": "تلميح: يتم تخزين تهيئة الاشتراك مؤقتًا محليًا. تغيير الرابط يمسح ذاكرة التخزين المؤقت القديمة لتجنب الالتباس.",
+ "list": {
+ "description": "تتوفر السلاسل المفعّلة في تدفقات المحفظة/الأصول.",
+ "empty": "لا توجد تهيئات سلاسل.",
+ "title": "قائمة السلاسل"
},
"manual": {
- "title": "إضافة يدويًا",
- "description": "الصق JSON (كائن واحد أو مصفوفة)، ثم اضغط على إضافة.",
"add": "إضافة",
- "placeholder": "مثال: {\"id\":\"mychain\",\"version\":\"1.0\",\"type\":\"custom\",\"name\":\"MyChain\",\"symbol\":\"MC\",\"decimals\":8}",
+ "description": "الصق JSON (كائن واحد أو مصفوفة)، ثم اضغط على إضافة.",
"duplicate": {
- "title": "معرّفات سلاسل مكررة",
+ "cancel": "إلغاء",
"description": "هذه المعرّفات موجودة بالفعل. هل تريد استبدالها؟",
"replace": "استبدال",
- "cancel": "إلغاء"
- }
- },
- "list": {
- "title": "قائمة السلاسل",
- "description": "تتوفر السلاسل المفعّلة في تدفقات المحفظة/الأصول.",
- "empty": "لا توجد تهيئات سلاسل."
+ "title": "معرّفات سلاسل مكررة"
+ },
+ "placeholder": "مثال: {\"id\":\"mychain\",\"version\":\"1.0\",\"type\":\"custom\",\"name\":\"MyChain\",\"symbol\":\"MC\",\"decimals\":8}",
+ "title": "إضافة يدويًا"
},
"source": {
"default": "افتراضي",
- "subscription": "اشتراك",
- "manual": "يدوي"
+ "manual": "يدوي",
+ "subscription": "اشتراك"
},
- "warning": {
- "incompatibleMajor": "إصدار غير متوافق: {{version}} (يدعم ≤ {{supportedMajor}}.x)"
+ "subscription": {
+ "description": "أدخل default لاستخدام تهيئة السلاسل المضمنة.",
+ "lastUpdated": "آخر تحديث: {{value}}",
+ "placeholder": "default أو https://example.com/chains.json",
+ "refreshAriaLabel": "تحديث الاشتراك",
+ "save": "حفظ",
+ "title": "رابط الاشتراك"
},
+ "title": "تهيئة السلاسل",
"toast": {
- "subscriptionSaved": "تم حفظ رابط الاشتراك",
+ "manualAdded": "تمت إضافة تهيئة السلسلة",
"subscriptionRefreshed": "تم تحديث الاشتراك",
- "manualAdded": "تمت إضافة تهيئة السلسلة"
+ "subscriptionSaved": "تم حفظ رابط الاشتراك"
},
- "hint": "تلميح: يتم تخزين تهيئة الاشتراك مؤقتًا محليًا. تغيير الرابط يمسح ذاكرة التخزين المؤقت القديمة لتجنب الالتباس."
+ "warning": {
+ "incompatibleMajor": "إصدار غير متوافق: {{version}} (يدعم ≤ {{supportedMajor}}.x)"
+ }
+ },
+ "changeWalletLock": {
+ "forgotPattern": "نسيت النمط؟ إعادة تعيين باستخدام عبارة الاسترداد",
+ "invalidMnemonic": "تنسيق عبارة الاسترداد غير صالح",
+ "mnemonicVerifyDesc": "أدخل عبارة الاسترداد التي احتفظت بها عند إنشاء المحفظة",
+ "mnemonicVerifyFailed": "فشل التحقق من عبارة الاسترداد، يرجى التحقق من صحتها",
+ "mnemonicVerifyTitle": "التحقق بعبارة الاسترداد",
+ "noWallet": "الرجاء إنشاء أو استيراد محفظة أولاً",
+ "returning": "جاري العودة إلى الإعدادات...",
+ "success": "تم تغيير قفل المحفظة بنجاح",
+ "title": "تغيير قفل المحفظة",
+ "usePattern": "العودة للتحقق بالنمط",
+ "verifyDesc": "ارسم نمط قفل المحفظة الحالي للمتابعة",
+ "verifyMnemonic": "تحقق من عبارة الاسترداد",
+ "verifyTitle": "تحقق من النمط الحالي",
+ "verifying": "جاري التحقق...",
+ "walletIncomplete": "بيانات المحفظة غير مكتملة"
},
"clearData": {
- "title": "مسح بيانات التطبيق",
- "warning": "ستحذف هذه العملية جميع البيانات المحلية، بما في ذلك المحافظ والإعدادات والذاكرة المؤقتة. لا يمكن التراجع عن هذه العملية!",
+ "confirm": "تأكيد المسح",
"item1": "سيتم حذف جميع بيانات المحفظة",
"item2": "ستتم استعادة جميع الإعدادات إلى الوضع الافتراضي",
"item3": "سيتم إعادة تشغيل التطبيق",
- "confirm": "تأكيد المسح"
+ "title": "مسح بيانات التطبيق",
+ "warning": "ستحذف هذه العملية جميع البيانات المحلية، بما في ذلك المحافظ والإعدادات والذاكرة المؤقتة. لا يمكن التراجع عن هذه العملية!"
+ },
+ "currency": {
+ "hint": "بعد اختيار العملة، ستُعرض قيم الأصول بالعملة المحددة.",
+ "names": {
+ "CNY": "اليوان الصيني",
+ "EUR": "اليورو",
+ "JPY": "الين الياباني",
+ "KRW": "الوون الكوري",
+ "USD": "الدولار الأمريكي"
+ },
+ "title": "العملة"
+ },
+ "items": {
+ "aboutApp": "حول BFM Pay",
+ "addressBook": "دفتر العناوين",
+ "appLock": "قفل التطبيق",
+ "appearance": "المظهر",
+ "chainConfig": "تكوين السلسلة",
+ "changeWalletLock": "تغيير كلمة المرور",
+ "clearData": "مسح بيانات التطبيق",
+ "currency": "العملة",
+ "language": "اللغة",
+ "storage": "التخزين",
+ "viewMnemonic": "عرض العبارة السرية",
+ "walletChains": "شبكات المحفظة",
+ "walletManagement": "إدارة المحفظة"
},
+ "language": "اللغة",
+ "languagePage": {
+ "hint": "بعد اختيار لغة الواجهة، سيتحول التطبيق فوراً إلى اللغة المحددة.",
+ "title": "اللغة"
+ },
+ "notSupported": "غير مدعوم",
+ "sections": {
+ "about": "حول",
+ "preferences": "التفضيلات",
+ "security": "الأمان",
+ "walletManagement": "إدارة المحفظة"
+ },
+ "settings": "الإعدادات",
"storage": {
- "title": "التخزين",
- "usage": "استخدام التخزين",
+ "available": "المساحة المتاحة",
"basedOn": "بناءً على حصة تخزين المتصفح",
- "used": "مستخدم",
+ "clearDesc": "مسح جميع البيانات المخزنة محليًا، بما في ذلك المحافظ والإعدادات والذاكرة المؤقتة.",
+ "clearTitle": "مسح البيانات",
+ "goToClear": "[MISSING:ar] 前往清理数据",
+ "migrationDesc": "[MISSING:ar] 检测到旧版本数据格式,需要清空本地数据库后才能继续使用。您的助记词和私钥不会受到影响,但需要重新导入钱包。",
+ "migrationRequired": "[MISSING:ar] 数据需要迁移",
+ "title": "التخزين",
"total": "المساحة الإجمالية",
- "usagePercent": "نسبة الاستخدام",
- "available": "المساحة المتاحة",
"unavailable": "تعذر الحصول على معلومات التخزين",
- "clearTitle": "مسح البيانات",
- "clearDesc": "مسح جميع البيانات المخزنة محليًا، بما في ذلك المحافظ والإعدادات والذاكرة المؤقتة."
+ "usage": "استخدام التخزين",
+ "usagePercent": "نسبة الاستخدام",
+ "used": "مستخدم"
+ },
+ "title": "الإعدادات",
+ "viewMnemonic": {
+ "autoHideNote": "سيتم الإخفاء تلقائياً بعد 30 ثانية",
+ "charCount": "{{count}} حرف",
+ "hide": "إخفاء",
+ "noWallet": "الرجاء إنشاء أو استيراد محفظة أولاً",
+ "patternError": "النمط غير صحيح",
+ "securityTip": "نصيحة أمان",
+ "securityWarningKey": "تأكد من عدم وجود أحد يراقب. أصولك معرضة للخطر إذا تم كشف المفتاح.",
+ "securityWarningMnemonic": "تأكد من عدم وجود أحد يراقب. أصولك معرضة للخطر إذا تم كشف العبارة السرية.",
+ "show": "إظهار",
+ "titleKey": "عرض المفتاح",
+ "titleMnemonic": "عرض العبارة السرية",
+ "verifyDescKey": "أدخل كلمة مرور المحفظة لعرض المفتاح",
+ "verifyDescMnemonic": "أدخل كلمة مرور المحفظة لعرض العبارة السرية",
+ "verifyTitle": "تحقق من كلمة المرور",
+ "walletIncomplete": "بيانات المحفظة غير مكتملة",
+ "wordCount": "عبارة سرية من {{count}} كلمة"
}
}
diff --git a/src/i18n/locales/ar/transaction.json b/src/i18n/locales/ar/transaction.json
index 5f68e2c37..d3862accb 100644
--- a/src/i18n/locales/ar/transaction.json
+++ b/src/i18n/locales/ar/transaction.json
@@ -65,23 +65,34 @@
"contractTransactionData": "Contract transaction data",
"destoryAmount": "Destory amount",
"destoryQuantityCannotBeGreaterThanBalance": "Destory quantity cannot be greater than balance",
+ "destroyPage": {
+ "amountLabel": "[MISSING:ar] 销毁数量",
+ "assetLabel": "[MISSING:ar] 销毁资产",
+ "confirm": "[MISSING:ar] 确认销毁",
+ "noDestroyableAssets": "[MISSING:ar] 暂无可销毁的资产",
+ "notSupported": "[MISSING:ar] 当前链不支持资产销毁",
+ "resultTitle": "[MISSING:ar] 销毁结果",
+ "title": "[MISSING:ar] 销毁",
+ "warning": "[MISSING:ar] 销毁操作不可撤销,请仔细核对销毁数量。"
+ },
"detail": {
+ "allowance": "Allowance",
"blockHeight": "ارتفاع الكتلة",
"confirmations": "التأكيدات",
+ "contractAddress": "Contract Address",
"copied": "تم النسخ",
"copyHash": "نسخ الهاش",
- "contractAddress": "Contract Address",
- "spender": "Spender",
- "allowance": "Allowance",
"fee": "الرسوم",
"fromAddress": "عنوان المرسل",
"hash": "هاش المعاملة",
"info": "معلومات المعاملة",
+ "invalidData": "فشل تحليل بيانات المعاملة",
"method": "Method",
"network": "الشبكة",
- "invalidData": "فشل تحليل بيانات المعاملة",
"notFound": "المعاملة غير موجودة أو منتهية الصلاحية",
+ "openInExplorer": "[MISSING:ar] 在浏览器中打开",
"share": "Share",
+ "spender": "Spender",
"swapFrom": "From",
"swapTo": "To",
"time": "الوقت",
@@ -89,23 +100,6 @@
"toAddress": "عنوان المستلم",
"viewExplorer": "عرض في المستكشف"
},
- "method": {
- "approve": "Approve",
- "transfer": "Transfer",
- "swap": "Swap",
- "swapExactTokensForETH": "Swap Tokens for ETH",
- "swapExactTokensForTokens": "Swap Tokens",
- "swapExactETHForTokens": "Swap ETH for Tokens",
- "multicall": "Multi-call",
- "execute": "Execute",
- "deposit": "Deposit",
- "withdraw": "Withdraw",
- "stake": "Stake",
- "unstake": "Unstake",
- "claim": "Claim",
- "mint": "Mint",
- "burn": "Burn"
- },
"dpTransactionDetails": "DP Details",
"easyTransfer": "Easy Transfer",
"enterAmount": "Enter Amount",
@@ -115,15 +109,15 @@
"failed": "Failed",
"fee": "Fee",
"feeEdit": {
- "belowMinFee": "[MISSING:ar] 手续费不能低于最低手续费 {{minFee}} {{symbol}}",
- "currentFee": "[MISSING:ar] 当前手续费",
- "customFee": "[MISSING:ar] 自定义手续费",
- "invalidFee": "[MISSING:ar] 请输入有效的手续费",
- "minFee": "[MISSING:ar] 最低手续费",
- "presetFast": "[MISSING:ar] 快速",
- "presetMin": "[MISSING:ar] 最低",
- "presetRecommend": "[MISSING:ar] 推荐",
- "title": "[MISSING:ar] 编辑网络手续费"
+ "belowMinFee": "Fee cannot be lower than minimum fee {{minFee}} {{symbol}}",
+ "currentFee": "Current Fee",
+ "customFee": "Custom Fee",
+ "invalidFee": "Please enter a valid fee",
+ "minFee": "Minimum Fee",
+ "presetFast": "Fast",
+ "presetMin": "Minimum",
+ "presetRecommend": "Recommended",
+ "title": "Edit Network Fee"
},
"finallyReceive": "Finally receive:",
"forOvertimeTransactionTips": "For overtime transactions, you can check whether the transaction is successful through the blockchain explorer.",
@@ -134,13 +128,13 @@
"emptyDesc": "لم يتم العثور على معاملات بالفلاتر الحالية",
"emptyTitle": "لا توجد معاملات",
"filter": {
- "chainLabel": "السلسلة",
- "periodLabel": "الفترة",
"allChains": "جميع السلاسل",
"allTime": "الكل",
+ "chainLabel": "السلسلة",
"days30": "30 يومًا",
"days7": "7 أيام",
- "days90": "90 يومًا"
+ "days90": "90 يومًا",
+ "periodLabel": "الفترة"
},
"noWallet": "يرجى إنشاء أو استيراد محفظة أولاً",
"title": "سجل المعاملات",
@@ -152,6 +146,23 @@
"initiateTransfer": "Initiate Transfer",
"insufficientBalanceIn_{symbol}": "Insufficient balance in",
"insufficientBalanceUnableToConductTransactions": "Insufficient balance, unable to conduct transactions",
+ "method": {
+ "approve": "Approve",
+ "burn": "Burn",
+ "claim": "Claim",
+ "deposit": "Deposit",
+ "execute": "Execute",
+ "mint": "Mint",
+ "multicall": "Multi-call",
+ "stake": "Stake",
+ "swap": "Swap",
+ "swapExactETHForTokens": "Swap ETH for Tokens",
+ "swapExactTokensForETH": "Swap Tokens for ETH",
+ "swapExactTokensForTokens": "Swap Tokens",
+ "transfer": "Transfer",
+ "unstake": "Unstake",
+ "withdraw": "Withdraw"
+ },
"minerFee": "Miner fee",
"minnerFee": "Miner fee",
"newTransfer": "New Transfer",
@@ -183,9 +194,11 @@
"send": "Send",
"sendPage": {
"amountLabel": "المبلغ",
+ "assetLabel": "[MISSING:ar] 转账资产",
"cameraPermissionRequired": "مطلوب إذن الكاميرا للمسح",
"continue": "متابعة",
"explorerNotImplemented": "ميزة مستكشف البلوك قادمة قريبًا",
+ "fee": "[MISSING:ar] 手续费",
"from": "من",
"networkWarning": "يرجى التأكد من أن عنوان المستلم هو عنوان شبكة {{chain}}. لا يمكن استرداد المرسل إلى الشبكة الخاطئة",
"resultTitle": "نتيجة الإرسال",
@@ -249,17 +262,6 @@
"tronLessFeeTips": "At present, there is less TRX, and continued transfers may fail due to insufficient handling fees.",
"tronNetworkFeeLongTips_1": "The network fee will vary according to networksituation, once the transaction is confirmed, the fee cannot be refunded regardless of whether the transaction is successful or not.",
"tronNetworkFeeLongTips_2": "The network fee is charged by the TRON network, and BFM Pay does not charge any fees !",
- "feeEdit": {
- "title": "Edit Network Fee",
- "currentFee": "Current Fee",
- "minFee": "Minimum Fee",
- "customFee": "Custom Fee",
- "presetMin": "Minimum",
- "presetRecommend": "Recommended",
- "presetFast": "Fast",
- "invalidFee": "Please enter a valid fee",
- "belowMinFee": "Fee cannot be lower than minimum fee {{minFee}} {{symbol}}"
- },
"txStatus": {
"broadcasted": "Broadcast Successful",
"broadcastedDesc": "Transaction broadcasted, awaiting block confirmation...",
@@ -274,30 +276,30 @@
"viewDetails": "View Details"
},
"type": {
+ "approve": "Approve",
"certificate": "Certificate",
"dapp": "DApp",
"destroy": "Destroy",
"destroyEntity": "Destroy Entity",
"emigrate": "Emigrate",
"exchange": "Exchange",
- "swap": "Swap",
"gift": "Gift",
"grab": "Accept Gift",
"immigrate": "Immigrate",
"increaseAsset": "Increase Asset",
- "mint": "Mint",
+ "interaction": "Contract Interaction",
"issueAsset": "Issue Asset",
"issueEntity": "Issue Entity",
"locationName": "Location Name",
"mark": "Mark",
- "approve": "Approve",
- "interaction": "Contract Interaction",
+ "mint": "Mint",
"other": "Other",
"receive": "استلام",
"send": "إرسال",
"signFor": "Sign For",
"signature": "Set Security Password",
"stake": "تخزين",
+ "swap": "Swap",
"trust": "Trust",
"unstake": "إلغاء التخزين"
},
diff --git a/src/i18n/locales/en/ecosystem.json b/src/i18n/locales/en/ecosystem.json
new file mode 100644
index 000000000..8d8d20237
--- /dev/null
+++ b/src/i18n/locales/en/ecosystem.json
@@ -0,0 +1,24 @@
+{
+ "title": "[MISSING:en] 生态",
+ "empty": "[MISSING:en] 暂无小程序",
+ "loading": "[MISSING:en] 加载中...",
+ "notFound": "[MISSING:en] 小程序不存在",
+ "refresh": "[MISSING:en] 刷新",
+ "sources": {
+ "title": "[MISSING:en] 可信源管理",
+ "add": "[MISSING:en] 添加订阅",
+ "remove": "[MISSING:en] 移除",
+ "empty": "[MISSING:en] 暂无订阅源"
+ },
+ "permissions": {
+ "title": "[MISSING:en] 权限请求",
+ "grant": "[MISSING:en] 授权",
+ "deny": "[MISSING:en] 拒绝",
+ "bio_requestAccounts": "[MISSING:en] 访问钱包地址",
+ "bio_selectAccount": "[MISSING:en] 选择账户",
+ "bio_pickWallet": "[MISSING:en] 选择其他钱包",
+ "bio_signMessage": "[MISSING:en] 签名消息",
+ "bio_signTypedData": "[MISSING:en] 签名数据",
+ "bio_sendTransaction": "[MISSING:en] 发起交易"
+ }
+}
diff --git a/src/i18n/locales/zh-TW/authorize.json b/src/i18n/locales/zh-TW/authorize.json
index 4ab51abae..add7eda65 100644
--- a/src/i18n/locales/zh-TW/authorize.json
+++ b/src/i18n/locales/zh-TW/authorize.json
@@ -1,62 +1,63 @@
{
- "title": {
- "address": "地址授權",
- "signature": "交易授權"
- },
- "app": {
- "requesting": "{{appName}} 正在請求存取",
- "from": "來自:{{appHome}}"
- },
"address": {
- "shareWith": "是否與該應用分享地址?",
- "scope": {
- "main": "僅目前錢包",
- "network": "所有 {{chainName}} 地址",
- "all": "所有錢包"
- },
+ "confirmDescription": "請繪製圖案以繼續",
+ "noWallets": "暫無錢包",
"permissions": {
- "viewAddress": "查看你的錢包地址",
- "viewPublicKey": "查看你的公鑰",
+ "getMain": "查看你的錢包匯入密語(主密鑰)",
"signMessage": "簽名驗證訊息",
- "getMain": "查看你的錢包匯入密語(主密鑰)"
- },
- "confirmDescription": "請繪製圖案以繼續",
- "noWallets": "暫無錢包"
- },
- "signature": {
- "reviewTransaction": "請確認此交易",
- "messageToSign": "待簽名訊息",
- "confirmDescription": "請繪製圖案進行簽名",
- "type": {
- "message": "訊息簽名",
- "transfer": "代幣轉帳",
- "destroy": "資產銷毀"
+ "viewAddress": "查看你的錢包地址",
+ "viewPublicKey": "查看你的公鑰"
},
- "details": {
- "from": "從",
- "to": "到",
- "amount": "金額",
- "fee": "網路手續費",
- "total": "合計"
+ "scope": {
+ "all": "所有錢包",
+ "main": "僅目前錢包",
+ "network": "所有 {{chainName}} 地址"
},
- "balanceWarning": {
- "need": "需要:{{amount}} {{symbol}}",
- "have": "擁有:{{amount}} {{symbol}}"
- }
+ "selectNetwork": "[MISSING:zh-TW] 选择网络",
+ "shareWith": "是否與該應用分享地址?"
+ },
+ "app": {
+ "from": "來自:{{appHome}}",
+ "requesting": "{{appName}} 正在請求存取"
},
"button": {
"approve": "同意",
- "reject": "拒絕",
- "confirm": "繪製圖案確認"
+ "confirm": "繪製圖案確認",
+ "reject": "拒絕"
},
"error": {
- "insufficientBalance": "餘額不足,無法完成此交易",
- "unknownChain": "不支援的區塊鏈",
"authFailed": "授權失敗",
+ "insufficientBalance": "餘額不足,無法完成此交易",
"patternIncorrect": "圖案錯誤",
- "timeout": "請求已逾時"
+ "timeout": "請求已逾時",
+ "unknownChain": "不支援的區塊鏈"
},
"passwordConfirm": {
"title": "Confirm Password"
+ },
+ "signature": {
+ "balanceWarning": {
+ "have": "擁有:{{amount}} {{symbol}}",
+ "need": "需要:{{amount}} {{symbol}}"
+ },
+ "confirmDescription": "請繪製圖案進行簽名",
+ "details": {
+ "amount": "金額",
+ "fee": "網路手續費",
+ "from": "從",
+ "to": "到",
+ "total": "合計"
+ },
+ "messageToSign": "待簽名訊息",
+ "reviewTransaction": "請確認此交易",
+ "type": {
+ "destroy": "資產銷毀",
+ "message": "訊息簽名",
+ "transfer": "代幣轉帳"
+ }
+ },
+ "title": {
+ "address": "地址授權",
+ "signature": "交易授權"
}
}
diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json
index 264bc9b8e..0c0ced70d 100644
--- a/src/i18n/locales/zh-TW/common.json
+++ b/src/i18n/locales/zh-TW/common.json
@@ -1,7 +1,7 @@
{
"a11y": {
- "addWallet": "新增錢包",
"addContact": "新增聯絡人",
+ "addWallet": "新增錢包",
"appInfo": "應用程式資訊",
"back": "返回",
"chainSelector": "選擇區塊鏈網絡",
@@ -44,24 +44,42 @@
"acquisitionTime": "入手時間",
"add": "新增",
"addressBook": {
+ "addContact": "新增聯繫人",
+ "addresses": "地址",
+ "changeAvatar": "點擊切換頭像",
"delete": "刪除",
"deleteConfirm": "確定要刪除聯繫人 \"{{name}}\" 嗎?",
"deleteTitle": "刪除聯繫人",
"edit": "編輯",
+ "memo": "備註",
+ "memoPlaceholder": "新增備註(可選)",
+ "namePlaceholder": "輸入聯繫人名稱",
"noContacts": "還沒有聯繫人",
"noResults": "沒有找到聯繫人",
"passwordError": "密碼錯誤",
+ "scanToAdd": "掃碼新增聯繫人",
"searchPlaceholder": "搜索聯繫人",
- "title": "通訊錄",
- "verifyFailed": "驗證失敗",
- "addContact": "新增聯繫人",
- "namePlaceholder": "輸入聯繫人名稱",
- "addresses": "地址",
- "memo": "備註",
- "memoPlaceholder": "新增備註(可選)",
"shareContact": "分享名片",
- "scanToAdd": "掃碼新增聯繫人",
- "changeAvatar": "點擊切換頭像"
+ "title": "通訊錄",
+ "verifyFailed": "驗證失敗"
+ },
+ "addressLookup": {
+ "address": "[MISSING:zh-TW] 地址",
+ "addressOrHash": "[MISSING:zh-TW] 地址或交易哈希",
+ "addressOrHashPlaceholder": "[MISSING:zh-TW] 输入地址或交易哈希",
+ "addressPlaceholder": "[MISSING:zh-TW] 输入钱包地址",
+ "balanceTitle": "[MISSING:zh-TW] 地址余额查询",
+ "chain": "[MISSING:zh-TW] 链",
+ "error": "[MISSING:zh-TW] 查询失败",
+ "explorerHint": "[MISSING:zh-TW] 交易记录需要通过区块浏览器查询",
+ "noTransactions": "[MISSING:zh-TW] 暂无交易记录",
+ "onChain": "[MISSING:zh-TW] 在 {{chain}} 上",
+ "openExplorer": "[MISSING:zh-TW] 打开 {{name}} 浏览器",
+ "otherChains": "[MISSING:zh-TW] 其他链",
+ "queryError": "[MISSING:zh-TW] 查询交易失败,请稍后重试",
+ "transactionsTitle": "[MISSING:zh-TW] 地址交易查询",
+ "useExplorerHint": "[MISSING:zh-TW] 该链不支持直接查询交易历史,请使用浏览器查看",
+ "viewOnExplorer": "[MISSING:zh-TW] 在 {{name}} 浏览器中查看"
},
"addressPlaceholder": "輸入或貼上地址",
"advancedEncryptionTechnologyDigitalWealthIsMoreSecure": "先進的加密技術 <br /> 數字財富更安全",
@@ -86,6 +104,7 @@
"cancel": "取消",
"cardWorks": "[MISSING:zh-TW] card works!",
"certificate": "認證",
+ "clickToCopy": "[MISSING:zh-TW] 点击复制",
"clickToShowTheQr": "Click to show the QR",
"clickToShowTheQrCodeOfThePrivateKey": "點擊展示私鑰二維碼",
"close": "退出",
@@ -99,9 +118,13 @@
"confirmToTurnOffVerification": "確定要關閉驗證嗎?",
"contact": {
"add": "新增",
+ "addAddress": "新增地址",
"addTitle": "新增聯絡人",
"address": "地址",
+ "addressLabel": "標籤",
+ "addressLabelPlaceholder": "如:主帳戶",
"addressPlaceholder": "請輸入錢包地址",
+ "addresses": "地址",
"cancel": "取消",
"editTitle": "編輯聯絡人",
"memoOptional": "備註(選填)",
@@ -113,11 +136,7 @@
"save": "儲存",
"selectAddress": "選擇地址",
"selectContact": "選擇聯絡人",
- "viewAll": "查看全部聯絡人",
- "addresses": "地址",
- "addressLabel": "標籤",
- "addressLabelPlaceholder": "如:主帳戶",
- "addAddress": "新增地址"
+ "viewAll": "查看全部聯絡人"
},
"continue": "繼續",
"contractMethods": "合約方法",
@@ -134,6 +153,7 @@
"delete": "刪除",
"destroy": "銷毀",
"done": "[MISSING:zh-TW] 完成",
+ "download": "下載",
"dpInTab": "dp-in-tab",
"dpName": "DP名稱",
"dpType": "類型",
@@ -250,10 +270,10 @@
"resubmit": "重新提交",
"retry": "[MISSING:zh-TW] 重试",
"save": "保存",
- "saving": "保存中...",
- "saved": "已保存",
"saveQrCode": "保存二維碼",
"saveSuccessful": "保存成功",
+ "saved": "已保存",
+ "saving": "保存中...",
"scan": "掃描",
"scanAgain": "再次掃描",
"scanner": "掃一掃",
@@ -261,7 +281,6 @@
"selectNetwork": "選擇網絡",
"selectionNetwork": "選擇網絡",
"share": "分享",
- "download": "下載",
"shareCancel": "分享取消",
"shareCanceled": "分享取消",
"shareSuccessful": "分享成功",
diff --git a/src/i18n/locales/zh-TW/ecosystem.json b/src/i18n/locales/zh-TW/ecosystem.json
new file mode 100644
index 000000000..ca961c705
--- /dev/null
+++ b/src/i18n/locales/zh-TW/ecosystem.json
@@ -0,0 +1,24 @@
+{
+ "title": "[MISSING:zh-TW] 生态",
+ "empty": "[MISSING:zh-TW] 暂无小程序",
+ "loading": "[MISSING:zh-TW] 加载中...",
+ "notFound": "[MISSING:zh-TW] 小程序不存在",
+ "refresh": "[MISSING:zh-TW] 刷新",
+ "sources": {
+ "title": "[MISSING:zh-TW] 可信源管理",
+ "add": "[MISSING:zh-TW] 添加订阅",
+ "remove": "[MISSING:zh-TW] 移除",
+ "empty": "[MISSING:zh-TW] 暂无订阅源"
+ },
+ "permissions": {
+ "title": "[MISSING:zh-TW] 权限请求",
+ "grant": "[MISSING:zh-TW] 授权",
+ "deny": "[MISSING:zh-TW] 拒绝",
+ "bio_requestAccounts": "[MISSING:zh-TW] 访问钱包地址",
+ "bio_selectAccount": "[MISSING:zh-TW] 选择账户",
+ "bio_pickWallet": "[MISSING:zh-TW] 选择其他钱包",
+ "bio_signMessage": "[MISSING:zh-TW] 签名消息",
+ "bio_signTypedData": "[MISSING:zh-TW] 签名数据",
+ "bio_sendTransaction": "[MISSING:zh-TW] 发起交易"
+ }
+}
diff --git a/src/i18n/locales/zh-TW/home.json b/src/i18n/locales/zh-TW/home.json
index 7c581da56..7391661ee 100644
--- a/src/i18n/locales/zh-TW/home.json
+++ b/src/i18n/locales/zh-TW/home.json
@@ -1,17 +1,20 @@
{
- "welcome": {
- "title": "歡迎使用 BFM Pay",
- "subtitle": "創建或導入錢包開始使用",
- "createWallet": "創建新錢包",
- "importWallet": "導入已有錢包"
- },
"wallet": {
- "send": "轉賬",
- "receive": "收款",
+ "addressCopied": "地址已複製",
"assets": "資產",
+ "destroy": "[MISSING:zh-TW] 销毁",
"noAssets": "暫無資產",
"noAssetsOnChain": "{{chain}} 鏈上暫無代幣",
+ "receive": "收款",
"selectNetwork": "選擇網絡",
- "addressCopied": "地址已複製"
+ "send": "轉賬",
+ "tokenActions": "[MISSING:zh-TW] 资产操作",
+ "transfer": "[MISSING:zh-TW] 转账"
+ },
+ "welcome": {
+ "createWallet": "創建新錢包",
+ "importWallet": "導入已有錢包",
+ "subtitle": "創建或導入錢包開始使用",
+ "title": "歡迎使用 BFM Pay"
}
}
diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json
index c30f74525..56fbae8f6 100644
--- a/src/i18n/locales/zh-TW/settings.json
+++ b/src/i18n/locales/zh-TW/settings.json
@@ -1,146 +1,149 @@
{
- "language": "語言",
- "settings": "設定",
- "title": "設定",
- "sections": {
- "walletManagement": "錢包管理",
- "security": "安全",
- "preferences": "偏好設定",
- "about": "關於"
- },
- "items": {
- "walletManagement": "錢包管理",
- "walletChains": "錢包網路",
- "addressBook": "通訊錄",
- "appLock": "應用程式鎖定",
- "viewMnemonic": "查看助記詞",
- "changeWalletLock": "修改密碼",
- "language": "語言",
- "currency": "貨幣單位",
- "chainConfig": "鏈配置",
- "appearance": "外觀",
- "aboutApp": "關於 BFM Pay",
- "clearData": "清空應用程式資料",
- "storage": "儲存空間"
- },
"appearance": {
- "title": "外觀設定",
- "system": "跟隨系統",
+ "dark": "深色模式",
"light": "淺色模式",
- "dark": "深色模式"
- },
- "languagePage": {
- "title": "語言",
- "hint": "選擇介面語言後,應用程式將立即切換到所選語言。"
- },
- "currency": {
- "title": "計價貨幣",
- "hint": "選擇計價貨幣後,資產價值將以所選貨幣顯示。",
- "names": {
- "USD": "美元",
- "CNY": "人民幣",
- "EUR": "歐元",
- "JPY": "日圓",
- "KRW": "韓圓"
- }
- },
- "viewMnemonic": {
- "titleMnemonic": "查看助記詞",
- "titleKey": "查看密鑰",
- "noWallet": "請先建立或匯入錢包",
- "walletIncomplete": "錢包資料不完整",
- "patternError": "圖案錯誤",
- "securityTip": "安全提示",
- "securityWarningMnemonic": "請確保周圍無人窺視。助記詞一旦洩露,資產將面臨風險。",
- "securityWarningKey": "請確保周圍無人窺視。密鑰一旦洩露,資產將面臨風險。",
- "charCount": "{{count}} 字元",
- "wordCount": "{{count}} 位助記詞",
- "show": "顯示",
- "hide": "隱藏",
- "autoHideNote": "30秒後將自動隱藏",
- "verifyTitle": "驗證密碼",
- "verifyDescMnemonic": "請輸入錢包密碼以查看助記詞",
- "verifyDescKey": "請輸入錢包密碼以查看密鑰"
- },
- "changeWalletLock": {
- "title": "修改錢包鎖",
- "noWallet": "請先建立或匯入錢包",
- "walletIncomplete": "錢包資料不完整",
- "success": "錢包鎖修改成功",
- "returning": "正在返回設定頁面...",
- "verifyTitle": "驗證目前圖案",
- "verifyDesc": "請繪製目前錢包鎖圖案以繼續",
- "forgotPattern": "忘記圖案?使用助記詞重設",
- "mnemonicVerifyTitle": "使用助記詞驗證",
- "mnemonicVerifyDesc": "輸入建立錢包時備份的助記詞",
- "verifyMnemonic": "驗證助記詞",
- "verifying": "驗證中...",
- "usePattern": "返回使用圖案驗證",
- "invalidMnemonic": "助記詞格式無效",
- "mnemonicVerifyFailed": "助記詞驗證失敗,請檢查是否正確"
+ "system": "跟隨系統",
+ "title": "外觀設定"
},
"chainAddressCount": "{{count}} 個鏈地址",
- "notSupported": "暫不支援",
"chainConfig": {
- "title": "鏈配置",
- "subscription": {
- "title": "訂閱 URL",
- "description": "輸入 default 使用內建鏈配置。",
- "refreshAriaLabel": "刷新訂閱",
- "save": "儲存",
- "placeholder": "default 或 https://example.com/chains.json",
- "lastUpdated": "最近更新:{{value}}"
+ "hint": "提示:訂閱配置會快取到本機,切換 URL 會清空舊快取以避免混淆。",
+ "list": {
+ "description": "啟用後會在錢包/資產相關流程中可用。",
+ "empty": "暫無鏈配置",
+ "title": "鏈清單"
},
"manual": {
- "title": "手動新增",
- "description": "貼上 JSON(單一物件或陣列),點擊新增。",
"add": "新增",
- "placeholder": "例如:{\"id\":\"mychain\",\"version\":\"1.0\",\"type\":\"custom\",\"name\":\"MyChain\",\"symbol\":\"MC\",\"decimals\":8}",
+ "description": "貼上 JSON(單一物件或陣列),點擊新增。",
"duplicate": {
- "title": "鏈 ID 已存在",
+ "cancel": "取消",
"description": "以下鏈 ID 已存在,是否取代?",
"replace": "取代",
- "cancel": "取消"
- }
- },
- "list": {
- "title": "鏈清單",
- "description": "啟用後會在錢包/資產相關流程中可用。",
- "empty": "暫無鏈配置"
+ "title": "鏈 ID 已存在"
+ },
+ "placeholder": "例如:{\"id\":\"mychain\",\"version\":\"1.0\",\"type\":\"custom\",\"name\":\"MyChain\",\"symbol\":\"MC\",\"decimals\":8}",
+ "title": "手動新增"
},
"source": {
"default": "預設",
- "subscription": "訂閱",
- "manual": "手動"
+ "manual": "手動",
+ "subscription": "訂閱"
},
- "warning": {
- "incompatibleMajor": "版本不相容:{{version}}(支援 ≤ {{supportedMajor}}.x)"
+ "subscription": {
+ "description": "輸入 default 使用內建鏈配置。",
+ "lastUpdated": "最近更新:{{value}}",
+ "placeholder": "default 或 https://example.com/chains.json",
+ "refreshAriaLabel": "刷新訂閱",
+ "save": "儲存",
+ "title": "訂閱 URL"
},
+ "title": "鏈配置",
"toast": {
- "subscriptionSaved": "已儲存訂閱 URL",
+ "manualAdded": "已新增鏈配置",
"subscriptionRefreshed": "訂閱已更新",
- "manualAdded": "已新增鏈配置"
+ "subscriptionSaved": "已儲存訂閱 URL"
},
- "hint": "提示:訂閱配置會快取到本機,切換 URL 會清空舊快取以避免混淆。"
+ "warning": {
+ "incompatibleMajor": "版本不相容:{{version}}(支援 ≤ {{supportedMajor}}.x)"
+ }
+ },
+ "changeWalletLock": {
+ "forgotPattern": "忘記圖案?使用助記詞重設",
+ "invalidMnemonic": "助記詞格式無效",
+ "mnemonicVerifyDesc": "輸入建立錢包時備份的助記詞",
+ "mnemonicVerifyFailed": "助記詞驗證失敗,請檢查是否正確",
+ "mnemonicVerifyTitle": "使用助記詞驗證",
+ "noWallet": "請先建立或匯入錢包",
+ "returning": "正在返回設定頁面...",
+ "success": "錢包鎖修改成功",
+ "title": "修改錢包鎖",
+ "usePattern": "返回使用圖案驗證",
+ "verifyDesc": "請繪製目前錢包鎖圖案以繼續",
+ "verifyMnemonic": "驗證助記詞",
+ "verifyTitle": "驗證目前圖案",
+ "verifying": "驗證中...",
+ "walletIncomplete": "錢包資料不完整"
},
"clearData": {
- "title": "清空應用程式資料",
- "warning": "此操作將刪除所有本機資料,包括錢包、設定和快取。此操作不可撤銷!",
+ "confirm": "確認清空",
"item1": "所有錢包資料將被刪除",
"item2": "所有設定將恢復預設",
"item3": "應用程式將重新啟動",
- "confirm": "確認清空"
+ "title": "清空應用程式資料",
+ "warning": "此操作將刪除所有本機資料,包括錢包、設定和快取。此操作不可撤銷!"
+ },
+ "currency": {
+ "hint": "選擇計價貨幣後,資產價值將以所選貨幣顯示。",
+ "names": {
+ "CNY": "人民幣",
+ "EUR": "歐元",
+ "JPY": "日圓",
+ "KRW": "韓圓",
+ "USD": "美元"
+ },
+ "title": "計價貨幣"
+ },
+ "items": {
+ "aboutApp": "關於 BFM Pay",
+ "addressBook": "通訊錄",
+ "appLock": "應用程式鎖定",
+ "appearance": "外觀",
+ "chainConfig": "鏈配置",
+ "changeWalletLock": "修改密碼",
+ "clearData": "清空應用程式資料",
+ "currency": "貨幣單位",
+ "language": "語言",
+ "storage": "儲存空間",
+ "viewMnemonic": "查看助記詞",
+ "walletChains": "錢包網路",
+ "walletManagement": "錢包管理"
},
+ "language": "語言",
+ "languagePage": {
+ "hint": "選擇介面語言後,應用程式將立即切換到所選語言。",
+ "title": "語言"
+ },
+ "notSupported": "暫不支援",
+ "sections": {
+ "about": "關於",
+ "preferences": "偏好設定",
+ "security": "安全",
+ "walletManagement": "錢包管理"
+ },
+ "settings": "設定",
"storage": {
- "title": "儲存空間",
- "usage": "儲存使用情況",
+ "available": "可用空間",
"basedOn": "基於瀏覽器儲存配額",
- "used": "已使用",
+ "clearDesc": "清空所有本機儲存的資料,包括錢包、設定和快取。",
+ "clearTitle": "清理資料",
+ "goToClear": "[MISSING:zh-TW] 前往清理数据",
+ "migrationDesc": "[MISSING:zh-TW] 检测到旧版本数据格式,需要清空本地数据库后才能继续使用。您的助记词和私钥不会受到影响,但需要重新导入钱包。",
+ "migrationRequired": "[MISSING:zh-TW] 数据需要迁移",
+ "title": "儲存空間",
"total": "總空間",
- "usagePercent": "使用率",
- "available": "可用空間",
"unavailable": "無法取得儲存資訊",
- "clearTitle": "清理資料",
- "clearDesc": "清空所有本機儲存的資料,包括錢包、設定和快取。"
+ "usage": "儲存使用情況",
+ "usagePercent": "使用率",
+ "used": "已使用"
+ },
+ "title": "設定",
+ "viewMnemonic": {
+ "autoHideNote": "30秒後將自動隱藏",
+ "charCount": "{{count}} 字元",
+ "hide": "隱藏",
+ "noWallet": "請先建立或匯入錢包",
+ "patternError": "圖案錯誤",
+ "securityTip": "安全提示",
+ "securityWarningKey": "請確保周圍無人窺視。密鑰一旦洩露,資產將面臨風險。",
+ "securityWarningMnemonic": "請確保周圍無人窺視。助記詞一旦洩露,資產將面臨風險。",
+ "show": "顯示",
+ "titleKey": "查看密鑰",
+ "titleMnemonic": "查看助記詞",
+ "verifyDescKey": "請輸入錢包密碼以查看密鑰",
+ "verifyDescMnemonic": "請輸入錢包密碼以查看助記詞",
+ "verifyTitle": "驗證密碼",
+ "walletIncomplete": "錢包資料不完整",
+ "wordCount": "{{count}} 位助記詞"
}
}
diff --git a/src/i18n/locales/zh-TW/transaction.json b/src/i18n/locales/zh-TW/transaction.json
index 7836497f7..0422e7213 100644
--- a/src/i18n/locales/zh-TW/transaction.json
+++ b/src/i18n/locales/zh-TW/transaction.json
@@ -65,23 +65,34 @@
"contractTransactionData": "合约交易数据",
"destoryAmount": "銷毀數量",
"destoryQuantityCannotBeGreaterThanBalance": "銷毀數量不能大於餘額",
+ "destroyPage": {
+ "amountLabel": "[MISSING:zh-TW] 销毁数量",
+ "assetLabel": "[MISSING:zh-TW] 销毁资产",
+ "confirm": "[MISSING:zh-TW] 确认销毁",
+ "noDestroyableAssets": "[MISSING:zh-TW] 暂无可销毁的资产",
+ "notSupported": "[MISSING:zh-TW] 当前链不支持资产销毁",
+ "resultTitle": "[MISSING:zh-TW] 销毁结果",
+ "title": "[MISSING:zh-TW] 销毁",
+ "warning": "[MISSING:zh-TW] 销毁操作不可撤销,请仔细核对销毁数量。"
+ },
"detail": {
+ "allowance": "授權額度",
"blockHeight": "區塊高度",
"confirmations": "確認數",
+ "contractAddress": "合約地址",
"copied": "已複製",
"copyHash": "複製哈希",
- "contractAddress": "合約地址",
- "spender": "授權對象",
- "allowance": "授權額度",
"fee": "手續費",
"fromAddress": "發送地址",
"hash": "交易哈希",
"info": "交易資訊",
+ "invalidData": "交易資料解析失敗",
"method": "方法",
"network": "網路",
- "invalidData": "交易資料解析失敗",
"notFound": "交易不存在或已過期",
+ "openInExplorer": "[MISSING:zh-TW] 在浏览器中打开",
"share": "分享",
+ "spender": "授權對象",
"swapFrom": "換出",
"swapTo": "換入",
"time": "交易時間",
@@ -89,23 +100,6 @@
"toAddress": "收款地址",
"viewExplorer": "區塊瀏覽器"
},
- "method": {
- "approve": "授權",
- "transfer": "轉帳",
- "swap": "兌換",
- "swapExactTokensForETH": "代幣兌換 ETH",
- "swapExactTokensForTokens": "代幣兌換",
- "swapExactETHForTokens": "ETH 兌換代幣",
- "multicall": "批量調用",
- "execute": "執行",
- "deposit": "存入",
- "withdraw": "提取",
- "stake": "質押",
- "unstake": "解押",
- "claim": "領取",
- "mint": "鑄造",
- "burn": "銷毀"
- },
"dpTransactionDetails": "DP詳情",
"easyTransfer": "輕鬆轉賬",
"enterAmount": "輸入數量",
@@ -115,15 +109,15 @@
"failed": "[MISSING:zh-TW] 失败",
"fee": "礦工費",
"feeEdit": {
- "belowMinFee": "[MISSING:zh-TW] 手续费不能低于最低手续费 {{minFee}} {{symbol}}",
- "currentFee": "[MISSING:zh-TW] 当前手续费",
- "customFee": "[MISSING:zh-TW] 自定义手续费",
- "invalidFee": "[MISSING:zh-TW] 请输入有效的手续费",
- "minFee": "[MISSING:zh-TW] 最低手续费",
- "presetFast": "[MISSING:zh-TW] 快速",
- "presetMin": "[MISSING:zh-TW] 最低",
- "presetRecommend": "[MISSING:zh-TW] 推荐",
- "title": "[MISSING:zh-TW] 编辑网络手续费"
+ "belowMinFee": "手續費不能低於最低手續費 {{minFee}} {{symbol}}",
+ "currentFee": "當前手續費",
+ "customFee": "自定義手續費",
+ "invalidFee": "請輸入有效的手續費",
+ "minFee": "最低手續費",
+ "presetFast": "快速",
+ "presetMin": "最低",
+ "presetRecommend": "推薦",
+ "title": "編輯網絡手續費"
},
"finallyReceive": "實際收到:",
"forOvertimeTransactionTips": "對於超時交易,您可以通過區塊鏈瀏覽器查看交易是否成功。",
@@ -134,13 +128,13 @@
"emptyDesc": "目前篩選條件下沒有交易記錄",
"emptyTitle": "暫無交易記錄",
"filter": {
- "chainLabel": "網路",
- "periodLabel": "時間",
"allChains": "全部鏈",
"allTime": "全部",
+ "chainLabel": "網路",
"days30": "30天",
"days7": "7天",
- "days90": "90天"
+ "days90": "90天",
+ "periodLabel": "時間"
},
"noWallet": "請先建立或匯入錢包",
"title": "交易記錄",
@@ -152,6 +146,23 @@
"initiateTransfer": "發起轉賬",
"insufficientBalanceIn_{symbol}": "餘額不足",
"insufficientBalanceUnableToConductTransactions": "餘額不足,無法發起事件",
+ "method": {
+ "approve": "授權",
+ "burn": "銷毀",
+ "claim": "領取",
+ "deposit": "存入",
+ "execute": "執行",
+ "mint": "鑄造",
+ "multicall": "批量調用",
+ "stake": "質押",
+ "swap": "兌換",
+ "swapExactETHForTokens": "ETH 兌換代幣",
+ "swapExactTokensForETH": "代幣兌換 ETH",
+ "swapExactTokensForTokens": "代幣兌換",
+ "transfer": "轉帳",
+ "unstake": "解押",
+ "withdraw": "提取"
+ },
"minerFee": "礦工費",
"minnerFee": "礦工費",
"newTransfer": "繼續轉賬",
@@ -183,9 +194,11 @@
"send": "[MISSING:zh-TW] 发送",
"sendPage": {
"amountLabel": "金額",
+ "assetLabel": "[MISSING:zh-TW] 转账资产",
"cameraPermissionRequired": "需要相機權限才能掃描",
"continue": "繼續",
"explorerNotImplemented": "區塊瀏覽器功能待實現",
+ "fee": "[MISSING:zh-TW] 手续费",
"from": "發送地址",
"networkWarning": "請確保收款地址為 {{chain}} 網絡地址,發送到錯誤網絡將無法找回",
"resultTitle": "發送結果",
@@ -249,17 +262,6 @@
"tronLessFeeTips": "目前TRX較少,可能不足以支付手續費而導致轉帳失敗。",
"tronNetworkFeeLongTips_1": "網絡費用會根據網絡情況有所不同,一旦交易確認,無論交易成功與否,費用均不退還。",
"tronNetworkFeeLongTips_2": "網絡費用是由TRON網絡收取, BFM Pay不會收取任何費用!",
- "feeEdit": {
- "title": "編輯網絡手續費",
- "currentFee": "當前手續費",
- "minFee": "最低手續費",
- "customFee": "自定義手續費",
- "presetMin": "最低",
- "presetRecommend": "推薦",
- "presetFast": "快速",
- "invalidFee": "請輸入有效的手續費",
- "belowMinFee": "手續費不能低於最低手續費 {{minFee}} {{symbol}}"
- },
"txStatus": {
"broadcasted": "广播成功",
"broadcastedDesc": "交易已广播,等待区块确认...",
@@ -274,30 +276,30 @@
"viewDetails": "查看详情"
},
"type": {
+ "approve": "授權",
"certificate": "凭证操作",
"dapp": "DApp操作",
"destroy": "权益销毁",
"destroyEntity": "销毁资产",
"emigrate": "权益迁出",
"exchange": "权益交换",
- "swap": "Swap",
"gift": "发起赠送",
"grab": "接受赠送",
"immigrate": "权益迁入",
"increaseAsset": "增发权益",
+ "interaction": "合約交互",
"issueAsset": "创建权益",
- "mint": "Mint",
"issueEntity": "创建资产",
"locationName": "位名操作",
"mark": "数据存证",
- "approve": "授權",
- "interaction": "合約交互",
+ "mint": "Mint",
"other": "其他",
"receive": "接收",
"send": "發送",
"signFor": "签收委托",
"signature": "设置安全密码",
"stake": "質押",
+ "swap": "Swap",
"trust": "发起委托",
"unstake": "解押"
},
diff --git a/src/pages/history/detail.test.tsx b/src/pages/history/detail.test.tsx
index b7b8c30ea..ff6641871 100644
--- a/src/pages/history/detail.test.tsx
+++ b/src/pages/history/detail.test.tsx
@@ -215,9 +215,10 @@ describe('TransactionDetailPage', () => {
renderWithProviders()
expect(await screen.findByText(t('transaction:detail.invalidData'))).toBeInTheDocument()
- const copyButton = screen.getByText(t('transaction:detail.copyHash'))
+ // CopyableText component uses clickToCopy aria-label
+ const copyButton = screen.getByLabelText(t('common:clickToCopy'))
await userEvent.click(copyButton)
- expect(mockClipboardWrite).toHaveBeenCalledWith({ text: 'ethereum--0xdeadbeef'.split('--')[1] })
+ expect(mockClipboardWrite).toHaveBeenCalledWith({ text: '0xdeadbeef' })
})
})
@@ -269,19 +270,22 @@ describe('TransactionDetailPage', () => {
it('copies hash to clipboard when button is clicked', async () => {
renderWithProviders()
- const copyButton = screen.getByText(t('transaction:detail.copyHash'))
+ // CopyableText component displays the hash and uses clickToCopy aria-label
+ const mockTx = createMockTransaction()
+ const copyButton = await screen.findByLabelText(t('common:clickToCopy'))
await userEvent.click(copyButton)
- expect(mockClipboardWrite).toHaveBeenCalledWith({ text: createMockTransaction().hash })
+ expect(mockClipboardWrite).toHaveBeenCalledWith({ text: mockTx.hash })
})
it('shows copied state after clicking', async () => {
renderWithProviders()
- const copyButton = screen.getByText(t('transaction:detail.copyHash'))
+ // CopyableText component changes aria-label to copiedToClipboard after click
+ const copyButton = await screen.findByLabelText(t('common:clickToCopy'))
await userEvent.click(copyButton)
- expect(screen.getByText(t('transaction:detail.copied'))).toBeInTheDocument()
+ expect(screen.getByLabelText(t('common:copiedToClipboard'))).toBeInTheDocument()
})
})
diff --git a/src/services/ecosystem/my-apps.ts b/src/services/ecosystem/my-apps.ts
index 023ffb842..7c2e3b6b6 100644
--- a/src/services/ecosystem/my-apps.ts
+++ b/src/services/ecosystem/my-apps.ts
@@ -1,21 +1,19 @@
/**
- * My Apps - 本地已安装应用管理
+ * My Apps - Local installed app persistence layer
+ *
+ * NOTE: Do not use this directly for state management. Use ecosystemStore instead.
*/
-const MY_APPS_KEY = 'ecosystem_my_apps'
+import type { MyAppRecord } from './types'
-export interface MyAppRecord {
- appId: string
- installedAt: number
- lastUsedAt: number
-}
+const MY_APPS_KEY = 'ecosystem_my_apps'
const LEGACY_APP_ID_MAP: Record = {
teleport: 'xin.dweb.teleport',
forge: 'xin.dweb.forge',
}
-function normalizeAppId(appId: string): string {
+export function normalizeAppId(appId: string): string {
return LEGACY_APP_ID_MAP[appId] ?? appId
}
@@ -44,31 +42,4 @@ export function saveMyApps(apps: MyAppRecord[]): void {
localStorage.setItem(MY_APPS_KEY, JSON.stringify(apps))
}
-export function isInMyApps(appId: string): boolean {
- return loadMyApps().some((a) => a.appId === normalizeAppId(appId))
-}
-export function addToMyApps(appId: string): void {
- appId = normalizeAppId(appId)
- const apps = loadMyApps()
- if (!apps.some((a) => a.appId === appId)) {
- apps.unshift({ appId, installedAt: Date.now(), lastUsedAt: Date.now() })
- saveMyApps(apps)
- }
-}
-
-export function updateLastUsed(appId: string): void {
- appId = normalizeAppId(appId)
- const apps = loadMyApps()
- const app = apps.find((a) => a.appId === appId)
- if (app) {
- app.lastUsedAt = Date.now()
- saveMyApps(apps)
- }
-}
-
-export function removeFromMyApps(appId: string): void {
- appId = normalizeAppId(appId)
- const apps = loadMyApps().filter((a) => a.appId !== appId)
- saveMyApps(apps)
-}
diff --git a/src/services/ecosystem/types.ts b/src/services/ecosystem/types.ts
index 11730bb18..719f4833e 100644
--- a/src/services/ecosystem/types.ts
+++ b/src/services/ecosystem/types.ts
@@ -270,6 +270,15 @@ export interface SourceRecord {
builtin?: boolean
}
+/**
+ * My Apps - Local installed app record
+ */
+export interface MyAppRecord {
+ appId: string
+ installedAt: number
+ lastUsedAt: number
+}
+
/** Method handler */
export type MethodHandler = (
params: unknown,
diff --git a/src/stackflow/activities/MiniappDetailActivity.tsx b/src/stackflow/activities/MiniappDetailActivity.tsx
index 9c806c2c6..2eaba2f10 100644
--- a/src/stackflow/activities/MiniappDetailActivity.tsx
+++ b/src/stackflow/activities/MiniappDetailActivity.tsx
@@ -4,6 +4,7 @@
import { useEffect, useState, useCallback } from 'react'
import type { ActivityComponentType } from '@stackflow/react'
+import { useStore } from '@tanstack/react-store'
import { AppScreen } from '@stackflow/plugin-basic-ui'
import { useTranslation } from 'react-i18next'
import { useFlow } from '../stackflow'
@@ -11,9 +12,6 @@ import {
getAppById,
initRegistry,
refreshSources,
- addToMyApps,
- isInMyApps,
- updateLastUsed,
type MiniappManifest,
KNOWN_PERMISSIONS
} from '@/services/ecosystem'
@@ -32,7 +30,7 @@ import {
} from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { launchApp } from '@/services/miniapp-runtime'
-import { ecosystemActions } from '@/stores/ecosystem'
+import { ecosystemActions, ecosystemStore, ecosystemSelectors } from '@/stores/ecosystem'
type MiniappDetailActivityParams = {
appId: string
@@ -118,7 +116,11 @@ export const MiniappDetailActivity: ActivityComponentType(null)
const [loading, setLoading] = useState(true)
const [descExpanded, setDescExpanded] = useState(false)
- const [installed, setInstalled] = useState(false)
+
+ // Use store selector for reactivity (Single Source of Truth)
+ const installed = useStore(ecosystemStore, (state) =>
+ ecosystemSelectors.isAppInstalled(state, params.appId)
+ )
useEffect(() => {
let disposed = false
@@ -137,7 +139,6 @@ export const MiniappDetailActivity: ActivityComponentType {
if (!app) return
- addToMyApps(app.id)
- setInstalled(true)
+ ecosystemActions.installApp(app.id)
}, [app])
const handleOpen = useCallback(() => {
if (!app) return
- updateLastUsed(app.id)
+ ecosystemActions.updateAppLastUsed(app.id)
ecosystemActions.setActiveSubPage('mine')
launchApp(app.id, { ...app, targetDesktop: 'mine' })
pop()
@@ -414,3 +414,4 @@ export const MiniappDetailActivity: ActivityComponentType
)
}
+
diff --git a/src/stackflow/activities/tabs/EcosystemTab.tsx b/src/stackflow/activities/tabs/EcosystemTab.tsx
index b8bba8b3a..e805839c5 100644
--- a/src/stackflow/activities/tabs/EcosystemTab.tsx
+++ b/src/stackflow/activities/tabs/EcosystemTab.tsx
@@ -1,28 +1,27 @@
import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
+import { useStore } from '@tanstack/react-store';
import { useFlow } from '../../stackflow';
import { LoadingSpinner } from '@/components/common/loading-spinner';
import {
initRegistry,
getApps,
subscribeApps,
- loadMyApps,
- addToMyApps,
- updateLastUsed,
- removeFromMyApps,
type MyAppRecord,
} from '@/services/ecosystem';
import type { MiniappManifest } from '@/services/ecosystem';
import { EcosystemDesktop, type EcosystemDesktopHandle } from '@/components/ecosystem';
import { computeFeaturedScore } from '@/services/ecosystem/scoring';
import { launchApp, useMiniappVisibilityRestore } from '@/services/miniapp-runtime';
-import { ecosystemActions } from '@/stores/ecosystem';
+import { ecosystemActions, ecosystemStore } from '@/stores/ecosystem';
export function EcosystemTab() {
const { push } = useFlow();
const desktopRef = useRef(null);
const [apps, setApps] = useState([]);
- const [myAppRecords, setMyAppRecords] = useState([]);
const [loading, setLoading] = useState(true);
+
+ // Subscribe to myApps from the store (Single Source of Truth)
+ const myAppRecords = useStore(ecosystemStore, (state) => state.myApps);
// 初始化数据
useEffect(() => {
@@ -30,7 +29,6 @@ export function EcosystemTab() {
initRegistry({ refresh: true }).then(() => {
setApps(getApps());
- setMyAppRecords(loadMyApps());
setLoading(false);
});
@@ -50,9 +48,9 @@ export function EcosystemTab() {
const handleAppOpen = useCallback(
(app: MiniappManifest) => {
- addToMyApps(app.id);
- updateLastUsed(app.id);
- setMyAppRecords(loadMyApps());
+ // Use store actions instead of direct service calls
+ ecosystemActions.installApp(app.id);
+ ecosystemActions.updateAppLastUsed(app.id);
// 为了保证 shared layout 捕获到 icon 的起点位置,先固定到 mine
ecosystemActions.setActiveSubPage('mine');
@@ -65,8 +63,7 @@ export function EcosystemTab() {
);
const handleAppRemove = useCallback((appId: string) => {
- removeFromMyApps(appId);
- setMyAppRecords(loadMyApps());
+ ecosystemActions.uninstallApp(appId);
}, []);
// 计算数据
@@ -121,3 +118,4 @@ export function EcosystemTab() {
/>
);
}
+
diff --git a/src/stores/ecosystem.test.ts b/src/stores/ecosystem.test.ts
new file mode 100644
index 000000000..6956dc2f9
--- /dev/null
+++ b/src/stores/ecosystem.test.ts
@@ -0,0 +1,264 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { ecosystemStore, ecosystemActions, ecosystemSelectors } from './ecosystem'
+
+// Mock localStorage
+const localStorageMock = (() => {
+ let store: Record = {}
+ return {
+ getItem: vi.fn((key: string) => store[key] ?? null),
+ setItem: vi.fn((key: string, value: string) => {
+ store[key] = value
+ }),
+ removeItem: vi.fn((key: string) => {
+ delete store[key]
+ }),
+ clear: vi.fn(() => {
+ store = {}
+ }),
+ }
+})()
+
+Object.defineProperty(global, 'localStorage', { value: localStorageMock })
+
+// Mock import.meta.env
+vi.stubGlobal('import', { meta: { env: { BASE_URL: '/' } } })
+
+describe('ecosystemStore - myApps management', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ localStorageMock.clear()
+ // Reset store state
+ ecosystemStore.setState(() => ({
+ permissions: [],
+ sources: [],
+ myApps: [],
+ availableSubPages: ['discover', 'mine'],
+ activeSubPage: 'discover',
+ swiperProgress: 0,
+ syncSource: null,
+ }))
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('installApp', () => {
+ it('adds app to myApps list', () => {
+ ecosystemActions.installApp('test.app.id')
+
+ expect(ecosystemStore.state.myApps).toHaveLength(1)
+ expect(ecosystemStore.state.myApps[0]?.appId).toBe('test.app.id')
+ })
+
+ it('sets installedAt and lastUsedAt timestamps', () => {
+ const before = Date.now()
+ ecosystemActions.installApp('test.app.id')
+ const after = Date.now()
+
+ const app = ecosystemStore.state.myApps[0]
+ expect(app?.installedAt).toBeGreaterThanOrEqual(before)
+ expect(app?.installedAt).toBeLessThanOrEqual(after)
+ expect(app?.lastUsedAt).toBeGreaterThanOrEqual(before)
+ expect(app?.lastUsedAt).toBeLessThanOrEqual(after)
+ })
+
+ it('adds new app to the front of the list', () => {
+ ecosystemActions.installApp('app.first')
+ ecosystemActions.installApp('app.second')
+
+ expect(ecosystemStore.state.myApps[0]?.appId).toBe('app.second')
+ expect(ecosystemStore.state.myApps[1]?.appId).toBe('app.first')
+ })
+
+ it('does not duplicate already installed app', () => {
+ ecosystemActions.installApp('test.app.id')
+ ecosystemActions.installApp('test.app.id')
+
+ expect(ecosystemStore.state.myApps).toHaveLength(1)
+ })
+
+ it('normalizes legacy app IDs', () => {
+ ecosystemActions.installApp('teleport')
+
+ expect(ecosystemStore.state.myApps[0]?.appId).toBe('xin.dweb.teleport')
+ })
+
+ it('persists to localStorage', () => {
+ ecosystemActions.installApp('test.app.id')
+
+ expect(localStorageMock.setItem).toHaveBeenCalled()
+ // Find the last call to ecosystem_my_apps
+ const myAppsCalls = localStorageMock.setItem.mock.calls.filter(
+ (call) => call[0] === 'ecosystem_my_apps'
+ )
+ expect(myAppsCalls.length).toBeGreaterThan(0)
+ const lastCall = myAppsCalls[myAppsCalls.length - 1]
+ const savedApps = JSON.parse(lastCall![1])
+ expect(savedApps.some((a: { appId: string }) => a.appId === 'test.app.id')).toBe(true)
+ })
+ })
+
+ describe('uninstallApp', () => {
+ beforeEach(() => {
+ ecosystemActions.installApp('app.one')
+ ecosystemActions.installApp('app.two')
+ vi.clearAllMocks()
+ })
+
+ it('removes app from myApps list', () => {
+ ecosystemActions.uninstallApp('app.one')
+
+ expect(ecosystemStore.state.myApps).toHaveLength(1)
+ expect(ecosystemStore.state.myApps[0]?.appId).toBe('app.two')
+ })
+
+ it('does nothing if app not found', () => {
+ ecosystemActions.uninstallApp('app.nonexistent')
+
+ expect(ecosystemStore.state.myApps).toHaveLength(2)
+ })
+
+ it('normalizes legacy app IDs', () => {
+ ecosystemActions.installApp('forge')
+ ecosystemActions.uninstallApp('forge')
+
+ expect(ecosystemStore.state.myApps.find((a) => a.appId === 'xin.dweb.forge')).toBeUndefined()
+ })
+
+ it('persists to localStorage', () => {
+ ecosystemActions.uninstallApp('app.one')
+
+ expect(localStorageMock.setItem).toHaveBeenCalled()
+ })
+ })
+
+ describe('updateAppLastUsed', () => {
+ beforeEach(() => {
+ ecosystemActions.installApp('test.app.id')
+ vi.clearAllMocks()
+ })
+
+ it('updates lastUsedAt timestamp', async () => {
+ const originalLastUsed = ecosystemStore.state.myApps[0]?.lastUsedAt ?? 0
+
+ // Wait a bit to ensure timestamp difference
+ await new Promise((r) => setTimeout(r, 10))
+
+ ecosystemActions.updateAppLastUsed('test.app.id')
+
+ const updatedLastUsed = ecosystemStore.state.myApps[0]?.lastUsedAt ?? 0
+ expect(updatedLastUsed).toBeGreaterThan(originalLastUsed)
+ })
+
+ it('does nothing if app not installed', () => {
+ const stateBefore = { ...ecosystemStore.state }
+ ecosystemActions.updateAppLastUsed('nonexistent.app')
+
+ expect(ecosystemStore.state.myApps).toEqual(stateBefore.myApps)
+ })
+
+ it('persists to localStorage', () => {
+ ecosystemActions.updateAppLastUsed('test.app.id')
+
+ expect(localStorageMock.setItem).toHaveBeenCalled()
+ })
+ })
+
+ describe('isAppInstalled selector', () => {
+ beforeEach(() => {
+ ecosystemActions.installApp('installed.app')
+ })
+
+ it('returns true for installed app', () => {
+ const result = ecosystemSelectors.isAppInstalled(ecosystemStore.state, 'installed.app')
+ expect(result).toBe(true)
+ })
+
+ it('returns false for non-installed app', () => {
+ const result = ecosystemSelectors.isAppInstalled(ecosystemStore.state, 'not.installed')
+ expect(result).toBe(false)
+ })
+
+ it('normalizes legacy app IDs', () => {
+ ecosystemActions.installApp('teleport')
+
+ const result = ecosystemSelectors.isAppInstalled(ecosystemStore.state, 'teleport')
+ expect(result).toBe(true)
+
+ const resultNormalized = ecosystemSelectors.isAppInstalled(
+ ecosystemStore.state,
+ 'xin.dweb.teleport'
+ )
+ expect(resultNormalized).toBe(true)
+ })
+ })
+
+ describe('store subscription reactivity', () => {
+ it('triggers subscribers on installApp', () => {
+ const subscriber = vi.fn()
+ const unsubscribe = ecosystemStore.subscribe(subscriber)
+
+ ecosystemActions.installApp('new.app')
+
+ expect(subscriber).toHaveBeenCalled()
+ unsubscribe()
+ })
+
+ it('triggers subscribers on uninstallApp', () => {
+ ecosystemActions.installApp('app.to.remove')
+
+ const subscriber = vi.fn()
+ const unsubscribe = ecosystemStore.subscribe(subscriber)
+
+ ecosystemActions.uninstallApp('app.to.remove')
+
+ expect(subscriber).toHaveBeenCalled()
+ unsubscribe()
+ })
+
+ it('triggers subscribers on updateAppLastUsed', () => {
+ ecosystemActions.installApp('app.to.update')
+
+ const subscriber = vi.fn()
+ const unsubscribe = ecosystemStore.subscribe(subscriber)
+
+ ecosystemActions.updateAppLastUsed('app.to.update')
+
+ expect(subscriber).toHaveBeenCalled()
+ unsubscribe()
+ })
+ })
+})
+
+describe('ecosystemStore - activeSubPage', () => {
+ beforeEach(() => {
+ ecosystemStore.setState(() => ({
+ permissions: [],
+ sources: [],
+ myApps: [],
+ availableSubPages: ['discover', 'mine'],
+ activeSubPage: 'discover',
+ swiperProgress: 0,
+ syncSource: null,
+ }))
+ })
+
+ it('setActiveSubPage updates the active sub page', () => {
+ ecosystemActions.setActiveSubPage('mine')
+ expect(ecosystemStore.state.activeSubPage).toBe('mine')
+ })
+
+ it('persists activeSubPage to localStorage', () => {
+ ecosystemActions.setActiveSubPage('mine')
+
+ // Find the last call to ecosystem_store
+ const storeCalls = localStorageMock.setItem.mock.calls.filter(
+ (call) => call[0] === 'ecosystem_store'
+ )
+ expect(storeCalls.length).toBeGreaterThan(0)
+ const lastCall = storeCalls[storeCalls.length - 1]
+ const savedState = JSON.parse(lastCall![1])
+ expect(savedState.activeSubPage).toBe('mine')
+ })
+})
diff --git a/src/stores/ecosystem.ts b/src/stores/ecosystem.ts
index 8554f9da8..f58b2b021 100644
--- a/src/stores/ecosystem.ts
+++ b/src/stores/ecosystem.ts
@@ -4,6 +4,8 @@
*/
import { Store } from '@tanstack/react-store'
+import { type MyAppRecord } from '@/services/ecosystem/types'
+import { loadMyApps, normalizeAppId, saveMyApps } from '@/services/ecosystem/my-apps'
/** 权限记录 */
export interface PermissionRecord {
@@ -43,6 +45,7 @@ export type SyncSource = 'swiper' | 'indicator' | null
export interface EcosystemState {
permissions: PermissionRecord[]
sources: SourceRecord[]
+ myApps: MyAppRecord[]
/** 当前可用子页面(由 EcosystemDesktop 根据配置/运行态写入) */
availableSubPages: EcosystemSubPage[]
/** 当前子页面(发现/我的) */
@@ -81,6 +84,7 @@ function loadState(): EcosystemState {
enabled: true,
},
],
+ myApps: loadMyApps(),
availableSubPages: fixedAvailableSubPages,
activeSubPage,
swiperProgress: 0,
@@ -100,6 +104,7 @@ function loadState(): EcosystemState {
enabled: true,
},
],
+ myApps: loadMyApps(),
availableSubPages: DEFAULT_AVAILABLE_SUBPAGES,
activeSubPage: 'discover',
swiperProgress: 0,
@@ -110,7 +115,14 @@ function loadState(): EcosystemState {
/** 保存状态到 localStorage */
function saveState(state: EcosystemState): void {
try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
+ permissions: state.permissions,
+ sources: state.sources,
+ availableSubPages: state.availableSubPages,
+ activeSubPage: state.activeSubPage,
+ // myApps is saved separately
+ }))
+ saveMyApps(state.myApps)
} catch {
// ignore
}
@@ -141,10 +153,60 @@ export const ecosystemSelectors = {
getEnabledSources: (state: EcosystemState): SourceRecord[] => {
return state.sources.filter((s) => s.enabled)
},
+
+ /** 检查应用是否已安装 */
+ isAppInstalled: (state: EcosystemState, appId: string): boolean => {
+ const normalized = normalizeAppId(appId)
+ return state.myApps.some((a) => a.appId === normalized)
+ },
}
/** Actions */
export const ecosystemActions = {
+ /** 安装应用 */
+ installApp: (appId: string): void => {
+ ecosystemStore.setState((state) => {
+ const normalized = normalizeAppId(appId)
+ if (state.myApps.some((a) => a.appId === normalized)) {
+ return state // 已安装
+ }
+ return {
+ ...state,
+ myApps: [
+ { appId: normalized, installedAt: Date.now(), lastUsedAt: Date.now() },
+ ...state.myApps,
+ ],
+ }
+ })
+ },
+
+ /** 卸载应用 */
+ uninstallApp: (appId: string): void => {
+ ecosystemStore.setState((state) => {
+ const normalized = normalizeAppId(appId)
+ return {
+ ...state,
+ myApps: state.myApps.filter((a) => a.appId !== normalized),
+ }
+ })
+ },
+
+ /** 更新应用最后使用时间 */
+ updateAppLastUsed: (appId: string): void => {
+ ecosystemStore.setState((state) => {
+ const normalized = normalizeAppId(appId)
+ const existing = state.myApps.find((a) => a.appId === normalized)
+ if (!existing) return state
+
+ return {
+ ...state,
+ myApps: state.myApps.map((a) =>
+ a.appId === normalized ? { ...a, lastUsedAt: Date.now() } : a
+ ),
+ }
+ })
+ },
+
/** 授予权限 */
grantPermissions: (appId: string, permissions: string[]): void => {
ecosystemStore.setState((state) => {