chore: remove all cn comments (#277)

This commit is contained in:
tecvan
2025-07-30 14:52:35 +08:00
committed by GitHub
parent 875e97a40d
commit f93f26fc48
26 changed files with 5109 additions and 0 deletions

View File

@@ -0,0 +1,470 @@
import {
SourceFile,
ChineseComment,
ParsedComment,
FileWithComments,
CommentType,
MultiLineContext
} from '../types/index';
import { getCommentPatterns } from '../utils/language';
import { containsChinese, cleanCommentText } from '../utils/chinese';
/**
* 检查指定位置是否在字符串字面量内部
*/
const isInsideStringLiteral = (line: string, position: number): boolean => {
let insideDoubleQuote = false;
let insideSingleQuote = false;
let insideBacktick = false;
let escapeNext = false;
for (let i = 0; i < position; i++) {
const char = line[i];
if (escapeNext) {
escapeNext = false;
continue;
}
if (char === '\\') {
escapeNext = true;
continue;
}
if (char === '"' && !insideSingleQuote && !insideBacktick) {
insideDoubleQuote = !insideDoubleQuote;
} else if (char === "'" && !insideDoubleQuote && !insideBacktick) {
insideSingleQuote = !insideSingleQuote;
} else if (char === '`' && !insideDoubleQuote && !insideSingleQuote) {
insideBacktick = !insideBacktick;
}
}
return insideDoubleQuote || insideSingleQuote || insideBacktick;
};
/**
* 解析单行注释
*/
const parseSingleLineComments = (
content: string,
pattern: RegExp,
language?: string,
): ParsedComment[] => {
const comments: ParsedComment[] = [];
const lines = content.split('\n');
// 添加安全检查
const maxLines = 5000; // 降低到5000行
if (lines.length > maxLines) {
console.warn(`⚠️ 文件行数过多 (${lines.length}行),跳过单行注释解析`);
return comments;
}
lines.forEach((line, index) => {
pattern.lastIndex = 0; // 重置正则表达式索引
let match: RegExpExecArray | null;
// 查找所有匹配,但只保留不在字符串内的
let matchCount = 0;
const maxMatches = 100; // 限制每行最多匹配100次
let lastIndex = 0;
while ((match = pattern.exec(line)) !== null) {
// 防止无限循环的多重保护
matchCount++;
if (matchCount > maxMatches) {
console.warn(`⚠️ 单行匹配次数过多,中断处理: ${line.substring(0, 50)}...`);
break;
}
// 检查 lastIndex 是否前进,防止无限循环
if (pattern.global) {
if (pattern.lastIndex <= lastIndex) {
// 如果 lastIndex 没有前进,手动前进一位避免无限循环
pattern.lastIndex = lastIndex + 1;
if (pattern.lastIndex >= line.length) {
break;
}
}
lastIndex = pattern.lastIndex;
}
if (match[1]) {
const commentContent = match[1];
let commentStartIndex = match.index!;
let commentLength = 2; // 默认为 //
// 根据语言确定注释符号
if (
language === 'yaml' ||
language === 'toml' ||
language === 'shell' ||
language === 'python' ||
language === 'ruby'
) {
commentStartIndex = line.indexOf('#', match.index!);
commentLength = 1; // # 长度为 1
} else if (language === 'ini') {
// INI 文件可能使用 # 或 ;
const hashIndex = line.indexOf('#', match.index!);
const semicolonIndex = line.indexOf(';', match.index!);
if (
hashIndex >= 0 &&
(semicolonIndex < 0 || hashIndex < semicolonIndex)
) {
commentStartIndex = hashIndex;
commentLength = 1;
} else if (semicolonIndex >= 0) {
commentStartIndex = semicolonIndex;
commentLength = 1;
}
} else if (language === 'php') {
// PHP 可能使用 // 或 #
const slashIndex = line.indexOf('//', match.index!);
const hashIndex = line.indexOf('#', match.index!);
if (slashIndex >= 0 && (hashIndex < 0 || slashIndex < hashIndex)) {
commentStartIndex = slashIndex;
commentLength = 2;
} else if (hashIndex >= 0) {
commentStartIndex = hashIndex;
commentLength = 1;
}
} else {
// JavaScript/TypeScript/Go/Java/C/C++/C# style
commentStartIndex = line.indexOf('//', match.index!);
commentLength = 2;
}
const startColumn = commentStartIndex;
const endColumn = startColumn + commentLength + commentContent.length;
// 检查注释开始位置是否在字符串内部
if (
commentStartIndex >= 0 &&
!isInsideStringLiteral(line, commentStartIndex)
) {
comments.push({
content: commentContent,
startLine: index + 1,
endLine: index + 1,
startColumn,
endColumn,
type: 'single-line',
});
}
}
// 防止无限循环
if (!pattern.global) break;
}
});
return comments;
};
/**
* 解析多行注释
*/
const parseMultiLineComments = (
content: string,
startPattern: RegExp,
endPattern: RegExp,
): ParsedComment[] => {
const comments: ParsedComment[] = [];
const lines = content.split('\n');
let inComment = false;
let commentStart: { line: number; column: number } | null = null;
let commentLines: string[] = [];
// 添加安全检查
const maxLines = 5000; // 降低到5000行
if (lines.length > maxLines) {
console.warn(`⚠️ 文件行数过多 (${lines.length}行),跳过多行注释解析`);
return comments;
}
// 添加处理计数器,防止无限循环
let processedLines = 0;
const maxProcessedLines = 10000;
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
const line = lines[lineIndex];
// 防止无限处理
processedLines++;
if (processedLines > maxProcessedLines) {
console.warn(`⚠️ 处理行数超限,中断解析`);
break;
}
if (!inComment) {
startPattern.lastIndex = 0;
const startMatch = startPattern.exec(line);
if (startMatch && !isInsideStringLiteral(line, startMatch.index!)) {
inComment = true;
commentStart = { line: lineIndex + 1, column: startMatch.index! };
// 检查是否在同一行结束
endPattern.lastIndex = startMatch.index! + startMatch[0].length;
const endMatch = endPattern.exec(line);
if (endMatch) {
// 单行多行注释
const commentContent = line.substring(
startMatch.index! + startMatch[0].length,
endMatch.index!,
);
comments.push({
content: commentContent,
startLine: lineIndex + 1,
endLine: lineIndex + 1,
startColumn: startMatch.index!,
endColumn: endMatch.index! + endMatch[0].length,
type: 'multi-line',
});
inComment = false;
commentStart = null;
} else {
// 多行注释开始
const commentContent = line.substring(
startMatch.index! + startMatch[0].length,
);
commentLines = [commentContent];
}
}
} else {
// 在多行注释中
endPattern.lastIndex = 0;
const endMatch = endPattern.exec(line);
if (endMatch) {
// 多行注释结束
const commentContent = line.substring(0, endMatch.index!);
commentLines.push(commentContent);
comments.push({
content: commentLines.join('\n'),
startLine: commentStart!.line,
endLine: lineIndex + 1,
startColumn: commentStart!.column,
endColumn: endMatch.index! + endMatch[0].length,
type: 'multi-line',
});
inComment = false;
commentStart = null;
commentLines = [];
} else {
// 继续多行注释
commentLines.push(line);
}
}
}
return comments;
};
/**
* 解析文件中的所有注释
*/
export const parseComments = (file: SourceFile): ParsedComment[] => {
const patterns = getCommentPatterns(file.language);
if (!patterns) return [];
const singleLineComments = parseSingleLineComments(
file.content,
patterns.single,
file.language,
);
const multiLineComments = parseMultiLineComments(
file.content,
patterns.multiStart,
patterns.multiEnd,
);
return [...singleLineComments, ...multiLineComments];
};
/**
* 过滤包含中文的注释,对多行注释进行逐行处理
*/
export const filterChineseComments = (
comments: ParsedComment[],
language?: string,
): ChineseComment[] => {
const result: ChineseComment[] = [];
for (const comment of comments) {
if (comment.type === 'multi-line' && comment.content.includes('\n')) {
// 多行注释:逐行处理
const multiLineResults = processMultiLineCommentForChinese(comment, language);
result.push(...multiLineResults);
} else if (containsChinese(comment.content)) {
// 单行注释或单行多行注释
result.push({
...comment,
content: cleanCommentText(
comment.content,
comment.type === 'documentation' ? 'multi-line' : comment.type,
language,
),
});
}
}
return result;
};
/**
* 处理多行注释,提取含中文的行作为独立的注释单元
*/
const processMultiLineCommentForChinese = (
comment: ParsedComment,
language?: string,
): ChineseComment[] => {
const lines = comment.content.split('\n');
const result: ChineseComment[] = [];
lines.forEach((line, lineIndex) => {
const cleanedLine = cleanCommentText(line, 'multi-line', language);
if (containsChinese(cleanedLine)) {
// 计算这一行在原始文件中的位置
const actualLineNumber = comment.startLine + lineIndex;
// 创建一个表示这一行的注释对象
const lineComment: ChineseComment = {
content: cleanedLine,
startLine: actualLineNumber,
endLine: actualLineNumber,
startColumn: 0, // 这个值需要更精确计算但对于多行注释内的行处理暂时用0
endColumn: line.length,
type: 'multi-line',
// 添加多行注释的元数据,用于后续处理
multiLineContext: {
isPartOfMultiLine: true,
originalComment: comment,
lineIndexInComment: lineIndex,
totalLinesInComment: lines.length
}
};
result.push(lineComment);
}
});
return result;
};
/**
* 检测文件中的中文注释
*/
export const detectChineseInFile = (file: SourceFile): ChineseComment[] => {
try {
// 简单防护:跳过大文件
if (file.content.length > 500000) {
// 500KB
console.warn(
`⚠️ 跳过大文件: ${file.path} (${file.content.length} 字符)`,
);
return [];
}
// 简单防护:跳过行数过多的文件
const lines = file.content.split('\n');
if (lines.length > 10000) {
console.warn(`⚠️ 跳过多行文件: ${file.path} (${lines.length} 行)`);
return [];
}
const allComments = parseComments(file);
return filterChineseComments(allComments, file.language);
} catch (error) {
console.error(`❌ 文件处理失败: ${file.path} - ${error}`);
return [];
}
};
/**
* 批量检测多个文件中的中文注释
*/
export const detectChineseInFiles = (files: SourceFile[]): FileWithComments[] => {
const results: FileWithComments[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileName = file.path.split('/').pop() || file.path;
console.log(`🔍 检测进度: ${i + 1}/${files.length} (当前: ${fileName})`);
try {
const chineseComments = detectChineseInFile(file);
if (chineseComments.length > 0) {
results.push({
file,
chineseComments,
});
}
console.log(
`✅ 完成: ${fileName} (找到 ${chineseComments.length} 条中文注释)`,
);
} catch (error) {
console.error(`❌ 处理文件失败: ${fileName} - ${error}`);
// 继续处理其他文件
continue;
}
}
return results;
};
/**
* 获取注释统计信息
*/
export const getCommentStats = (files: SourceFile[]): {
totalFiles: number;
filesWithComments: number;
totalComments: number;
chineseComments: number;
commentsByType: Record<CommentType, number>;
} => {
let totalComments = 0;
let chineseComments = 0;
let filesWithComments = 0;
const commentsByType: Record<CommentType, number> = {
'single-line': 0,
'multi-line': 0,
'documentation': 0
};
files.forEach(file => {
const allComments = parseComments(file);
const chineseCommentsInFile = filterChineseComments(allComments, file.language);
if (chineseCommentsInFile.length > 0) {
filesWithComments++;
}
totalComments += allComments.length;
chineseComments += chineseCommentsInFile.length;
chineseCommentsInFile.forEach(comment => {
commentsByType[comment.type]++;
});
});
return {
totalFiles: files.length,
filesWithComments,
totalComments,
chineseComments,
commentsByType
};
};

