Skip to content

Commit d0f58f9

Browse files
committed
feat: implement docs-cli for translating MDX files
- Removed old translate-docs.mjs script and replaced it with a new CLI tool. - Added @docs/cli package with commands for translating documentation files. - Introduced translation utilities for handling file paths and OpenAI API interactions. - Configured .gitignore for the new package. - Established pnpm workspace for better package management.
1 parent ac2a229 commit d0f58f9

File tree

10 files changed

+522
-145
lines changed

10 files changed

+522
-145
lines changed

.github/workflows/ai-translate.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ jobs:
3232
files: content/docs/**/*.mdx
3333
separator: ","
3434

35+
- name: Install pnpm
36+
uses: pnpm/action-setup@v3
37+
with:
38+
version: 8
39+
3540
- name: Install dependencies
3641
if: steps.changed-files.outputs.any_changed == 'true'
37-
run: npm install openai
42+
run: pnpm install
3843

3944
- name: Run translation script
4045
if: steps.changed-files.outputs.any_changed == 'true'
@@ -45,7 +50,7 @@ jobs:
4550
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
4651
OPENAI_MODEL: "gpt-4o"
4752
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
48-
run: node scripts/translate-docs.mjs
53+
run: pnpm translate
4954

5055
- name: Create Pull Request
5156
if: steps.changed-files.outputs.any_changed == 'true'

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
"build": "next build",
88
"start": "next start",
99
"lint": "next lint",
10-
"translate": "node scripts/translate-docs.mjs",
11-
"translate:all": "node scripts/translate-docs.mjs --all"
10+
"translate": "docs-cli translate",
11+
"translate:all": "docs-cli translate --all"
12+
},
13+
"devDependencies": {
14+
"@docs/cli": "workspace:*"
1215
},
1316
"dependencies": {
1417
"@fumadocs/ui": "^16.4.7",

packages/docs-cli/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
dist
3+
.env
4+
.DS_Store
5+
*.log
6+
coverage

packages/docs-cli/bin/cli.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
import { cac } from 'cac';
3+
import 'dotenv/config';
4+
import { registerTranslateCommand } from '../src/commands/translate.mjs';
5+
6+
const cli = cac('docs-cli');
7+
8+
registerTranslateCommand(cli);
9+
10+
cli.help();
11+
cli.version('0.0.1');
12+
13+
cli.parse();
14+

packages/docs-cli/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@docs/cli",
3+
"version": "0.0.1",
4+
"type": "module",
5+
"bin": {
6+
"docs-cli": "./bin/cli.mjs"
7+
},
8+
"scripts": {
9+
"test": "echo \"Error: no test specified\" && exit 1"
10+
},
11+
"dependencies": {
12+
"openai": "^4.0.0",
13+
"cac": "^6.7.14",
14+
"dotenv": "^16.4.5"
15+
}
16+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import OpenAI from 'openai';
4+
import { getAllMdxFiles, resolveTranslatedFilePath, translateContent } from '../utils/translate.mjs';
5+
6+
export function registerTranslateCommand(cli) {
7+
cli
8+
.command('translate [files...]', 'Translate documentation files')
9+
.option('--all', 'Translate all files in content/docs')
10+
.option('--model <model>', 'OpenAI model to use', { default: 'gpt-4o' })
11+
.action(async (files, options) => {
12+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
13+
const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL;
14+
15+
if (!OPENAI_API_KEY) {
16+
console.error('Error: Missing OPENAI_API_KEY environment variable.');
17+
process.exit(1);
18+
}
19+
20+
const openai = new OpenAI({
21+
apiKey: OPENAI_API_KEY,
22+
baseURL: OPENAI_BASE_URL,
23+
});
24+
25+
let targetFiles = [];
26+
27+
if (options.all) {
28+
console.log('Scanning for all .mdx files in content/docs...');
29+
targetFiles = getAllMdxFiles('content/docs');
30+
} else if (files && files.length > 0) {
31+
targetFiles = files;
32+
} else if (process.env.CHANGED_FILES) {
33+
targetFiles = process.env.CHANGED_FILES.split(',');
34+
}
35+
36+
if (targetFiles.length === 0) {
37+
console.log('No files to translate.');
38+
console.log('Usage:');
39+
console.log(' docs-cli translate content/docs/file.mdx');
40+
console.log(' docs-cli translate --all');
41+
console.log(' (CI): Set CHANGED_FILES environment variable');
42+
return;
43+
}
44+
45+
console.log(`Processing ${targetFiles.length} files...`);
46+
47+
for (const file of targetFiles) {
48+
const enFilePath = path.resolve(process.cwd(), file);
49+
50+
if (!fs.existsSync(enFilePath)) {
51+
console.log(`File skipped (not found): ${file}`);
52+
continue;
53+
}
54+
55+
const zhFilePath = resolveTranslatedFilePath(enFilePath);
56+
57+
if (zhFilePath === enFilePath) {
58+
console.log(`Skipping: Source and destination are the same for ${file}`);
59+
continue;
60+
}
61+
62+
console.log(`Translating: ${file} -> ${path.relative(process.cwd(), zhFilePath)}`);
63+
64+
try {
65+
const content = fs.readFileSync(enFilePath, 'utf-8');
66+
const translatedContent = await translateContent(content, openai, options.model);
67+
68+
const dir = path.dirname(zhFilePath);
69+
if (!fs.existsSync(dir)) {
70+
fs.mkdirSync(dir, { recursive: true });
71+
}
72+
73+
fs.writeFileSync(zhFilePath, translatedContent);
74+
console.log(`✓ Automatically translated: ${zhFilePath}`);
75+
} catch (error) {
76+
console.error(`✗ Failed to translate ${file}:`, error);
77+
}
78+
}
79+
});
80+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import OpenAI from 'openai';
4+
5+
export function getAllMdxFiles(dir) {
6+
let results = [];
7+
if (!fs.existsSync(dir)) return results;
8+
9+
const list = fs.readdirSync(dir);
10+
list.forEach(file => {
11+
file = path.join(dir, file);
12+
const stat = fs.statSync(file);
13+
if (stat && stat.isDirectory()) {
14+
results = results.concat(getAllMdxFiles(file));
15+
} else {
16+
if (file.endsWith('.mdx')) {
17+
results.push(path.relative(process.cwd(), file));
18+
}
19+
}
20+
});
21+
return results;
22+
}
23+
24+
export function resolveTranslatedFilePath(enFilePath) {
25+
// Strategy: content/docs/path/to/file.mdx -> content/docs-zh-CN/path/to/file.mdx
26+
const docsRoot = path.join(process.cwd(), 'content/docs');
27+
28+
// If input path is relative, make it absolute first to check
29+
const absPath = path.resolve(enFilePath);
30+
31+
if (!absPath.startsWith(docsRoot)) {
32+
// Fallback or specific logic if file is not in content/docs
33+
return enFilePath.replace('content/docs', 'content/docs-zh-CN');
34+
}
35+
36+
const relativePath = path.relative(docsRoot, enFilePath);
37+
return path.join(process.cwd(), 'content/docs-zh-CN', relativePath);
38+
}
39+
40+
export async function translateContent(content, openai, model) {
41+
const prompt = `
42+
You are a technical documentation translator for "ObjectStack".
43+
Translate the following MDX documentation from English to Chinese (Simplified).
44+
45+
Rules:
46+
1. Preserve all MDX frontmatter (keys and structure). only translate the values if they are regular text.
47+
2. Preserve all code blocks exactly as they are. Do not translate code comments unless they are purely explanatory and not part of the logic.
48+
3. Use professional software terminology (e.g. "ObjectStack", "ObjectQL", "ObjectUI" should strictly remain in English).
49+
4. "Local-First" translate to "本地优先".
50+
5. "Protocol-Driven" translate to "协议驱动".
51+
6. Maintain the original markdown formatting (links, bold, italics).
52+
53+
Content to translate:
54+
---
55+
${content}
56+
---
57+
`;
58+
59+
try {
60+
const response = await openai.chat.completions.create({
61+
model: model,
62+
messages: [{ role: 'user', content: prompt }],
63+
temperature: 0.1,
64+
});
65+
66+
return response.choices[0].message.content.trim();
67+
} catch (error) {
68+
console.error('Translation failed:', error);
69+
throw error;
70+
}
71+
}

0 commit comments

Comments
 (0)