diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd0ac4a71..9b7b487f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: if [ "${{ steps.changes.outputs.code }}" == "true" ]; then # 运行静态检查 + pnpm i18n:run pnpm turbo run lint:run typecheck:run build i18n:run theme:run test:run test:storybook e2e:audit # 使用 e2e:runner 分片运行 E2E 测试,避免单次全部运行 @@ -56,6 +57,7 @@ jobs: pnpm e2e:runner --all -j 2 pnpm e2e:ci:real else + pnpm i18n:run pnpm turbo run lint:run typecheck:run build i18n:run theme:run test:run test:storybook fi @@ -121,8 +123,10 @@ jobs: run: | if [ "${{ steps.changes.outputs.code }}" == "true" ]; then # 运行所有测试:lint + 单元测试 + Storybook 组件测试 + E2E 测试 + 主题检查 + pnpm i18n:run pnpm turbo run lint:run typecheck:run build i18n:run theme:run test:run test:storybook e2e:audit e2e:ci e2e:ci:mock e2e:ci:real else + pnpm i18n:run pnpm turbo run lint:run typecheck:run build i18n:run theme:run test:run test:storybook fi diff --git a/scripts/i18n-check.ts b/scripts/i18n-check.ts index 572d1fa4b..9068256cf 100644 --- a/scripts/i18n-check.ts +++ b/scripts/i18n-check.ts @@ -104,6 +104,13 @@ interface ChineseLiteral { content: string; } +interface UsedTranslationKey { + namespace: string; + key: string; + file: string; + line: number; +} + // ==================== Utilities ==================== /** @@ -221,6 +228,115 @@ function sortObjectKeys(obj: TranslationFile): TranslationFile { return sorted; } +function extractDefaultNamespaces(content: string): string[] { + const matches = content.match(/useTranslation\s*\(([\s\S]*?)\)/g); + if (!matches || matches.length === 0) return []; + + const firstMatch = matches[0]; + const argsMatch = firstMatch.match(/useTranslation\s*\(([\s\S]*?)\)/); + if (!argsMatch) return []; + + const args = argsMatch[1].trim(); + if (!args) return []; + + const arrayMatch = args.match(/^\s*\[([\s\S]*?)\]/); + if (arrayMatch) { + const namespaces: string[] = []; + const regex = /'([^']+)'|"([^"]+)"/g; + let m; + while ((m = regex.exec(arrayMatch[1])) !== null) { + namespaces.push(m[1] || m[2]); + } + return namespaces; + } + + const literalMatch = args.match(/^\s*['"]([^'"]+)['"]/); + if (literalMatch) { + return [literalMatch[1]]; + } + + return []; +} + +function stripCommentsPreserveLines(content: string): string { + const withoutBlock = content.replace(/\/\*[\s\S]*?\*\//g, (match) => { + const lines = match.split('\n').length; + return '\n'.repeat(Math.max(0, lines - 1)); + }); + return withoutBlock.replace(/\/\/.*$/gm, ''); +} + +function extractUsedTranslationKeys(file: string, content: string): UsedTranslationKey[] { + if (!content.includes('useTranslation') && !content.includes('i18n.t') && !content.includes('t(')) { + return []; + } + + const searchable = stripCommentsPreserveLines(content); + const defaultNamespaces = extractDefaultNamespaces(content); + const defaultNamespace = defaultNamespaces[0]; + const results: UsedTranslationKey[] = []; + const regex = /\b(?:t|i18n\.t)\s*\(\s*(['"`])([^'"`]+)\1/g; + let match: RegExpExecArray | null; + + while ((match = regex.exec(searchable)) !== null) { + const rawKey = match[2].trim(); + if (!rawKey || rawKey.includes('${')) continue; + + let namespace = ''; + let key = ''; + if (rawKey.includes(':')) { + const [ns, rest] = rawKey.split(':', 2); + namespace = ns; + key = rest; + } else if (defaultNamespace) { + namespace = defaultNamespace; + key = rawKey; + } else { + continue; + } + + const line = searchable.slice(0, match.index).split('\n').length; + results.push({ namespace, key, file, line }); + } + + return results; +} + +function buildReferenceIndex(namespaces: string[]): Map> { + const index = new Map>(); + for (const namespace of namespaces) { + const refPath = join(LOCALES_DIR, REFERENCE_LOCALE, `${namespace}.json`); + if (!existsSync(refPath)) continue; + const refData: TranslationFile = JSON.parse(readFileSync(refPath, 'utf-8')); + index.set(namespace, new Set(extractKeys(refData))); + } + return index; +} + +function checkUsedTranslationKeys(referenceIndex: Map>, verbose: boolean): UsedTranslationKey[] { + const files = scanSourceFiles(); + if (verbose) { + log.dim(`Scanning ${files.length} source files for translation key usage...`); + } + + const missing: UsedTranslationKey[] = []; + + for (const file of files) { + const filePath = join(ROOT, file); + const content = readFileSync(filePath, 'utf-8'); + const usedKeys = extractUsedTranslationKeys(file, content); + + for (const used of usedKeys) { + const keys = referenceIndex.get(used.namespace); + if (!keys || !keys.has(used.key)) { + missing.push(used); + } + } + } + + return missing; +} + // ==================== Chinese Literal Detection ==================== /** @@ -501,6 +617,7 @@ ${colors.cyan}╔═════════════════════ const namespaces = getNamespaces(); log.info(`Found ${namespaces.length} namespaces`); + const referenceIndex = buildReferenceIndex(namespaces); // Check for unregistered namespaces log.step('Checking namespace registration'); @@ -624,7 +741,43 @@ ${colors.green}✓ No missing or untranslated keys${colors.reset} `); } - // Step 3: Check for Chinese literals in source code + // Step 3: Check for missing keys referenced in source code + log.step('Checking translation key usage in source code'); + const missingUsedKeys = checkUsedTranslationKeys(referenceIndex, verbose); + + if (missingUsedKeys.length > 0) { + log.error(`Found ${missingUsedKeys.length} missing translation key(s) referenced in source code:`); + + const byNamespace = new Map(); + for (const item of missingUsedKeys) { + if (!byNamespace.has(item.namespace)) byNamespace.set(item.namespace, []); + byNamespace.get(item.namespace)!.push(item); + } + + for (const [namespace, items] of byNamespace) { + console.log(`\n${colors.bold}${namespace}${colors.reset}`); + for (const item of items.slice(0, 5)) { + log.dim(`${item.file}:${item.line} - ${item.namespace}:${item.key}`); + } + if (items.length > 5) { + log.dim(`... and ${items.length - 5} more`); + } + } + + console.log(` +${colors.red}✗ Missing translation keys referenced in code${colors.reset} + +${colors.bold}To fix:${colors.reset} + 1. Add the missing keys to ${REFERENCE_LOCALE} locale files + 2. Run ${colors.cyan}pnpm i18n:check --fix${colors.reset} if you want placeholders added to other locales +`); + + process.exit(1); + } else { + log.success('All referenced translation keys exist'); + } + + // Step 4: Check for Chinese literals in source code log.step('Checking for Chinese literals in source code'); const chineseLiterals = checkChineseLiterals(verbose); diff --git a/src/i18n/locales/ar/common.json b/src/i18n/locales/ar/common.json index 81d3ede99..f9a240246 100644 --- a/src/i18n/locales/ar/common.json +++ b/src/i18n/locales/ar/common.json @@ -85,10 +85,14 @@ "useExplorerHint": "This chain does not support direct history query, please use the explorer", "viewOnExplorer": "View on {{name}}" }, - "sign": { + "beta": "نسخة تجريبية", + "signSymbol": { "plus": "+", "minus": "-" }, + "walletLock": { + "error": "فشل التحقق من قفل المحفظة" + }, "addressPlaceholder": "أدخل أو الصق العنوان", "advancedEncryptionTechnologyDigitalWealthIsMoreSecure": "Advanced encryption technology <br /> Digital wealth is more secure", "afterConfirmation_{appName}WillDeleteAllLocalDataTips": "After confirmation, will delete all local data and all wallets will be removed locally. Are you sure to exit?", @@ -510,9 +514,11 @@ "description": "سلاسل متوافقة مع آلة إيثريوم الافتراضية" }, "bitcoin": { + "name": "بيتكوين", "description": "شبكة Bitcoin" }, "tron": { + "name": "ترون", "description": "شبكة Tron" }, "custom": { diff --git a/src/i18n/locales/ar/error.json b/src/i18n/locales/ar/error.json index 44dc290c3..a52013d7c 100644 --- a/src/i18n/locales/ar/error.json +++ b/src/i18n/locales/ar/error.json @@ -19,11 +19,14 @@ "enterAmount": "Please enter amount", "enterValidAmount": "Please enter a valid amount", "exceedsBalance": "Amount exceeds balance", + "selectAsset": "يرجى اختيار أصل", "enterReceiverAddress": "يرجى إدخال عنوان المستلم", - "invalidAddress": "تنسيق العنوان غير صالح" + "invalidAddress": "تنسيق العنوان غير صالح", + "unsupportedChainType": "نوع سلسلة غير مدعوم" }, "transaction": { "failed": "Transaction failed, please try again later", + "transactionFailed": "فشلت المعاملة، يرجى المحاولة لاحقاً", "burnFailed": "Burn failed, please try again later", "chainNotSupported": "This chain does not support full transaction flow", "chainNotSupportedWithId": "This chain does not support full transaction flow: {{chainId}}", @@ -35,11 +38,16 @@ "issuerAddressNotFound": "Unable to get asset issuer address", "issuerAddressNotReady": "Asset issuer address not ready", "insufficientGas": "رسوم الغاز غير كافية", + "invalidAmount": "مبلغ غير صالح", + "paramsIncomplete": "معلمات المعاملة غير مكتملة", "retryLater": "فشلت المعاملة، يرجى المحاولة لاحقاً", "transferFailed": "فشل التحويل", "securityPasswordWrong": "كلمة مرور الأمان غير صحيحة", "unknownError": "خطأ غير معروف" }, + "burn": { + "bioforestOnly": "الحرق مدعوم فقط على BioForest" + }, "crypto": { "keyDerivationFailed": "فشل اشتقاق المفتاح", "decryptionFailed": "Decryption failed: wrong password or corrupted data", diff --git a/src/i18n/locales/ar/guide.json b/src/i18n/locales/ar/guide.json index 67177cd44..447d58e44 100644 --- a/src/i18n/locales/ar/guide.json +++ b/src/i18n/locales/ar/guide.json @@ -4,6 +4,7 @@ "next": "التالي", "getStarted": "ابدأ الآن", "haveWallet": "لدي محفظة", + "migrateFromMpay": "الترحيل من MPay", "goToSlide": "انتقل إلى الشريحة {{number}}", "slides": { "transfer": { diff --git a/src/i18n/locales/ar/transaction.json b/src/i18n/locales/ar/transaction.json index eae819dde..69311ac09 100644 --- a/src/i18n/locales/ar/transaction.json +++ b/src/i18n/locales/ar/transaction.json @@ -4,6 +4,11 @@ "amountShouldBe_{min}-{max}-{assettype}": "Amount should be -", "approve": "Approve", "balance_:": "Balance:", + "assetSelector": { + "selectAsset": "اختر الأصل", + "balance": "الرصيد", + "noAssets": "لا توجد أصول" + }, "bioforestChainFeeToLow": "If the miner fee is too low, it will affect the transaction on-chain.", "bioforestChainTransactionTypeAcceptAnyAssetExchange": "Accept Any Asset Exchange", "bioforestChainTransactionTypeAcceptAnyAssetGift": "Accept Any Asset Gift", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index f61ab7845..24ba710e9 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -425,10 +425,14 @@ "openExplorer": "Open {{name}} Explorer", "viewOnExplorer": "View on {{name}}" }, - "sign": { + "beta": "Beta", + "signSymbol": { "plus": "+", "minus": "-" }, + "walletLock": { + "error": "Wallet lock verification failed" + }, "tabs": { "assets": "Assets", "history": "History" @@ -510,9 +514,11 @@ "description": "Ethereum Virtual Machine compatible chains" }, "bitcoin": { + "name": "Bitcoin", "description": "Bitcoin network" }, "tron": { + "name": "TRON", "description": "Tron network" }, "custom": { diff --git a/src/i18n/locales/en/error.json b/src/i18n/locales/en/error.json index 722741ddb..6d5a8801f 100644 --- a/src/i18n/locales/en/error.json +++ b/src/i18n/locales/en/error.json @@ -19,11 +19,14 @@ "enterAmount": "Please enter amount", "enterValidAmount": "Please enter a valid amount", "exceedsBalance": "Amount exceeds balance", + "selectAsset": "Please select an asset", "enterReceiverAddress": "Please enter recipient address", - "invalidAddress": "Invalid address format" + "invalidAddress": "Invalid address format", + "unsupportedChainType": "Unsupported chain type" }, "transaction": { "failed": "Transaction failed, please try again later", + "transactionFailed": "Transaction failed, please try again later", "burnFailed": "Burn failed, please try again later", "chainNotSupported": "This chain does not support full transaction flow", "chainNotSupportedWithId": "This chain does not support full transaction flow: {{chainId}}", @@ -35,11 +38,16 @@ "issuerAddressNotFound": "Unable to get asset issuer address", "issuerAddressNotReady": "Asset issuer address not ready", "insufficientGas": "Insufficient gas fee", + "invalidAmount": "Invalid amount", + "paramsIncomplete": "Transaction parameters incomplete", "retryLater": "Transaction failed, please try again later", "transferFailed": "Transfer failed", "securityPasswordWrong": "Security password incorrect", "unknownError": "Unknown error" }, + "burn": { + "bioforestOnly": "Burn is only supported on BioForest" + }, "crypto": { "keyDerivationFailed": "Key derivation failed", "decryptionFailed": "Decryption failed: wrong password or corrupted data", diff --git a/src/i18n/locales/en/guide.json b/src/i18n/locales/en/guide.json index 146c26922..2a83f3b67 100644 --- a/src/i18n/locales/en/guide.json +++ b/src/i18n/locales/en/guide.json @@ -4,6 +4,7 @@ "next": "Next", "getStarted": "Get Started", "haveWallet": "I have a wallet", + "migrateFromMpay": "Migrate from MPay", "goToSlide": "Go to slide {{number}}", "slides": { "transfer": { diff --git a/src/i18n/locales/en/transaction.json b/src/i18n/locales/en/transaction.json index 89dba09b6..1edff60d5 100644 --- a/src/i18n/locales/en/transaction.json +++ b/src/i18n/locales/en/transaction.json @@ -4,6 +4,11 @@ "amountShouldBe_{min}-{max}-{assettype}": "Amount should be -", "approve": "Approve", "balance_:": "Balance:", + "assetSelector": { + "selectAsset": "Select asset", + "balance": "Balance", + "noAssets": "No assets" + }, "bioforestChainFeeToLow": "If the miner fee is too low, it will affect the transaction on-chain.", "bioforestChainTransactionTypeAcceptAnyAssetExchange": "Accept Any Asset Exchange", "bioforestChainTransactionTypeAcceptAnyAssetGift": "Accept Any Asset Gift", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index ea1846766..7b3fe96b0 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -425,10 +425,14 @@ "openExplorer": "打开 {{name}} 浏览器", "viewOnExplorer": "在 {{name}} 浏览器中查看" }, - "sign": { + "beta": "测试版", + "signSymbol": { "plus": "+", "minus": "-" }, + "walletLock": { + "error": "钱包锁验证失败" + }, "tabs": { "assets": "资产", "history": "交易" @@ -510,9 +514,11 @@ "description": "以太坊虚拟机兼容链" }, "bitcoin": { + "name": "比特币", "description": "Bitcoin 网络" }, "tron": { + "name": "波场", "description": "Tron 网络" }, "custom": { diff --git a/src/i18n/locales/zh-CN/error.json b/src/i18n/locales/zh-CN/error.json index 8270d8bda..19a61f24f 100644 --- a/src/i18n/locales/zh-CN/error.json +++ b/src/i18n/locales/zh-CN/error.json @@ -19,11 +19,14 @@ "enterAmount": "请输入金额", "enterValidAmount": "请输入有效金额", "exceedsBalance": "销毁数量不能大于余额", + "selectAsset": "请选择资产", "enterReceiverAddress": "请输入收款地址", - "invalidAddress": "无效的地址格式" + "invalidAddress": "无效的地址格式", + "unsupportedChainType": "不支持的链类型" }, "transaction": { "failed": "交易失败,请稍后重试", + "transactionFailed": "交易失败,请稍后重试", "burnFailed": "销毁失败,请稍后重试", "chainNotSupported": "该链不支持完整交易流程", "chainNotSupportedWithId": "该链不支持完整交易流程: {{chainId}}", @@ -35,11 +38,16 @@ "issuerAddressNotFound": "无法获取资产发行地址", "issuerAddressNotReady": "资产发行地址未获取", "insufficientGas": "手续费不足", + "invalidAmount": "金额无效", + "paramsIncomplete": "交易参数不完整", "retryLater": "交易失败,请稍后重试", "transferFailed": "转账失败", "securityPasswordWrong": "安全密码错误", "unknownError": "未知错误" }, + "burn": { + "bioforestOnly": "销毁仅支持 BioForest 链" + }, "crypto": { "keyDerivationFailed": "密钥派生失败", "decryptionFailed": "解密失败:密码错误或数据损坏", diff --git a/src/i18n/locales/zh-CN/guide.json b/src/i18n/locales/zh-CN/guide.json index bcb8b7c16..a0b3a26ab 100644 --- a/src/i18n/locales/zh-CN/guide.json +++ b/src/i18n/locales/zh-CN/guide.json @@ -4,6 +4,7 @@ "next": "下一步", "getStarted": "开始使用", "haveWallet": "我有钱包", + "migrateFromMpay": "从 MPay 迁移", "goToSlide": "跳转到第 {{number}} 页", "slides": { "transfer": { diff --git a/src/i18n/locales/zh-CN/transaction.json b/src/i18n/locales/zh-CN/transaction.json index 120099166..613b29ed1 100644 --- a/src/i18n/locales/zh-CN/transaction.json +++ b/src/i18n/locales/zh-CN/transaction.json @@ -10,6 +10,11 @@ "yesterday": "昨天", "daysAgo": "{{days}}天前" }, + "assetSelector": { + "selectAsset": "选择资产", + "balance": "余额", + "noAssets": "暂无资产" + }, "receivePage": { "title": "收款", "scanQrCode": "扫描二维码向此地址转账", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index a79489d45..493469974 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -85,10 +85,14 @@ "useExplorerHint": "此鏈不支援直接查詢交易歷史,請使用瀏覽器查看", "viewOnExplorer": "在 {{name}} 瀏覽器中查看" }, - "sign": { + "beta": "測試版", + "signSymbol": { "plus": "+", "minus": "-" }, + "walletLock": { + "error": "錢包鎖驗證失敗" + }, "addressPlaceholder": "輸入或貼上地址", "advancedEncryptionTechnologyDigitalWealthIsMoreSecure": "先進的加密技術 <br /> 數字財富更安全", "afterConfirmation_{appName}WillDeleteAllLocalDataTips": "確定後, 將刪除所有本地數據,所有錢包將從本地移除,確定退出?", @@ -510,9 +514,11 @@ "description": "以太坊虛擬機兼容鏈" }, "bitcoin": { + "name": "比特幣", "description": "Bitcoin 網絡" }, "tron": { + "name": "波場", "description": "Tron 網絡" }, "custom": { diff --git a/src/i18n/locales/zh-TW/error.json b/src/i18n/locales/zh-TW/error.json index 8854f0437..4c41c2aeb 100644 --- a/src/i18n/locales/zh-TW/error.json +++ b/src/i18n/locales/zh-TW/error.json @@ -19,11 +19,14 @@ "enterAmount": "請輸入金額", "enterValidAmount": "請輸入有效金額", "exceedsBalance": "銷毀數量不能大於餘額", + "selectAsset": "請選擇資產", "enterReceiverAddress": "請輸入收款地址", - "invalidAddress": "無效的地址格式" + "invalidAddress": "無效的地址格式", + "unsupportedChainType": "不支援的鏈類型" }, "transaction": { "failed": "交易失敗,請稍後重試", + "transactionFailed": "交易失敗,請稍後重試", "burnFailed": "銷毀失敗,請稍後重試", "chainNotSupported": "該鏈不支持完整交易流程", "chainNotSupportedWithId": "該鏈不支持完整交易流程: {{chainId}}", @@ -35,11 +38,16 @@ "issuerAddressNotFound": "無法獲取資產發行地址", "issuerAddressNotReady": "資產發行地址未獲取", "insufficientGas": "手續費不足", + "invalidAmount": "金額無效", + "paramsIncomplete": "交易參數不完整", "retryLater": "交易失敗,請稍後重試", "transferFailed": "轉帳失敗", "securityPasswordWrong": "安全密碼錯誤", "unknownError": "未知錯誤" }, + "burn": { + "bioforestOnly": "銷毀僅支援 BioForest 鏈" + }, "crypto": { "keyDerivationFailed": "密鑰派生失敗", "decryptionFailed": "解密失敗:密碼錯誤或資料損壞", diff --git a/src/i18n/locales/zh-TW/guide.json b/src/i18n/locales/zh-TW/guide.json index 07f17f4d7..9195f6f03 100644 --- a/src/i18n/locales/zh-TW/guide.json +++ b/src/i18n/locales/zh-TW/guide.json @@ -4,6 +4,7 @@ "next": "下一步", "getStarted": "開始使用", "haveWallet": "我有錢包", + "migrateFromMpay": "從 MPay 遷移", "goToSlide": "跳轉到第 {{number}} 頁", "slides": { "transfer": { diff --git a/src/i18n/locales/zh-TW/transaction.json b/src/i18n/locales/zh-TW/transaction.json index faa318777..29189625b 100644 --- a/src/i18n/locales/zh-TW/transaction.json +++ b/src/i18n/locales/zh-TW/transaction.json @@ -4,6 +4,11 @@ "amountShouldBe_{min}-{max}-{assettype}": "數量應在 - 之間", "approve": "授權", "balance_:": "餘額:", + "assetSelector": { + "selectAsset": "選擇資產", + "balance": "餘額", + "noAssets": "暫無資產" + }, "bioforestChainFeeToLow": "如果礦工費過低,將影響交易上鏈。", "bioforestChainTransactionTypeAcceptAnyAssetExchange": "接受任意資產交換", "bioforestChainTransactionTypeAcceptAnyAssetGift": "接受任意資產贈送", diff --git a/src/pages/address-transactions/index.tsx b/src/pages/address-transactions/index.tsx index 072212360..6fc3643a1 100644 --- a/src/pages/address-transactions/index.tsx +++ b/src/pages/address-transactions/index.tsx @@ -29,7 +29,7 @@ function TransactionItem({ tx, address }: { tx: Transaction; address: string }) const value = primaryAsset ? primaryAsset.value : '0' const symbol = primaryAsset ? primaryAsset.symbol : '' const decimals = primaryAsset ? primaryAsset.decimals : 0 - const sign = isOutgoing ? t('sign.minus') : t('sign.plus') + const sign = isOutgoing ? t('signSymbol.minus') : t('signSymbol.plus') const directionLabel = isOutgoing ? t('addressLookup.toLabel') : t('addressLookup.fromLabel') return (