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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 5109 additions and 0 deletions

2
.gitignore vendored
View File

@ -52,3 +52,5 @@ common/temp
backend/conf/model/*.yaml
*.tsbuildinfo

View File

@ -0,0 +1,19 @@
{
"name": "rush-commands",
"version": "1.0.0",
"description": "",
"keywords": [],
"license": "Apache-2.0",
"author": "",
"main": "index.js",
"scripts": {},
"dependencies": {
"commander": "^11.0.0",
"simple-git": "^3.20.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.19.2",
"typescript": "^5.0.0"
}
}

View File

@ -0,0 +1,382 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
commander:
specifier: ^11.0.0
version: 11.1.0
simple-git:
specifier: ^3.20.0
version: 3.28.0
devDependencies:
'@types/node':
specifier: ^20.0.0
version: 20.19.9
tsx:
specifier: ^4.19.2
version: 4.20.3
typescript:
specifier: ^5.0.0
version: 5.8.3
packages:
/@esbuild/aix-ppc64@0.25.8:
resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-arm64@0.25.8:
resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-arm@0.25.8:
resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-x64@0.25.8:
resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-arm64@0.25.8:
resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-x64@0.25.8:
resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-arm64@0.25.8:
resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-x64@0.25.8:
resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm64@0.25.8:
resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm@0.25.8:
resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ia32@0.25.8:
resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-loong64@0.25.8:
resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-mips64el@0.25.8:
resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ppc64@0.25.8:
resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-riscv64@0.25.8:
resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-s390x@0.25.8:
resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-x64@0.25.8:
resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/netbsd-arm64@0.25.8:
resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/netbsd-x64@0.25.8:
resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/openbsd-arm64@0.25.8:
resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/openbsd-x64@0.25.8:
resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/openharmony-arm64@0.25.8:
resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
requiresBuild: true
dev: true
optional: true
/@esbuild/sunos-x64@0.25.8:
resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-arm64@0.25.8:
resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-ia32@0.25.8:
resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-x64@0.25.8:
resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@kwsites/file-exists@1.1.1:
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
dependencies:
debug: 4.4.1
transitivePeerDependencies:
- supports-color
dev: false
/@kwsites/promise-deferred@1.1.1:
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
dev: false
/@types/node@20.19.9:
resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==}
dependencies:
undici-types: 6.21.0
dev: true
/commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
dev: false
/debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.3
dev: false
/esbuild@0.25.8:
resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
engines: {node: '>=18'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.8
'@esbuild/android-arm': 0.25.8
'@esbuild/android-arm64': 0.25.8
'@esbuild/android-x64': 0.25.8
'@esbuild/darwin-arm64': 0.25.8
'@esbuild/darwin-x64': 0.25.8
'@esbuild/freebsd-arm64': 0.25.8
'@esbuild/freebsd-x64': 0.25.8
'@esbuild/linux-arm': 0.25.8
'@esbuild/linux-arm64': 0.25.8
'@esbuild/linux-ia32': 0.25.8
'@esbuild/linux-loong64': 0.25.8
'@esbuild/linux-mips64el': 0.25.8
'@esbuild/linux-ppc64': 0.25.8
'@esbuild/linux-riscv64': 0.25.8
'@esbuild/linux-s390x': 0.25.8
'@esbuild/linux-x64': 0.25.8
'@esbuild/netbsd-arm64': 0.25.8
'@esbuild/netbsd-x64': 0.25.8
'@esbuild/openbsd-arm64': 0.25.8
'@esbuild/openbsd-x64': 0.25.8
'@esbuild/openharmony-arm64': 0.25.8
'@esbuild/sunos-x64': 0.25.8
'@esbuild/win32-arm64': 0.25.8
'@esbuild/win32-ia32': 0.25.8
'@esbuild/win32-x64': 0.25.8
dev: true
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
dependencies:
resolve-pkg-maps: 1.0.0
dev: true
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: false
/resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
dev: true
/simple-git@3.28.0:
resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==}
dependencies:
'@kwsites/file-exists': 1.1.1
'@kwsites/promise-deferred': 1.1.1
debug: 4.4.1
transitivePeerDependencies:
- supports-color
dev: false
/tsx@4.20.3:
resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
engines: {node: '>=18.0.0'}
hasBin: true
dependencies:
esbuild: 0.25.8
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
dev: true
/typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
/undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
dev: true

View File

@ -0,0 +1,284 @@
# 中文备注转换为英文 - 项目概览
## 📖 项目简介
本项目是一个TypeScript命令行工具用于自动将代码仓库中的中文注释翻译为英文。通过调用OpenAI API实现高质量的代码注释翻译同时保持原有的代码格式和结构。
## 🎯 功能特性
- ✅ **智能文件扫描**自动识别Git仓库中的源码文件
- ✅ **多语言支持**支持TypeScript、JavaScript、Go、Markdown等文件格式
- ✅ **精确注释解析**:准确定位和提取不同语言的注释内容
- ✅ **高质量翻译**集成OpenAI API提供专业的翻译服务
- ✅ **格式保持**:保持原有的缩进、换行和注释结构
- ✅ **安全备份**:自动创建文件备份,支持回滚操作
- ✅ **并发处理**:支持并发翻译,提高处理效率
- ✅ **详细报告**:生成完整的处理报告和统计信息
- ✅ **函数式设计**采用FP编程范式代码简洁易维护
## 🚀 快速开始
### 安装依赖
```bash
npm install
```
### 基本使用
```bash
# 翻译指定目录下的所有支持文件
ai-translate --root ./src
# 指定文件扩展名
ai-translate --root ./src --exts ts,js,go
# 仅分析不修改(预览模式)
ai-translate --root ./src --dry-run
# 详细输出模式
ai-translate --root ./src --verbose
```
### 配置OpenAI API
```bash
# 通过环境变量设置
export OPENAI_API_KEY=your-api-key
# 或通过命令行参数
ai-translate --root ./src --openai-key your-api-key
```
## 📁 项目结构
```
src/convert-comments/
├── 📄 requirements.md # 需求文档
├── 📄 implementation-plan.md # 实现方案
├── 📄 technical-specification.md # 技术规格
├── 📄 README.md # 项目概览(本文件)
├── 📦 index.ts # 主入口文件
├── 🗂️ cli/ # 命令行接口
│ ├── command.ts # Commander.js命令定义
│ └── config.ts # 配置管理
├── 🗂️ modules/ # 核心功能模块
│ ├── file-scan.ts # 文件扫描模块
│ ├── chinese-detection.ts # 中文检测模块
│ ├── translation.ts # 翻译服务模块
│ ├── file-replacement.ts # 文件替换模块
│ └── report.ts # 报告生成模块
├── 🗂️ utils/ # 工具函数
│ ├── git.ts # Git操作工具
│ ├── language.ts # 编程语言识别
│ ├── chinese.ts # 中文字符检测
│ └── fp.ts # 函数式编程工具
├── 🗂️ types/ # TypeScript类型定义
│ ├── index.ts # 主要类型定义
│ └── config.ts # 配置类型
└── 🗂️ __tests__/ # 测试文件
├── unit/ # 单元测试
└── integration/ # 集成测试
```
## 🔧 核心模块
### 1. 文件扫描模块 (FileScanModule)
- 调用Git命令获取仓库文件列表
- 根据扩展名过滤目标文件
- 识别编程语言类型
### 2. 中文检测模块 (ChineseDetectionModule)
- 解析不同语言的注释语法
- 识别包含中文字符的注释
- 提取注释的精确位置信息
### 3. 翻译服务模块 (TranslationModule)
- 调用OpenAI API进行翻译
- 处理翻译错误和重试机制
- 优化翻译提示词和上下文
### 4. 文件替换模块 (FileReplacementModule)
- 精确替换文件中的中文注释
- 保持代码格式和缩进
- 实现备份和回滚机制
### 5. 报告生成模块 (ReportModule)
- 收集处理过程的统计信息
- 生成详细的处理报告
- 支持多种输出格式
## ⚡ 技术亮点
### 函数式编程范式
采用纯函数设计和不可变数据结构:
```typescript
const processRepository = pipe(
getGitTrackedFiles,
asyncMap(readFile),
asyncFilter(hasChineseComments),
asyncMap(extractChineseComments),
asyncMap(translateComments),
asyncMap(applyTranslations),
generateReport
);
```
### 性能优化
- **并发控制**使用Semaphore控制API调用频率
- **缓存机制**:避免重复翻译相同内容
- **增量处理**:仅处理修改过的文件
- **流式处理**:支持大文件分块处理
### 错误处理
- **Result模式**:使用函数式错误处理
- **重试机制**自动重试失败的API调用
- **部分失败**:支持部分文件失败时继续处理
## 🛠️ 开发指南
### 环境准备
1. **Node.js 环境**:建议使用 Node.js 16+
2. **TypeScript**项目使用TypeScript开发
3. **OpenAI API Key**需要有效的OpenAI API密钥
### 开发流程
1. **安装依赖**
```bash
npm install
```
2. **开发模式运行**
```bash
npm run dev
```
3. **运行测试**
```bash
npm test
```
4. **构建项目**
```bash
npm run build
```
### 贡献指南
1. **Fork 项目**到自己的GitHub账号
2. **创建功能分支**`git checkout -b feature/new-feature`
3. **提交更改**`git commit -am 'Add new feature'`
4. **推送分支**`git push origin feature/new-feature`
5. **创建 Pull Request**
### 代码规范
- **TypeScript严格模式**:启用所有严格类型检查
- **ESLint规则**遵循项目ESLint配置
- **Prettier格式化**:保持代码格式一致
- **单元测试**:新功能需要对应的单元测试
## 📋 命令行参数
### 必需参数
- `--root, -r <directory>`:需要处理的根目录
### 可选参数
- `--exts, -e <extensions>`:文件扩展名,如 "ts,js,go,md"
- `--openai-key <key>`OpenAI API密钥
- `--model <model>`OpenAI模型名称默认gpt-3.5-turbo
- `--dry-run`:仅分析不实际修改文件
- `--backup`:创建文件备份(默认启用)
- `--verbose, -v`:详细输出模式
- `--output <file>`:报告输出文件路径
### 使用示例
```bash
# 基本使用
ai-translate --root ./src --exts ts,js
# 预览模式(不修改文件)
ai-translate --root ./src --dry-run --verbose
# 使用GPT-4模型
ai-translate --root ./src --model gpt-4
# 生成JSON格式报告
ai-translate --root ./src --output report.json
```
## 🔍 配置文件
支持使用配置文件来管理默认设置:
```json
{
"translation": {
"model": "gpt-3.5-turbo",
"maxRetries": 3,
"timeout": 30000,
"concurrency": 3
},
"processing": {
"defaultExtensions": ["ts", "js", "go", "md"],
"createBackup": true,
"outputFormat": "console"
},
"git": {
"ignorePatterns": ["node_modules/**", ".git/**", "dist/**"],
"includeUntracked": false
}
}
```
## 📊 输出报告
处理完成后会生成详细的统计报告:
```
📊 翻译处理报告
==================
总文件数: 45
处理成功: 42
跳过文件: 3
翻译注释: 128
错误数量: 0
处理时间: 45.32秒
✅ 处理完成,无错误
```
## ⚠️ 注意事项
### API限制
- OpenAI API有调用频率限制建议合理设置并发数量
- 长时间运行可能消耗较多API配额
### 翻译质量
- 自动翻译可能不够准确,建议人工审核重要注释
- 提供dry-run模式预览翻译结果
### 文件安全
- 默认创建备份文件,避免意外损失
- 建议在版本控制环境下使用
## 🔗 相关文档
- [需求文档](./requirements.md) - 详细的功能需求说明
- [实现方案](./implementation-plan.md) - 整体架构和设计方案
- [技术规格](./technical-specification.md) - 详细的技术实现规格
## 📞 问题反馈
如有问题或建议,请通过以下方式联系:
- 创建 GitHub Issue
- 提交 Pull Request
- 发送邮件至开发团队
---
**Happy Coding! 🎉**

View File

@ -0,0 +1,108 @@
import { Command } from 'commander';
import { CliOptions } from '../types/config';
/**
*
*/
export const createProgram = (): Command => {
const program = new Command();
program
.name('ai-translate')
.description('将代码仓库中的中文注释翻译为英文')
.version('1.0.0');
program
.requiredOption('-r, --root <directory>', '需要处理的根目录')
.option('-e, --exts <extensions>', '文件扩展名,用逗号分隔 (例: ts,js,go)', '')
.option('--access-key-id <key>', '火山引擎 Access Key ID')
.option('--secret-access-key <key>', '火山引擎 Secret Access Key')
.option('--region <region>', '火山引擎服务区域', 'cn-beijing')
.option('--source-language <lang>', '源语言代码', 'zh')
.option('--target-language <lang>', '目标语言代码', 'en')
.option('--dry-run', '仅分析不实际修改文件')
.option('-v, --verbose', '详细输出模式')
.option('-o, --output <file>', '报告输出文件路径')
.option('-c, --config <file>', '配置文件路径')
.option('--concurrency <number>', '并发翻译数量', '3')
.option('--max-retries <number>', '最大重试次数', '3')
.option('--timeout <number>', 'API超时时间(毫秒)', '30000');
return program;
};
/**
*
*/
export const parseOptions = (program: Command): CliOptions => {
const options = program.opts();
return {
root: options.root,
exts: options.exts,
accessKeyId: options.accessKeyId,
secretAccessKey: options.secretAccessKey,
region: options.region,
sourceLanguage: options.sourceLanguage,
targetLanguage: options.targetLanguage,
dryRun: options.dryRun,
verbose: options.verbose,
output: options.output,
config: options.config
};
};
/**
*
*/
export const showHelp = (): void => {
console.log(`
🤖 AI翻译工具 -
使:
ai-translate --root <目录> []
:
# 使
ai-translate --root ./src --access-key-id <YOUR_KEY_ID> --secret-access-key <YOUR_SECRET>
#
ai-translate --root ./src --exts ts,js,go --source-language zh --target-language en
#
ai-translate --root ./src --dry-run
#
ai-translate --root ./src --region ap-southeast-1 --concurrency 5
#
ai-translate --root ./src --verbose --output report.json
:
VOLC_ACCESS_KEY_ID Access Key ID
VOLC_SECRET_ACCESS_KEY Secret Access Key
(config.json):
{
"translation": {
"accessKeyId": "your-access-key-id",
"secretAccessKey": "your-secret-access-key",
"region": "cn-beijing",
"sourceLanguage": "zh",
"targetLanguage": "en",
"maxRetries": 3,
"concurrency": 3
},
"processing": {
"defaultExtensions": ["ts", "js", "go", "md"]
}
}
`);
};
/**
*
*/
export const showVersion = (): void => {
console.log('ai-translate version 1.0.0');
};

