From 14e35c7667a1f4ad54911fecbd3471482d021b29 Mon Sep 17 00:00:00 2001 From: alger Date: Wed, 4 Feb 2026 20:09:11 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=A2=9E=E5=8A=A0=20i18n=20=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E8=84=9A=E6=9C=AC=E4=B8=8E=E6=8F=90=E4=BA=A4=E9=92=A9?= =?UTF-8?q?=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++ .husky/pre-commit | 3 + package.json | 6 +- scripts/check_i18n.ts | 103 +++++++++++++++++++++++++++ scripts/export_i18n_report.ts | 130 ++++++++++++++++++++++++++++++++++ 5 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 scripts/check_i18n.ts create mode 100644 scripts/export_i18n_report.ts diff --git a/.gitignore b/.gitignore index ec206c2..8f256f9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,11 @@ android/app/release .cursor .windsurf .agent +.claude +CLAUDE.md +AGENTS.md +.sisyphus + .auto-imports.d.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 6b7a3b7..701541a 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -4,4 +4,7 @@ npx lint-staged echo "运行类型检查..." npm run typecheck +echo "运行国际化检查..." +npm run lint:i18n + echo "所有检查通过,准备提交..." \ No newline at end of file diff --git a/package.json b/package.json index 6658237..7cb7b89 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "scripts": { "prepare": "husky", "format": "prettier --write ./src", - "lint": "eslint ./src --fix", + "lint": "eslint ./src --fix && npm run lint:i18n", + "lint:i18n": "bun scripts/check_i18n.ts", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "npm run typecheck:node && npm run typecheck:web", @@ -52,7 +53,6 @@ "node-id3": "^0.2.9", "node-machine-id": "^1.1.12", "pinia-plugin-persistedstate": "^4.7.1", - "sharp": "^0.34.5", "vue-i18n": "^11.2.2" }, "devDependencies": { @@ -78,7 +78,7 @@ "autoprefixer": "^10.4.22", "axios": "^1.13.2", "cross-env": "^7.0.3", - "electron": "^39.2.7", + "electron": "^40.1.0", "electron-builder": "^26.0.12", "electron-vite": "^5.0.0", "eslint": "^9.39.2", diff --git a/scripts/check_i18n.ts b/scripts/check_i18n.ts new file mode 100644 index 0000000..67f8c1c --- /dev/null +++ b/scripts/check_i18n.ts @@ -0,0 +1,103 @@ +import fs from 'fs'; +import path from 'path'; + +async function main() { + const langDir = path.join(process.cwd(), 'src/i18n/lang'); + const sourceLang = 'zh-CN'; + const targetLangs = ['en-US', 'ja-JP', 'ko-KR', 'zh-Hant']; + + const sourcePath = path.join(langDir, sourceLang); + const files = fs.readdirSync(sourcePath).filter((f) => f.endsWith('.ts')); + + function getFlatObject(obj: any, prefix = ''): Record { + const result: Record = {}; + for (const key in obj) { + const val = obj[key]; + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof val === 'object' && val !== null && !Array.isArray(val)) { + Object.assign(result, getFlatObject(val, fullKey)); + } else { + result[fullKey] = String(val); + } + } + return result; + } + + const report: any = {}; + let hasMissing = false; + + for (const fileName of files) { + // Dynamic import might be tricky with ESM/TS in Bun without proper setup + // We'll read the file and extract the export default object using a simple regex/eval approach + // or just use Bun.file and eval if it's safe enough for this tool. + + const getContent = async (filePath: string) => { + if (!fs.existsSync(filePath)) return null; + const content = fs.readFileSync(filePath, 'utf-8'); + // Simple extraction of the default export object + const match = content.match(/export\s+default\s+([\s\S]+);/); + if (!match) return null; + try { + // This is a bit hacky but works for simple object literals in i18n files + // We replace potential comments and import statements if they existed, + // but here it's mostly just a JS object. + const objStr = match[1].trim(); + // Remove trailing comma if it exists before closing brace + // objStr = objStr.replace(/,\s*}/g, '}'); + + // Use a more robust way: wrap in () and eval + // Note: this assumes the i18n files don't have complex logic or external imports + return eval(`(${objStr})`); + } catch (e) { + console.error(`Error parsing ${filePath}:`, e); + return null; + } + }; + + const sourceObj = await getContent(path.join(sourcePath, fileName)); + if (!sourceObj) continue; + const sourceKeysMap = getFlatObject(sourceObj); + const sourceKeys = Object.keys(sourceKeysMap); + + for (const lang of targetLangs) { + if (!report[lang]) report[lang] = {}; + + const targetFilePath = path.join(langDir, lang, fileName); + const targetObj = await getContent(targetFilePath); + + const targetKeysMap = targetObj ? getFlatObject(targetObj) : {}; + const targetKeys = Object.keys(targetKeysMap); + + const missing = sourceKeys.filter((k) => !targetKeys.includes(k)); + + if (missing.length > 0) { + hasMissing = true; + report[lang][fileName] = missing.map((k) => ({ + key: k, + zh: sourceKeysMap[k] + })); + } + } + } + + if (hasMissing) { + console.error('发现国际化键值缺失:'); + for (const lang in report) { + const files = report[lang]; + if (Object.keys(files).length === 0) continue; + console.error(`\n语言: ${lang}`); + for (const fileName in files) { + console.error(` 文件: ${fileName}`); + files[fileName].forEach((item: any) => { + console.error(` - [${item.key}]: ${item.zh}`); + }); + } + } + process.exit(1); + } else { + console.log('所有国际化键值检查通过!'); + process.exit(0); + } +} + +main(); diff --git a/scripts/export_i18n_report.ts b/scripts/export_i18n_report.ts new file mode 100644 index 0000000..c12969d --- /dev/null +++ b/scripts/export_i18n_report.ts @@ -0,0 +1,130 @@ +import fs from 'fs'; +import path from 'path'; + +async function main() { + const rootDir = process.cwd(); + const langDir = path.join(rootDir, 'src/i18n/lang/zh-CN'); + + const definedKeys = new Set(); + const langFiles = fs.readdirSync(langDir).filter((f) => f.endsWith('.ts')); + + function getKeys(obj: any, prefix = '') { + for (const key in obj) { + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + getKeys(obj[key], fullKey); + } else { + definedKeys.add(fullKey); + } + } + } + + for (const file of langFiles) { + const content = fs.readFileSync(path.join(langDir, file), 'utf-8'); + const match = content.match(/export\s+default\s+([\s\S]+);/); + if (match) { + try { + const obj = eval(`(${match[1]})`); + getKeys(obj, file.replace('.ts', '')); + } catch (error) { + console.warn('Failed to parse i18n file:', file, error); + } + } + } + + // @ts-ignore + const glob = new Bun.Glob('src/renderer/**/*.{vue,ts,js}'); + // @ts-ignore + const files = Array.from( + glob.scanSync({ + cwd: rootDir, + onlyFiles: true + }) + ); + + const report = { + hardcodedChinese: [] as any[], + missingKeys: [] as any[] + }; + + const chineseMatchRegex = /[\u4e00-\u9fa5]+/g; + const i18nRegex = /\bt\(['"]([^'"]+)['"]\)/g; + + for (const relativeFile of files) { + const rel = relativeFile as string; + if ( + rel.includes('node_modules') || + rel.includes('android/') || + rel.includes('resources/') || + rel.includes('scripts/') || + rel.endsWith('.d.ts') + ) + continue; + + const file = path.join(rootDir, rel); + let content = fs.readFileSync(file, 'utf-8'); + + content = content.replace(/\/\*[\s\S]*?\*\//g, (match) => { + const lines = match.split('\n').length - 1; + return '\n'.repeat(lines); + }); + + content = content.replace(//g, (match) => { + const lines = match.split('\n').length - 1; + return '\n'.repeat(lines); + }); + + const lines = content.split('\n'); + let isInConsole = false; + + lines.forEach((line, index) => { + const lineNumber = index + 1; + const cleanLine = line.split('//')[0]; + + if (cleanLine.includes('console.')) { + isInConsole = true; + } + + if (!isInConsole && !cleanLine.includes('import')) { + const chineseMatches = cleanLine.match(chineseMatchRegex); + if (chineseMatches) { + chineseMatches.forEach((text) => { + report.hardcodedChinese.push({ + file: rel, + line: lineNumber, + text: text.trim(), + context: line.trim() + }); + }); + } + } + + if (isInConsole && cleanLine.includes(');')) { + isInConsole = false; + } + + let i18nMatch; + while ((i18nMatch = i18nRegex.exec(cleanLine)) !== null) { + const key = i18nMatch[1]; + if (!definedKeys.has(key)) { + report.missingKeys.push({ + file: rel, + line: lineNumber, + key: key, + context: line.trim() + }); + } + } + }); + } + + const outputPath = path.join(rootDir, 'i18n_report.json'); + fs.writeFileSync(outputPath, JSON.stringify(report, null, 2)); + + console.log(`\n报告生成成功!`); + console.log(`- 硬编码中文: ${report.hardcodedChinese.length} 处`); + console.log(`- 缺失的 Key: ${report.missingKeys.length} 处`); + console.log(`- 报告路径: ${outputPath}\n`); +} + +main();