extract-i18n.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. //提取中文到zh.json和en.json文件中,方便网页的中英文切换
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import { fileURLToPath } from 'node:url';
  5. // 基础路径处理
  6. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  7. const args = process.argv.slice(2);
  8. const targetPath = args[0] ? path.resolve(__dirname, '../', args[0]) : null;
  9. // 语言包路径配置
  10. const localesDir = path.resolve(__dirname, '../src/locales');
  11. const zhPath = path.join(localesDir, 'zh.json');
  12. const enPath = path.join(localesDir, 'en.json');
  13. // 初始化语言包数据
  14. let zh = {};
  15. let en = {};
  16. // 读取已有语言包(保留已有翻译)
  17. try {
  18. if (fs.existsSync(zhPath)) {
  19. zh = JSON.parse(fs.readFileSync(zhPath, 'utf-8'));
  20. }
  21. if (fs.existsSync(enPath)) {
  22. en = JSON.parse(fs.readFileSync(enPath, 'utf-8'));
  23. }
  24. } catch (e) {
  25. console.error('⚠️ 语言包解析错误,将使用空对象初始化', e);
  26. zh = {};
  27. en = {};
  28. }
  29. /**
  30. * 递归设置嵌套对象的值(处理键路径)
  31. * @param {Object} obj - 目标对象
  32. * @param {string} keyPath - 键路径(如 "a.b.c")
  33. * @param {string} value - 要设置的值
  34. */
  35. function setNestedValue(obj, keyPath, value) {
  36. const keys = keyPath.split('.');
  37. let current = obj;
  38. for (let i = 0; i < keys.length - 1; i++) {
  39. const key = keys[i];
  40. // 处理键名冲突(已存在字符串值时跳过)
  41. if (typeof current[key] === 'string') {
  42. console.warn(`⚠️ 键名冲突:"${key}" 已作为字符串存在,跳过路径 "${keyPath}"`);
  43. return;
  44. }
  45. // 初始化不存在的键为对象
  46. if (!current[key] || typeof current[key] !== 'object') {
  47. current[key] = {};
  48. }
  49. current = current[key];
  50. }
  51. const lastKey = keys[keys.length - 1];
  52. // 仅添加新键,不覆盖已有值
  53. if (current[lastKey] === undefined) {
  54. current[lastKey] = value;
  55. console.log(`✅ 新增翻译:${keyPath} → ${value}`);
  56. }
  57. }
  58. /**
  59. * 扫描目标路径下的所有Vue和TS文件
  60. * @param {string[]} fileList - 文件列表容器
  61. * @returns {string[]} 扫描到的文件路径列表
  62. */
  63. function scanFiles(fileList = []) {
  64. if (!targetPath) {
  65. // 未指定路径时默认扫描src目录
  66. const scanRoot = path.resolve(__dirname, '../src');
  67. scanDir(scanRoot, fileList);
  68. } else {
  69. const stats = fs.statSync(targetPath);
  70. if (stats.isDirectory()) {
  71. scanDir(targetPath, fileList);
  72. } else if (targetPath.endsWith('.vue') || targetPath.endsWith('.ts')) {
  73. fileList.push(targetPath);
  74. } else {
  75. console.warn(`⚠️ 跳过非Vue/TS文件:${targetPath}`);
  76. }
  77. }
  78. return fileList;
  79. }
  80. /**
  81. * 递归扫描目录中的文件
  82. * @param {string} dir - 目录路径
  83. * @param {string[]} fileList - 文件列表容器
  84. */
  85. function scanDir(dir, fileList) {
  86. const files = fs.readdirSync(dir);
  87. for (const file of files) {
  88. const fullPath = path.join(dir, file);
  89. // 跳过无关目录
  90. if (file.includes('node_modules') || file.includes('dist') || file.includes('.git')) {
  91. continue;
  92. }
  93. const stats = fs.statSync(fullPath);
  94. if (stats.isDirectory()) {
  95. scanDir(fullPath, fileList);
  96. } else if (fullPath.endsWith('.vue') || fullPath.endsWith('.ts')) {
  97. fileList.push(fullPath);
  98. }
  99. }
  100. }
  101. /**
  102. * 从文件内容中提取i18n标记文本
  103. * @param {string} content - 文件内容
  104. * @param {string} filePath - 文件路径(用于错误提示)
  105. */
  106. function extractI18nMarkers(content, filePath) {
  107. // 匹配格式:<!--i18n:key-->中文文本(支持换行和常见标点)
  108. const regex = /<!--i18n:(\S+)-->([\u4e00-\u9fa5\w\s,.,。;;!!??::()()]+?)(?=\r?\n|['"<]|$)/g;
  109. let match;
  110. while ((match = regex.exec(content)) !== null) {
  111. const key = match[1].trim();
  112. const text = match[2].trim().replace(/\s+/g, ' '); // 清理多余空格
  113. if (!key) {
  114. console.warn(`⚠️ 缺少键名(文件:${filePath}):${match[0]}`);
  115. continue;
  116. }
  117. if (!text) {
  118. console.warn(`⚠️ 缺少文本内容(文件:${filePath},键:${key})`);
  119. continue;
  120. }
  121. // 写入语言包
  122. setNestedValue(zh, key, text);
  123. setNestedValue(en, key, en[key] || ''); // 英文保留已有翻译,否则留空
  124. }
  125. }
  126. /**
  127. * 执行提取流程
  128. */
  129. function runExtraction() {
  130. const files = scanFiles();
  131. if (files.length === 0) {
  132. console.log('⚠️ 未找到任何需要处理的Vue/TS文件');
  133. return;
  134. }
  135. // 处理所有扫描到的文件
  136. files.forEach(file => {
  137. try {
  138. const content = fs.readFileSync(file, 'utf-8');
  139. extractI18nMarkers(content, file);
  140. } catch (e) {
  141. console.error(`⚠️ 处理文件失败:${file}`, e);
  142. }
  143. });
  144. // 确保语言包目录存在
  145. if (!fs.existsSync(localesDir)) {
  146. fs.mkdirSync(localesDir, { recursive: true });
  147. }
  148. // 写入语言包文件
  149. fs.writeFileSync(zhPath, JSON.stringify(zh, null, 2), 'utf-8');
  150. fs.writeFileSync(enPath, JSON.stringify(en, null, 2), 'utf-8');
  151. console.log(`\n✅ 提取完成!共处理 ${files.length} 个文件`);
  152. console.log(`📄 中文语言包:${zhPath}`);
  153. console.log(`📄 英文语言包:${enPath}`);
  154. }
  155. // 启动提取
  156. runExtraction();