View File

@@ -0,0 +1,421 @@
import {
Replacement,
ReplacementOperation,
SourceFile,
ChineseComment,
TranslationResult,
} from '../types/index';
import { tryCatch } from '../utils/fp';
/**
* 检查字符串是否包含中文字符
*/
const containsChinese = (text: string): boolean => {
return /[\u4e00-\u9fff]/.test(text);
};
/**
* 保持注释的原始格式,支持逐行翻译多行注释
*/
export const preserveCommentFormat = (
originalComment: string,
translatedComment: string,
commentType: 'single-line' | 'multi-line',
): string => {
if (commentType === 'single-line') {
// 保持单行注释的前缀空格和注释符 - 支持多种语言
let match = originalComment.match(/^(\s*\/\/\s*)/); // JavaScript/TypeScript style
if (match) {
return match[1] + translatedComment.trim();
}
match = originalComment.match(/^(\s*#\s*)/); // Shell/Python/YAML style
if (match) {
return match[1] + translatedComment.trim();
}
match = originalComment.match(/^(\s*;\s*)/); // Some config files
if (match) {
return match[1] + translatedComment.trim();
}
// 如果无法识别,尝试从原始内容推断
if (originalComment.includes('#')) {
const hashMatch = originalComment.match(/^(\s*#\s*)/);
return (hashMatch ? hashMatch[1] : '# ') + translatedComment.trim();
}
// 默认使用 JavaScript 风格
return '// ' + translatedComment.trim();
}
if (commentType === 'multi-line') {
const lines = originalComment.split('\n');
if (lines.length === 1) {
// 单行多行注释 /* ... */ 或 /** ... */
const startMatch = originalComment.match(/^(\s*\/\*\*?\s*)/);
const endMatch = originalComment.match(/(\s*\*\/\s*)$/);
let prefix = '/* ';
let suffix = ' */';
if (startMatch) {
prefix = startMatch[1];
}
if (endMatch) {
suffix = endMatch[1];
}
return prefix + translatedComment.trim() + suffix;
} else {
// 多行注释 - 需要逐行处理
return processMultiLineComment(originalComment, translatedComment);
}
}
return translatedComment;
};
/**
* 处理多行注释,逐行翻译含中文的行,保持其他行原样
*/
export const processMultiLineComment = (
originalComment: string,
translatedContent: string,
): string => {
const originalLines = originalComment.split('\n');
// 提取每行的注释内容(去除 /** * 等前缀)
const extractedLines = originalLines.map(line => {
// 匹配不同类型的注释行
if (line.match(/^\s*\/\*\*?\s*/)) {
// 开始行: /** 或 /*
return { prefix: line.match(/^\s*\/\*\*?\s*/)![0], content: line.replace(/^\s*\/\*\*?\s*/, '') };
} else if (line.match(/^\s*\*\/\s*$/)) {
// 结束行: */
return { prefix: line.match(/^\s*\*\/\s*$/)![0], content: '' };
} else if (line.match(/^\s*\*\s*/)) {
// 中间行: * content
const match = line.match(/^(\s*\*\s*)(.*)/);
return { prefix: match![1], content: match![2] };
} else {
// 其他情况
return { prefix: '', content: line };
}
});
// 收集需要翻译的行
const linesToTranslate = extractedLines
.map((line, index) => ({ index, content: line.content }))
.filter(item => containsChinese(item.content));
// 如果没有中文内容,返回原始注释
if (linesToTranslate.length === 0) {
return originalComment;
}
// 解析翻译结果 - 假设翻译服务按顺序返回翻译后的行
const translatedLines = translatedContent.split('\n');
const translations = new Map<number, string>();
// 将翻译结果映射到对应的行
linesToTranslate.forEach((item, transIndex) => {
if (transIndex < translatedLines.length) {
translations.set(item.index, translatedLines[transIndex].trim());
}
});
// 重建注释,保持原始结构
return extractedLines
.map((line, index) => {
if (translations.has(index)) {
// 使用翻译内容,保持原始前缀
return line.prefix + translations.get(index);
} else {
// 保持原样
return originalLines[index];
}
})
.join('\n');
};
/**
* 创建替换操作
*/
export const createReplacements = (
file: SourceFile,
comments: ChineseComment[],
translations: TranslationResult[],
): Replacement[] => {
const replacements: Replacement[] = [];
comments.forEach((comment, index) => {
const translation = translations[index];
if (!translation) return;
if (comment.multiLineContext?.isPartOfMultiLine) {
// 处理多行注释中的单行
const replacement = createMultiLineReplacement(file, comment, translation);
if (replacement) {
replacements.push(replacement);
}
} else {
// 处理普通注释(单行注释或整个多行注释)
const replacement = createRegularReplacement(file, comment, translation);
if (replacement) {
replacements.push(replacement);
}
}
});
return replacements;
};
/**
* 为多行注释中的单行创建替换操作
*/
const createMultiLineReplacement = (
file: SourceFile,
comment: ChineseComment,
translation: TranslationResult,
): Replacement | null => {
const lines = file.content.split('\n');
const lineIndex = comment.startLine - 1;
if (lineIndex >= lines.length) return null;
const originalLine = lines[lineIndex];
// 查找这一行中中文内容的位置
const cleanedContent = comment.content;
// 更精确地查找中文内容在原始行中的位置
const commentContentRegex = new RegExp(cleanedContent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const contentMatch = originalLine.match(commentContentRegex);
if (!contentMatch) {
return null;
}
const chineseStart = contentMatch.index!;
const chineseEnd = chineseStart + contentMatch[0].length;
// 计算在整个文件中的位置
let start = 0;
for (let i = 0; i < lineIndex; i++) {
start += lines[i].length + 1; // +1 for newline
}
start += chineseStart;
const end = start + (chineseEnd - chineseStart);
return {
start,
end,
original: originalLine.substring(chineseStart, chineseEnd),
replacement: translation.translated,
};
};
/**
* 为普通注释创建替换操作
*/
const createRegularReplacement = (
file: SourceFile,
comment: ChineseComment,
translation: TranslationResult,
): Replacement | null => {
const lines = file.content.split('\n');
const startLineIndex = comment.startLine - 1;
const endLineIndex = comment.endLine - 1;
// 计算原始注释在文件中的精确位置
let start = 0;
for (let i = 0; i < startLineIndex; i++) {
start += lines[i].length + 1; // +1 for newline
}
start += comment.startColumn;
let end = start;
if (comment.startLine === comment.endLine) {
// 同一行
end = start + (comment.endColumn - comment.startColumn);
} else {
// 跨行 - 重新计算end位置
end = 0;
for (let i = 0; i < endLineIndex; i++) {
end += lines[i].length + 1; // +1 for newline
}
end += comment.endColumn;
}
// 获取原始注释文本
const originalText = file.content.substring(start, end);
// 应用格式保持
const formattedTranslation = preserveCommentFormat(
originalText,
translation.translated,
comment.type === 'documentation' ? 'multi-line' : comment.type,
);
return {
start,
end,
original: originalText,
replacement: formattedTranslation,
};
};
/**
* 应用替换操作到文本内容
*/
export const applyReplacements = (
content: string,
replacements: Replacement[],
): string => {
// 按位置倒序排列,避免替换后位置偏移
const sortedReplacements = [...replacements].sort(
(a, b) => b.start - a.start,
);
let result = content;
for (const replacement of sortedReplacements) {
const before = result.substring(0, replacement.start);
const after = result.substring(replacement.end);
result = before + replacement.replacement + after;
}
return result;
};
/**
* 替换文件中的注释
*/
export const replaceCommentsInFile = async (
file: SourceFile,
operation: ReplacementOperation,
): Promise<{ success: boolean; error?: string }> => {
return tryCatch(async () => {
const fs = await import('fs/promises');
// 应用替换
const newContent = applyReplacements(
file.content,
operation.replacements,
);
// 写入文件
await fs.writeFile(file.path, newContent, 'utf-8');
return { success: true };
}).then(result => {
if (result.success) {
return result.data;
} else {
return {
success: false,
error:
result.error instanceof Error
? result.error.message
: String(result.error),
};
}
});
};
/**
* 批量替换多个文件
*/
export const batchReplaceFiles = async (
operations: ReplacementOperation[],
): Promise<
Array<{
file: string;
success: boolean;
error?: string;
}>
> => {
const results = await Promise.allSettled(
operations.map(async operation => {
const fs = await import('fs/promises');
const content = await fs.readFile(operation.file, 'utf-8');
const sourceFile: SourceFile = {
path: operation.file,
content,
language: 'other', // 临时值,实际应该检测
};
const result = await replaceCommentsInFile(
sourceFile,
operation,
);
return { file: operation.file, ...result };
}),
);
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
file: operations[index].file,
success: false,
error:
result.reason instanceof Error
? result.reason.message
: String(result.reason),
};
}
});
};
/**
* 验证替换操作
*/
export const validateReplacements = (
content: string,
replacements: Replacement[],
): { valid: boolean; errors: string[] } => {
const errors: string[] = [];
// 检查位置是否有效
replacements.forEach((replacement, index) => {
if (replacement.start < 0 || replacement.end > content.length) {
errors.push(`Replacement ${index}: Invalid position range`);
}
if (replacement.start >= replacement.end) {
errors.push(
`Replacement ${index}: Start position must be less than end position`,
);
}
// 检查原文是否匹配
const actualText = content.substring(replacement.start, replacement.end);
if (actualText !== replacement.original) {
errors.push(`Replacement ${index}: Original text mismatch`);
}
});
// 检查是否有重叠
const sortedReplacements = [...replacements].sort(
(a, b) => a.start - b.start,
);
for (let i = 0; i < sortedReplacements.length - 1; i++) {
const current = sortedReplacements[i];
const next = sortedReplacements[i + 1];
if (current.end > next.start) {
errors.push(
`Overlapping replacements at positions ${current.start}-${current.end} and ${next.start}-${next.end}`,
);
}
}
return { valid: errors.length === 0, errors };
};

View File

@@ -0,0 +1,123 @@
import { promises as fs } from 'fs';
import { SourceFile, FileScanConfig, Result } from '../types/index';
import { detectLanguage, filterFilesByExtensions, isTextFile } from '../utils/language';
import { getGitTrackedFiles, getAllGitFiles } from '../utils/git';
import { tryCatch } from '../utils/fp';
/**
* 读取文件内容并创建SourceFile对象
*/
export const readSourceFile = async (filePath: string): Promise<Result<SourceFile>> => {
return tryCatch(async () => {
const content = await fs.readFile(filePath, 'utf-8');
const language = detectLanguage(filePath);
return {
path: filePath,
content,
language
};
});
};
/**
* 批量读取源文件
*/
export const readSourceFiles = async (filePaths: string[]): Promise<SourceFile[]> => {
const results = await Promise.allSettled(
filePaths.map(path => readSourceFile(path))
);
return results
.filter((result): result is PromiseFulfilledResult<Result<SourceFile>> =>
result.status === 'fulfilled' && result.value.success
)
.map(result => (result.value as { success: true; data: SourceFile }).data);
};
/**
* 获取Git仓库中的源码文件
*/
export const getSourceFiles = async (config: FileScanConfig): Promise<Result<string[]>> => {
const { root, extensions, includeUntracked } = config;
return tryCatch(async () => {
// 获取Git文件列表
const gitFilesResult = includeUntracked
? await getAllGitFiles(root)
: await getGitTrackedFiles(root);
if (!gitFilesResult.success) {
throw gitFilesResult.error;
}
let files = gitFilesResult.data;
// 过滤文本文件
files = files.filter(isTextFile);
// 根据扩展名过滤
if (extensions.length > 0) {
files = filterFilesByExtensions(files, extensions);
}
return files;
});
};
/**
* 扫描并读取所有源码文件
*/
export const scanSourceFiles = async (config: FileScanConfig): Promise<Result<SourceFile[]>> => {
return tryCatch(async () => {
const filesResult = await getSourceFiles(config);
if (!filesResult.success) {
throw filesResult.error;
}
const sourceFiles = await readSourceFiles(filesResult.data);
return sourceFiles;
});
};
/**
* 检查文件是否存在且可读
*/
export const isFileAccessible = async (filePath: string): Promise<boolean> => {
try {
await fs.access(filePath, fs.constants.R_OK);
return true;
} catch {
return false;
}
};
/**
* 获取文件统计信息
*/
export const getFileStats = async (filePaths: string[]): Promise<{
total: number;
accessible: number;
textFiles: number;
supportedFiles: number;
}> => {
const accessibilityResults = await Promise.allSettled(
filePaths.map(isFileAccessible)
);
const accessible = accessibilityResults.filter(
(result): result is PromiseFulfilledResult<boolean> =>
result.status === 'fulfilled' && result.value
).length;
const textFiles = filePaths.filter(isTextFile).length;
const supportedFiles = filePaths.filter(path => detectLanguage(path) !== 'other').length;
return {
total: filePaths.length,
accessible,
textFiles,
supportedFiles
};
};

View File

@@ -0,0 +1,302 @@
import {
ProcessingReport,
ProcessingStats,
FileProcessingDetail,
} from '../types/index.js';
/**
* 报告收集器类
*/
export class ReportCollector {
private stats: ProcessingStats = {
totalFiles: 0,
processedFiles: 0,
translatedComments: 0,
skippedFiles: 0,
errors: [],
startTime: Date.now(),
endTime: 0,
};
private fileDetails: Map<string, FileProcessingDetail> = new Map();
/**
* 记录文件处理开始
*/
recordFileStart(filePath: string): void {
this.stats.totalFiles++;
this.fileDetails.set(filePath, {
file: filePath,
commentCount: 0,
status: 'processing',
startTime: Date.now(),
});
}
/**
* 记录文件处理完成
*/
recordFileComplete(filePath: string, commentCount: number): void {
const detail = this.fileDetails.get(filePath);
if (detail) {
detail.status = 'success';
detail.commentCount = commentCount;
detail.endTime = Date.now();
this.stats.processedFiles++;
this.stats.translatedComments += commentCount;
}
}
/**
* 记录文件跳过
*/
recordFileSkipped(filePath: string, reason?: string): void {
const detail = this.fileDetails.get(filePath);
if (detail) {
detail.status = 'skipped';
detail.errorMessage = reason;
detail.endTime = Date.now();
this.stats.skippedFiles++;
}
}
/**
* 记录处理错误
*/
recordError(filePath: string, error: Error): void {
const detail = this.fileDetails.get(filePath);
if (detail) {
detail.status = 'error';
detail.errorMessage = error.message;
detail.endTime = Date.now();
}
this.stats.errors.push({ file: filePath, error: error.message });
}
/**
* 完成统计
*/
finalize(): void {
this.stats.endTime = Date.now();
}
/**
* 获取统计信息
*/
getStats(): ProcessingStats {
return { ...this.stats };
}
/**
* 获取文件详情
*/
getFileDetails(): FileProcessingDetail[] {
return Array.from(this.fileDetails.values());
}
/**
* 生成完整报告
*/
generateReport(): ProcessingReport {
this.finalize();
const duration = (this.stats.endTime - this.stats.startTime) / 1000;
return {
stats: this.getStats(),
details: this.getFileDetails(),
duration,
};
}
/**
* 重置收集器
*/
reset(): void {
this.stats = {
totalFiles: 0,
processedFiles: 0,
translatedComments: 0,
skippedFiles: 0,
errors: [],
startTime: Date.now(),
endTime: 0,
};
this.fileDetails.clear();
}
}
/**
* 生成控制台报告
*/
export const generateConsoleReport = (report: ProcessingReport): string => {
const { stats, duration } = report;
const successRate =
stats.totalFiles > 0
? ((stats.processedFiles / stats.totalFiles) * 100).toFixed(1)
: '0';
let output = `
📊 翻译处理报告
==================
总文件数: ${stats.totalFiles}
处理成功: ${stats.processedFiles}
跳过文件: ${stats.skippedFiles}
翻译注释: ${stats.translatedComments}
错误数量: ${stats.errors.length}
成功率: ${successRate}%
处理时间: ${duration.toFixed(2)}
`;
if (stats.errors.length > 0) {
output += '\n❌ 错误详情:\n';
stats.errors.forEach(error => {
output += ` ${error.file}: ${error.error}\n`;
});
} else {
output += '\n✅ 处理完成,无错误';
}
return output;
};
/**
* 生成Markdown报告
*/
export const generateMarkdownReport = (report: ProcessingReport): string => {
const { stats, details, duration } = report;
const successRate =
stats.totalFiles > 0
? ((stats.processedFiles / stats.totalFiles) * 100).toFixed(1)
: '0';
let markdown = `# 中文注释翻译报告
## 📊 统计概览
| 指标 | 数值 |
|------|------|
| 总文件数 | ${stats.totalFiles} |
| 处理成功 | ${stats.processedFiles} |
| 跳过文件 | ${stats.skippedFiles} |
| 翻译注释 | ${stats.translatedComments} |
| 错误数量 | ${stats.errors.length} |
| 成功率 | ${successRate}% |
| 处理时间 | ${duration.toFixed(2)}秒 |
## 📁 文件详情
| 文件路径 | 状态 | 注释数量 | 耗时(ms) | 备注 |
|----------|------|----------|----------|------|
`;
details.forEach(detail => {
const duration =
detail.endTime && detail.startTime
? detail.endTime - detail.startTime
: 0;
const status =
detail.status === 'success'
? '✅'
: detail.status === 'error'
? '❌'
: detail.status === 'skipped'
? '⏭️'
: '🔄';
markdown += `| ${detail.file} | ${status} | ${detail.commentCount} | ${duration} | ${detail.errorMessage || '-'} |\n`;
});
if (stats.errors.length > 0) {
markdown += '\n## ❌ 错误详情\n\n';
stats.errors.forEach((error, index) => {
markdown += `${index + 1}. **${error.file}**\n \`\`\`\n ${error.error}\n \`\`\`\n\n`;
});
}
return markdown;
};
/**
* 生成JSON报告
*/
export const generateJsonReport = (report: ProcessingReport): string => {
return JSON.stringify(report, null, 2);
};
/**
* 根据格式生成报告
*/
export const generateReport = (
report: ProcessingReport,
format: 'json' | 'markdown' | 'console' = 'console',
): string => {
switch (format) {
case 'json':
return generateJsonReport(report);
case 'markdown':
return generateMarkdownReport(report);
case 'console':
default:
return generateConsoleReport(report);
}
};
/**
* 保存报告到文件
*/
export const saveReportToFile = async (
report: ProcessingReport,
filePath: string,
format: 'json' | 'markdown' | 'console' = 'json',
): Promise<void> => {
const content = generateReport(report, format);
const fs = await import('fs/promises');
await fs.writeFile(filePath, content, 'utf-8');
};
/**
* 在控制台显示实时进度
*/
export class ProgressDisplay {
private total: number = 0;
private current: number = 0;
private startTime: number = Date.now();
constructor(total: number) {
this.total = total;
}
/**
* 更新进度
*/
update(current: number, currentFile?: string): void {
this.current = current;
const percentage = ((current / this.total) * 100).toFixed(1);
const elapsed = (Date.now() - this.startTime) / 1000;
const speed = current / elapsed;
const eta = speed > 0 ? (this.total - current) / speed : 0;
let line = `进度: ${current}/${this.total} (${percentage}%) | 耗时: ${elapsed.toFixed(1)}s`;
if (eta > 0) {
line += ` | 预计剩余: ${eta.toFixed(1)}s`;
}
if (currentFile) {
line += ` | 当前: ${currentFile}`;
}
// 清除当前行并输出新进度
process.stdout.write(
'\r' + ' '.repeat(process.stdout.columns || 80) + '\r',
);
process.stdout.write(line);
}
/**
* 完成进度显示
*/
complete(): void {
process.stdout.write('\n');
}
}

View File

@@ -0,0 +1,239 @@
import {
TranslationResult,
TranslationContext,
ChineseComment,
TranslationError,
} from '../types/index';
import { TranslationConfig } from '../types/config';
import { retry, chunk } from '../utils/fp';
import { isValidTranslation } from '../utils/chinese';
import { translate as volcTranslate, TranslateConfig as VolcTranslateConfig } from '../volc/translate';
/**
* 翻译服务类
*/
export class TranslationService {
private config: TranslationConfig;
private cache = new Map<string, TranslationResult>();
constructor(config: TranslationConfig) {
this.config = config;
}
/**
* 转换为火山引擎翻译配置
*/
private toVolcConfig(): VolcTranslateConfig {
return {
accessKeyId: this.config.accessKeyId,
secretAccessKey: this.config.secretAccessKey,
region: this.config.region,
sourceLanguage: this.config.sourceLanguage,
targetLanguage: this.config.targetLanguage,
};
}
/**
* 计算翻译置信度(简单实现)
*/
private calculateConfidence(translated: string, original: string): number {
// 基于长度比例和有效性的简单置信度计算
const lengthRatio = translated.length / original.length;
if (!isValidTranslation(original, translated)) {
return 0;
}
// 理想的长度比例在0.8-2.0之间
let confidence = 0.8;
if (lengthRatio >= 0.8 && lengthRatio <= 2.0) {
confidence = 0.9;
}
return confidence;
}
/**
* 调用火山引擎API进行翻译
*/
private async callVolcTranslate(texts: string[]): Promise<string[]> {
const volcConfig = this.toVolcConfig();
const response = await volcTranslate(texts, volcConfig);
return response.TranslationList.map(item => item.Translation);
}
/**
* 翻译单个注释
*/
async translateComment(
comment: string,
context?: TranslationContext,
): Promise<TranslationResult> {
// 检查缓存
const cacheKey = this.getCacheKey(comment, context);
const cached = this.cache.get(cacheKey);
if (cached) {
return cached;
}
try {
const translations = await retry(
() => this.callVolcTranslate([comment]),
this.config.maxRetries,
1000,
);
const translated = translations[0];
if (!translated) {
throw new Error('Empty translation response');
}
const result: TranslationResult = {
original: comment,
translated,
confidence: this.calculateConfidence(translated, comment),
};
// 缓存结果
this.cache.set(cacheKey, result);
return result;
} catch (error) {
throw new TranslationError(
`Translation failed: ${error instanceof Error ? error.message : String(error)}`,
comment,
);
}
}
/**
* 生成缓存键
*/
private getCacheKey(comment: string, context?: TranslationContext): string {
const contextStr = context
? `${context.language}-${context.commentType}-${context.nearbyCode || ''}`
: '';
return `${comment}|${contextStr}`;
}
/**
* 批量翻译注释
*/
async batchTranslate(
comments: ChineseComment[],
concurrency: number = this.config.concurrency,
): Promise<TranslationResult[]> {
// 提取未缓存的注释
const uncachedComments: { comment: ChineseComment; index: number }[] = [];
const results: TranslationResult[] = new Array(comments.length);
// 检查缓存
comments.forEach((comment, index) => {
const cacheKey = this.getCacheKey(comment.content);
const cached = this.cache.get(cacheKey);
if (cached) {
results[index] = cached;
} else {
uncachedComments.push({ comment, index });
}
});
// 如果所有注释都已缓存,直接返回
if (uncachedComments.length === 0) {
return results;
}
// 分批翻译未缓存的注释
const chunks = chunk(uncachedComments, concurrency);
for (const chunkItems of chunks) {
try {
const textsToTranslate = chunkItems.map(item => item.comment.content);
const translations = await retry(
() => this.callVolcTranslate(textsToTranslate),
this.config.maxRetries,
1000,
);
// 处理翻译结果
chunkItems.forEach((item, chunkIndex) => {
const translated = translations[chunkIndex];
if (translated) {
const result: TranslationResult = {
original: item.comment.content,
translated,
confidence: this.calculateConfidence(translated, item.comment.content),
};
// 缓存结果
const cacheKey = this.getCacheKey(item.comment.content);
this.cache.set(cacheKey, result);
results[item.index] = result;
} else {
// 如果翻译失败,创建一个错误结果
results[item.index] = {
original: item.comment.content,
translated: item.comment.content, // 翻译失败时保持原文
confidence: 0,
};
}
});
} catch (error) {
// 如果整个批次翻译失败,为这个批次的所有注释创建错误结果
chunkItems.forEach(item => {
results[item.index] = {
original: item.comment.content,
translated: item.comment.content, // 翻译失败时保持原文
confidence: 0,
};
});
console.warn(`批量翻译失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
return results;
}
/**
* 保存翻译缓存到文件
*/
async saveCache(filePath: string): Promise<void> {
const cacheData = Object.fromEntries(this.cache);
const fs = await import('fs/promises');
await fs.writeFile(filePath, JSON.stringify(cacheData, null, 2));
}
/**
* 从文件加载翻译缓存
*/
async loadCache(filePath: string): Promise<void> {
try {
const fs = await import('fs/promises');
const data = await fs.readFile(filePath, 'utf-8');
const cacheData = JSON.parse(data);
this.cache = new Map(Object.entries(cacheData));
} catch {
// 缓存文件不存在或损坏,忽略
}
}
/**
* 清空缓存
*/
clearCache(): void {
this.cache.clear();
}
/**
* 获取缓存统计
*/
getCacheStats(): { size: number; hitRate: number } {
return {
size: this.cache.size,
hitRate: 0, // 需要实际统计命中率
};
}
}