From 9efb96a0630face3464272cf2ffd83ae17e8d4a7 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Mon, 29 Dec 2025 17:02:00 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(miniapps):=20=E5=AE=8C=E5=96=84=20i18n?= =?UTF-8?q?=20=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=92=8C=20e2e=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit i18n 改进: - 添加类型定义文件 i18next.d.ts - 使用标准语言代码 zh-CN/zh-TW 替代 zh - 导出语言配置和工具函数(languages, getLanguageDirection, isRTL) - 添加单元测试验证多语言完整性 e2e 改进: - 添加 e2e/helpers/i18n.ts 辅助模块 - 使用多语言正则替代硬编码中文文本 - 移除硬等待 waitForTimeout,使用语义等待 - 定义 UI_TEXT 和 TEST_IDS 常量 --- miniapps/forge/e2e/helpers/i18n.ts | 62 ++++++++++++ miniapps/forge/e2e/ui.spec.ts | 72 +++++++------- miniapps/forge/src/i18n/i18next.d.ts | 12 +++ miniapps/forge/src/i18n/index.test.ts | 77 +++++++++++++++ miniapps/forge/src/i18n/index.ts | 41 +++++--- .../src/i18n/locales/{zh.json => zh-CN.json} | 0 miniapps/forge/src/i18n/locales/zh-TW.json | 47 +++++++++ miniapps/teleport/e2e/helpers/i18n.ts | 57 +++++++++++ miniapps/teleport/e2e/ui.spec.ts | 97 +++++++++---------- miniapps/teleport/src/i18n/i18next.d.ts | 12 +++ miniapps/teleport/src/i18n/index.test.ts | 77 +++++++++++++++ miniapps/teleport/src/i18n/index.ts | 41 +++++--- .../src/i18n/locales/{zh.json => zh-CN.json} | 0 miniapps/teleport/src/i18n/locales/zh-TW.json | 49 ++++++++++ 14 files changed, 528 insertions(+), 116 deletions(-) create mode 100644 miniapps/forge/e2e/helpers/i18n.ts create mode 100644 miniapps/forge/src/i18n/i18next.d.ts create mode 100644 miniapps/forge/src/i18n/index.test.ts rename miniapps/forge/src/i18n/locales/{zh.json => zh-CN.json} (100%) create mode 100644 miniapps/forge/src/i18n/locales/zh-TW.json create mode 100644 miniapps/teleport/e2e/helpers/i18n.ts create mode 100644 miniapps/teleport/src/i18n/i18next.d.ts create mode 100644 miniapps/teleport/src/i18n/index.test.ts rename miniapps/teleport/src/i18n/locales/{zh.json => zh-CN.json} (100%) create mode 100644 miniapps/teleport/src/i18n/locales/zh-TW.json diff --git a/miniapps/forge/e2e/helpers/i18n.ts b/miniapps/forge/e2e/helpers/i18n.ts new file mode 100644 index 00000000..007f32f2 --- /dev/null +++ b/miniapps/forge/e2e/helpers/i18n.ts @@ -0,0 +1,62 @@ +/** + * Forge E2E 测试国际化辅助 + */ + +import type { Page, Locator } from '@playwright/test' + +export const UI_TEXT = { + connect: { + button: /连接钱包|Connect Wallet/i, + loading: /连接中|Connecting/i, + }, + swap: { + pay: /支付|Pay/i, + receive: /获得|Receive/i, + button: /兑换|Swap/i, + confirm: /确认兑换|Confirm Swap/i, + preview: /预览交易|Preview|预览/i, + max: /全部|Max/i, + }, + confirm: { + title: /确认兑换|Confirm Swap/i, + button: /确认|Confirm/i, + cancel: /取消|Cancel/i, + }, + success: { + title: /兑换成功|Swap Successful/i, + done: /完成|Done/i, + }, + token: { + select: /选择代币|Select Token/i, + }, +} as const + +export const TEST_IDS = { + connectButton: 'connect-button', + swapForm: 'swap-form', + payAmountInput: 'pay-amount-input', + receiveAmountInput: 'receive-amount-input', + payTokenSelector: 'pay-token-selector', + receiveTokenSelector: 'receive-token-selector', + swapButton: 'swap-button', + confirmButton: 'confirm-button', + cancelButton: 'cancel-button', + tokenPicker: 'token-picker', + tokenList: 'token-list', + successDialog: 'success-dialog', + doneButton: 'done-button', +} as const + +export function byTestId(page: Page, testId: string): Locator { + return page.locator(`[data-testid="${testId}"]`) +} + +export function i18nLocator(page: Page, selector: string, text: RegExp): Locator { + return page.locator(`${selector}:has-text("${text.source}")`) +} + +export async function setLanguage(page: Page, lang: string) { + await page.addInitScript((language) => { + localStorage.setItem('i18nextLng', language) + }, lang) +} diff --git a/miniapps/forge/e2e/ui.spec.ts b/miniapps/forge/e2e/ui.spec.ts index ab9e3dc4..6a91df23 100644 --- a/miniapps/forge/e2e/ui.spec.ts +++ b/miniapps/forge/e2e/ui.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test' +import { UI_TEXT, TEST_IDS, byTestId } from './helpers/i18n' -// Mock bio SDK const mockBioSDK = ` window.bio = { request: async ({ method }) => { @@ -12,75 +12,73 @@ const mockBioSDK = ` } ` -test.describe('Forge 小程序 UI', () => { +test.describe('Forge UI', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }) }) - test('01 - 连接页面', async ({ page }) => { + test('01 - connect page', async ({ page }) => { await page.goto('/') await page.waitForLoadState('networkidle') - await page.waitForTimeout(800) + await expect(page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first()).toBeVisible() await expect(page).toHaveScreenshot('01-connect.png') }) - test('02 - 兑换页面', async ({ page }) => { + test('02 - swap page after connect', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - // 点击连接按钮 - await page.click('button:has-text("连接钱包")') - await page.waitForTimeout(500) - + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await expect(page.locator(`button:has-text("${UI_TEXT.swap.button.source}")`).first()).toBeVisible() + await expect(page).toHaveScreenshot('02-swap.png') }) - test('03 - 兑换页面 - 输入金额', async ({ page }) => { + test('03 - swap page with amount', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - await page.click('button:has-text("连接钱包")') - await page.waitForTimeout(300) - - // 输入金额 + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await page.waitForSelector('input[type="number"]') + await page.fill('input[type="number"]', '1.5') - await page.waitForTimeout(300) - + await expect(page.locator('input[type="number"]')).toHaveValue('1.5') + await expect(page).toHaveScreenshot('03-swap-amount.png') }) - test('04 - 代币选择器', async ({ page }) => { + test('04 - token picker', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - await page.click('button:has-text("连接钱包")') - await page.waitForTimeout(500) - - // 点击代币选择按钮 + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await page.waitForSelector('button:has-text("ETH")') + await page.click('button:has-text("ETH")') - await page.waitForTimeout(800) - + await expect(page.locator(`text=${UI_TEXT.token.select.source}`).first()).toBeVisible() + await expect(page).toHaveScreenshot('04-token-picker.png') }) - test('05 - 确认页面', async ({ page }) => { + test('05 - confirm page', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - await page.click('button:has-text("连接钱包")') - await page.waitForTimeout(300) - + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await page.waitForSelector('input[type="number"]') + await page.fill('input[type="number"]', '1.5') - await page.waitForTimeout(200) - - // 点击预览 - await page.click('button:has-text("预览交易")') - await page.waitForTimeout(300) - + + const previewButton = page.locator(`button:has-text("${UI_TEXT.swap.preview.source}")`).first() + if (await previewButton.isVisible()) { + await previewButton.click() + await expect(page.locator(`text=${UI_TEXT.confirm.title.source}`).first()).toBeVisible() + } + await expect(page).toHaveScreenshot('05-confirm.png') }) }) diff --git a/miniapps/forge/src/i18n/i18next.d.ts b/miniapps/forge/src/i18n/i18next.d.ts new file mode 100644 index 00000000..37a273db --- /dev/null +++ b/miniapps/forge/src/i18n/i18next.d.ts @@ -0,0 +1,12 @@ +import 'i18next' + +import type translation from './locales/zh-CN.json' + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'translation' + resources: { + translation: typeof translation + } + } +} diff --git a/miniapps/forge/src/i18n/index.test.ts b/miniapps/forge/src/i18n/index.test.ts new file mode 100644 index 00000000..f4b2eb71 --- /dev/null +++ b/miniapps/forge/src/i18n/index.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest' +import { languages, getLanguageDirection, isRTL, defaultLanguage } from './index' + +import en from './locales/en.json' +import zhCN from './locales/zh-CN.json' +import zhTW from './locales/zh-TW.json' + +function countKeys(obj: Record): number { + let count = 0 + for (const value of Object.values(obj)) { + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + count += countKeys(value as Record) + } else { + count += 1 + } + } + return count +} + +describe('forge i18n', () => { + describe('languages', () => { + it('has Chinese and English', () => { + expect(languages['zh-CN']).toBeDefined() + expect(languages['zh-TW']).toBeDefined() + expect(languages['en']).toBeDefined() + }) + + it('all languages are LTR', () => { + expect(languages['zh-CN'].dir).toBe('ltr') + expect(languages['zh-TW'].dir).toBe('ltr') + expect(languages['en'].dir).toBe('ltr') + }) + }) + + describe('getLanguageDirection', () => { + it('returns ltr for Chinese', () => { + expect(getLanguageDirection('zh-CN')).toBe('ltr') + }) + + it('returns ltr for English', () => { + expect(getLanguageDirection('en')).toBe('ltr') + }) + }) + + describe('isRTL', () => { + it('returns false for all supported languages', () => { + expect(isRTL('zh-CN')).toBe(false) + expect(isRTL('zh-TW')).toBe(false) + expect(isRTL('en')).toBe(false) + }) + }) + + describe('defaultLanguage', () => { + it('is zh-CN', () => { + expect(defaultLanguage).toBe('zh-CN') + }) + }) + + describe('locale completeness', () => { + it('all locales have same number of keys', () => { + const enKeys = countKeys(en) + const zhCNKeys = countKeys(zhCN) + const zhTWKeys = countKeys(zhTW) + expect(zhCNKeys).toBe(enKeys) + expect(zhTWKeys).toBe(enKeys) + }) + + it('all locales have required keys', () => { + const requiredKeys = ['app', 'connect', 'swap', 'confirm', 'success', 'error', 'token'] + for (const key of requiredKeys) { + expect(en).toHaveProperty(key) + expect(zhCN).toHaveProperty(key) + expect(zhTW).toHaveProperty(key) + } + }) + }) +}) diff --git a/miniapps/forge/src/i18n/index.ts b/miniapps/forge/src/i18n/index.ts index 49c83d4a..05f0a8ed 100644 --- a/miniapps/forge/src/i18n/index.ts +++ b/miniapps/forge/src/i18n/index.ts @@ -1,28 +1,39 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' -import zh from './locales/zh.json' import en from './locales/en.json' +import zhCN from './locales/zh-CN.json' +import zhTW from './locales/zh-TW.json' -/** - * Miniapp i18n 配置 - * - * Fallback 规则: - * - zh-CN, zh-TW, zh-HK, zh-* → zh - * - 其他语言 → en - */ +export const languages = { + 'zh-CN': { name: '简体中文', dir: 'ltr' as const }, + 'zh-TW': { name: '中文(繁體)', dir: 'ltr' as const }, + 'en': { name: 'English', dir: 'ltr' as const }, +} as const + +export type LanguageCode = keyof typeof languages + +export const defaultLanguage: LanguageCode = 'zh-CN' + +export function getLanguageDirection(lang: LanguageCode): 'ltr' | 'rtl' { + return languages[lang]?.dir ?? 'ltr' +} + +export function isRTL(lang: LanguageCode): boolean { + return getLanguageDirection(lang) === 'rtl' +} i18n.use(initReactI18next).init({ resources: { - zh: { translation: zh }, - en: { translation: en }, + 'en': { translation: en }, + 'zh-CN': { translation: zhCN }, + 'zh-TW': { translation: zhTW }, }, - lng: 'zh', + lng: defaultLanguage, fallbackLng: { - 'zh-CN': ['zh'], - 'zh-TW': ['zh'], - 'zh-HK': ['zh'], - 'zh': ['zh'], + 'zh-TW': ['zh-CN'], + 'zh-HK': ['zh-TW', 'zh-CN'], + 'zh': ['zh-CN'], 'default': ['en'], }, interpolation: { diff --git a/miniapps/forge/src/i18n/locales/zh.json b/miniapps/forge/src/i18n/locales/zh-CN.json similarity index 100% rename from miniapps/forge/src/i18n/locales/zh.json rename to miniapps/forge/src/i18n/locales/zh-CN.json diff --git a/miniapps/forge/src/i18n/locales/zh-TW.json b/miniapps/forge/src/i18n/locales/zh-TW.json new file mode 100644 index 00000000..f2b7a145 --- /dev/null +++ b/miniapps/forge/src/i18n/locales/zh-TW.json @@ -0,0 +1,47 @@ +{ + "app": { + "title": "兌換中心", + "subtitle": "多鏈兌換", + "description": "安全、快速地在不同代幣之間進行兌換" + }, + "connect": { + "button": "連接錢包", + "loading": "連接中..." + }, + "swap": { + "pay": "支付", + "receive": "獲得", + "balance": "餘額", + "rate": "兌換比率", + "button": "兌換", + "confirm": "確認兌換", + "processing": "處理中...", + "max": "全部" + }, + "confirm": { + "title": "確認兌換", + "from": "支付", + "to": "獲得", + "rate": "匯率", + "fee": "網絡費用", + "feeEstimate": "預估", + "button": "確認", + "cancel": "取消" + }, + "success": { + "title": "兌換成功!", + "message": "您的兌換已提交", + "txWait": "交易確認可能需要幾分鐘", + "done": "完成" + }, + "error": { + "sdkNotInit": "Bio SDK 未初始化", + "connectionFailed": "連接失敗", + "invalidAmount": "請輸入有效金額", + "insufficientBalance": "餘額不足" + }, + "token": { + "select": "選擇代幣", + "search": "搜索代幣" + } +} diff --git a/miniapps/teleport/e2e/helpers/i18n.ts b/miniapps/teleport/e2e/helpers/i18n.ts new file mode 100644 index 00000000..193782c3 --- /dev/null +++ b/miniapps/teleport/e2e/helpers/i18n.ts @@ -0,0 +1,57 @@ +/** + * Teleport E2E 测试国际化辅助 + */ + +import type { Page, Locator } from '@playwright/test' + +export const UI_TEXT = { + connect: { + button: /选择源钱包|启动传送门|Select Source Wallet/i, + loading: /连接中|Connecting/i, + }, + asset: { + select: /选择要传送的资产|Select asset/i, + }, + amount: { + next: /下一步|Next/i, + max: /全部|Max/i, + }, + target: { + title: /选择目标钱包|Select Target Wallet/i, + button: /选择目标钱包|Select Target Wallet/i, + }, + confirm: { + title: /确认传送|Confirm Transfer/i, + button: /确认传送|Confirm Transfer/i, + }, + success: { + title: /传送成功|Transfer Successful/i, + done: /完成|Done/i, + }, +} as const + +export const TEST_IDS = { + connectButton: 'connect-button', + assetList: 'asset-list', + assetCard: 'asset-card', + amountInput: 'amount-input', + nextButton: 'next-button', + targetButton: 'target-button', + confirmButton: 'confirm-button', + successDialog: 'success-dialog', + doneButton: 'done-button', +} as const + +export function byTestId(page: Page, testId: string): Locator { + return page.locator(`[data-testid="${testId}"]`) +} + +export function i18nLocator(page: Page, selector: string, text: RegExp): Locator { + return page.locator(`${selector}:has-text("${text.source}")`) +} + +export async function setLanguage(page: Page, lang: string) { + await page.addInitScript((language) => { + localStorage.setItem('i18nextLng', language) + }, lang) +} diff --git a/miniapps/teleport/e2e/ui.spec.ts b/miniapps/teleport/e2e/ui.spec.ts index ba345e33..4d1690b0 100644 --- a/miniapps/teleport/e2e/ui.spec.ts +++ b/miniapps/teleport/e2e/ui.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test' +import { UI_TEXT, TEST_IDS, byTestId } from './helpers/i18n' -// Mock bio SDK const mockBioSDK = ` window.bio = { request: async ({ method }) => { @@ -15,97 +15,96 @@ const mockBioSDK = ` } ` -test.describe('Teleport 小程序 UI', () => { +test.describe('Teleport UI', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }) }) - test('01 - 连接页面', async ({ page }) => { + test('01 - connect page', async ({ page }) => { await page.goto('/') await page.waitForLoadState('networkidle') - await page.waitForTimeout(800) + await expect(page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first()).toBeVisible() await expect(page).toHaveScreenshot('01-connect.png') }) - test('02 - 选择资产页面', async ({ page }) => { + test('02 - asset selection page', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - await page.click('button:has-text("启动传送门")') - await page.waitForTimeout(500) - + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await expect(page.locator('[data-slot="card"]').first()).toBeVisible() + await expect(page).toHaveScreenshot('02-select-asset.png') }) - test('03 - 输入金额页面', async ({ page }) => { + test('03 - amount input page', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - await page.click('button:has-text("启动传送门")') - await page.waitForTimeout(300) - - // 选择 BFM 资产 + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await page.waitForSelector('[data-slot="card"]') + await page.click('[data-slot="card"]:has-text("BFM")') - await page.waitForTimeout(300) - + await expect(page.locator('input[type="number"]')).toBeVisible() + await expect(page).toHaveScreenshot('03-input-amount.png') }) - test('04 - 输入金额后', async ({ page }) => { + test('04 - amount filled', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - await page.click('button:has-text("启动传送门")') - await page.waitForTimeout(300) - + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await page.waitForSelector('[data-slot="card"]') + await page.click('[data-slot="card"]:has-text("BFM")') - await page.waitForTimeout(300) - + await page.waitForSelector('input[type="number"]') + await page.fill('input[type="number"]', '500') - await page.waitForTimeout(300) - + await expect(page.locator('input[type="number"]')).toHaveValue('500') + await expect(page).toHaveScreenshot('04-amount-filled.png') }) - test('05 - 选择目标钱包页面', async ({ page }) => { + test('05 - target wallet selection', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - await page.click('button:has-text("启动传送门")') - await page.waitForTimeout(300) - + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await page.waitForSelector('[data-slot="card"]') + await page.click('[data-slot="card"]:has-text("BFM")') - await page.waitForTimeout(300) - + await page.waitForSelector('input[type="number"]') + await page.fill('input[type="number"]', '500') - await page.click('button:has-text("下一步")') - await page.waitForTimeout(300) - + await page.locator(`button:has-text("${UI_TEXT.amount.next.source}")`).first().click() + await expect(page.locator(`text=${UI_TEXT.target.title.source}`).first()).toBeVisible() + await expect(page).toHaveScreenshot('05-select-target.png') }) - test('06 - 确认页面', async ({ page }) => { + test('06 - confirm page', async ({ page }) => { await page.addInitScript(mockBioSDK) await page.goto('/') await page.waitForLoadState('networkidle') - - await page.click('button:has-text("启动传送门")') - await page.waitForTimeout(300) - + + await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + await page.waitForSelector('[data-slot="card"]') + await page.click('[data-slot="card"]:has-text("BFM")') - await page.waitForTimeout(300) - + await page.waitForSelector('input[type="number"]') + await page.fill('input[type="number"]', '500') - await page.click('button:has-text("下一步")') - await page.waitForTimeout(300) - - await page.click('button:has-text("选择目标钱包")') - await page.waitForTimeout(300) - + await page.locator(`button:has-text("${UI_TEXT.amount.next.source}")`).first().click() + await page.waitForSelector(`text=${UI_TEXT.target.title.source}`) + + await page.locator(`button:has-text("${UI_TEXT.target.button.source}")`).first().click() + await expect(page.locator(`text=${UI_TEXT.confirm.title.source}`).first()).toBeVisible() + await expect(page).toHaveScreenshot('06-confirm.png') }) }) diff --git a/miniapps/teleport/src/i18n/i18next.d.ts b/miniapps/teleport/src/i18n/i18next.d.ts new file mode 100644 index 00000000..37a273db --- /dev/null +++ b/miniapps/teleport/src/i18n/i18next.d.ts @@ -0,0 +1,12 @@ +import 'i18next' + +import type translation from './locales/zh-CN.json' + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'translation' + resources: { + translation: typeof translation + } + } +} diff --git a/miniapps/teleport/src/i18n/index.test.ts b/miniapps/teleport/src/i18n/index.test.ts new file mode 100644 index 00000000..8fa524f2 --- /dev/null +++ b/miniapps/teleport/src/i18n/index.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest' +import { languages, getLanguageDirection, isRTL, defaultLanguage } from './index' + +import en from './locales/en.json' +import zhCN from './locales/zh-CN.json' +import zhTW from './locales/zh-TW.json' + +function countKeys(obj: Record): number { + let count = 0 + for (const value of Object.values(obj)) { + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + count += countKeys(value as Record) + } else { + count += 1 + } + } + return count +} + +describe('teleport i18n', () => { + describe('languages', () => { + it('has Chinese and English', () => { + expect(languages['zh-CN']).toBeDefined() + expect(languages['zh-TW']).toBeDefined() + expect(languages['en']).toBeDefined() + }) + + it('all languages are LTR', () => { + expect(languages['zh-CN'].dir).toBe('ltr') + expect(languages['zh-TW'].dir).toBe('ltr') + expect(languages['en'].dir).toBe('ltr') + }) + }) + + describe('getLanguageDirection', () => { + it('returns ltr for Chinese', () => { + expect(getLanguageDirection('zh-CN')).toBe('ltr') + }) + + it('returns ltr for English', () => { + expect(getLanguageDirection('en')).toBe('ltr') + }) + }) + + describe('isRTL', () => { + it('returns false for all supported languages', () => { + expect(isRTL('zh-CN')).toBe(false) + expect(isRTL('zh-TW')).toBe(false) + expect(isRTL('en')).toBe(false) + }) + }) + + describe('defaultLanguage', () => { + it('is zh-CN', () => { + expect(defaultLanguage).toBe('zh-CN') + }) + }) + + describe('locale completeness', () => { + it('all locales have same number of keys', () => { + const enKeys = countKeys(en) + const zhCNKeys = countKeys(zhCN) + const zhTWKeys = countKeys(zhTW) + expect(zhCNKeys).toBe(enKeys) + expect(zhTWKeys).toBe(enKeys) + }) + + it('all locales have required keys', () => { + const requiredKeys = ['app', 'connect', 'asset', 'amount', 'target', 'confirm', 'success', 'error'] + for (const key of requiredKeys) { + expect(en).toHaveProperty(key) + expect(zhCN).toHaveProperty(key) + expect(zhTW).toHaveProperty(key) + } + }) + }) +}) diff --git a/miniapps/teleport/src/i18n/index.ts b/miniapps/teleport/src/i18n/index.ts index 49c83d4a..05f0a8ed 100644 --- a/miniapps/teleport/src/i18n/index.ts +++ b/miniapps/teleport/src/i18n/index.ts @@ -1,28 +1,39 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' -import zh from './locales/zh.json' import en from './locales/en.json' +import zhCN from './locales/zh-CN.json' +import zhTW from './locales/zh-TW.json' -/** - * Miniapp i18n 配置 - * - * Fallback 规则: - * - zh-CN, zh-TW, zh-HK, zh-* → zh - * - 其他语言 → en - */ +export const languages = { + 'zh-CN': { name: '简体中文', dir: 'ltr' as const }, + 'zh-TW': { name: '中文(繁體)', dir: 'ltr' as const }, + 'en': { name: 'English', dir: 'ltr' as const }, +} as const + +export type LanguageCode = keyof typeof languages + +export const defaultLanguage: LanguageCode = 'zh-CN' + +export function getLanguageDirection(lang: LanguageCode): 'ltr' | 'rtl' { + return languages[lang]?.dir ?? 'ltr' +} + +export function isRTL(lang: LanguageCode): boolean { + return getLanguageDirection(lang) === 'rtl' +} i18n.use(initReactI18next).init({ resources: { - zh: { translation: zh }, - en: { translation: en }, + 'en': { translation: en }, + 'zh-CN': { translation: zhCN }, + 'zh-TW': { translation: zhTW }, }, - lng: 'zh', + lng: defaultLanguage, fallbackLng: { - 'zh-CN': ['zh'], - 'zh-TW': ['zh'], - 'zh-HK': ['zh'], - 'zh': ['zh'], + 'zh-TW': ['zh-CN'], + 'zh-HK': ['zh-TW', 'zh-CN'], + 'zh': ['zh-CN'], 'default': ['en'], }, interpolation: { diff --git a/miniapps/teleport/src/i18n/locales/zh.json b/miniapps/teleport/src/i18n/locales/zh-CN.json similarity index 100% rename from miniapps/teleport/src/i18n/locales/zh.json rename to miniapps/teleport/src/i18n/locales/zh-CN.json diff --git a/miniapps/teleport/src/i18n/locales/zh-TW.json b/miniapps/teleport/src/i18n/locales/zh-TW.json new file mode 100644 index 00000000..d7661b8f --- /dev/null +++ b/miniapps/teleport/src/i18n/locales/zh-TW.json @@ -0,0 +1,49 @@ +{ + "app": { + "title": "一鍵傳送", + "subtitle": "跨錢包傳送", + "description": "安全地將資產從一個錢包轉移到另一個錢包" + }, + "connect": { + "button": "選擇源錢包", + "loading": "連接中..." + }, + "asset": { + "select": "選擇要傳送的資產", + "balance": "可用" + }, + "amount": { + "label": "傳送數量", + "placeholder": "0.00", + "max": "全部", + "next": "下一步" + }, + "target": { + "title": "選擇目標錢包", + "description": "選擇接收資產的目標錢包", + "button": "選擇目標錢包", + "loading": "選擇中..." + }, + "confirm": { + "title": "確認傳送", + "from": "從", + "to": "到", + "amount": "傳送", + "network": "網絡", + "button": "確認傳送", + "loading": "處理中..." + }, + "success": { + "title": "傳送成功!", + "message": "已發送", + "txWait": "交易確認可能需要幾分鐘", + "done": "完成" + }, + "error": { + "sdkNotInit": "Bio SDK 未初始化", + "connectionFailed": "連接失敗", + "selectFailed": "選擇失敗", + "transferFailed": "轉賬失敗", + "invalidAmount": "請輸入有效金額" + } +} From 216f1a846389a93d0a004c6b0a6ba183e4790249 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Mon, 29 Dec 2025 17:57:10 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(teleport):=20=E4=BF=AE=E5=A4=8D=20lint?= =?UTF-8?q?=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除未使用的 import (CardHeader, CardFooter, vi, TEST_IDS, byTestId) - 修复非空断言 (!) 改为可选链 (?.) - 修复自闭合标签 --- miniapps/teleport/e2e/ui.spec.ts | 2 +- miniapps/teleport/src/App.tsx | 6 +++--- miniapps/teleport/src/components/AuroraBackground.tsx | 2 +- miniapps/teleport/src/main.tsx | 2 +- miniapps/teleport/src/test-setup.ts | 1 - 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/miniapps/teleport/e2e/ui.spec.ts b/miniapps/teleport/e2e/ui.spec.ts index 4d1690b0..f7ff1265 100644 --- a/miniapps/teleport/e2e/ui.spec.ts +++ b/miniapps/teleport/e2e/ui.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { UI_TEXT, TEST_IDS, byTestId } from './helpers/i18n' +import { UI_TEXT } from './helpers/i18n' const mockBioSDK = ` window.bio = { diff --git a/miniapps/teleport/src/App.tsx b/miniapps/teleport/src/App.tsx index 199c343b..8d57b745 100644 --- a/miniapps/teleport/src/App.tsx +++ b/miniapps/teleport/src/App.tsx @@ -1,7 +1,7 @@ import { useState, useCallback } from 'react' import type { BioAccount } from '@biochain/bio-sdk' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card' +import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' import { Badge } from '@/components/ui/badge' @@ -306,7 +306,7 @@ export default function App() { 即将传送
- + {amount} {selectedAsset?.symbol}
@@ -352,7 +352,7 @@ export default function App() {
发送
- + {amount} {selectedAsset?.symbol}
diff --git a/miniapps/teleport/src/components/AuroraBackground.tsx b/miniapps/teleport/src/components/AuroraBackground.tsx index a6934e56..77807ebb 100644 --- a/miniapps/teleport/src/components/AuroraBackground.tsx +++ b/miniapps/teleport/src/components/AuroraBackground.tsx @@ -40,7 +40,7 @@ export const AuroraBackground = ({ showRadialGradient && `[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]` )} - > + /> {children} diff --git a/miniapps/teleport/src/main.tsx b/miniapps/teleport/src/main.tsx index b8b83297..44e34ffc 100644 --- a/miniapps/teleport/src/main.tsx +++ b/miniapps/teleport/src/main.tsx @@ -4,7 +4,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById('root')).render( diff --git a/miniapps/teleport/src/test-setup.ts b/miniapps/teleport/src/test-setup.ts index eb352fb0..8c7f86e9 100644 --- a/miniapps/teleport/src/test-setup.ts +++ b/miniapps/teleport/src/test-setup.ts @@ -1,4 +1,3 @@ import '@testing-library/jest-dom/vitest' -import { vi } from 'vitest' // Mock window.bio is set per test for proper isolation From 8b1fcfc68f58b3129e414db075e490917952c3b0 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Mon, 29 Dec 2025 18:01:04 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(miniapps):=20=E4=BF=AE=E5=A4=8D=20i18n?= =?UTF-8?q?=20fallback=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 zh.json 作为中文基础 fallback - 修改 fallback 规则: zh-CN/zh-TW/zh-HK → zh - 修复未使用 import 的 lint 警告 --- miniapps/forge/src/i18n/index.ts | 8 ++-- miniapps/forge/src/i18n/locales/zh.json | 47 +++++++++++++++++++++ miniapps/teleport/src/i18n/index.ts | 8 ++-- miniapps/teleport/src/i18n/locales/zh.json | 49 ++++++++++++++++++++++ 4 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 miniapps/forge/src/i18n/locales/zh.json create mode 100644 miniapps/teleport/src/i18n/locales/zh.json diff --git a/miniapps/forge/src/i18n/index.ts b/miniapps/forge/src/i18n/index.ts index 05f0a8ed..6da1678e 100644 --- a/miniapps/forge/src/i18n/index.ts +++ b/miniapps/forge/src/i18n/index.ts @@ -2,6 +2,7 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import en from './locales/en.json' +import zh from './locales/zh.json' import zhCN from './locales/zh-CN.json' import zhTW from './locales/zh-TW.json' @@ -26,14 +27,15 @@ export function isRTL(lang: LanguageCode): boolean { i18n.use(initReactI18next).init({ resources: { 'en': { translation: en }, + 'zh': { translation: zh }, 'zh-CN': { translation: zhCN }, 'zh-TW': { translation: zhTW }, }, lng: defaultLanguage, fallbackLng: { - 'zh-TW': ['zh-CN'], - 'zh-HK': ['zh-TW', 'zh-CN'], - 'zh': ['zh-CN'], + 'zh-CN': ['zh'], + 'zh-TW': ['zh'], + 'zh-HK': ['zh'], 'default': ['en'], }, interpolation: { diff --git a/miniapps/forge/src/i18n/locales/zh.json b/miniapps/forge/src/i18n/locales/zh.json new file mode 100644 index 00000000..1c60e36f --- /dev/null +++ b/miniapps/forge/src/i18n/locales/zh.json @@ -0,0 +1,47 @@ +{ + "app": { + "title": "兑换中心", + "subtitle": "多链兑换", + "description": "安全、快速地在不同代币之间进行兑换" + }, + "connect": { + "button": "连接钱包", + "loading": "连接中..." + }, + "swap": { + "pay": "支付", + "receive": "获得", + "balance": "余额", + "rate": "兑换比率", + "button": "兑换", + "confirm": "确认兑换", + "processing": "处理中...", + "max": "全部" + }, + "confirm": { + "title": "确认兑换", + "from": "支付", + "to": "获得", + "rate": "汇率", + "fee": "网络费用", + "feeEstimate": "预估", + "button": "确认", + "cancel": "取消" + }, + "success": { + "title": "兑换成功!", + "message": "您的兑换已提交", + "txWait": "交易确认可能需要几分钟", + "done": "完成" + }, + "error": { + "sdkNotInit": "Bio SDK 未初始化", + "connectionFailed": "连接失败", + "invalidAmount": "请输入有效金额", + "insufficientBalance": "余额不足" + }, + "token": { + "select": "选择代币", + "search": "搜索代币" + } +} diff --git a/miniapps/teleport/src/i18n/index.ts b/miniapps/teleport/src/i18n/index.ts index 05f0a8ed..6da1678e 100644 --- a/miniapps/teleport/src/i18n/index.ts +++ b/miniapps/teleport/src/i18n/index.ts @@ -2,6 +2,7 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import en from './locales/en.json' +import zh from './locales/zh.json' import zhCN from './locales/zh-CN.json' import zhTW from './locales/zh-TW.json' @@ -26,14 +27,15 @@ export function isRTL(lang: LanguageCode): boolean { i18n.use(initReactI18next).init({ resources: { 'en': { translation: en }, + 'zh': { translation: zh }, 'zh-CN': { translation: zhCN }, 'zh-TW': { translation: zhTW }, }, lng: defaultLanguage, fallbackLng: { - 'zh-TW': ['zh-CN'], - 'zh-HK': ['zh-TW', 'zh-CN'], - 'zh': ['zh-CN'], + 'zh-CN': ['zh'], + 'zh-TW': ['zh'], + 'zh-HK': ['zh'], 'default': ['en'], }, interpolation: { diff --git a/miniapps/teleport/src/i18n/locales/zh.json b/miniapps/teleport/src/i18n/locales/zh.json new file mode 100644 index 00000000..e12a19a7 --- /dev/null +++ b/miniapps/teleport/src/i18n/locales/zh.json @@ -0,0 +1,49 @@ +{ + "app": { + "title": "一键传送", + "subtitle": "跨钱包传送", + "description": "安全地将资产从一个钱包转移到另一个钱包" + }, + "connect": { + "button": "选择源钱包", + "loading": "连接中..." + }, + "asset": { + "select": "选择要传送的资产", + "balance": "可用" + }, + "amount": { + "label": "传送数量", + "placeholder": "0.00", + "max": "全部", + "next": "下一步" + }, + "target": { + "title": "选择目标钱包", + "description": "选择接收资产的目标钱包", + "button": "选择目标钱包", + "loading": "选择中..." + }, + "confirm": { + "title": "确认传送", + "from": "从", + "to": "到", + "amount": "传送", + "network": "网络", + "button": "确认传送", + "loading": "处理中..." + }, + "success": { + "title": "传送成功!", + "message": "已发送", + "txWait": "交易确认可能需要几分钟", + "done": "完成" + }, + "error": { + "sdkNotInit": "Bio SDK 未初始化", + "connectionFailed": "连接失败", + "selectFailed": "选择失败", + "transferFailed": "转账失败", + "invalidAmount": "请输入有效金额" + } +} From a032ed4e762f8ae0e852840a613d6a1b3062b091 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Mon, 29 Dec 2025 18:02:30 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(teleport):=20=E4=BF=AE=E5=A4=8D=20TypeS?= =?UTF-8?q?cript=20=E7=B1=BB=E5=9E=8B=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniapps/teleport/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniapps/teleport/src/main.tsx b/miniapps/teleport/src/main.tsx index 44e34ffc..9d212c56 100644 --- a/miniapps/teleport/src/main.tsx +++ b/miniapps/teleport/src/main.tsx @@ -4,7 +4,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' -createRoot(document.getElementById('root')).render( +createRoot(document.getElementById('root') as HTMLElement).render(