View File

@ -0,0 +1,182 @@
import { AppConfig, CliOptions, TranslationConfig, ProcessingConfig } from '../types/config';
import { deepMerge } from '../utils/fp';
/**
*
*/
const DEFAULT_CONFIG: AppConfig = {
translation: {
accessKeyId: process.env.VOLC_ACCESS_KEY_ID || '',
secretAccessKey: process.env.VOLC_SECRET_ACCESS_KEY || '',
region: 'cn-beijing',
sourceLanguage: 'zh',
targetLanguage: 'en',
maxRetries: 3,
timeout: 30000,
concurrency: 3
},
processing: {
defaultExtensions: [
'ts', 'tsx', 'js', 'jsx', 'go', 'md', 'txt', 'json',
'yaml', 'yml', 'toml', 'ini', 'conf', 'config',
'sh', 'bash', 'zsh', 'fish', 'py', 'css', 'scss', 'sass', 'less',
'html', 'htm', 'xml', 'php', 'rb', 'rs', 'java', 'c', 'h',
'cpp', 'cxx', 'cc', 'hpp', 'cs'
],
outputFormat: 'console'
},
git: {
ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
includeUntracked: false
}
};
/**
*
*/
export const loadConfigFromFile = async (configPath: string): Promise<Partial<AppConfig>> => {
try {
const fs = await import('fs/promises');
const configContent = await fs.readFile(configPath, 'utf-8');
return JSON.parse(configContent);
} catch (error) {
console.warn(`配置文件加载失败: ${configPath}`, error);
return {};
}
};
/**
*
*/
export const createConfigFromOptions = (options: CliOptions): Partial<AppConfig> => {
const config: Partial<AppConfig> = {};
// 翻译配置
if (options.accessKeyId || options.secretAccessKey || options.region || options.sourceLanguage || options.targetLanguage) {
config.translation = {} as Partial<TranslationConfig>;
if (options.accessKeyId) {
config.translation!.accessKeyId = options.accessKeyId;
}
if (options.secretAccessKey) {
config.translation!.secretAccessKey = options.secretAccessKey;
}
if (options.region) {
config.translation!.region = options.region;
}
if (options.sourceLanguage) {
config.translation!.sourceLanguage = options.sourceLanguage;
}
if (options.targetLanguage) {
config.translation!.targetLanguage = options.targetLanguage;
}
}
// 处理配置
if (options.output) {
config.processing = {} as Partial<ProcessingConfig>;
// 根据输出文件扩展名推断格式
const ext = options.output.toLowerCase().split('.').pop();
if (ext === 'json') {
config.processing!.outputFormat = 'json';
} else if (ext === 'md') {
config.processing!.outputFormat = 'markdown';
}
}
return config;
};
/**
*
*/
export const mergeConfigs = (...configs: Partial<AppConfig>[]): AppConfig => {
return configs.reduce(
(merged, config) => deepMerge(merged, config),
{ ...DEFAULT_CONFIG }
) as AppConfig;
};
/**
*
*/
export const loadConfig = async (options: CliOptions): Promise<AppConfig> => {
const configs: Partial<AppConfig>[] = [DEFAULT_CONFIG];
// 加载配置文件
if (options.config) {
const fileConfig = await loadConfigFromFile(options.config);
configs.push(fileConfig);
}
// 加载命令行选项配置
const optionsConfig = createConfigFromOptions(options);
configs.push(optionsConfig);
return mergeConfigs(...configs);
};
/**
*
*/
export const validateConfig = (config: AppConfig): { valid: boolean; errors: string[] } => {
const errors: string[] = [];
// 验证火山引擎 Access Key ID
if (!config.translation.accessKeyId) {
errors.push('火山引擎 Access Key ID 未设置请通过环境变量VOLC_ACCESS_KEY_ID或--access-key-id参数提供');
}
// 验证火山引擎 Secret Access Key
if (!config.translation.secretAccessKey) {
errors.push('火山引擎 Secret Access Key 未设置请通过环境变量VOLC_SECRET_ACCESS_KEY或--secret-access-key参数提供');
}
// 验证区域
const validRegions = ['cn-beijing', 'ap-southeast-1', 'us-east-1'];
if (!validRegions.includes(config.translation.region)) {
console.warn(`未知的区域: ${config.translation.region},建议使用: ${validRegions.join(', ')}`);
}
// 验证语言代码
const validLanguages = ['zh', 'en', 'ja', 'ko', 'fr', 'de', 'es', 'pt', 'ru'];
if (!validLanguages.includes(config.translation.sourceLanguage)) {
console.warn(`未知的源语言: ${config.translation.sourceLanguage},建议使用: ${validLanguages.join(', ')}`);
}
if (!validLanguages.includes(config.translation.targetLanguage)) {
console.warn(`未知的目标语言: ${config.translation.targetLanguage},建议使用: ${validLanguages.join(', ')}`);
}
// 验证并发数
if (config.translation.concurrency < 1 || config.translation.concurrency > 10) {
errors.push('并发数应该在1-10之间');
}
// 验证超时时间
if (config.translation.timeout < 1000 || config.translation.timeout > 300000) {
errors.push('超时时间应该在1000-300000毫秒之间');
}
return { valid: errors.length === 0, errors };
};
/**
*
*/
export const printConfigInfo = (config: AppConfig, verbose: boolean = false): void => {
console.log('🔧 当前配置:');
console.log(` 区域: ${config.translation.region}`);
console.log(` 源语言: ${config.translation.sourceLanguage}`);
console.log(` 目标语言: ${config.translation.targetLanguage}`);
console.log(` 并发数: ${config.translation.concurrency}`);
console.log(` 重试次数: ${config.translation.maxRetries}`);
console.log(` 输出格式: ${config.processing.outputFormat}`);
if (verbose) {
console.log(` Access Key ID: ${config.translation.accessKeyId ? '已设置' : '未设置'}`);
console.log(` Secret Access Key: ${config.translation.secretAccessKey ? '已设置' : '未设置'}`);
console.log(` 超时时间: ${config.translation.timeout}ms`);
console.log(` 默认扩展名: ${config.processing.defaultExtensions.join(', ')}`);
console.log(` 忽略模式: ${config.git.ignorePatterns.join(', ')}`);
console.log(` 包含未跟踪文件: ${config.git.includeUntracked ? '是' : '否'}`);
}
};

