chore: remove all cn comments (#277)
This commit is contained in:
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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, // 需要实际统计命中率
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user