335 lines
9.2 KiB
JavaScript
335 lines
9.2 KiB
JavaScript
#!/usr/bin/env node
|
||
import { fileURLToPath } from 'url';
|
||
import { dirname, join } from 'path';
|
||
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const rootDir = join(__dirname, '..');
|
||
|
||
// 需要扫描的目录
|
||
const SCAN_DIRS = [
|
||
'packages/web/src',
|
||
'packages/core/src',
|
||
'packages/shared/src'
|
||
];
|
||
|
||
// 需要扫描的文件扩展名
|
||
const FILE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.js', '.jsx'];
|
||
|
||
// 提取 t`...` 的正则表达式
|
||
const T_REGEX = /\bt`([^`]*)`/g;
|
||
|
||
// 检测 t`...` 中包含 ${} 的正则表达式
|
||
const TEMPLATE_LITERAL_IN_T_REGEX = /\bt`([^`]*\$\{[^}]*\}[^`]*)`/g;
|
||
|
||
// 存储所有找到的文本
|
||
const foundTexts = new Set();
|
||
|
||
// 存储发现的问题
|
||
const i18nIssues = [];
|
||
|
||
/**
|
||
* 递归扫描目录获取所有文件
|
||
*/
|
||
function getAllFiles(dir, fileList = []) {
|
||
const fullPath = join(rootDir, dir);
|
||
|
||
if (!existsSync(fullPath)) {
|
||
console.warn(`⚠️ 目录不存在: ${fullPath}`);
|
||
return fileList;
|
||
}
|
||
|
||
const files = readdirSync(fullPath);
|
||
|
||
files.forEach(file => {
|
||
const filePath = join(fullPath, file);
|
||
const stat = statSync(filePath);
|
||
|
||
if (stat.isDirectory()) {
|
||
// 跳过 node_modules, dist, build 等目录
|
||
if (!['node_modules', 'dist', 'build', '.git'].includes(file)) {
|
||
getAllFiles(join(dir, file), fileList);
|
||
}
|
||
} else {
|
||
// 只处理指定扩展名的文件
|
||
if (FILE_EXTENSIONS.some(ext => file.endsWith(ext))) {
|
||
fileList.push(join(dir, file));
|
||
}
|
||
}
|
||
});
|
||
|
||
return fileList;
|
||
}
|
||
|
||
/**
|
||
* 检测 t`...` 中的国际化问题
|
||
*/
|
||
function detectI18nIssues(content, filePath) {
|
||
const issues = [];
|
||
const lines = content.split('\n');
|
||
|
||
lines.forEach((line, lineNumber) => {
|
||
// 跳过注释行
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*') || trimmedLine.startsWith('*')) {
|
||
return;
|
||
}
|
||
|
||
// 检测 t`...` 中包含 ${} 的情况
|
||
let match;
|
||
const regex = new RegExp(TEMPLATE_LITERAL_IN_T_REGEX.source, TEMPLATE_LITERAL_IN_T_REGEX.flags);
|
||
|
||
while ((match = regex.exec(line)) !== null) {
|
||
const text = match[1];
|
||
const fullMatch = match[0];
|
||
|
||
issues.push({
|
||
type: 'template-literal-in-t',
|
||
message: 'Found template literal inside t`...`. Use template literal outside instead.',
|
||
suggestion: 'Move variables outside: `${t`Text`}: ${variable}`',
|
||
text: text,
|
||
fullMatch: fullMatch,
|
||
file: filePath,
|
||
line: lineNumber + 1,
|
||
lineContent: line.trim()
|
||
});
|
||
}
|
||
});
|
||
|
||
return issues;
|
||
}
|
||
|
||
/**
|
||
* 从文件内容中提取 t`...` 文本
|
||
*/
|
||
function extractTextsFromContent(content, filePath) {
|
||
const matches = [];
|
||
const lines = content.split('\n');
|
||
|
||
lines.forEach((line, lineNumber) => {
|
||
// 跳过注释行
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*') || trimmedLine.startsWith('*')) {
|
||
return;
|
||
}
|
||
|
||
// 查找该行中的 t`...` 模式
|
||
let match;
|
||
const regex = new RegExp(T_REGEX.source, T_REGEX.flags);
|
||
|
||
while ((match = regex.exec(line)) !== null) {
|
||
const text = match[1];
|
||
|
||
// 过滤掉明显错误的提取
|
||
if (text.length > 0 && text.length < 100 && // 限制长度
|
||
!text.includes('\n') && !text.includes('\r') && // 不包含换行符
|
||
!text.includes('\\n') && !text.includes('\\r') && // 不包含转义换行符
|
||
!/^\s*$/.test(text) && // 不是纯空白
|
||
!text.includes('function') && // 不包含代码关键字
|
||
!text.includes('console') &&
|
||
!text.includes('return') &&
|
||
text.split(' ').length < 20) { // 单词数量限制
|
||
matches.push({
|
||
text,
|
||
file: filePath,
|
||
line: lineNumber + 1
|
||
});
|
||
foundTexts.add(text);
|
||
}
|
||
}
|
||
});
|
||
|
||
return matches;
|
||
}
|
||
|
||
/**
|
||
* 处理单个文件
|
||
*/
|
||
function processFile(filePath) {
|
||
try {
|
||
const fullPath = join(rootDir, filePath);
|
||
const content = readFileSync(fullPath, 'utf-8');
|
||
|
||
// 提取正常的 i18n 文本
|
||
const matches = extractTextsFromContent(content, filePath);
|
||
|
||
// 检测 i18n 问题
|
||
const issues = detectI18nIssues(content, filePath);
|
||
|
||
if (matches.length > 0) {
|
||
console.log(`📄 ${filePath}: 找到 ${matches.length} 个文本`);
|
||
matches.forEach(match => {
|
||
console.log(` 第${match.line}行: "${match.text}"`);
|
||
});
|
||
}
|
||
|
||
// 添加发现的问题到全局列表
|
||
i18nIssues.push(...issues);
|
||
|
||
return { matches, issues };
|
||
} catch (error) {
|
||
console.error(`❌ 处理文件失败 ${filePath}:`, error.message);
|
||
return { matches: [], issues: [] };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新 i18n.json 文件
|
||
*/
|
||
function updateI18nFile() {
|
||
const i18nPath = join(rootDir, 'packages/shared/src/i18n/i18n.json');
|
||
let existingTranslations = {};
|
||
|
||
// 读取现有翻译
|
||
if (existsSync(i18nPath)) {
|
||
try {
|
||
const content = readFileSync(i18nPath, 'utf-8');
|
||
existingTranslations = JSON.parse(content);
|
||
console.log(`📖 已读取现有翻译文件: ${Object.keys(existingTranslations).length} 条`);
|
||
} catch (error) {
|
||
console.warn(`⚠️ 读取现有翻译文件失败:`, error.message);
|
||
}
|
||
}
|
||
|
||
// 生成新的翻译映射
|
||
const newTranslations = { ...existingTranslations };
|
||
let newCount = 0;
|
||
let updatedCount = 0;
|
||
|
||
Array.from(foundTexts).sort().forEach(text => {
|
||
if (!newTranslations[text]) {
|
||
// 使用新格式:{ "text": {} } - 只提取key,不提供翻译
|
||
newTranslations[text] = {};
|
||
newCount++;
|
||
} else {
|
||
updatedCount++;
|
||
}
|
||
});
|
||
|
||
// 写入文件
|
||
try {
|
||
writeFileSync(i18nPath, JSON.stringify(newTranslations, null, 2), 'utf-8');
|
||
console.log(`\n✅ i18n.json 已更新:`);
|
||
console.log(` 📝 新增条目: ${newCount} 条`);
|
||
console.log(` 🔄 保持现有: ${updatedCount} 条`);
|
||
console.log(` 📊 总计: ${Object.keys(newTranslations).length} 条`);
|
||
console.log(` 📁 文件位置: ${i18nPath}`);
|
||
} catch (error) {
|
||
console.error(`❌ 写入翻译文件失败:`, error.message);
|
||
}
|
||
|
||
return newTranslations;
|
||
}
|
||
|
||
/**
|
||
* 报告发现的 i18n 问题
|
||
*/
|
||
function reportI18nIssues() {
|
||
if (i18nIssues.length === 0) {
|
||
console.log(`\n✅ 未发现 i18n 问题`);
|
||
return;
|
||
}
|
||
|
||
console.log(`\n⚠️ 发现 ${i18nIssues.length} 个 i18n 问题:`);
|
||
console.log(`=`.repeat(60));
|
||
|
||
// 按文件分组显示问题
|
||
const issuesByFile = {};
|
||
i18nIssues.forEach(issue => {
|
||
if (!issuesByFile[issue.file]) {
|
||
issuesByFile[issue.file] = [];
|
||
}
|
||
issuesByFile[issue.file].push(issue);
|
||
});
|
||
|
||
Object.entries(issuesByFile).forEach(([file, issues]) => {
|
||
console.log(`\n📁 ${file}:`);
|
||
issues.forEach(issue => {
|
||
console.log(` 🚫 第${issue.line}行: ${issue.fullMatch}`);
|
||
console.log(` ✨ ${issue.suggestion}`);
|
||
console.log(` 📝 ${issue.message}`);
|
||
console.log(` 📄 ${issue.lineContent}`);
|
||
console.log('');
|
||
});
|
||
});
|
||
|
||
console.log(`\n💡 修复建议:`);
|
||
console.log(` - 模板字符串应该在 t\`...\` 外面使用`);
|
||
console.log(` - 错误写法: t\`Hello \${name}\``);
|
||
console.log(` - 正确写法: \`\${t\`Hello\`} \${name}\``);
|
||
}
|
||
|
||
/**
|
||
* 生成提取报告
|
||
*/
|
||
function generateReport(allMatches) {
|
||
console.log(`\n📊 提取报告:`);
|
||
console.log(`=`.repeat(50));
|
||
|
||
// 按文件统计
|
||
const fileStats = {};
|
||
allMatches.forEach(match => {
|
||
if (!fileStats[match.file]) {
|
||
fileStats[match.file] = 0;
|
||
}
|
||
fileStats[match.file]++;
|
||
});
|
||
|
||
console.log(`\n📁 按文件统计:`);
|
||
Object.entries(fileStats)
|
||
.sort(([,a], [,b]) => b - a)
|
||
.slice(0, 10)
|
||
.forEach(([file, count]) => {
|
||
console.log(` ${file}: ${count} 个`);
|
||
});
|
||
|
||
console.log(`\n✨ 提取完成! 总共找到 ${foundTexts.size} 个唯一文本`);
|
||
}
|
||
|
||
/**
|
||
* 主函数
|
||
*/
|
||
function main() {
|
||
console.log(`🚀 开始提取 i18n 文本...`);
|
||
console.log(`📂 扫描目录: ${SCAN_DIRS.join(', ')}`);
|
||
console.log(`📄 文件类型: ${FILE_EXTENSIONS.join(', ')}`);
|
||
console.log('');
|
||
|
||
// 获取所有需要处理的文件
|
||
let allFiles = [];
|
||
SCAN_DIRS.forEach(dir => {
|
||
const files = getAllFiles(dir);
|
||
allFiles = allFiles.concat(files);
|
||
});
|
||
|
||
console.log(`📋 找到 ${allFiles.length} 个文件\n`);
|
||
|
||
// 处理所有文件
|
||
const allMatches = [];
|
||
allFiles.forEach(file => {
|
||
const result = processFile(file);
|
||
allMatches.push(...result.matches);
|
||
});
|
||
|
||
console.log(`\n🔍 扫描完成! 找到 ${allMatches.length} 个 t\`...\` 用法`);
|
||
console.log(`📝 唯一文本: ${foundTexts.size} 个`);
|
||
|
||
// 报告发现的 i18n 问题
|
||
reportI18nIssues();
|
||
|
||
// 更新翻译文件
|
||
updateI18nFile();
|
||
|
||
// 生成报告
|
||
generateReport(allMatches);
|
||
|
||
// 提供下一步建议
|
||
console.log(`\n📋 下一步建议:`);
|
||
console.log(` 1. 检查生成的 packages/shared/src/i18n/i18n.json 文件`);
|
||
console.log(` 2. 手动添加需要的翻译到每个条目中`);
|
||
console.log(` 3. 格式: { "English Text": { "zh": "中文翻译" } }`);
|
||
console.log(` 4. 测试前端界面的国际化效果`);
|
||
}
|
||
|
||
// 运行脚本
|
||
main(); |