View File

@ -0,0 +1,25 @@
{
"translation": {
"model": "gpt-3.5-turbo",
"maxRetries": 3,
"timeout": 30000,
"concurrency": 3
},
"processing": {
"defaultExtensions": ["ts", "tsx", "js", "jsx", "go", "md"],
"createBackup": true,
"outputFormat": "console"
},
"git": {
"ignorePatterns": [
"node_modules/**",
".git/**",
"dist/**",
"build/**",
"coverage/**",
"*.min.js",
"*.bundle.js"
],
"includeUntracked": false
}
}

View File

@ -0,0 +1,306 @@
# 中文备注转换为英文 - 实现方案
## 项目概述
基于需求文档本项目需要实现一个TypeScript脚本用于将代码仓库内的中文备注自动转换为英文备注。
## 核心模块设计
### 1. 文件扫描模块 (FileScanModule)
```typescript
interface FileScanConfig {
root: string;
extensions: string[];
}
interface SourceFile {
path: string;
content: string;
language: 'typescript' | 'javascript' | 'go' | 'markdown' | 'other';
}
```
**功能职责:**
- 调用git命令获取仓库所有源码文件
- 根据文件扩展名过滤目标文件
- 读取文件内容并识别编程语言类型
**核心函数:**
- `getGitTrackedFiles(root: string): Promise<string[]>`
- `filterFilesByExtensions(files: string[], extensions: string[]): string[]`
- `readSourceFiles(filePaths: string[]): Promise<SourceFile[]>`
### 2. 中文检测模块 (ChineseDetectionModule)
```typescript
interface ChineseComment {
content: string;
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
type: 'single-line' | 'multi-line' | 'documentation';
}
interface FileWithComments {
file: SourceFile;
chineseComments: ChineseComment[];
}
```
**功能职责:**
- 解析不同语言的注释语法
- 识别包含中文字符的注释
- 提取注释的精确位置信息
**核心函数:**
- `detectChineseInComments(file: SourceFile): ChineseComment[]`
- `parseCommentsByLanguage(content: string, language: string): Comment[]`
- `containsChinese(text: string): boolean`
### 3. 翻译服务模块 (TranslationModule)
```typescript
interface TranslationConfig {
apiKey: string;
model: string;
maxRetries: number;
timeout: number;
}
interface TranslationResult {
original: string;
translated: string;
confidence: number;
}
```
**功能职责:**
- 调用OpenAI API进行翻译
- 处理翻译错误和重试
- 保持代码注释的格式和结构
**核心函数:**
- `translateComment(comment: string, context?: string): Promise<TranslationResult>`
- `batchTranslate(comments: string[]): Promise<TranslationResult[]>`
- `createTranslationPrompt(comment: string, language: string): string`
### 4. 文件替换模块 (FileReplacementModule)
```typescript
interface ReplacementOperation {
file: string;
replacements: Array<{
start: number;
end: number;
original: string;
replacement: string;
}>;
}
```
**功能职责:**
- 精确替换文件中的中文注释
- 保持代码格式和缩进
- 创建备份机制
**核心函数:**
- `replaceCommentsInFile(file: SourceFile, replacements: ReplacementOperation): Promise<void>`
- `createBackup(filePath: string): Promise<string>`
- `applyReplacements(content: string, replacements: Replacement[]): string`
### 5. 报告生成模块 (ReportModule)
```typescript
interface ProcessingReport {
totalFiles: number;
processedFiles: number;
translatedComments: number;
errors: Error[];
duration: number;
details: FileProcessingDetail[];
}
interface FileProcessingDetail {
file: string;
commentCount: number;
status: 'success' | 'error' | 'skipped';
errorMessage?: string;
}
```
**功能职责:**
- 记录处理过程中的统计信息
- 生成详细的处理报告
- 记录错误和异常情况
## 命令行接口设计
### 主命令
```bash
ai-translate --root <directory> --exts <extensions> [options]
```
### 参数说明
- `--root, -r <directory>`: 需要处理的根目录(必填)
- `--exts, -e <extensions>`: 文件扩展名数组,如 "ts,js,go,md"(可选,默认处理所有文本文件)
- `--openai-key <key>`: OpenAI API密钥可选也可通过环境变量提供
- `--model <model>`: OpenAI模型名称可选默认gpt-3.5-turbo
- `--dry-run`: 仅分析不实际修改文件(可选)
- `--backup`: 创建文件备份(可选,默认启用)
- `--verbose, -v`: 详细输出模式(可选)
- `--output <file>`: 报告输出文件(可选)
## 技术实现细节
### 1. 函数式编程范式
采用FP风格主要体现在
- 纯函数设计:无副作用的数据转换
- 不可变数据结构使用immutable.js或原生不可变操作
- 函数组合通过pipe和compose构建处理流水线
- 错误处理使用Either/Maybe模式处理异常
### 2. 异步处理策略
```typescript
// 使用函数式风格的异步处理管道
const processRepository = pipe(
getGitTrackedFiles,
asyncMap(readFile),
asyncFilter(hasChineseComments),
asyncMap(extractChineseComments),
asyncMap(translateComments),
asyncMap(applyTranslations),
generateReport
);
```
### 3. 错误处理机制
- 使用Result类型封装成功/失败状态
- 实现重试机制处理网络错误
- 记录详细的错误日志和上下文
- 支持部分失败的情况下继续处理
### 4. 性能优化
- 并发处理合理控制并发数量避免API限制
- 缓存机制:避免重复翻译相同内容
- 增量处理:仅处理修改过的文件
- 流式处理:大文件分块处理
## 项目结构
```
src/convert-comments/
├── index.ts # 主入口文件
├── cli/
│ ├── command.ts # Commander.js命令定义
│ └── config.ts # 配置管理
├── modules/
│ ├── file-scan.ts # 文件扫描模块
│ ├── chinese-detection.ts # 中文检测模块
│ ├── translation.ts # 翻译服务模块
│ ├── file-replacement.ts # 文件替换模块
│ └── report.ts # 报告生成模块
├── utils/
│ ├── git.ts # Git操作工具
│ ├── language.ts # 编程语言识别
│ ├── chinese.ts # 中文字符检测
│ └── fp.ts # 函数式编程工具
├── types/
│ ├── index.ts # 类型定义
│ └── config.ts # 配置类型
└── __tests__/
├── unit/ # 单元测试
└── integration/ # 集成测试
```
## 依赖包选择
### 核心依赖
- `commander`: 命令行接口
- `openai`: OpenAI API客户端
- `simple-git`: Git操作
- `ramda``lodash/fp`: 函数式编程工具
### 开发依赖
- `typescript`: TypeScript编译器
- `@types/node`: Node.js类型定义
- `jest`: 测试框架
- `prettier`: 代码格式化
- `eslint`: 代码检查
## 开发阶段规划
### 阶段1基础框架搭建
1. 项目初始化和依赖安装
2. TypeScript配置和构建脚本
3. 命令行接口框架
4. 基础类型定义
### 阶段2核心功能实现
1. 文件扫描模块
2. 中文检测模块
3. 翻译服务模块
4. 文件替换模块
### 阶段3完善和优化
1. 报告生成模块
2. 错误处理和重试机制
3. 性能优化
4. 单元测试和集成测试
### 阶段4发布准备
1. 文档完善
2. 使用示例
3. 打包和发布脚本
4. CI/CD配置
## 使用示例
```bash
# 基本使用
ai-translate --root ./src --exts ts,js,go
# 仅分析不修改
ai-translate --root ./src --dry-run
# 指定OpenAI配置
ai-translate --root ./src --openai-key sk-... --model gpt-4
# 生成详细报告
ai-translate --root ./src --verbose --output report.json
```
## 质量保证
### 测试策略
- 单元测试:覆盖所有核心函数
- 集成测试:端到端流程验证
- 性能测试:大型仓库处理能力
- 安全测试API密钥保护
### 代码质量
- TypeScript严格模式
- ESLint规则检查
- Prettier代码格式化
- 代码覆盖率要求>90%
## 风险和缓解措施
### 主要风险
1. **API配额限制**OpenAI API调用频率限制
2. **翻译质量**:自动翻译可能不够准确
3. **文件损坏**:替换操作可能破坏文件
4. **性能问题**:大型仓库处理时间过长
### 缓解措施
1. 实现智能重试和降级机制
2. 提供人工审核和修正功能
3. 强制备份和回滚机制
4. 并发控制和进度显示

