Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions miniapps/forge/e2e/helpers/i18n.ts
Original file line number Diff line number Diff line change
@@ -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)
}
72 changes: 35 additions & 37 deletions miniapps/forge/e2e/ui.spec.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -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')
})
})
12 changes: 12 additions & 0 deletions miniapps/forge/src/i18n/i18next.d.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
}
77 changes: 77 additions & 0 deletions miniapps/forge/src/i18n/index.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): 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<string, unknown>)
} 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)
}
})
})
})
37 changes: 25 additions & 12 deletions miniapps/forge/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'

import zh from './locales/zh.json'
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'

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'
}

/**
* Miniapp i18n 配置
*
* Fallback 规则:
* - zh-CN, zh-TW, zh-HK, zh-* → zh
* - 其他语言 → en
*/
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': { translation: zh },
'zh-CN': { translation: zhCN },
'zh-TW': { translation: zhTW },
},
lng: 'zh',
lng: defaultLanguage,
fallbackLng: {
'zh-CN': ['zh'],
'zh-TW': ['zh'],
'zh-HK': ['zh'],
'zh': ['zh'],
'default': ['en'],
},
interpolation: {
Expand Down
47 changes: 47 additions & 0 deletions miniapps/forge/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
@@ -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": "搜索代币"
}
}
Loading