scripts/translate/translate.mjs (136 lines of code) (raw):
/**
* 使用前需要在env.process中配置IDEALAB_API_KEY和IDEALAB_BASE_URL
* @see https://aliyuque.antfin.com/wushuxuan.wsx/mobfr5/gwebrbfu6dkl0zlk?singleDoc#%20
* @example node scripts/generate-md/generateMd.mjs
* @example node scripts/translate/translate.mjs --all
* @example node scripts/translate/translate.mjs --file ./packages/bui-icons/src/index.zh-CN.md
*/
import OpenAI from 'openai';
import dotenv from 'dotenv';
import fs from 'node:fs';
import path from 'path';
import minimist from 'minimist';
import ora from 'ora';
import { input } from '@inquirer/prompts';
import { fileURLToPath } from 'url';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const openai = new OpenAI({
apiKey: process.env.IDEALAB_API_KEY,
baseURL: process.env.IDEALAB_BASE_URL,
});
async function translateMarkdownFile(inputFilePath, outputFilePath) {
try {
const markdownContent = fs.readFileSync(inputFilePath, 'utf-8');
fs.writeFileSync(outputFilePath, '');
const completionStream = await openai.chat.completions.create({
model: 'gpt-4o-0513',
messages: [
{
role: 'system',
content: `你是一名专业的技术文档翻译专家。请将以下 Markdown 文档从中文翻译成英文:
1. 保持所有 Markdown 语法格式不变,包括标题、列表、代码块、表格等
2. 保留所有代码示例、变量名和技术术语不翻译
3. 严格保留所有转义字符,包括但不限于:
- 泛型类型中的转义反斜杠,如 Partial<Props\> 中的 \
- 竖线分隔符中的转义反斜杠,如 A \| B 中的 \
- 所有其他以反斜杠 \ 开头的转义序列
4. 翻译时使用技术文档的专业用语和表达方式
5. 保持英文表达的自然流畅,避免直译
6. 保留所有 HTML 标签和特殊标记不变
7. 数字、标点符号格式遵循英文规范`,
},
{
role: 'user',
content: `请翻译以下 Markdown 文档:\n\n${markdownContent}`,
},
],
stream: true,
});
for await (const chunk of completionStream) {
if (
chunk &&
chunk.choices &&
chunk.choices.length > 0 &&
chunk.choices[0].delta.content
) {
const deltaText = chunk.choices[0].delta.content;
fs.appendFileSync(outputFilePath, deltaText);
}
}
console.log(`\n翻译成功并已追加到 ${outputFilePath}`);
} catch (error) {
console.error('翻译过程中发生错误:', error);
}
}
/**
* 获取所有组件目录
* @returns {string[]} 组件目录路径数组
*/
function getAllComponentDirectories() {
const componentsDir = path.resolve(__dirname, '../../packages/bui-core/src');
return fs
.readdirSync(componentsDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => path.join(componentsDir, dirent.name));
}
/**
* 翻译所有组件的文档
*/
async function translateAllComponents() {
const componentDirs = getAllComponentDirectories();
const spinner = ora('准备翻译所有组件文档...').start();
let translatedCount = 0;
let skippedCount = 0;
for (const dir of componentDirs) {
const inputFilePath = path.join(dir, 'index.zh-CN.md');
const outputFilePath = path.join(dir, 'index.en-US.md');
if (fs.existsSync(inputFilePath)) {
spinner.text = `正在翻译 ${path.basename(dir)} 组件文档...`;
await translateMarkdownFile(inputFilePath, outputFilePath);
translatedCount++;
} else {
skippedCount++;
}
}
spinner.succeed(
`翻译完成!成功翻译 ${translatedCount} 个组件文档,跳过 ${skippedCount} 个组件(无中文文档)`,
);
}
async function main() {
const argv = minimist(process.argv.slice(2));
// 处理 --all 参数
if (argv.all) {
await translateAllComponents();
return;
}
// 原有逻辑
let componentName = '';
if (!argv.file) {
while (!componentName) {
componentName = await input({ message: '组件名称' });
}
}
let inputFilePath;
let outputFilePath;
if (componentName) {
const directory = path.resolve(
__dirname,
'../../packages/bui-core/src',
`${componentName}`,
);
inputFilePath = path.resolve(directory, 'index.zh-CN.md');
outputFilePath = path.join(directory, 'index.en-US.md');
} else if (argv.file) {
inputFilePath = path.resolve(__dirname, '../../', argv.file);
let splitPath = inputFilePath.split('/');
outputFilePath = path.resolve(
inputFilePath,
`../${splitPath[splitPath.length - 1].split('.')[0]}.en-US.md`,
);
}
// 翻译文件
await translateMarkdownFile(inputFilePath, outputFilePath);
}
main();