View File

@ -0,0 +1,232 @@
#!/usr/bin/env node
import { createProgram, parseOptions, showHelp } from './cli/command';
import { loadConfig, validateConfig, printConfigInfo } from './cli/config';
import { scanSourceFiles } from './modules/file-scan';
import { detectChineseInFiles } from './modules/chinese-detection';
import { TranslationService } from './modules/translation';
import {
createReplacements,
replaceCommentsInFile,
} from './modules/file-replacement';
import {
ReportCollector,
ProgressDisplay,
generateReport,
saveReportToFile,
} from './modules/report';
import { FileScanConfig } from './types/index';
/**
*
*/
async function processRepository(
rootPath: string,
extensions: string[],
config: any,
dryRun: boolean = false,
verbose: boolean = false,
): Promise<void> {
const reportCollector = new ReportCollector();
try {
console.log('🚀 开始处理代码仓库...');
if (verbose) {
printConfigInfo(config, true);
}
// 1. 扫描源文件
console.log('\n📁 扫描源文件...');
const scanConfig: FileScanConfig = {
root: rootPath,
extensions,
ignorePatterns: config.git.ignorePatterns,
includeUntracked: config.git.includeUntracked,
};
const filesResult = await scanSourceFiles(scanConfig);
if (!filesResult.success) {
throw new Error(`文件扫描失败: ${filesResult.error}`);
}
const sourceFiles = filesResult.data;
console.log(`✅ 找到 ${sourceFiles.length} 个源文件`);
if (sourceFiles.length === 0) {
console.log('⚠️ 未找到任何源文件,请检查根目录和文件扩展名设置');
return;
}
// 2. 检测中文注释
console.log('\n🔍 检测中文注释...');
const filesWithComments = detectChineseInFiles(sourceFiles);
const totalComments = filesWithComments.reduce(
(sum, file) => sum + file.chineseComments.length,
0,
);
console.log(
`✅ 在 ${filesWithComments.length} 个文件中找到 ${totalComments} 条中文注释`,
);
if (totalComments === 0) {
console.log('✅ 未发现中文注释,无需处理');
return;
}
// 3. 初始化翻译服务
console.log('\n🤖 初始化翻译服务...');
const translationService = new TranslationService(config.translation);
// 4. 处理文件
console.log('\n🔄 开始翻译处理...');
const progressDisplay = new ProgressDisplay(filesWithComments.length);
for (let i = 0; i < filesWithComments.length; i++) {
const fileWithComments = filesWithComments[i];
const { file, chineseComments } = fileWithComments;
progressDisplay.update(i + 1, file.path);
reportCollector.recordFileStart(file.path);
try {
// 翻译注释
const translations = await translationService.batchTranslate(
chineseComments,
config.translation.concurrency,
);
if (verbose) {
console.log(`\n📝 ${file.path}:`);
translations.forEach((translation, index) => {
console.log(
` ${index + 1}. "${translation.original}" → "${translation.translated}"`,
);
});
}
// 如果不是干运行模式,则替换文件内容
if (!dryRun) {
const replacements = createReplacements(
file,
chineseComments,
translations,
);
const operation = { file: file.path, replacements };
const result = await replaceCommentsInFile(
file,
operation,
);
if (!result.success) {
throw new Error(result.error || '文件替换失败');
}
}
reportCollector.recordFileComplete(file.path, chineseComments.length);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`\n❌ 处理文件失败: ${file.path} - ${errorMessage}`);
reportCollector.recordError(
file.path,
error instanceof Error ? error : new Error(errorMessage),
);
}
}
progressDisplay.complete();
// 5. 生成报告
console.log('\n📊 生成处理报告...');
const report = reportCollector.generateReport();
if (dryRun) {
console.log('\n🔍 预览模式 - 未实际修改文件');
}
// 显示报告
const reportText = generateReport(report, 'console');
console.log(reportText);
// 保存报告到文件(如果指定了输出路径)
if (config.outputFile) {
await saveReportToFile(
report,
config.outputFile,
config.processing.outputFormat,
);
console.log(`📄 报告已保存到: ${config.outputFile}`);
}
} catch (error) {
console.error('\n💥 处理过程中发生错误:', error);
process.exit(1);
}
}
/**
*
*/
async function main(): Promise<void> {
try {
const program = createProgram();
// 解析命令行参数
program.parse();
const options = parseOptions(program);
// 加载配置
const config = await loadConfig(options);
// 验证配置
const validation = validateConfig(config);
if (!validation.valid) {
console.error('❌ 配置验证失败:');
validation.errors.forEach(error => console.error(` - ${error}`));
showHelp();
process.exit(1);
}
// 解析文件扩展名
const extensions = options.exts
? options.exts.split(',').map(ext => ext.trim())
: config.processing.defaultExtensions;
// 添加输出文件配置
const fullConfig = {
...config,
outputFile: options.output,
};
// 执行处理
await processRepository(
options.root,
extensions,
fullConfig,
options.dryRun || false,
options.verbose || false,
);
} catch (error) {
console.error('💥 程序执行失败:', error);
process.exit(1);
}
}
// 处理未捕获的异常
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
process.exit(1);
});
process.on('uncaughtException', error => {
console.error('未捕获的异常:', error);
process.exit(1);
});
// 运行主函数
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}

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, // 需要实际统计命中率
};
}
}

View File

@ -0,0 +1,27 @@
# 代码仓库中的中文备注转换为英文
## 简介
一段 JS 脚本,用于将代码仓库内的中文备注转换为英文。
## 执行逻辑:
1. 调用 git 命令,先查出本仓库下所有源码文件
2. 读取文件内容,分析文件中(可能包含 go 和ts、js、md 等代码,主要关注文本型文件)是否包含中文备注,是的话将文件目录加进待处理数组 tasks 中
3. 遍历 tasks 中的文件,提取出中文代码备注,调用 openai 接口,进行翻译,翻译后插入源位置
4. 所有文件均处理完毕后,输出报告
## 命令行参数:
命令名称ai-translate
参数:
- `root`: 需要执行命令的目录
- `exts`: 需要进行处理的文件扩展名,支持数组,默认为空
## 技术约束
- 使用 ts 编写代码
- 调用 openai api 做翻译
- 使用 commander 实现命令行交互
- 优先使用 FP 编写代码

View File

