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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ 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 测试,避免单次全部运行
pnpm e2e:runner --all --mock -j 2
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
Expand Down Expand Up @@ -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
Expand Down
155 changes: 154 additions & 1 deletion scripts/i18n-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ interface ChineseLiteral {
content: string;
}

interface UsedTranslationKey {
namespace: string;
key: string;
file: string;
line: number;
}

// ==================== Utilities ====================

/**
Expand Down Expand Up @@ -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<string, Set<string>> {
const index = new Map<string, Set<string>>();
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<string, Set<string>>, 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 ====================

/**
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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<string, UsedTranslationKey[]>();
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);
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/ar/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 &lt;br /&gt; 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?",
Expand Down Expand Up @@ -510,9 +514,11 @@
"description": "سلاسل متوافقة مع آلة إيثريوم الافتراضية"
},
"bitcoin": {
"name": "بيتكوين",
"description": "شبكة Bitcoin"
},
"tron": {
"name": "ترون",
"description": "شبكة Tron"
},
"custom": {
Expand Down
10 changes: 9 additions & 1 deletion src/i18n/locales/ar/error.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ar/guide.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"next": "التالي",
"getStarted": "ابدأ الآن",
"haveWallet": "لدي محفظة",
"migrateFromMpay": "الترحيل من MPay",
"goToSlide": "انتقل إلى الشريحة {{number}}",
"slides": {
"transfer": {
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/locales/ar/transaction.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -510,9 +514,11 @@
"description": "Ethereum Virtual Machine compatible chains"
},
"bitcoin": {
"name": "Bitcoin",
"description": "Bitcoin network"
},
"tron": {
"name": "TRON",
"description": "Tron network"
},
"custom": {
Expand Down
10 changes: 9 additions & 1 deletion src/i18n/locales/en/error.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en/guide.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/locales/en/transaction.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,14 @@
"openExplorer": "打开 {{name}} 浏览器",
"viewOnExplorer": "在 {{name}} 浏览器中查看"
},
"sign": {
"beta": "测试版",
"signSymbol": {
"plus": "+",
"minus": "-"
},
"walletLock": {
"error": "钱包锁验证失败"
},
"tabs": {
"assets": "资产",
"history": "交易"
Expand Down Expand Up @@ -510,9 +514,11 @@
"description": "以太坊虚拟机兼容链"
},
"bitcoin": {
"name": "比特币",
"description": "Bitcoin 网络"
},
"tron": {
"name": "波场",
"description": "Tron 网络"
},
"custom": {
Expand Down
Loading