//提取中文到zh.json和en.json文件中,方便网页的中英文切换 import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; // 基础路径处理 const __dirname = path.dirname(fileURLToPath(import.meta.url)); const args = process.argv.slice(2); const targetPath = args[0] ? path.resolve(__dirname, '../', args[0]) : null; // 语言包路径配置 const localesDir = path.resolve(__dirname, '../src/locales'); const zhPath = path.join(localesDir, 'zh.json'); const enPath = path.join(localesDir, 'en.json'); // 初始化语言包数据 let zh = {}; let en = {}; // 读取已有语言包(保留已有翻译) try { if (fs.existsSync(zhPath)) { zh = JSON.parse(fs.readFileSync(zhPath, 'utf-8')); } if (fs.existsSync(enPath)) { en = JSON.parse(fs.readFileSync(enPath, 'utf-8')); } } catch (e) { console.error('⚠️ 语言包解析错误,将使用空对象初始化', e); zh = {}; en = {}; } /** * 递归设置嵌套对象的值(处理键路径) * @param {Object} obj - 目标对象 * @param {string} keyPath - 键路径(如 "a.b.c") * @param {string} value - 要设置的值 */ function setNestedValue(obj, keyPath, value) { const keys = keyPath.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; // 处理键名冲突(已存在字符串值时跳过) if (typeof current[key] === 'string') { console.warn(`⚠️ 键名冲突:"${key}" 已作为字符串存在,跳过路径 "${keyPath}"`); return; } // 初始化不存在的键为对象 if (!current[key] || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } const lastKey = keys[keys.length - 1]; // 仅添加新键,不覆盖已有值 if (current[lastKey] === undefined) { current[lastKey] = value; console.log(`✅ 新增翻译:${keyPath} → ${value}`); } } /** * 扫描目标路径下的所有Vue和TS文件 * @param {string[]} fileList - 文件列表容器 * @returns {string[]} 扫描到的文件路径列表 */ function scanFiles(fileList = []) { if (!targetPath) { // 未指定路径时默认扫描src目录 const scanRoot = path.resolve(__dirname, '../src'); scanDir(scanRoot, fileList); } else { const stats = fs.statSync(targetPath); if (stats.isDirectory()) { scanDir(targetPath, fileList); } else if (targetPath.endsWith('.vue') || targetPath.endsWith('.ts')) { fileList.push(targetPath); } else { console.warn(`⚠️ 跳过非Vue/TS文件:${targetPath}`); } } return fileList; } /** * 递归扫描目录中的文件 * @param {string} dir - 目录路径 * @param {string[]} fileList - 文件列表容器 */ function scanDir(dir, fileList) { const files = fs.readdirSync(dir); for (const file of files) { const fullPath = path.join(dir, file); // 跳过无关目录 if (file.includes('node_modules') || file.includes('dist') || file.includes('.git')) { continue; } const stats = fs.statSync(fullPath); if (stats.isDirectory()) { scanDir(fullPath, fileList); } else if (fullPath.endsWith('.vue') || fullPath.endsWith('.ts')) { fileList.push(fullPath); } } } /** * 从文件内容中提取i18n标记文本 * @param {string} content - 文件内容 * @param {string} filePath - 文件路径(用于错误提示) */ function extractI18nMarkers(content, filePath) { // 匹配格式:中文文本(支持换行和常见标点) const regex = /([\u4e00-\u9fa5\w\s,.,。;;!!??::()()]+?)(?=\r?\n|['"<]|$)/g; let match; while ((match = regex.exec(content)) !== null) { const key = match[1].trim(); const text = match[2].trim().replace(/\s+/g, ' '); // 清理多余空格 if (!key) { console.warn(`⚠️ 缺少键名(文件:${filePath}):${match[0]}`); continue; } if (!text) { console.warn(`⚠️ 缺少文本内容(文件:${filePath},键:${key})`); continue; } // 写入语言包 setNestedValue(zh, key, text); setNestedValue(en, key, en[key] || ''); // 英文保留已有翻译,否则留空 } } /** * 执行提取流程 */ function runExtraction() { const files = scanFiles(); if (files.length === 0) { console.log('⚠️ 未找到任何需要处理的Vue/TS文件'); return; } // 处理所有扫描到的文件 files.forEach(file => { try { const content = fs.readFileSync(file, 'utf-8'); extractI18nMarkers(content, file); } catch (e) { console.error(`⚠️ 处理文件失败:${file}`, e); } }); // 确保语言包目录存在 if (!fs.existsSync(localesDir)) { fs.mkdirSync(localesDir, { recursive: true }); } // 写入语言包文件 fs.writeFileSync(zhPath, JSON.stringify(zh, null, 2), 'utf-8'); fs.writeFileSync(enPath, JSON.stringify(en, null, 2), 'utf-8'); console.log(`\n✅ 提取完成!共处理 ${files.length} 个文件`); console.log(`📄 中文语言包:${zhPath}`); console.log(`📄 英文语言包:${enPath}`); } // 启动提取 runExtraction();