@ -0,0 +1,636 @@
# 中文备注转换为英文 - 技术规格说明
## 1. 文件扫描模块详细设计
### 1.1 Git文件获取
```typescript
import simpleGit from 'simple-git';
/**
* 获取Git仓库中的所有已跟踪文件
*/
export const getGitTrackedFiles = async (root: string): Promise<string[]> => {
const git = simpleGit(root);
const files = await git.raw(['ls-files']);
return files
.split('\n')
.filter(Boolean)
.map(file => path.resolve(root, file));
};
```
### 1.2 文件扩展名过滤
```typescript
/**
* 根据扩展名过滤文件
*/
export const filterFilesByExtensions = (
files: string[],
extensions: string[]
): string[] => {
if (extensions.length === 0) {
// 默认支持的文本文件扩展名
const defaultExtensions = ['.ts', '.js', '.jsx', '.tsx', '.go', '.md', '.txt', '.json'];
return files.filter(file =>
defaultExtensions.some(ext => file.endsWith(ext))
);
}
return files.filter(file =>
extensions.some(ext => file.endsWith(`.${ext}`))
);
};
```
### 1.3 编程语言识别
```typescript
export const detectLanguage = (filePath: string): SourceFileLanguage => {
const ext = path.extname(filePath).toLowerCase();
const languageMap: Record<string, SourceFileLanguage> = {
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.go': 'go',
'.md': 'markdown',
'.txt': 'text',
'.json': 'json'
};
return languageMap[ext] || 'other';
};
```
## 2. 中文检测模块详细设计
### 2.1 注释解析器
```typescript
interface CommentPattern {
single: RegExp;
multiStart: RegExp;
multiEnd: RegExp;
}
const commentPatterns: Record<string, CommentPattern> = {
typescript: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
javascript: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
go: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
markdown: {
single: /<!--(.*)-->/g,
multiStart: /<!--/g,
multiEnd: /-->/g
}
};
```
### 2.2 中文字符检测
```typescript
/**
* 检测文本是否包含中文字符
*/
export const containsChinese = (text: string): boolean => {
// Unicode范围中日韩统一表意文字
const chineseRegex = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/;
return chineseRegex.test(text);
};
/**
* 提取文本中的中文部分
*/
export const extractChineseParts = (text: string): string[] => {
const chineseRegex = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3000-\u303f\uff00-\uffef]+/g;
return text.match(chineseRegex) || [];
};
```
### 2.3 注释位置定位
```typescript
export const parseComments = (content: string, language: string): ParsedComment[] => {
const pattern = commentPatterns[language];
if (!pattern) return [];
const comments: ParsedComment[] = [];
const lines = content.split('\n');
// 解析单行注释
lines.forEach((line, index) => {
const match = pattern.single.exec(line);
if (match && containsChinese(match[1])) {
comments.push({
content: match[1].trim(),
startLine: index + 1,
endLine: index + 1,
startColumn: match.index,
endColumn: match.index + match[0].length,
type: 'single-line'
});
}
});
// 解析多行注释
const multiLineComments = parseMultiLineComments(content, pattern);
comments.push(...multiLineComments);
return comments;
};
```
## 3. 翻译服务模块详细设计
### 3.1 OpenAI API集成
```typescript
import OpenAI from 'openai';
export class TranslationService {
private openai: OpenAI;
private config: TranslationConfig;
constructor(config: TranslationConfig) {
this.config = config;
this.openai = new OpenAI({
apiKey: config.apiKey,
timeout: config.timeout,
});
}
async translateComment(
comment: string,
context?: TranslationContext
): Promise<TranslationResult> {
const prompt = this.createTranslationPrompt(comment, context);
try {
const response = await this.openai.chat.completions.create({
model: this.config.model,
messages: [
{
role: 'system',
content: 'You are a professional code comment translator. Translate Chinese comments to English while preserving the original meaning and code formatting.'
},
{
role: 'user',
content: prompt
}
],
temperature: 0.3,
max_tokens: 200
});
const translated = response.choices[0]?.message?.content?.trim();
if (!translated) {
throw new Error('Empty translation response');
}
return {
original: comment,
translated,
confidence: this.calculateConfidence(response)
};
} catch (error) {
throw new TranslationError(`Translation failed: ${error.message}`, comment);
}
}
}
```
### 3.2 翻译提示词优化
```typescript
private createTranslationPrompt(
comment: string,
context?: TranslationContext
): string {
const basePrompt = `
Translate the following Chinese code comment to English:
Chinese comment: "${comment}"
Requirements:
1. Keep the same tone and style
2. Preserve any code-related terminology
3. Maintain brevity and clarity
4. Return only the translated text without quotes
`;
if (context) {
return basePrompt + `
Context:
- File type: ${context.language}
- Function/Variable name: ${context.nearbyCode}
- Comment type: ${context.commentType}
`;
}
return basePrompt;
}
```
### 3.3 批量翻译优化
```typescript
export const batchTranslate = async (
comments: ChineseComment[],
service: TranslationService,
concurrency: number = 3
): Promise<TranslationResult[]> => {
const semaphore = new Semaphore(concurrency);
return Promise.all(
comments.map(async (comment) => {
await semaphore.acquire();
try {
return await service.translateComment(comment.content);
} finally {
semaphore.release();
}
})
);
};
```
## 4. 文件替换模块详细设计
### 4.1 精确位置替换
```typescript
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;
};
```
### 4.2 备份机制
```typescript
export const createBackup = async (filePath: string): Promise<string> => {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${filePath}.backup.${timestamp}`;
await fs.copyFile(filePath, backupPath);
return backupPath;
};
export const restoreFromBackup = async (
originalPath: string,
backupPath: string
): Promise<void> => {
await fs.copyFile(backupPath, originalPath);
await fs.unlink(backupPath);
};
```
### 4.3 格式保持
```typescript
/**
* 保持注释的原始缩进和格式
*/
export const preserveCommentFormat = (
originalComment: string,
translatedComment: string,
commentType: CommentType
): string => {
const originalLines = originalComment.split('\n');
const translatedLines = translatedComment.split('\n');
if (commentType === 'single-line') {
// 保持单行注释的前缀空格
const leadingSpaces = originalComment.match(/^(\s*)/)?.[1] || '';
return leadingSpaces + translatedComment.trim();
}
if (commentType === 'multi-line') {
// 保持多行注释的对齐
return translatedLines
.map((line, index) => {
const originalLine = originalLines[index];
if (originalLine) {
const leadingSpaces = originalLine.match(/^(\s*)/)?.[1] || '';
return leadingSpaces + line.trim();
}
return line;
})
.join('\n');
}
return translatedComment;
};
```
## 5. 报告生成模块详细设计
### 5.1 统计数据收集
```typescript
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;
}
}
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 });
}
}
```
### 5.2 报告格式化
```typescript
export const generateReport = (
collector: ReportCollector,
format: 'json' | 'markdown' | 'console' = 'console'
): string => {
const stats = collector.getStats();
switch (format) {
case 'json':
return JSON.stringify(stats, null, 2);
case 'markdown':
return generateMarkdownReport(stats);
case 'console':
default:
return generateConsoleReport(stats);
}
};
const generateConsoleReport = (stats: ProcessingStats): string => {
const duration = (stats.endTime - stats.startTime) / 1000;
return `
📊 翻译处理报告
==================
总文件数: ${stats.totalFiles}
处理成功: ${stats.processedFiles}
跳过文件: ${stats.skippedFiles}
翻译注释: ${stats.translatedComments}
错误数量: ${stats.errors.length}
处理时间: ${duration.toFixed(2)}秒
${stats.errors.length > 0 ? '❌ 错误详情:\n' + stats.errors.map(e => ` ${e.file}: ${e.error}`).join('\n') : '✅ 处理完成,无错误'}
`;
};
```
## 6. 函数式编程工具
### 6.1 基础FP工具
```typescript
export const pipe = <T>(...fns: Function[]) => (value: T) =>
fns.reduce((acc, fn) => fn(acc), value);
export const compose = <T>(...fns: Function[]) => (value: T) =>
fns.reduceRight((acc, fn) => fn(acc), value);
export const curry = (fn: Function) => (...args: any[]) =>
args.length >= fn.length
? fn(...args)
: (...more: any[]) => curry(fn)(...args, ...more);
```
### 6.2 异步处理工具
```typescript
export const asyncMap = curry(
async <T, U>(fn: (item: T) => Promise<U>, items: T[]): Promise<U[]> =>
Promise.all(items.map(fn))
);
export const asyncFilter = curry(
async <T>(predicate: (item: T) => Promise<boolean>, items: T[]): Promise<T[]> => {
const results = await Promise.all(items.map(predicate));
return items.filter((_, index) => results[index]);
}
);
export const asyncReduce = curry(
async <T, U>(
fn: (acc: U, item: T) => Promise<U>,
initial: U,
items: T[]
): Promise<U> => {
let result = initial;
for (const item of items) {
result = await fn(result, item);
}
return result;
}
);
```
### 6.3 错误处理
```typescript
export type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
export const success = <T>(data: T): Result<T> => ({ success: true, data });
export const failure = <E>(error: E): Result<never, E> => ({ success: false, error });
export const tryCatch = async <T>(
fn: () => Promise<T>
): Promise<Result<T>> => {
try {
const data = await fn();
return success(data);
} catch (error) {
return failure(error instanceof Error ? error : new Error(String(error)));
}
};
```
## 7. 配置管理
### 7.1 配置文件结构
```typescript
interface AppConfig {
translation: {
apiKey: string;
model: string;
maxRetries: number;
timeout: number;
concurrency: number;
};
processing: {
defaultExtensions: string[];
createBackup: boolean;
outputFormat: 'json' | 'markdown' | 'console';
};
git: {
ignorePatterns: string[];
includeUntracked: boolean;
};
}
```
### 7.2 配置加载
```typescript
export const loadConfig = async (configPath?: string): Promise<AppConfig> => {
const defaultConfig: AppConfig = {
translation: {
apiKey: process.env.OPENAI_API_KEY || '',
model: 'gpt-3.5-turbo',
maxRetries: 3,
timeout: 30000,
concurrency: 3
},
processing: {
defaultExtensions: ['ts', 'js', 'go', 'md'],
createBackup: true,
outputFormat: 'console'
},
git: {
ignorePatterns: ['node_modules/**', '.git/**', 'dist/**'],
includeUntracked: false
}
};
if (configPath && await fs.access(configPath).then(() => true).catch(() => false)) {
const userConfig = JSON.parse(await fs.readFile(configPath, 'utf-8'));
return deepMerge(defaultConfig, userConfig);
}
return defaultConfig;
};
```
## 8. 性能优化策略
### 8.1 并发控制
```typescript
export class Semaphore {
private permits: number;
private waiting: (() => void)[] = [];
constructor(permits: number) {
this.permits = permits;
}
async acquire(): Promise<void> {
if (this.permits > 0) {
this.permits--;
return;
}
return new Promise(resolve => {
this.waiting.push(resolve);
});
}
release(): void {
this.permits++;
const next = this.waiting.shift();
if (next) {
this.permits--;
next();
}
}
}
```
### 8.2 缓存机制
```typescript
export class TranslationCache {
private cache = new Map<string, TranslationResult>();
private hashFn = (text: string) => crypto.createHash('md5').update(text).digest('hex');
get(comment: string): TranslationResult | undefined {
const key = this.hashFn(comment);
return this.cache.get(key);
}
set(comment: string, result: TranslationResult): void {
const key = this.hashFn(comment);
this.cache.set(key, result);
}
async save(filePath: string): Promise<void> {
const data = Object.fromEntries(this.cache);
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
}
async load(filePath: string): Promise<void> {
try {
const data = JSON.parse(await fs.readFile(filePath, 'utf-8'));
this.cache = new Map(Object.entries(data));
} catch {
// 缓存文件不存在或损坏,忽略
}
}
}
```

View File

@ -0,0 +1,65 @@
/**
*
*/
export interface TranslationConfig {
accessKeyId: string;
secretAccessKey: string;
region: string;
sourceLanguage: string;
targetLanguage: string;
maxRetries: number;
timeout: number;
concurrency: number;
}
/**
*
*/
export interface FileScanConfig {
root: string;
extensions: string[];
ignorePatterns: string[];
includeUntracked: boolean;
}
/**
*
*/
export interface ProcessingConfig {
defaultExtensions: string[];
outputFormat: 'json' | 'markdown' | 'console';
}
/**
* Git配置
*/
export interface GitConfig {
ignorePatterns: string[];
includeUntracked: boolean;
}
/**
*
*/
export interface AppConfig {
translation: TranslationConfig;
processing: ProcessingConfig;
git: GitConfig;
}
/**
*
*/
export interface CliOptions {
root: string;
exts?: string;
accessKeyId?: string;
secretAccessKey?: string;
region?: string;
sourceLanguage?: string;
targetLanguage?: string;
dryRun?: boolean;
verbose?: boolean;
output?: string;
config?: string;
}

View File

@ -0,0 +1,192 @@
/**
*
*/
export type SourceFileLanguage =
| 'typescript'
| 'javascript'
| 'go'
| 'markdown'
| 'text'
| 'json'
| 'yaml'
| 'toml'
| 'ini'
| 'shell'
| 'python'
| 'css'
| 'html'
| 'xml'
| 'php'
| 'ruby'
| 'rust'
| 'java'
| 'c'
| 'cpp'
| 'csharp'
| 'other';
/**
*
*/
export type CommentType = 'single-line' | 'multi-line' | 'documentation';
/**
*
*/
export interface SourceFile {
path: string;
content: string;
language: SourceFileLanguage;
}
/**
*
*/
export interface MultiLineContext {
isPartOfMultiLine: boolean;
originalComment: ParsedComment;
lineIndexInComment: number;
totalLinesInComment: number;
}
/**
*
*/
export interface ChineseComment {
content: string;
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
type: CommentType;
multiLineContext?: MultiLineContext;
}
/**
*
*/
export interface FileWithComments {
file: SourceFile;
chineseComments: ChineseComment[];
}
/**
*
*/
export interface TranslationResult {
original: string;
translated: string;
confidence: number;
}
/**
*
*/
export interface TranslationContext {
language: string;
nearbyCode?: string;
commentType: CommentType;
}
/**
*
*/
export interface Replacement {
start: number;
end: number;
original: string;
replacement: string;
}
/**
*
*/
export interface ReplacementOperation {
file: string;
replacements: Replacement[];
}
/**
*
*/
export interface FileProcessingDetail {
file: string;
commentCount: number;
status: 'processing' | 'success' | 'error' | 'skipped';
errorMessage?: string;
startTime?: number;
endTime?: number;
}
/**
*
*/
export interface ProcessingStats {
totalFiles: number;
processedFiles: number;
translatedComments: number;
skippedFiles: number;
errors: Array<{ file: string; error: string }>;
startTime: number;
endTime: number;
}
/**
*
*/
export interface ProcessingReport {
stats: ProcessingStats;
details: FileProcessingDetail[];
duration: number;
}
/**
*
*/
export interface FileScanConfig {
root: string;
extensions: string[];
ignorePatterns: string[];
includeUntracked: boolean;
}
/**
*
*/
export interface ParsedComment {
content: string;
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
type: CommentType;
}
/**
*
*/
export interface CommentPattern {
single: RegExp;
multiStart: RegExp;
multiEnd: RegExp;
}
/**
*
*/
export type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
/**
*
*/
export class TranslationError extends Error {
constructor(
message: string,
public originalComment: string,
) {
super(message);
this.name = 'TranslationError';
}
}

View File

@ -0,0 +1,120 @@
/**
* Unicode范围正则表达式
*/
const CHINESE_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/;
const CHINESE_EXTRACT_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3000-\u303f\uff00-\uffef]+/g;
/**
*
*/
export const containsChinese = (text: string): boolean => {
return CHINESE_REGEX.test(text);
};
/**
*
*/
export const extractChineseParts = (text: string): string[] => {
return text.match(CHINESE_EXTRACT_REGEX) || [];
};
/**
*
*/
export const countChineseCharacters = (text: string): number => {
const matches = text.match(CHINESE_EXTRACT_REGEX);
if (!matches) return 0;
return matches.reduce((count, match) => count + match.length, 0);
};
/**
*
*/
export const isPrimarilyChinese = (text: string, threshold: number = 0.5): boolean => {
const totalLength = text.length;
if (totalLength === 0) return false;
const chineseLength = countChineseCharacters(text);
return chineseLength / totalLength >= threshold;
};
/**
*
*/
export const cleanCommentText = (
text: string,
commentType: 'single-line' | 'multi-line',
language?: string
): string => {
let cleaned = text;
if (commentType === 'single-line') {
// 根据语言类型移除不同的单行注释符号
switch (language) {
case 'yaml':
case 'toml':
case 'shell':
case 'python':
case 'ruby':
cleaned = cleaned.replace(/^#\s*/, '');
break;
case 'ini':
cleaned = cleaned.replace(/^[;#]\s*/, '');
break;
case 'php':
cleaned = cleaned.replace(/^(?:\/\/|#)\s*/, '');
break;
default:
// JavaScript/TypeScript/Go/Java/C/C++/C# style
cleaned = cleaned.replace(/^\/\/\s*/, '');
}
} else if (commentType === 'multi-line') {
// 根据语言类型移除不同的多行注释符号
switch (language) {
case 'html':
case 'xml':
case 'markdown':
cleaned = cleaned.replace(/^<!--\s*/, '').replace(/\s*-->$/, '');
break;
case 'python':
cleaned = cleaned.replace(/^"""\s*/, '').replace(/\s*"""$/, '');
break;
case 'ruby':
cleaned = cleaned.replace(/^=begin\s*/, '').replace(/\s*=end$/, '');
break;
default:
// JavaScript/TypeScript/Go/Java/C/C++/C#/CSS style
cleaned = cleaned.replace(/^\/\*\s*/, '').replace(/\s*\*\/$/, '');
// 移除每行开头的 * 符号
cleaned = cleaned.replace(/^\s*\*\s?/gm, '');
}
}
// 移除多余的空格和换行
cleaned = cleaned.trim();
return cleaned;
};
/**
*
*/
export const isValidTranslation = (original: string, translated: string): boolean => {
// 基本验证
if (!translated || translated.trim().length === 0) {
return false;
}
// 检查是否还包含中文(可能翻译失败)
if (containsChinese(translated)) {
return false;
}
// 检查长度是否合理(翻译后的文本不应该比原文长太多)
if (translated.length > original.length * 3) {
return false;
}
return true;
};

View File

@ -0,0 +1,173 @@
import { Result } from '../types/index';
/**
* -
*/
export const pipe =
<T>(...fns: Function[]) =>
(value: T) =>
fns.reduce((acc, fn) => fn(acc), value);
/**
* -
*/
export const compose =
<T>(...fns: Function[]) =>
(value: T) =>
fns.reduceRight((acc, fn) => fn(acc), value);
/**
*
*/
export const curry =
(fn: Function) =>
(...args: any[]) =>
args.length >= fn.length
? fn(...args)
: (...more: any[]) => curry(fn)(...args, ...more);
/**
*
*/
export const asyncMap = curry(
async <T, U>(fn: (item: T) => Promise<U>, items: T[]): Promise<U[]> =>
Promise.all(items.map(fn)),
);
/**
*
*/
export const asyncFilter = curry(
async <T>(
predicate: (item: T) => Promise<boolean>,
items: T[],
): Promise<T[]> => {
const results = await Promise.all(items.map(predicate));
return items.filter((_, index) => results[index]);
},
);
/**
*
*/
export const asyncReduce = curry(
async <T, U>(
fn: (acc: U, item: T) => Promise<U>,
initial: U,
items: T[],
): Promise<U> => {
let result = initial;
for (const item of items) {
result = await fn(result, item);
}
return result;
},
);
/**
*
*/
export const success = <T>(data: T): Result<T> => ({ success: true, data });
/**
*
*/
export const failure = <E>(error: E): Result<never, E> => ({
success: false,
error,
});
/**
*
*/
export const tryCatch = async <T>(fn: () => Promise<T>): Promise<Result<T>> => {
try {
const data = await fn();
return success(data);
} catch (error) {
return failure(error instanceof Error ? error : new Error(String(error)));
}
};
/**
*
*/
export const tryCatchSync = <T>(fn: () => T): Result<T> => {
try {
const data = fn();
return success(data);
} catch (error) {
return failure(error instanceof Error ? error : new Error(String(error)));
}
};
/**
*
*/
export const chunk = <T>(array: T[], size: number): T[][] => {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
};
/**
*
*/
export const delay = (ms: number): Promise<void> =>
new Promise(resolve => setTimeout(resolve, ms));
/**
*
*/
export const retry = async <T>(
fn: () => Promise<T>,
maxAttempts: number = 3,
delayMs: number = 1000,
): Promise<T> => {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxAttempts) {
await delay(delayMs * attempt); // 指数退避
}
}
}
throw lastError!;
};
/**
*
*/
export const deepMerge = <T extends Record<string, any>>(
target: T,
source: Partial<T>,
): T => {
const result = { ...target } as T;
for (const key in source) {
if (source[key] !== undefined) {
if (
typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key]) &&
typeof target[key] === 'object' &&
target[key] !== null &&
!Array.isArray(target[key])
) {
(result as any)[key] = deepMerge(target[key], source[key]!);
} else {
(result as any)[key] = source[key]!;
}
}
}
return result;
};

View File

@ -0,0 +1,94 @@
import { simpleGit } from 'simple-git';
import * as path from 'path';
import { tryCatch } from './fp';
import { Result } from '../types/index';
/**
* Git仓库中的所有已跟踪文件
*/
export const getGitTrackedFiles = async (
root: string,
): Promise<Result<string[]>> => {
return tryCatch(async () => {
const git = simpleGit(root);
const files = await git.raw(['ls-files']);
return files
.split('\n')
.filter(Boolean)
.map(file => path.resolve(root, file));
});
};
/**
* Git仓库中的所有文件
*/
export const getAllGitFiles = async (
root: string,
): Promise<Result<string[]>> => {
return tryCatch(async () => {
const git = simpleGit(root);
// 获取已跟踪的文件
const trackedFiles = await git.raw(['ls-files']);
const trackedFilesArray = trackedFiles
.split('\n')
.filter(Boolean)
.map(file => path.resolve(root, file));
// 获取未跟踪的文件
const status = await git.status();
const untrackedFiles = status.not_added.map(file =>
path.resolve(root, file),
);
// 合并并去重
const allFiles = [...new Set([...trackedFilesArray, ...untrackedFiles])];
return allFiles;
});
};
/**
* Git仓库
*/
export const isGitRepository = async (
root: string,
): Promise<Result<boolean>> => {
return tryCatch(async () => {
const git = simpleGit(root);
await git.status();
return true;
});
};
/**
* Git仓库的根目录
*/
export const getGitRoot = async (cwd: string): Promise<Result<string>> => {
return tryCatch(async () => {
const git = simpleGit(cwd);
const root = await git.revparse(['--show-toplevel']);
return root.trim();
});
};
/**
* Git忽略
*/
export const isIgnoredByGit = async (
root: string,
filePath: string,
): Promise<Result<boolean>> => {
return tryCatch(async () => {
const git = simpleGit(root);
const relativePath = path.relative(root, filePath);
try {
await git.raw(['check-ignore', relativePath]);
return true; // 文件被忽略
} catch {
return false; // 文件未被忽略
}
});
};

View File

@ -0,0 +1,227 @@
import { SourceFileLanguage, CommentPattern } from '../types/index';
/**
*
*/
export const detectLanguage = (filePath: string): SourceFileLanguage => {
const ext = filePath.toLowerCase().split('.').pop();
const languageMap: Record<string, SourceFileLanguage> = {
'ts': 'typescript',
'tsx': 'typescript',
'js': 'javascript',
'jsx': 'javascript',
'go': 'go',
'md': 'markdown',
'txt': 'text',
'json': 'json',
'yaml': 'yaml',
'yml': 'yaml',
'toml': 'toml',
'ini': 'ini',
'conf': 'ini',
'config': 'ini',
'sh': 'shell',
'bash': 'shell',
'zsh': 'shell',
'fish': 'shell',
'py': 'python',
'css': 'css',
'scss': 'css',
'sass': 'css',
'less': 'css',
'html': 'html',
'htm': 'html',
'xml': 'xml',
'php': 'php',
'rb': 'ruby',
'rs': 'rust',
'java': 'java',
'c': 'c',
'h': 'c',
'cpp': 'cpp',
'cxx': 'cpp',
'cc': 'cpp',
'hpp': 'cpp',
'cs': 'csharp'
};
return languageMap[ext || ''] || 'other';
};
/**
*
*/
export const filterFilesByExtensions = (
files: string[],
extensions: string[]
): string[] => {
if (extensions.length === 0) {
// 默认支持的文本文件扩展名
const defaultExtensions = [
'.ts', '.tsx', '.js', '.jsx', '.go', '.md', '.txt', '.json',
'.yaml', '.yml', '.toml', '.ini', '.conf', '.config',
'.sh', '.bash', '.zsh', '.fish', '.py', '.css', '.scss', '.sass', '.less',
'.html', '.htm', '.xml', '.php', '.rb', '.rs', '.java', '.c', '.h',
'.cpp', '.cxx', '.cc', '.hpp', '.cs'
];
return files.filter(file =>
defaultExtensions.some(ext => file.toLowerCase().endsWith(ext))
);
}
return files.filter(file => {
const lowerFile = file.toLowerCase();
return extensions.some(ext => {
const lowerExt = ext.toLowerCase();
// 如果扩展名已经有点号,直接使用;否则添加点号
const extWithDot = lowerExt.startsWith('.') ? lowerExt : `.${lowerExt}`;
return lowerFile.endsWith(extWithDot);
});
});
};
/**
*
*/
export const getCommentPatterns = (language: SourceFileLanguage): CommentPattern | null => {
const commentPatterns: Record<SourceFileLanguage, CommentPattern> = {
typescript: {
single: /(?:^|[^:])\s*\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
javascript: {
single: /(?:^|[^:])\s*\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
go: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
markdown: {
single: /<!--(.*)-->/g,
multiStart: /<!--/g,
multiEnd: /-->/g
},
text: {
single: /^(.*)$/gm, // 文本文件每行都可能是注释
multiStart: /^/g,
multiEnd: /$/g
},
json: {
single: /\/\/(.*)$/gm, // JSON通常不支持注释但一些工具支持
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
yaml: {
single: /#(.*)$/gm,
multiStart: /^$/g, // YAML不支持多行注释
multiEnd: /^$/g
},
toml: {
single: /#(.*)$/gm,
multiStart: /^$/g, // TOML不支持多行注释
multiEnd: /^$/g
},
ini: {
single: /[;#](.*)$/gm, // INI文件支持 ; 和 # 作为注释
multiStart: /^$/g, // INI不支持多行注释
multiEnd: /^$/g
},
shell: {
single: /#(.*)$/gm,
multiStart: /^$/g, // Shell脚本不支持多行注释
multiEnd: /^$/g
},
python: {
single: /#(.*)$/gm,
multiStart: /"""[\s\S]*?$/gm, // Python docstring
multiEnd: /[\s\S]*?"""/gm
},
css: {
single: /^$/g, // CSS不支持单行注释
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
html: {
single: /^$/g, // HTML不支持单行注释
multiStart: /<!--/g,
multiEnd: /-->/g
},
xml: {
single: /^$/g, // XML不支持单行注释
multiStart: /<!--/g,
multiEnd: /-->/g
},
php: {
single: /(?:\/\/|#)(.*)$/gm, // PHP支持 // 和 # 作为单行注释
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
ruby: {
single: /#(.*)$/gm,
multiStart: /=begin/g,
multiEnd: /=end/g
},
rust: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
java: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
c: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
cpp: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
csharp: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
},
other: {
single: /\/\/(.*)$/gm,
multiStart: /\/\*/g,
multiEnd: /\*\//g
}
};
return commentPatterns[language] || null;
};
/**
*
*/
export const isSupportedFile = (filePath: string): boolean => {
const language = detectLanguage(filePath);
return language !== 'other';
};
/**
* MIME类型
*/
export const isTextFile = (filePath: string): boolean => {
const textExtensions = [
'.ts', '.tsx', '.js', '.jsx', '.go', '.md', '.txt', '.json',
'.css', '.scss', '.sass', '.less', '.html', '.htm', '.xml',
'.yaml', '.yml', '.toml', '.ini', '.conf', '.config',
'.sh', '.bash', '.zsh', '.fish', '.py', '.java', '.c', '.cpp', '.h', '.hpp', '.cs',
'.php', '.rb', '.rs', '.kt', '.swift', '.dart', '.scala'
];
return textExtensions.some(ext =>
filePath.toLowerCase().endsWith(ext)
);
};

View File

@ -0,0 +1,51 @@
/**
*
*/
export class Semaphore {
private permits: number;
private waiting: (() => void)[] = [];
constructor(permits: number) {
this.permits = permits;
}
/**
*
*/
async acquire(): Promise<void> {
if (this.permits > 0) {
this.permits--;
return;
}
return new Promise(resolve => {
this.waiting.push(resolve);
});
}
/**
*
*/
release(): void {
this.permits++;
const next = this.waiting.shift();
if (next) {
this.permits--;
next();
}
}
/**
*
*/
available(): number {
return this.permits;
}
/**
*
*/
waitingCount(): number {
return this.waiting.length;
}
}

View File

@ -0,0 +1,387 @@
/*
Copyright (year) Beijing Volcano Engine Technology Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import crypto from 'crypto';
import util from 'util';
import { URLSearchParams } from 'url';
const debuglog = util.debuglog('signer');
/**
*
*/
export interface SignParams {
headers?: Record<string, string>;
query?: Record<string, string | string[] | undefined>;
region?: string;
serviceName?: string;
method?: string;
pathName?: string;
accessKeyId?: string;
secretAccessKey?: string;
needSignHeaderKeys?: string[];
bodySha?: string;
}
/**
*
*/
export type QueryParams = Record<string, string | string[] | undefined | null>;
/**
*
*/
export type Headers = Record<string, string>;
/**
*
*/
export interface TranslateRequest {
/** 源语言代码 */
SourceLanguage: string;
/** 目标语言代码 */
TargetLanguage: string;
/** 要翻译的文本列表 */
TextList: string[];
}
/**
*
*/
export interface TranslationExtra {
/** 输入字符数 */
input_characters: string;
/** 源语言 */
source_language: string;
}
/**
*
*/
export interface TranslationItem {
/** 翻译结果 */
Translation: string;
/** 检测到的源语言 */
DetectedSourceLanguage: string;
/** 额外信息 */
Extra: TranslationExtra;
}
/**
*
*/
export interface ResponseMetadata {
/** 请求ID */
RequestId: string;
/** 操作名称 */
Action: string;
/** API版本 */
Version: string;
/** 服务名称 */
Service: string;
/** 区域 */
Region: string;
}
/**
* API响应接口
*/
export interface VolcTranslateResponse {
/** 翻译结果列表 */
TranslationList: TranslationItem[];
/** 响应元数据 */
ResponseMetadata: ResponseMetadata;
/** 响应元数据(备用字段) */
ResponseMetaData?: ResponseMetadata;
}
/**
*
*/
export interface TranslateConfig {
/** 访问密钥ID */
accessKeyId: string;
/** 秘密访问密钥 */
secretAccessKey: string;
/** 服务区域 */
region?: string;
/** 源语言代码 */
sourceLanguage?: string;
/** 目标语言代码 */
targetLanguage?: string;
}
/**
* header key
*/
const HEADER_KEYS_TO_IGNORE = new Set([
'authorization',
'content-type',
'content-length',
'user-agent',
'presigned-expires',
'expect',
]);
/**
*
* @param textArray
* @param config 使
* @returns
*/
export async function translate(
textArray: string[],
config?: Partial<TranslateConfig>,
): Promise<VolcTranslateResponse> {
const translateConfig: TranslateConfig = {
accessKeyId: config?.accessKeyId!,
secretAccessKey: config?.secretAccessKey!,
region: config?.region!,
sourceLanguage: 'zh',
targetLanguage: 'en',
...config,
};
const requestBody: TranslateRequest = {
SourceLanguage: translateConfig.sourceLanguage!,
TargetLanguage: translateConfig.targetLanguage!,
TextList: textArray,
};
const requestBodyString = JSON.stringify(requestBody);
const signParams: SignParams = {
headers: {
// x-date header 是必传的
'X-Date': getDateTimeNow(),
'content-type': 'application/json',
},
method: 'POST',
query: {
Version: '2020-06-01',
Action: 'TranslateText',
},
accessKeyId: translateConfig.accessKeyId,
secretAccessKey: translateConfig.secretAccessKey,
serviceName: 'translate',
region: translateConfig.region!,
bodySha: getBodySha(requestBodyString),
};
// 正规化 query object 防止串化后出现 query 值为 undefined 情况
if (signParams.query) {
for (const [key, val] of Object.entries(signParams.query)) {
if (val === undefined || val === null) {
signParams.query[key] = '';
}
}
}
const authorization = sign(signParams);
const queryString = new URLSearchParams(
signParams.query as Record<string, string>,
).toString();
const res = await fetch(
`https://translate.volcengineapi.com/?${queryString}`,
{
headers: {
...signParams.headers,
Authorization: authorization,
},
body: requestBodyString,
method: signParams.method,
},
);
if (!res.ok) {
throw new Error(
`Translation request failed: ${res.status} ${res.statusText}`,
);
}
const result: VolcTranslateResponse = await res.json();
return result;
}
/**
*
*/
function sign(params: SignParams): string {
const {
headers = {},
query = {},
region = '',
serviceName = '',
method = '',
pathName = '/',
accessKeyId = '',
secretAccessKey = '',
needSignHeaderKeys = [],
bodySha,
} = params;
const datetime = headers['X-Date'];
const date = datetime.substring(0, 8); // YYYYMMDD
// 创建正规化请求
const [signedHeaders, canonicalHeaders] = getSignHeaders(
headers,
needSignHeaderKeys,
);
const canonicalRequest = [
method.toUpperCase(),
pathName,
queryParamsToString(query) || '',
`${canonicalHeaders}\n`,
signedHeaders,
bodySha || hash(''),
].join('\n');
const credentialScope = [date, region, serviceName, 'request'].join('/');
// 创建签名字符串
const stringToSign = [
'HMAC-SHA256',
datetime,
credentialScope,
hash(canonicalRequest),
].join('\n');
// 计算签名
const kDate = hmac(secretAccessKey, date);
const kRegion = hmac(kDate, region);
const kService = hmac(kRegion, serviceName);
const kSigning = hmac(kService, 'request');
const signature = hmac(kSigning, stringToSign).toString('hex');
debuglog(
'--------CanonicalString:\n%s\n--------SignString:\n%s',
canonicalRequest,
stringToSign,
);
return [
'HMAC-SHA256',
`Credential=${accessKeyId}/${credentialScope},`,
`SignedHeaders=${signedHeaders},`,
`Signature=${signature}`,
].join(' ');
}
/**
* HMAC-SHA256
*/
function hmac(secret: string | Buffer, s: string): Buffer {
return crypto.createHmac('sha256', secret).update(s, 'utf8').digest();
}
/**
* SHA256
*/
function hash(s: string): string {
return crypto.createHash('sha256').update(s, 'utf8').digest('hex');
}
/**
*
*/
function queryParamsToString(params: QueryParams): string {
return Object.keys(params)
.sort()
.map(key => {
const val = params[key];
if (typeof val === 'undefined' || val === null) {
return undefined;
}
const escapedKey = uriEscape(key);
if (!escapedKey) {
return undefined;
}
if (Array.isArray(val)) {
return `${escapedKey}=${val.map(uriEscape).sort().join(`&${escapedKey}=`)}`;
}
return `${escapedKey}=${uriEscape(val)}`;
})
.filter(v => v)
.join('&');
}
/**
*
*/
function getSignHeaders(
originHeaders: Headers,
needSignHeaders: string[],
): [string, string] {
function trimHeaderValue(header: string): string {
return header.toString?.().trim().replace(/\s+/g, ' ') ?? '';
}
let h = Object.keys(originHeaders);
// 根据 needSignHeaders 过滤
if (Array.isArray(needSignHeaders)) {
const needSignSet = new Set(
[...needSignHeaders, 'x-date', 'host'].map(k => k.toLowerCase()),
);
h = h.filter(k => needSignSet.has(k.toLowerCase()));
}
// 根据 ignore headers 过滤
h = h.filter(k => !HEADER_KEYS_TO_IGNORE.has(k.toLowerCase()));
const signedHeaderKeys = h
.slice()
.map(k => k.toLowerCase())
.sort()
.join(';');
const canonicalHeaders = h
.sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1))
.map(k => `${k.toLowerCase()}:${trimHeaderValue(originHeaders[k])}`)
.join('\n');
return [signedHeaderKeys, canonicalHeaders];
}
/**
* URI
*/
function uriEscape(str: string): string {
try {
return encodeURIComponent(str)
.replace(/[^A-Za-z0-9_.~\-%]+/g, match =>
match
.split('')
.map(c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
.join(''),
)
.replace(/[*]/g, ch => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
} catch (e) {
return '';
}
}
/**
*
*/
export function getDateTimeNow(): string {
const now = new Date();
return now.toISOString().replace(/[:-]|\.\d{3}/g, '');
}
/**
* body SHA256
*/
export function getBodySha(body: string | URLSearchParams | Buffer): string {
const hashInstance = crypto.createHash('sha256');
if (typeof body === 'string') {
hashInstance.update(body);
} else if (body instanceof URLSearchParams) {
hashInstance.update(body.toString());
} else if (Buffer.isBuffer(body)) {
hashInstance.update(body);
}
return hashInstance.digest('hex');
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"allowJs": false,
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"declaration": true,
"composite": true,
"incremental": true,
"strictNullChecks": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"esModuleInterop": true,
"experimentalDecorators": true,
"outDir": "./dist",
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"module": "es2020",
"noFallthroughCasesInSwitch": true,
// This general feedback will make the code verbose, tentatively follow the original bot's settings, close
"noImplicitReturns": false,
"removeComments": false,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"disableReferencedProjectLoad": true,
// "disableSolutionSearching": true,
// "disableSourceOfProjectReferenceRedirect": true,
"target": "es2018"
},
"watchOptions": {
"fallbackPolling": "dynamicpriority",
"synchronousWatchDirectory": false,
"watchDirectory": "fixedChunkSizePolling",
"watchFile": "useFsEventsOnParentDirectory"
}
}

View File

@ -3,6 +3,10 @@ const spawn = require('cross-spawn')
const defaultConfig = require('cz-customizable');
const { getChangedPackages } = require('./utils')
/**
* 针对不同类型的 commit prefix message
*/
const typesConfig = [
{ value: 'feat', name: 'A new feature' },
{ value: 'fix', name: 'A bug fix' },