添加了部分来自于BedrockWiki的文章!
This commit is contained in:
238
scripts/ai-translate.mjs
Normal file
238
scripts/ai-translate.mjs
Normal file
@@ -0,0 +1,238 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
import OpenAI from 'openai';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createInterface } from 'readline';
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config();
|
||||
|
||||
// 检查环境变量是否存在
|
||||
const requiredEnvVars = ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL'];
|
||||
const missingEnvVars = requiredEnvVars.filter(varName => !process.env[varName]);
|
||||
|
||||
if (missingEnvVars.length > 0) {
|
||||
console.error(`❌ 缺少必要的环境变量: ${missingEnvVars.join(', ')}`);
|
||||
console.error('请在.env文件中设置这些变量');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 初始化OpenAI客户端
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseURL: process.env.OPENAI_BASE_URL
|
||||
});
|
||||
|
||||
const model = process.env.OPENAI_MODEL || 'gpt-4-turbo';
|
||||
|
||||
// 获取命令行参数(相对路径)
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length !== 1) {
|
||||
console.error('❌ 请提供要翻译的目录的相对路径');
|
||||
console.error('用法: node ai-translate.mjs <目录相对路径>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 获取脚本所在目录的绝对路径
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
// 计算目标目录的绝对路径
|
||||
const targetDir = path.resolve(scriptDir, '..', args[0]);
|
||||
|
||||
async function findMarkdownFiles(dir) {
|
||||
const files = [];
|
||||
|
||||
// 检测文本是否包含中文的函数
|
||||
function containsChinese(text) {
|
||||
// 匹配中文字符的正则表达式
|
||||
const chineseRegex = /[\u4e00-\u9fa5]/;
|
||||
return chineseRegex.test(text);
|
||||
}
|
||||
|
||||
async function traverse(currentDir) {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await traverse(fullPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
try {
|
||||
// 读取文件内容
|
||||
const content = await fs.readFile(fullPath, 'utf-8');
|
||||
|
||||
// 如果文件不包含中文,则添加到待翻译列表
|
||||
if (!containsChinese(content)) {
|
||||
files.push(fullPath);
|
||||
} else {
|
||||
console.log(`⏭️ 跳过已包含中文的文件: ${path.relative(targetDir, fullPath)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ 读取文件 ${fullPath} 时出错:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await traverse(dir);
|
||||
return files;
|
||||
}
|
||||
async function translateMarkdown(filePath) {
|
||||
try {
|
||||
console.log(`🔄 正在处理文件: ${path.relative(targetDir, filePath)}`);
|
||||
|
||||
// 读取Markdown内容
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
// 创建翻译提示
|
||||
const prompt = `你是一位专业的Minecraft基岩版文档翻译者,需要结合给出的原始英文文档,准确翻译为简体中文,保留所有的vitepress特性和组件。在翻译时,尽量保证概念传达的准确性,但是同时需要满足中文母语者的生活自然语序和语法和词语,阅读时需要尽量轻松和容易。
|
||||
翻译要求:
|
||||
- 包括头部的matter yml内容,也需要对应翻译。
|
||||
- 在开头根据matter yml的title字段,添加h1大标题(如果没有的话就不用添加)
|
||||
- 不需要翻译代码,但是需要翻译代码块中的注释。
|
||||
- 部分在开发时遇到的专有词汇,需要考虑是否需要保留英文原文。比如:Tick、Component、Entity、Block、Item等
|
||||
- <CodeHeader>组件需要被替换为另一种表达形式,使用::: code-group :::包裹(别忘了末尾的:::),例子:
|
||||
::: code-group
|
||||
\`\`\`json [原始CodeHeader的值]
|
||||
xxx
|
||||
\`\`\`
|
||||
:::
|
||||
|
||||
以下为待翻译内容:
|
||||
${content}`;
|
||||
|
||||
console.log(`🧠 开始翻译,请耐心等待...`);
|
||||
|
||||
// 使用OpenAI API进行流式翻译
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "你是一个专业的Markdown文档翻译助手,能够准确地将英文Markdown内容翻译成简体中文,同时保持原有格式和结构。"
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
stream: true,
|
||||
max_tokens: 8 * 1024,
|
||||
response_format: { type: "text" },
|
||||
});
|
||||
|
||||
let translatedContent = '';
|
||||
let reasoningContent = '';
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// 提取思考过程和内容(如果有的话)
|
||||
const reasoning = chunk.choices[0]?.delta?.reasoning_content || '';
|
||||
const content = chunk.choices[0]?.delta?.content || '';
|
||||
|
||||
if (reasoning) {
|
||||
process.stdout.write(reasoning);
|
||||
reasoningContent += reasoning;
|
||||
}
|
||||
|
||||
if (content) {
|
||||
process.stdout.write(content);
|
||||
translatedContent += content;
|
||||
}
|
||||
}
|
||||
|
||||
// 将翻译后的内容写入文件
|
||||
await fs.writeFile(filePath, translatedContent.trim(), 'utf-8');
|
||||
console.log(`\n✅ 文件翻译完成: ${path.relative(targetDir, filePath)}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`\n❌ 翻译 ${path.relative(targetDir, filePath)} 时出错:`);
|
||||
console.error(error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForConfirmation(message) {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
rl.question(message, answer => {
|
||||
rl.close();
|
||||
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log(`🔍 正在扫描目录: ${targetDir}`);
|
||||
|
||||
// 检查目录是否存在
|
||||
try {
|
||||
await fs.access(targetDir);
|
||||
} catch (error) {
|
||||
console.error(`❌ 指定的目录不存在: ${targetDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 查找所有Markdown文件
|
||||
const markdownFiles = await findMarkdownFiles(targetDir);
|
||||
|
||||
if (markdownFiles.length === 0) {
|
||||
console.log(`⚠️ 在 ${targetDir} 中没有找到Markdown文件`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`🔎 找到 ${markdownFiles.length} 个Markdown文件`);
|
||||
markdownFiles.forEach((file, index) => {
|
||||
console.log(` ${index + 1}. ${path.relative(targetDir, file)}`);
|
||||
});
|
||||
|
||||
// 请求确认
|
||||
const confirmed = await promptForConfirmation(`⚠️ 此操作将翻译并覆盖以上文件。确认继续吗? (y/n): `);
|
||||
|
||||
if (!confirmed) {
|
||||
console.log('❌ 操作已取消');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('🚀 开始翻译...');
|
||||
|
||||
// 逐个翻译文件
|
||||
let successful = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (let i = 0; i < markdownFiles.length; i++) {
|
||||
const file = markdownFiles[i];
|
||||
console.log(`\n[${i + 1}/${markdownFiles.length}] 处理文件: ${path.relative(targetDir, file)}`);
|
||||
|
||||
const success = await translateMarkdown(file);
|
||||
|
||||
if (success) {
|
||||
successful++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n📊 翻译统计:');
|
||||
console.log(`✅ 成功: ${successful} 个文件`);
|
||||
console.log(`❌ 失败: ${failed} 个文件`);
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 发生错误:');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 通过node指令手动执行,因为需要带参数:node scripts/ai-translate.mjs <目录相对路径>
|
||||
main();
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import fg from 'fast-glob';
|
||||
import matter from 'gray-matter'
|
||||
|
||||
// 定义侧边栏项目接口
|
||||
interface SidebarItem {
|
||||
@@ -34,9 +35,16 @@ const CATEGORY_MAP: Record<string, string> = {
|
||||
* 从名称中提取排序数字
|
||||
* 例如:'0-概述' 返回 0,'1-基础' 返回 1
|
||||
*/
|
||||
function extractOrderNumber(name: string): number {
|
||||
function extractOrderNumber(name: string, matterData: any): number {
|
||||
const match = name.match(/^(\d+)-/);
|
||||
return match ? parseInt(match[1], 10) : Number.MAX_SAFE_INTEGER; // 没有数字前缀的排在最后
|
||||
if (match) {
|
||||
return parseInt(match[1], 10);
|
||||
} else if (matterData.order) {
|
||||
return parseInt(matterData.order);
|
||||
} else if (matterData.nav_order) {
|
||||
return parseInt(matterData.nav_order);
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,6 +62,14 @@ async function generateSidebar(): Promise<Record<string, SidebarItem[]>> {
|
||||
const segments = relativePath.split(path.sep);
|
||||
const categoryKey = segments[0];
|
||||
|
||||
// 读取文件内容,然后通过matter读取头部信息
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data: matterData } = matter(fileContent);
|
||||
|
||||
if (matterData.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 使用映射表获取显示名称
|
||||
const categoryName = CATEGORY_MAP[categoryKey] || categoryKey;
|
||||
|
||||
@@ -77,11 +93,11 @@ async function generateSidebar(): Promise<Record<string, SidebarItem[]>> {
|
||||
link = `/${segment}`;
|
||||
}
|
||||
|
||||
const order = extractOrderNumber(segment); // 提取排序号
|
||||
const order = extractOrderNumber(segment, matterData); // 提取排序号
|
||||
|
||||
if (isLast) {
|
||||
// 添加最终文件项
|
||||
const title = await getTitleFromFile(filePath);
|
||||
const title = await getTitleFromFile(filePath, matterData);
|
||||
// 添加 activeMatch 以支持更精确的高亮匹配
|
||||
const activeMatch = `^${link}(?:/|$)`;
|
||||
currentLevel.push({ text: title, link, order, activeMatch });
|
||||
@@ -158,25 +174,14 @@ function sortSidebarItems(items: SidebarItem[]): void {
|
||||
/**
|
||||
* 从文件 Frontmatter 或文件名提取标题
|
||||
*/
|
||||
async function getTitleFromFile(filePath: string): Promise<string> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
// 寻找 title 在 frontmatter 中的位置
|
||||
const frontmatterMatch = content.match(/^---\s*[\s\S]*?title:\s*(.*?)[\r\n][\s\S]*?---/);
|
||||
|
||||
if (frontmatterMatch && frontmatterMatch[1]) {
|
||||
// 清理标题(移除引号等)
|
||||
return frontmatterMatch[1].trim().replace(/['"]/g, '');
|
||||
}
|
||||
|
||||
// 如果没有 frontmatter title,从文件名获取
|
||||
const basename = path.basename(filePath, '.md');
|
||||
// 普通文件,移除数字前缀和连字符
|
||||
return basename.replace(/^\d+-\s*/, '').replace(/-/g, ' ');
|
||||
} catch (error) {
|
||||
console.error(`读取文件失败: ${filePath}`, error);
|
||||
return path.basename(filePath, '.md');
|
||||
async function getTitleFromFile(filePath: string, matterData: any): Promise<string> {
|
||||
if (matterData.title) {
|
||||
return matterData.title.trim().replace(/['"]/g, '');
|
||||
}
|
||||
// 如果没有 frontmatter title,从文件名获取
|
||||
const basename = path.basename(filePath, '.md');
|
||||
// 普通文件,移除数字前缀和连字符
|
||||
return basename.replace(/^\d+-\s*/, '').replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
export default generateSidebar;
|
||||
|
||||
Reference in New Issue
Block a user