FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

181 lines
5.9 KiB

  1. const fs = require('fs');
  2. const path = require('path');
  3. /**
  4. * 检查指定文件中使用的 t('...') 翻译键是否都在 JSON 文件中定义
  5. */
  6. function checkMissingTranslations(sourceFile, jsonFile) {
  7. // 读取源代码文件
  8. const sourceCode = fs.readFileSync(sourceFile, 'utf-8');
  9. // 读取翻译 JSON 文件
  10. const translations = JSON.parse(fs.readFileSync(jsonFile, 'utf-8'));
  11. // 只匹配 t('...') 和 t("...") 和 t(`...`),不包含模板变量
  12. const tRegex = /\bt\(["`']([^"`'${}]+)["`']\)/g;
  13. const matches = [...sourceCode.matchAll(tRegex)];
  14. // 获取所有使用的键(去重并清理空白)
  15. const usedKeys = [...new Set(
  16. matches
  17. .map(match => match[1].trim())
  18. .filter(key => key.length > 0)
  19. )];
  20. // 查找缺失的键
  21. const missingKeys = usedKeys.filter(key => !(key in translations));
  22. // 查找未使用的键
  23. const definedKeys = Object.keys(translations);
  24. const unusedKeys = definedKeys.filter(key => !usedKeys.includes(key));
  25. return {
  26. usedKeys: usedKeys.sort(),
  27. missingKeys: missingKeys.sort(),
  28. unusedKeys: unusedKeys.sort(),
  29. totalUsed: usedKeys.length,
  30. totalMissing: missingKeys.length,
  31. totalUnused: unusedKeys.length,
  32. totalDefined: definedKeys.length
  33. };
  34. }
  35. /**
  36. * 递归检查目录中所有文件
  37. */
  38. function checkDirectory(dir, jsonFile) {
  39. const results = {};
  40. let totalMissing = 0;
  41. function scanDir(directory) {
  42. const files = fs.readdirSync(directory);
  43. files.forEach(file => {
  44. const fullPath = path.join(directory, file);
  45. const stat = fs.statSync(fullPath);
  46. if (stat.isDirectory()) {
  47. scanDir(fullPath);
  48. } else if (file.endsWith('.tsx') || file.endsWith('.ts')) {
  49. try {
  50. const result = checkMissingTranslations(fullPath, jsonFile);
  51. if (result.missingKeys.length > 0) {
  52. results[fullPath] = result;
  53. totalMissing += result.missingKeys.length;
  54. }
  55. } catch (err) {
  56. console.error(`❌ 处理文件出错 ${fullPath}:`, err.message);
  57. }
  58. }
  59. });
  60. }
  61. scanDir(dir);
  62. return { results, totalMissing };
  63. }
  64. // 主程序
  65. const args = process.argv.slice(2);
  66. if (args.length === 0) {
  67. console.log('📚 翻译键检查工具\n');
  68. console.log('用法:');
  69. console.log(' 检查单个文件: node check-translations.js <source-file> <json-file>');
  70. console.log(' 检查整个目录: node check-translations.js --dir <directory> <json-file>');
  71. console.log('\n示例:');
  72. console.log(' node check-translations.js src/components/Jodetail/JodetailSearch.tsx src/i18n/zh/jo.json');
  73. console.log(' node check-translations.js --dir src/components/Jodetail src/i18n/zh/jo.json');
  74. console.log('\n注意:');
  75. console.log(' 只检查 t("key") 调用');
  76. console.log(' ❌ 忽略 alert(), console.log() 等普通字符串');
  77. console.log(' ❌ 忽略模板字符串中的 ${} 变量部分');
  78. process.exit(0);
  79. }
  80. if (args[0] === '--dir') {
  81. // 检查整个目录
  82. const directory = args[1];
  83. const jsonFile = args[2];
  84. console.log(`\n🔍 正在检查目录: ${directory}`);
  85. console.log(`📖 使用翻译文件: ${jsonFile}\n`);
  86. const { results, totalMissing } = checkDirectory(directory, jsonFile);
  87. if (Object.keys(results).length === 0) {
  88. console.log(' 太棒了!没有发现缺失的翻译键!');
  89. } else {
  90. console.log(`⚠️ 发现 ${Object.keys(results).length} 个文件有缺失的翻译键\n`);
  91. // 收集所有缺失的键(去重)
  92. const allMissingKeys = new Set();
  93. Object.entries(results).forEach(([file, result]) => {
  94. const relativePath = file.replace(process.cwd(), '').replace(/\\/g, '/');
  95. console.log(`\n📄 ${relativePath}`);
  96. console.log(` 使用: ${result.totalUsed} 个键 | ❌ 缺失: ${result.totalMissing} 个键`);
  97. result.missingKeys.forEach(key => {
  98. console.log(` - "${key}"`);
  99. allMissingKeys.add(key);
  100. });
  101. });
  102. // 输出可以直接复制的 JSON 格式
  103. console.log(`\n\n📋 需要添加到 ${jsonFile} 的翻译键 (共 ${allMissingKeys.size} 个):`);
  104. console.log('─'.repeat(60));
  105. allMissingKeys.forEach(key => {
  106. console.log(` "${key}": "",`);
  107. });
  108. console.log('─'.repeat(60));
  109. }
  110. } else {
  111. // 检查单个文件
  112. const sourceFile = args[0];
  113. const jsonFile = args[1];
  114. console.log(`\n🔍 正在检查文件: ${sourceFile}`);
  115. console.log(`📖 使用翻译文件: ${jsonFile}\n`);
  116. try {
  117. const result = checkMissingTranslations(sourceFile, jsonFile);
  118. console.log(`📊 统计信息:`);
  119. console.log(` 代码中使用的键: ${result.totalUsed}`);
  120. console.log(` JSON 中定义的键: ${result.totalDefined}`);
  121. console.log(` ❌ 缺失的键: ${result.totalMissing}`);
  122. console.log(` ⚠️ 未使用的键: ${result.totalUnused}`);
  123. if (result.missingKeys.length > 0) {
  124. console.log(`\n❌ 缺失的翻译键 (需要添加到 ${jsonFile}):`);
  125. console.log('─'.repeat(60));
  126. result.missingKeys.forEach(key => {
  127. console.log(` "${key}": "",`);
  128. });
  129. console.log('─'.repeat(60));
  130. } else {
  131. console.log('\n 太棒了!所有使用的翻译键都已定义!');
  132. }
  133. if (result.unusedKeys.length > 0 && result.unusedKeys.length <= 20) {
  134. console.log(`\n⚠️ 未使用的翻译键 (在 JSON 中但代码中未使用):`);
  135. result.unusedKeys.forEach(key => {
  136. console.log(` - "${key}"`);
  137. });
  138. }
  139. } catch (err) {
  140. console.error('❌ 错误:', err.message);
  141. process.exit(1);
  142. }
  143. }
  144. console.log('\n');
  145. {/*
  146. # 检查单个文件
  147. node check-translations.js src/components/Jodetail/JodetailSearch.tsx src/i18n/zh/jo.json
  148. # 检查整个 Jodetail 目录
  149. node check-translations.js --dir src/components/Jodetail src/i18n/zh/jo.json
  150. # 检查所有组件
  151. node check-translations.js --dir src/components src/i18n/zh/jo.json
  152. */}