chore: remove all cn comments (#277)
This commit is contained in:
parent
875e97a40d
commit
f93f26fc48
|
|
@ -52,3 +52,5 @@ common/temp
|
|||
|
||||
|
||||
backend/conf/model/*.yaml
|
||||
|
||||
*.tsbuildinfo
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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! 🎉**
|
||||
|
|
@ -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');
|
||||
};
|
||||
|
|
@ -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 ? '是' : '否'}`);
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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. 并发控制和进度显示
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -0,0 +1,470 @@
|
|||
import {
|
||||
SourceFile,
|
||||
ChineseComment,
|
||||
ParsedComment,
|
||||
FileWithComments,
|
||||
CommentType,
|
||||
MultiLineContext
|
||||
} from '../types/index';
|
||||
import { getCommentPatterns } from '../utils/language';
|
||||
import { containsChinese, cleanCommentText } from '../utils/chinese';
|
||||
|
||||
/**
|
||||
* 检查指定位置是否在字符串字面量内部
|
||||
*/
|
||||
const isInsideStringLiteral = (line: string, position: number): boolean => {
|
||||
let insideDoubleQuote = false;
|
||||
let insideSingleQuote = false;
|
||||
let insideBacktick = false;
|
||||
let escapeNext = false;
|
||||
|
||||
for (let i = 0; i < position; i++) {
|
||||
const char = line[i];
|
||||
|
||||
if (escapeNext) {
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' && !insideSingleQuote && !insideBacktick) {
|
||||
insideDoubleQuote = !insideDoubleQuote;
|
||||
} else if (char === "'" && !insideDoubleQuote && !insideBacktick) {
|
||||
insideSingleQuote = !insideSingleQuote;
|
||||
} else if (char === '`' && !insideDoubleQuote && !insideSingleQuote) {
|
||||
insideBacktick = !insideBacktick;
|
||||
}
|
||||
}
|
||||
|
||||
return insideDoubleQuote || insideSingleQuote || insideBacktick;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析单行注释
|
||||
*/
|
||||
const parseSingleLineComments = (
|
||||
content: string,
|
||||
pattern: RegExp,
|
||||
language?: string,
|
||||
): ParsedComment[] => {
|
||||
const comments: ParsedComment[] = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
// 添加安全检查
|
||||
const maxLines = 5000; // 降低到5000行
|
||||
if (lines.length > maxLines) {
|
||||
console.warn(`⚠️ 文件行数过多 (${lines.length}行),跳过单行注释解析`);
|
||||
return comments;
|
||||
}
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
pattern.lastIndex = 0; // 重置正则表达式索引
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// 查找所有匹配,但只保留不在字符串内的
|
||||
let matchCount = 0;
|
||||
const maxMatches = 100; // 限制每行最多匹配100次
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((match = pattern.exec(line)) !== null) {
|
||||
// 防止无限循环的多重保护
|
||||
matchCount++;
|
||||
if (matchCount > maxMatches) {
|
||||
console.warn(`⚠️ 单行匹配次数过多,中断处理: ${line.substring(0, 50)}...`);
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查 lastIndex 是否前进,防止无限循环
|
||||
if (pattern.global) {
|
||||
if (pattern.lastIndex <= lastIndex) {
|
||||
// 如果 lastIndex 没有前进,手动前进一位避免无限循环
|
||||
pattern.lastIndex = lastIndex + 1;
|
||||
if (pattern.lastIndex >= line.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
lastIndex = pattern.lastIndex;
|
||||
}
|
||||
|
||||
if (match[1]) {
|
||||
const commentContent = match[1];
|
||||
let commentStartIndex = match.index!;
|
||||
let commentLength = 2; // 默认为 //
|
||||
|
||||
// 根据语言确定注释符号
|
||||
if (
|
||||
language === 'yaml' ||
|
||||
language === 'toml' ||
|
||||
language === 'shell' ||
|
||||
language === 'python' ||
|
||||
language === 'ruby'
|
||||
) {
|
||||
commentStartIndex = line.indexOf('#', match.index!);
|
||||
commentLength = 1; // # 长度为 1
|
||||
} else if (language === 'ini') {
|
||||
// INI 文件可能使用 # 或 ;
|
||||
const hashIndex = line.indexOf('#', match.index!);
|
||||
const semicolonIndex = line.indexOf(';', match.index!);
|
||||
if (
|
||||
hashIndex >= 0 &&
|
||||
(semicolonIndex < 0 || hashIndex < semicolonIndex)
|
||||
) {
|
||||
commentStartIndex = hashIndex;
|
||||
commentLength = 1;
|
||||
} else if (semicolonIndex >= 0) {
|
||||
commentStartIndex = semicolonIndex;
|
||||
commentLength = 1;
|
||||
}
|
||||
} else if (language === 'php') {
|
||||
// PHP 可能使用 // 或 #
|
||||
const slashIndex = line.indexOf('//', match.index!);
|
||||
const hashIndex = line.indexOf('#', match.index!);
|
||||
if (slashIndex >= 0 && (hashIndex < 0 || slashIndex < hashIndex)) {
|
||||
commentStartIndex = slashIndex;
|
||||
commentLength = 2;
|
||||
} else if (hashIndex >= 0) {
|
||||
commentStartIndex = hashIndex;
|
||||
commentLength = 1;
|
||||
}
|
||||
} else {
|
||||
// JavaScript/TypeScript/Go/Java/C/C++/C# style
|
||||
commentStartIndex = line.indexOf('//', match.index!);
|
||||
commentLength = 2;
|
||||
}
|
||||
|
||||
const startColumn = commentStartIndex;
|
||||
const endColumn = startColumn + commentLength + commentContent.length;
|
||||
|
||||
// 检查注释开始位置是否在字符串内部
|
||||
if (
|
||||
commentStartIndex >= 0 &&
|
||||
!isInsideStringLiteral(line, commentStartIndex)
|
||||
) {
|
||||
comments.push({
|
||||
content: commentContent,
|
||||
startLine: index + 1,
|
||||
endLine: index + 1,
|
||||
startColumn,
|
||||
endColumn,
|
||||
type: 'single-line',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 防止无限循环
|
||||
if (!pattern.global) break;
|
||||
}
|
||||
});
|
||||
|
||||
return comments;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析多行注释
|
||||
*/
|
||||
const parseMultiLineComments = (
|
||||
content: string,
|
||||
startPattern: RegExp,
|
||||
endPattern: RegExp,
|
||||
): ParsedComment[] => {
|
||||
const comments: ParsedComment[] = [];
|
||||
const lines = content.split('\n');
|
||||
let inComment = false;
|
||||
let commentStart: { line: number; column: number } | null = null;
|
||||
let commentLines: string[] = [];
|
||||
|
||||
// 添加安全检查
|
||||
const maxLines = 5000; // 降低到5000行
|
||||
if (lines.length > maxLines) {
|
||||
console.warn(`⚠️ 文件行数过多 (${lines.length}行),跳过多行注释解析`);
|
||||
return comments;
|
||||
}
|
||||
|
||||
// 添加处理计数器,防止无限循环
|
||||
let processedLines = 0;
|
||||
const maxProcessedLines = 10000;
|
||||
|
||||
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
||||
const line = lines[lineIndex];
|
||||
|
||||
// 防止无限处理
|
||||
processedLines++;
|
||||
if (processedLines > maxProcessedLines) {
|
||||
console.warn(`⚠️ 处理行数超限,中断解析`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!inComment) {
|
||||
startPattern.lastIndex = 0;
|
||||
const startMatch = startPattern.exec(line);
|
||||
|
||||
if (startMatch && !isInsideStringLiteral(line, startMatch.index!)) {
|
||||
inComment = true;
|
||||
commentStart = { line: lineIndex + 1, column: startMatch.index! };
|
||||
|
||||
// 检查是否在同一行结束
|
||||
endPattern.lastIndex = startMatch.index! + startMatch[0].length;
|
||||
const endMatch = endPattern.exec(line);
|
||||
|
||||
if (endMatch) {
|
||||
// 单行多行注释
|
||||
const commentContent = line.substring(
|
||||
startMatch.index! + startMatch[0].length,
|
||||
endMatch.index!,
|
||||
);
|
||||
|
||||
comments.push({
|
||||
content: commentContent,
|
||||
startLine: lineIndex + 1,
|
||||
endLine: lineIndex + 1,
|
||||
startColumn: startMatch.index!,
|
||||
endColumn: endMatch.index! + endMatch[0].length,
|
||||
type: 'multi-line',
|
||||
});
|
||||
|
||||
inComment = false;
|
||||
commentStart = null;
|
||||
} else {
|
||||
// 多行注释开始
|
||||
const commentContent = line.substring(
|
||||
startMatch.index! + startMatch[0].length,
|
||||
);
|
||||
commentLines = [commentContent];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 在多行注释中
|
||||
endPattern.lastIndex = 0;
|
||||
const endMatch = endPattern.exec(line);
|
||||
|
||||
if (endMatch) {
|
||||
// 多行注释结束
|
||||
const commentContent = line.substring(0, endMatch.index!);
|
||||
commentLines.push(commentContent);
|
||||
|
||||
|
||||
comments.push({
|
||||
content: commentLines.join('\n'),
|
||||
startLine: commentStart!.line,
|
||||
endLine: lineIndex + 1,
|
||||
startColumn: commentStart!.column,
|
||||
endColumn: endMatch.index! + endMatch[0].length,
|
||||
type: 'multi-line',
|
||||
});
|
||||
|
||||
inComment = false;
|
||||
commentStart = null;
|
||||
commentLines = [];
|
||||
} else {
|
||||
// 继续多行注释
|
||||
commentLines.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return comments;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析文件中的所有注释
|
||||
*/
|
||||
export const parseComments = (file: SourceFile): ParsedComment[] => {
|
||||
const patterns = getCommentPatterns(file.language);
|
||||
if (!patterns) return [];
|
||||
|
||||
const singleLineComments = parseSingleLineComments(
|
||||
file.content,
|
||||
patterns.single,
|
||||
file.language,
|
||||
);
|
||||
const multiLineComments = parseMultiLineComments(
|
||||
file.content,
|
||||
patterns.multiStart,
|
||||
patterns.multiEnd,
|
||||
);
|
||||
|
||||
return [...singleLineComments, ...multiLineComments];
|
||||
};
|
||||
|
||||
/**
|
||||
* 过滤包含中文的注释,对多行注释进行逐行处理
|
||||
*/
|
||||
export const filterChineseComments = (
|
||||
comments: ParsedComment[],
|
||||
language?: string,
|
||||
): ChineseComment[] => {
|
||||
const result: ChineseComment[] = [];
|
||||
|
||||
for (const comment of comments) {
|
||||
if (comment.type === 'multi-line' && comment.content.includes('\n')) {
|
||||
// 多行注释:逐行处理
|
||||
const multiLineResults = processMultiLineCommentForChinese(comment, language);
|
||||
result.push(...multiLineResults);
|
||||
} else if (containsChinese(comment.content)) {
|
||||
// 单行注释或单行多行注释
|
||||
result.push({
|
||||
...comment,
|
||||
content: cleanCommentText(
|
||||
comment.content,
|
||||
comment.type === 'documentation' ? 'multi-line' : comment.type,
|
||||
language,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理多行注释,提取含中文的行作为独立的注释单元
|
||||
*/
|
||||
const processMultiLineCommentForChinese = (
|
||||
comment: ParsedComment,
|
||||
language?: string,
|
||||
): ChineseComment[] => {
|
||||
const lines = comment.content.split('\n');
|
||||
const result: ChineseComment[] = [];
|
||||
|
||||
lines.forEach((line, lineIndex) => {
|
||||
const cleanedLine = cleanCommentText(line, 'multi-line', language);
|
||||
|
||||
if (containsChinese(cleanedLine)) {
|
||||
// 计算这一行在原始文件中的位置
|
||||
const actualLineNumber = comment.startLine + lineIndex;
|
||||
|
||||
// 创建一个表示这一行的注释对象
|
||||
const lineComment: ChineseComment = {
|
||||
content: cleanedLine,
|
||||
startLine: actualLineNumber,
|
||||
endLine: actualLineNumber,
|
||||
startColumn: 0, // 这个值需要更精确计算,但对于多行注释内的行处理暂时用0
|
||||
endColumn: line.length,
|
||||
type: 'multi-line',
|
||||
// 添加多行注释的元数据,用于后续处理
|
||||
multiLineContext: {
|
||||
isPartOfMultiLine: true,
|
||||
originalComment: comment,
|
||||
lineIndexInComment: lineIndex,
|
||||
totalLinesInComment: lines.length
|
||||
}
|
||||
};
|
||||
|
||||
result.push(lineComment);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测文件中的中文注释
|
||||
*/
|
||||
export const detectChineseInFile = (file: SourceFile): ChineseComment[] => {
|
||||
try {
|
||||
// 简单防护:跳过大文件
|
||||
if (file.content.length > 500000) {
|
||||
// 500KB
|
||||
console.warn(
|
||||
`⚠️ 跳过大文件: ${file.path} (${file.content.length} 字符)`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 简单防护:跳过行数过多的文件
|
||||
const lines = file.content.split('\n');
|
||||
if (lines.length > 10000) {
|
||||
console.warn(`⚠️ 跳过多行文件: ${file.path} (${lines.length} 行)`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const allComments = parseComments(file);
|
||||
return filterChineseComments(allComments, file.language);
|
||||
} catch (error) {
|
||||
console.error(`❌ 文件处理失败: ${file.path} - ${error}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量检测多个文件中的中文注释
|
||||
*/
|
||||
export const detectChineseInFiles = (files: SourceFile[]): FileWithComments[] => {
|
||||
const results: FileWithComments[] = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const fileName = file.path.split('/').pop() || file.path;
|
||||
|
||||
console.log(`🔍 检测进度: ${i + 1}/${files.length} (当前: ${fileName})`);
|
||||
|
||||
try {
|
||||
const chineseComments = detectChineseInFile(file);
|
||||
|
||||
if (chineseComments.length > 0) {
|
||||
results.push({
|
||||
file,
|
||||
chineseComments,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ 完成: ${fileName} (找到 ${chineseComments.length} 条中文注释)`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`❌ 处理文件失败: ${fileName} - ${error}`);
|
||||
// 继续处理其他文件
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取注释统计信息
|
||||
*/
|
||||
export const getCommentStats = (files: SourceFile[]): {
|
||||
totalFiles: number;
|
||||
filesWithComments: number;
|
||||
totalComments: number;
|
||||
chineseComments: number;
|
||||
commentsByType: Record<CommentType, number>;
|
||||
} => {
|
||||
let totalComments = 0;
|
||||
let chineseComments = 0;
|
||||
let filesWithComments = 0;
|
||||
const commentsByType: Record<CommentType, number> = {
|
||||
'single-line': 0,
|
||||
'multi-line': 0,
|
||||
'documentation': 0
|
||||
};
|
||||
|
||||
files.forEach(file => {
|
||||
const allComments = parseComments(file);
|
||||
const chineseCommentsInFile = filterChineseComments(allComments, file.language);
|
||||
|
||||
if (chineseCommentsInFile.length > 0) {
|
||||
filesWithComments++;
|
||||
}
|
||||
|
||||
totalComments += allComments.length;
|
||||
chineseComments += chineseCommentsInFile.length;
|
||||
|
||||
chineseCommentsInFile.forEach(comment => {
|
||||
commentsByType[comment.type]++;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
totalFiles: files.length,
|
||||
filesWithComments,
|
||||
totalComments,
|
||||
chineseComments,
|
||||
commentsByType
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
import {
|
||||
Replacement,
|
||||
ReplacementOperation,
|
||||
SourceFile,
|
||||
ChineseComment,
|
||||
TranslationResult,
|
||||
} from '../types/index';
|
||||
import { tryCatch } from '../utils/fp';
|
||||
|
||||
|
||||
/**
|
||||
* 检查字符串是否包含中文字符
|
||||
*/
|
||||
const containsChinese = (text: string): boolean => {
|
||||
return /[\u4e00-\u9fff]/.test(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* 保持注释的原始格式,支持逐行翻译多行注释
|
||||
*/
|
||||
export const preserveCommentFormat = (
|
||||
originalComment: string,
|
||||
translatedComment: string,
|
||||
commentType: 'single-line' | 'multi-line',
|
||||
): string => {
|
||||
if (commentType === 'single-line') {
|
||||
// 保持单行注释的前缀空格和注释符 - 支持多种语言
|
||||
let match = originalComment.match(/^(\s*\/\/\s*)/); // JavaScript/TypeScript style
|
||||
if (match) {
|
||||
return match[1] + translatedComment.trim();
|
||||
}
|
||||
|
||||
match = originalComment.match(/^(\s*#\s*)/); // Shell/Python/YAML style
|
||||
if (match) {
|
||||
return match[1] + translatedComment.trim();
|
||||
}
|
||||
|
||||
match = originalComment.match(/^(\s*;\s*)/); // Some config files
|
||||
if (match) {
|
||||
return match[1] + translatedComment.trim();
|
||||
}
|
||||
|
||||
// 如果无法识别,尝试从原始内容推断
|
||||
if (originalComment.includes('#')) {
|
||||
const hashMatch = originalComment.match(/^(\s*#\s*)/);
|
||||
return (hashMatch ? hashMatch[1] : '# ') + translatedComment.trim();
|
||||
}
|
||||
|
||||
// 默认使用 JavaScript 风格
|
||||
return '// ' + translatedComment.trim();
|
||||
}
|
||||
|
||||
if (commentType === 'multi-line') {
|
||||
const lines = originalComment.split('\n');
|
||||
|
||||
if (lines.length === 1) {
|
||||
// 单行多行注释 /* ... */ 或 /** ... */
|
||||
const startMatch = originalComment.match(/^(\s*\/\*\*?\s*)/);
|
||||
const endMatch = originalComment.match(/(\s*\*\/\s*)$/);
|
||||
|
||||
let prefix = '/* ';
|
||||
let suffix = ' */';
|
||||
|
||||
if (startMatch) {
|
||||
prefix = startMatch[1];
|
||||
}
|
||||
|
||||
if (endMatch) {
|
||||
suffix = endMatch[1];
|
||||
}
|
||||
|
||||
return prefix + translatedComment.trim() + suffix;
|
||||
} else {
|
||||
// 多行注释 - 需要逐行处理
|
||||
return processMultiLineComment(originalComment, translatedComment);
|
||||
}
|
||||
}
|
||||
|
||||
return translatedComment;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理多行注释,逐行翻译含中文的行,保持其他行原样
|
||||
*/
|
||||
export const processMultiLineComment = (
|
||||
originalComment: string,
|
||||
translatedContent: string,
|
||||
): string => {
|
||||
const originalLines = originalComment.split('\n');
|
||||
|
||||
// 提取每行的注释内容(去除 /** * 等前缀)
|
||||
const extractedLines = originalLines.map(line => {
|
||||
// 匹配不同类型的注释行
|
||||
if (line.match(/^\s*\/\*\*?\s*/)) {
|
||||
// 开始行: /** 或 /*
|
||||
return { prefix: line.match(/^\s*\/\*\*?\s*/)![0], content: line.replace(/^\s*\/\*\*?\s*/, '') };
|
||||
} else if (line.match(/^\s*\*\/\s*$/)) {
|
||||
// 结束行: */
|
||||
return { prefix: line.match(/^\s*\*\/\s*$/)![0], content: '' };
|
||||
} else if (line.match(/^\s*\*\s*/)) {
|
||||
// 中间行: * content
|
||||
const match = line.match(/^(\s*\*\s*)(.*)/);
|
||||
return { prefix: match![1], content: match![2] };
|
||||
} else {
|
||||
// 其他情况
|
||||
return { prefix: '', content: line };
|
||||
}
|
||||
});
|
||||
|
||||
// 收集需要翻译的行
|
||||
const linesToTranslate = extractedLines
|
||||
.map((line, index) => ({ index, content: line.content }))
|
||||
.filter(item => containsChinese(item.content));
|
||||
|
||||
// 如果没有中文内容,返回原始注释
|
||||
if (linesToTranslate.length === 0) {
|
||||
return originalComment;
|
||||
}
|
||||
|
||||
// 解析翻译结果 - 假设翻译服务按顺序返回翻译后的行
|
||||
const translatedLines = translatedContent.split('\n');
|
||||
const translations = new Map<number, string>();
|
||||
|
||||
// 将翻译结果映射到对应的行
|
||||
linesToTranslate.forEach((item, transIndex) => {
|
||||
if (transIndex < translatedLines.length) {
|
||||
translations.set(item.index, translatedLines[transIndex].trim());
|
||||
}
|
||||
});
|
||||
|
||||
// 重建注释,保持原始结构
|
||||
return extractedLines
|
||||
.map((line, index) => {
|
||||
if (translations.has(index)) {
|
||||
// 使用翻译内容,保持原始前缀
|
||||
return line.prefix + translations.get(index);
|
||||
} else {
|
||||
// 保持原样
|
||||
return originalLines[index];
|
||||
}
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建替换操作
|
||||
*/
|
||||
export const createReplacements = (
|
||||
file: SourceFile,
|
||||
comments: ChineseComment[],
|
||||
translations: TranslationResult[],
|
||||
): Replacement[] => {
|
||||
const replacements: Replacement[] = [];
|
||||
|
||||
comments.forEach((comment, index) => {
|
||||
const translation = translations[index];
|
||||
if (!translation) return;
|
||||
|
||||
if (comment.multiLineContext?.isPartOfMultiLine) {
|
||||
// 处理多行注释中的单行
|
||||
const replacement = createMultiLineReplacement(file, comment, translation);
|
||||
if (replacement) {
|
||||
replacements.push(replacement);
|
||||
}
|
||||
} else {
|
||||
// 处理普通注释(单行注释或整个多行注释)
|
||||
const replacement = createRegularReplacement(file, comment, translation);
|
||||
if (replacement) {
|
||||
replacements.push(replacement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return replacements;
|
||||
};
|
||||
|
||||
/**
|
||||
* 为多行注释中的单行创建替换操作
|
||||
*/
|
||||
const createMultiLineReplacement = (
|
||||
file: SourceFile,
|
||||
comment: ChineseComment,
|
||||
translation: TranslationResult,
|
||||
): Replacement | null => {
|
||||
const lines = file.content.split('\n');
|
||||
const lineIndex = comment.startLine - 1;
|
||||
|
||||
if (lineIndex >= lines.length) return null;
|
||||
|
||||
const originalLine = lines[lineIndex];
|
||||
|
||||
// 查找这一行中中文内容的位置
|
||||
const cleanedContent = comment.content;
|
||||
|
||||
// 更精确地查找中文内容在原始行中的位置
|
||||
const commentContentRegex = new RegExp(cleanedContent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
const contentMatch = originalLine.match(commentContentRegex);
|
||||
|
||||
if (!contentMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chineseStart = contentMatch.index!;
|
||||
const chineseEnd = chineseStart + contentMatch[0].length;
|
||||
|
||||
// 计算在整个文件中的位置
|
||||
let start = 0;
|
||||
for (let i = 0; i < lineIndex; i++) {
|
||||
start += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
start += chineseStart;
|
||||
|
||||
const end = start + (chineseEnd - chineseStart);
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
original: originalLine.substring(chineseStart, chineseEnd),
|
||||
replacement: translation.translated,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 为普通注释创建替换操作
|
||||
*/
|
||||
const createRegularReplacement = (
|
||||
file: SourceFile,
|
||||
comment: ChineseComment,
|
||||
translation: TranslationResult,
|
||||
): Replacement | null => {
|
||||
const lines = file.content.split('\n');
|
||||
const startLineIndex = comment.startLine - 1;
|
||||
const endLineIndex = comment.endLine - 1;
|
||||
|
||||
// 计算原始注释在文件中的精确位置
|
||||
let start = 0;
|
||||
for (let i = 0; i < startLineIndex; i++) {
|
||||
start += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
start += comment.startColumn;
|
||||
|
||||
let end = start;
|
||||
if (comment.startLine === comment.endLine) {
|
||||
// 同一行
|
||||
end = start + (comment.endColumn - comment.startColumn);
|
||||
} else {
|
||||
// 跨行 - 重新计算end位置
|
||||
end = 0;
|
||||
for (let i = 0; i < endLineIndex; i++) {
|
||||
end += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
end += comment.endColumn;
|
||||
}
|
||||
|
||||
// 获取原始注释文本
|
||||
const originalText = file.content.substring(start, end);
|
||||
|
||||
// 应用格式保持
|
||||
const formattedTranslation = preserveCommentFormat(
|
||||
originalText,
|
||||
translation.translated,
|
||||
comment.type === 'documentation' ? 'multi-line' : comment.type,
|
||||
);
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
original: originalText,
|
||||
replacement: formattedTranslation,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用替换操作到文本内容
|
||||
*/
|
||||
export const applyReplacements = (
|
||||
content: string,
|
||||
replacements: Replacement[],
|
||||
): string => {
|
||||
// 按位置倒序排列,避免替换后位置偏移
|
||||
const sortedReplacements = [...replacements].sort(
|
||||
(a, b) => b.start - a.start,
|
||||
);
|
||||
|
||||
let result = content;
|
||||
|
||||
for (const replacement of sortedReplacements) {
|
||||
const before = result.substring(0, replacement.start);
|
||||
const after = result.substring(replacement.end);
|
||||
result = before + replacement.replacement + after;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 替换文件中的注释
|
||||
*/
|
||||
export const replaceCommentsInFile = async (
|
||||
file: SourceFile,
|
||||
operation: ReplacementOperation,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
return tryCatch(async () => {
|
||||
const fs = await import('fs/promises');
|
||||
|
||||
// 应用替换
|
||||
const newContent = applyReplacements(
|
||||
file.content,
|
||||
operation.replacements,
|
||||
);
|
||||
|
||||
// 写入文件
|
||||
await fs.writeFile(file.path, newContent, 'utf-8');
|
||||
|
||||
return { success: true };
|
||||
}).then(result => {
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
result.error instanceof Error
|
||||
? result.error.message
|
||||
: String(result.error),
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量替换多个文件
|
||||
*/
|
||||
export const batchReplaceFiles = async (
|
||||
operations: ReplacementOperation[],
|
||||
): Promise<
|
||||
Array<{
|
||||
file: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>
|
||||
> => {
|
||||
const results = await Promise.allSettled(
|
||||
operations.map(async operation => {
|
||||
const fs = await import('fs/promises');
|
||||
const content = await fs.readFile(operation.file, 'utf-8');
|
||||
const sourceFile: SourceFile = {
|
||||
path: operation.file,
|
||||
content,
|
||||
language: 'other', // 临时值,实际应该检测
|
||||
};
|
||||
|
||||
const result = await replaceCommentsInFile(
|
||||
sourceFile,
|
||||
operation,
|
||||
);
|
||||
return { file: operation.file, ...result };
|
||||
}),
|
||||
);
|
||||
|
||||
return results.map((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return result.value;
|
||||
} else {
|
||||
return {
|
||||
file: operations[index].file,
|
||||
success: false,
|
||||
error:
|
||||
result.reason instanceof Error
|
||||
? result.reason.message
|
||||
: String(result.reason),
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证替换操作
|
||||
*/
|
||||
export const validateReplacements = (
|
||||
content: string,
|
||||
replacements: Replacement[],
|
||||
): { valid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 检查位置是否有效
|
||||
replacements.forEach((replacement, index) => {
|
||||
if (replacement.start < 0 || replacement.end > content.length) {
|
||||
errors.push(`Replacement ${index}: Invalid position range`);
|
||||
}
|
||||
|
||||
if (replacement.start >= replacement.end) {
|
||||
errors.push(
|
||||
`Replacement ${index}: Start position must be less than end position`,
|
||||
);
|
||||
}
|
||||
|
||||
// 检查原文是否匹配
|
||||
const actualText = content.substring(replacement.start, replacement.end);
|
||||
if (actualText !== replacement.original) {
|
||||
errors.push(`Replacement ${index}: Original text mismatch`);
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否有重叠
|
||||
const sortedReplacements = [...replacements].sort(
|
||||
(a, b) => a.start - b.start,
|
||||
);
|
||||
for (let i = 0; i < sortedReplacements.length - 1; i++) {
|
||||
const current = sortedReplacements[i];
|
||||
const next = sortedReplacements[i + 1];
|
||||
|
||||
if (current.end > next.start) {
|
||||
errors.push(
|
||||
`Overlapping replacements at positions ${current.start}-${current.end} and ${next.start}-${next.end}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
};
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import { SourceFile, FileScanConfig, Result } from '../types/index';
|
||||
import { detectLanguage, filterFilesByExtensions, isTextFile } from '../utils/language';
|
||||
import { getGitTrackedFiles, getAllGitFiles } from '../utils/git';
|
||||
import { tryCatch } from '../utils/fp';
|
||||
|
||||
/**
|
||||
* 读取文件内容并创建SourceFile对象
|
||||
*/
|
||||
export const readSourceFile = async (filePath: string): Promise<Result<SourceFile>> => {
|
||||
return tryCatch(async () => {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const language = detectLanguage(filePath);
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
content,
|
||||
language
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量读取源文件
|
||||
*/
|
||||
export const readSourceFiles = async (filePaths: string[]): Promise<SourceFile[]> => {
|
||||
const results = await Promise.allSettled(
|
||||
filePaths.map(path => readSourceFile(path))
|
||||
);
|
||||
|
||||
return results
|
||||
.filter((result): result is PromiseFulfilledResult<Result<SourceFile>> =>
|
||||
result.status === 'fulfilled' && result.value.success
|
||||
)
|
||||
.map(result => (result.value as { success: true; data: SourceFile }).data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取Git仓库中的源码文件
|
||||
*/
|
||||
export const getSourceFiles = async (config: FileScanConfig): Promise<Result<string[]>> => {
|
||||
const { root, extensions, includeUntracked } = config;
|
||||
|
||||
return tryCatch(async () => {
|
||||
// 获取Git文件列表
|
||||
const gitFilesResult = includeUntracked
|
||||
? await getAllGitFiles(root)
|
||||
: await getGitTrackedFiles(root);
|
||||
|
||||
if (!gitFilesResult.success) {
|
||||
throw gitFilesResult.error;
|
||||
}
|
||||
|
||||
let files = gitFilesResult.data;
|
||||
|
||||
// 过滤文本文件
|
||||
files = files.filter(isTextFile);
|
||||
|
||||
// 根据扩展名过滤
|
||||
if (extensions.length > 0) {
|
||||
files = filterFilesByExtensions(files, extensions);
|
||||
}
|
||||
|
||||
return files;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 扫描并读取所有源码文件
|
||||
*/
|
||||
export const scanSourceFiles = async (config: FileScanConfig): Promise<Result<SourceFile[]>> => {
|
||||
return tryCatch(async () => {
|
||||
const filesResult = await getSourceFiles(config);
|
||||
|
||||
if (!filesResult.success) {
|
||||
throw filesResult.error;
|
||||
}
|
||||
|
||||
const sourceFiles = await readSourceFiles(filesResult.data);
|
||||
return sourceFiles;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查文件是否存在且可读
|
||||
*/
|
||||
export const isFileAccessible = async (filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.access(filePath, fs.constants.R_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取文件统计信息
|
||||
*/
|
||||
export const getFileStats = async (filePaths: string[]): Promise<{
|
||||
total: number;
|
||||
accessible: number;
|
||||
textFiles: number;
|
||||
supportedFiles: number;
|
||||
}> => {
|
||||
const accessibilityResults = await Promise.allSettled(
|
||||
filePaths.map(isFileAccessible)
|
||||
);
|
||||
|
||||
const accessible = accessibilityResults.filter(
|
||||
(result): result is PromiseFulfilledResult<boolean> =>
|
||||
result.status === 'fulfilled' && result.value
|
||||
).length;
|
||||
|
||||
const textFiles = filePaths.filter(isTextFile).length;
|
||||
const supportedFiles = filePaths.filter(path => detectLanguage(path) !== 'other').length;
|
||||
|
||||
return {
|
||||
total: filePaths.length,
|
||||
accessible,
|
||||
textFiles,
|
||||
supportedFiles
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
import {
|
||||
ProcessingReport,
|
||||
ProcessingStats,
|
||||
FileProcessingDetail,
|
||||
} from '../types/index.js';
|
||||
|
||||
/**
|
||||
* 报告收集器类
|
||||
*/
|
||||
export class ReportCollector {
|
||||
private stats: ProcessingStats = {
|
||||
totalFiles: 0,
|
||||
processedFiles: 0,
|
||||
translatedComments: 0,
|
||||
skippedFiles: 0,
|
||||
errors: [],
|
||||
startTime: Date.now(),
|
||||
endTime: 0,
|
||||
};
|
||||
|
||||
private fileDetails: Map<string, FileProcessingDetail> = new Map();
|
||||
|
||||
/**
|
||||
* 记录文件处理开始
|
||||
*/
|
||||
recordFileStart(filePath: string): void {
|
||||
this.stats.totalFiles++;
|
||||
this.fileDetails.set(filePath, {
|
||||
file: filePath,
|
||||
commentCount: 0,
|
||||
status: 'processing',
|
||||
startTime: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录文件处理完成
|
||||
*/
|
||||
recordFileComplete(filePath: string, commentCount: number): void {
|
||||
const detail = this.fileDetails.get(filePath);
|
||||
if (detail) {
|
||||
detail.status = 'success';
|
||||
detail.commentCount = commentCount;
|
||||
detail.endTime = Date.now();
|
||||
this.stats.processedFiles++;
|
||||
this.stats.translatedComments += commentCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录文件跳过
|
||||
*/
|
||||
recordFileSkipped(filePath: string, reason?: string): void {
|
||||
const detail = this.fileDetails.get(filePath);
|
||||
if (detail) {
|
||||
detail.status = 'skipped';
|
||||
detail.errorMessage = reason;
|
||||
detail.endTime = Date.now();
|
||||
this.stats.skippedFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录处理错误
|
||||
*/
|
||||
recordError(filePath: string, error: Error): void {
|
||||
const detail = this.fileDetails.get(filePath);
|
||||
if (detail) {
|
||||
detail.status = 'error';
|
||||
detail.errorMessage = error.message;
|
||||
detail.endTime = Date.now();
|
||||
}
|
||||
this.stats.errors.push({ file: filePath, error: error.message });
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成统计
|
||||
*/
|
||||
finalize(): void {
|
||||
this.stats.endTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStats(): ProcessingStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件详情
|
||||
*/
|
||||
getFileDetails(): FileProcessingDetail[] {
|
||||
return Array.from(this.fileDetails.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整报告
|
||||
*/
|
||||
generateReport(): ProcessingReport {
|
||||
this.finalize();
|
||||
const duration = (this.stats.endTime - this.stats.startTime) / 1000;
|
||||
|
||||
return {
|
||||
stats: this.getStats(),
|
||||
details: this.getFileDetails(),
|
||||
duration,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置收集器
|
||||
*/
|
||||
reset(): void {
|
||||
this.stats = {
|
||||
totalFiles: 0,
|
||||
processedFiles: 0,
|
||||
translatedComments: 0,
|
||||
skippedFiles: 0,
|
||||
errors: [],
|
||||
startTime: Date.now(),
|
||||
endTime: 0,
|
||||
};
|
||||
this.fileDetails.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成控制台报告
|
||||
*/
|
||||
export const generateConsoleReport = (report: ProcessingReport): string => {
|
||||
const { stats, duration } = report;
|
||||
const successRate =
|
||||
stats.totalFiles > 0
|
||||
? ((stats.processedFiles / stats.totalFiles) * 100).toFixed(1)
|
||||
: '0';
|
||||
|
||||
let output = `
|
||||
📊 翻译处理报告
|
||||
==================
|
||||
总文件数: ${stats.totalFiles}
|
||||
处理成功: ${stats.processedFiles}
|
||||
跳过文件: ${stats.skippedFiles}
|
||||
翻译注释: ${stats.translatedComments}
|
||||
错误数量: ${stats.errors.length}
|
||||
成功率: ${successRate}%
|
||||
处理时间: ${duration.toFixed(2)}秒
|
||||
`;
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
output += '\n❌ 错误详情:\n';
|
||||
stats.errors.forEach(error => {
|
||||
output += ` ${error.file}: ${error.error}\n`;
|
||||
});
|
||||
} else {
|
||||
output += '\n✅ 处理完成,无错误';
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成Markdown报告
|
||||
*/
|
||||
export const generateMarkdownReport = (report: ProcessingReport): string => {
|
||||
const { stats, details, duration } = report;
|
||||
const successRate =
|
||||
stats.totalFiles > 0
|
||||
? ((stats.processedFiles / stats.totalFiles) * 100).toFixed(1)
|
||||
: '0';
|
||||
|
||||
let markdown = `# 中文注释翻译报告
|
||||
|
||||
## 📊 统计概览
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 总文件数 | ${stats.totalFiles} |
|
||||
| 处理成功 | ${stats.processedFiles} |
|
||||
| 跳过文件 | ${stats.skippedFiles} |
|
||||
| 翻译注释 | ${stats.translatedComments} |
|
||||
| 错误数量 | ${stats.errors.length} |
|
||||
| 成功率 | ${successRate}% |
|
||||
| 处理时间 | ${duration.toFixed(2)}秒 |
|
||||
|
||||
## 📁 文件详情
|
||||
|
||||
| 文件路径 | 状态 | 注释数量 | 耗时(ms) | 备注 |
|
||||
|----------|------|----------|----------|------|
|
||||
`;
|
||||
|
||||
details.forEach(detail => {
|
||||
const duration =
|
||||
detail.endTime && detail.startTime
|
||||
? detail.endTime - detail.startTime
|
||||
: 0;
|
||||
const status =
|
||||
detail.status === 'success'
|
||||
? '✅'
|
||||
: detail.status === 'error'
|
||||
? '❌'
|
||||
: detail.status === 'skipped'
|
||||
? '⏭️'
|
||||
: '🔄';
|
||||
|
||||
markdown += `| ${detail.file} | ${status} | ${detail.commentCount} | ${duration} | ${detail.errorMessage || '-'} |\n`;
|
||||
});
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
markdown += '\n## ❌ 错误详情\n\n';
|
||||
stats.errors.forEach((error, index) => {
|
||||
markdown += `${index + 1}. **${error.file}**\n \`\`\`\n ${error.error}\n \`\`\`\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
return markdown;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成JSON报告
|
||||
*/
|
||||
export const generateJsonReport = (report: ProcessingReport): string => {
|
||||
return JSON.stringify(report, null, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据格式生成报告
|
||||
*/
|
||||
export const generateReport = (
|
||||
report: ProcessingReport,
|
||||
format: 'json' | 'markdown' | 'console' = 'console',
|
||||
): string => {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return generateJsonReport(report);
|
||||
case 'markdown':
|
||||
return generateMarkdownReport(report);
|
||||
case 'console':
|
||||
default:
|
||||
return generateConsoleReport(report);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存报告到文件
|
||||
*/
|
||||
export const saveReportToFile = async (
|
||||
report: ProcessingReport,
|
||||
filePath: string,
|
||||
format: 'json' | 'markdown' | 'console' = 'json',
|
||||
): Promise<void> => {
|
||||
const content = generateReport(report, format);
|
||||
const fs = await import('fs/promises');
|
||||
await fs.writeFile(filePath, content, 'utf-8');
|
||||
};
|
||||
|
||||
/**
|
||||
* 在控制台显示实时进度
|
||||
*/
|
||||
export class ProgressDisplay {
|
||||
private total: number = 0;
|
||||
private current: number = 0;
|
||||
private startTime: number = Date.now();
|
||||
|
||||
constructor(total: number) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度
|
||||
*/
|
||||
update(current: number, currentFile?: string): void {
|
||||
this.current = current;
|
||||
const percentage = ((current / this.total) * 100).toFixed(1);
|
||||
const elapsed = (Date.now() - this.startTime) / 1000;
|
||||
const speed = current / elapsed;
|
||||
const eta = speed > 0 ? (this.total - current) / speed : 0;
|
||||
|
||||
let line = `进度: ${current}/${this.total} (${percentage}%) | 耗时: ${elapsed.toFixed(1)}s`;
|
||||
|
||||
if (eta > 0) {
|
||||
line += ` | 预计剩余: ${eta.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
if (currentFile) {
|
||||
line += ` | 当前: ${currentFile}`;
|
||||
}
|
||||
|
||||
// 清除当前行并输出新进度
|
||||
process.stdout.write(
|
||||
'\r' + ' '.repeat(process.stdout.columns || 80) + '\r',
|
||||
);
|
||||
process.stdout.write(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成进度显示
|
||||
*/
|
||||
complete(): void {
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
import {
|
||||
TranslationResult,
|
||||
TranslationContext,
|
||||
ChineseComment,
|
||||
TranslationError,
|
||||
} from '../types/index';
|
||||
import { TranslationConfig } from '../types/config';
|
||||
import { retry, chunk } from '../utils/fp';
|
||||
import { isValidTranslation } from '../utils/chinese';
|
||||
import { translate as volcTranslate, TranslateConfig as VolcTranslateConfig } from '../volc/translate';
|
||||
|
||||
/**
|
||||
* 翻译服务类
|
||||
*/
|
||||
export class TranslationService {
|
||||
private config: TranslationConfig;
|
||||
private cache = new Map<string, TranslationResult>();
|
||||
|
||||
constructor(config: TranslationConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为火山引擎翻译配置
|
||||
*/
|
||||
private toVolcConfig(): VolcTranslateConfig {
|
||||
return {
|
||||
accessKeyId: this.config.accessKeyId,
|
||||
secretAccessKey: this.config.secretAccessKey,
|
||||
region: this.config.region,
|
||||
sourceLanguage: this.config.sourceLanguage,
|
||||
targetLanguage: this.config.targetLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算翻译置信度(简单实现)
|
||||
*/
|
||||
private calculateConfidence(translated: string, original: string): number {
|
||||
// 基于长度比例和有效性的简单置信度计算
|
||||
const lengthRatio = translated.length / original.length;
|
||||
|
||||
if (!isValidTranslation(original, translated)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 理想的长度比例在0.8-2.0之间
|
||||
let confidence = 0.8;
|
||||
if (lengthRatio >= 0.8 && lengthRatio <= 2.0) {
|
||||
confidence = 0.9;
|
||||
}
|
||||
|
||||
return confidence;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用火山引擎API进行翻译
|
||||
*/
|
||||
private async callVolcTranslate(texts: string[]): Promise<string[]> {
|
||||
const volcConfig = this.toVolcConfig();
|
||||
const response = await volcTranslate(texts, volcConfig);
|
||||
|
||||
return response.TranslationList.map(item => item.Translation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译单个注释
|
||||
*/
|
||||
async translateComment(
|
||||
comment: string,
|
||||
context?: TranslationContext,
|
||||
): Promise<TranslationResult> {
|
||||
// 检查缓存
|
||||
const cacheKey = this.getCacheKey(comment, context);
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const translations = await retry(
|
||||
() => this.callVolcTranslate([comment]),
|
||||
this.config.maxRetries,
|
||||
1000,
|
||||
);
|
||||
|
||||
const translated = translations[0];
|
||||
if (!translated) {
|
||||
throw new Error('Empty translation response');
|
||||
}
|
||||
|
||||
const result: TranslationResult = {
|
||||
original: comment,
|
||||
translated,
|
||||
confidence: this.calculateConfidence(translated, comment),
|
||||
};
|
||||
|
||||
// 缓存结果
|
||||
this.cache.set(cacheKey, result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TranslationError(
|
||||
`Translation failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
comment,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缓存键
|
||||
*/
|
||||
private getCacheKey(comment: string, context?: TranslationContext): string {
|
||||
const contextStr = context
|
||||
? `${context.language}-${context.commentType}-${context.nearbyCode || ''}`
|
||||
: '';
|
||||
return `${comment}|${contextStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量翻译注释
|
||||
*/
|
||||
async batchTranslate(
|
||||
comments: ChineseComment[],
|
||||
concurrency: number = this.config.concurrency,
|
||||
): Promise<TranslationResult[]> {
|
||||
// 提取未缓存的注释
|
||||
const uncachedComments: { comment: ChineseComment; index: number }[] = [];
|
||||
const results: TranslationResult[] = new Array(comments.length);
|
||||
|
||||
// 检查缓存
|
||||
comments.forEach((comment, index) => {
|
||||
const cacheKey = this.getCacheKey(comment.content);
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (cached) {
|
||||
results[index] = cached;
|
||||
} else {
|
||||
uncachedComments.push({ comment, index });
|
||||
}
|
||||
});
|
||||
|
||||
// 如果所有注释都已缓存,直接返回
|
||||
if (uncachedComments.length === 0) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// 分批翻译未缓存的注释
|
||||
const chunks = chunk(uncachedComments, concurrency);
|
||||
|
||||
for (const chunkItems of chunks) {
|
||||
try {
|
||||
const textsToTranslate = chunkItems.map(item => item.comment.content);
|
||||
const translations = await retry(
|
||||
() => this.callVolcTranslate(textsToTranslate),
|
||||
this.config.maxRetries,
|
||||
1000,
|
||||
);
|
||||
|
||||
// 处理翻译结果
|
||||
chunkItems.forEach((item, chunkIndex) => {
|
||||
const translated = translations[chunkIndex];
|
||||
if (translated) {
|
||||
const result: TranslationResult = {
|
||||
original: item.comment.content,
|
||||
translated,
|
||||
confidence: this.calculateConfidence(translated, item.comment.content),
|
||||
};
|
||||
|
||||
// 缓存结果
|
||||
const cacheKey = this.getCacheKey(item.comment.content);
|
||||
this.cache.set(cacheKey, result);
|
||||
|
||||
results[item.index] = result;
|
||||
} else {
|
||||
// 如果翻译失败,创建一个错误结果
|
||||
results[item.index] = {
|
||||
original: item.comment.content,
|
||||
translated: item.comment.content, // 翻译失败时保持原文
|
||||
confidence: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// 如果整个批次翻译失败,为这个批次的所有注释创建错误结果
|
||||
chunkItems.forEach(item => {
|
||||
results[item.index] = {
|
||||
original: item.comment.content,
|
||||
translated: item.comment.content, // 翻译失败时保持原文
|
||||
confidence: 0,
|
||||
};
|
||||
});
|
||||
|
||||
console.warn(`批量翻译失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存翻译缓存到文件
|
||||
*/
|
||||
async saveCache(filePath: string): Promise<void> {
|
||||
const cacheData = Object.fromEntries(this.cache);
|
||||
const fs = await import('fs/promises');
|
||||
await fs.writeFile(filePath, JSON.stringify(cacheData, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载翻译缓存
|
||||
*/
|
||||
async loadCache(filePath: string): Promise<void> {
|
||||
try {
|
||||
const fs = await import('fs/promises');
|
||||
const data = await fs.readFile(filePath, 'utf-8');
|
||||
const cacheData = JSON.parse(data);
|
||||
this.cache = new Map(Object.entries(cacheData));
|
||||
} catch {
|
||||
// 缓存文件不存在或损坏,忽略
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计
|
||||
*/
|
||||
getCacheStats(): { size: number; hitRate: number } {
|
||||
return {
|
||||
size: this.cache.size,
|
||||
hitRate: 0, // 需要实际统计命中率
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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 编写代码
|
||||
|
|
@ -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 {
|
||||
// 缓存文件不存在或损坏,忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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; // 文件未被忽略
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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' },
|
||||
|
|
|
|||
Loading…
Reference in New Issue