@objectdocs/cli 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +75 -0
- package/bin/cli.mjs +28 -0
- package/package.json +22 -0
- package/src/commands/build.mjs +106 -0
- package/src/commands/dev.mjs +105 -0
- package/src/commands/start.mjs +77 -0
- package/src/commands/translate.mjs +93 -0
- package/src/utils/translate.mjs +148 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @objectdocs/cli
|
|
2
|
+
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Bug fixes and improvements for ObjectDocs
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @objectdocs/site@0.2.1
|
|
10
|
+
|
|
11
|
+
## 0.2.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- Initial release of ObjectDocs - A modern documentation engine built on Next.js 14 and Fumadocs
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies
|
|
20
|
+
- @objectdocs/site@0.2.0
|
|
21
|
+
|
|
22
|
+
## 0.1.0
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- Initial release of ObjectDocs
|
|
27
|
+
|
|
28
|
+
- Modern documentation engine built on Next.js 14 and Fumadocs
|
|
29
|
+
- Metadata-driven architecture with configuration as code
|
|
30
|
+
- Support for low-code component embedding
|
|
31
|
+
- Enterprise-grade UI with dark mode support
|
|
32
|
+
- Multi-product documentation support
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- Updated dependencies
|
|
37
|
+
- @objectdocs/site@0.1.0
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @objectdocs/cli
|
|
2
|
+
|
|
3
|
+
The central CLI orchestration tool for the ObjectStack documentation platform.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This CLI acts as the unified interface for developing, building, and translating the ObjectStack documentation. It wraps the application logic in `@objectdocs/site` and adds workflow automation.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Site Orchestration**: Manages the Next.js development server and static build process.
|
|
12
|
+
- **AI Translation**: Automatically translates MDX documentation from English to Chinese using OpenAI.
|
|
13
|
+
- **Artifact Management**: Handles build output movement and directory structure organization.
|
|
14
|
+
- **Smart Updates**: Can process specific files or bulk translate the entire documentation.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
This package is part of the monorepo workspace. Install dependencies from the root:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Site Management
|
|
27
|
+
|
|
28
|
+
The CLI can also be used to run the documentation site locally with a VitePress-like experience.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Start Dev Server
|
|
32
|
+
# Usage: pnpm objectdocs dev [docs-directory]
|
|
33
|
+
pnpm objectdocs dev ./content/docs
|
|
34
|
+
|
|
35
|
+
# Build Static Site
|
|
36
|
+
# Usage: pnpm objectdocs build [docs-directory]
|
|
37
|
+
pnpm objectdocs build ./content/docs
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Translate Documentation
|
|
41
|
+
|
|
42
|
+
The `translate` command reads English documentation from `content/docs/*.mdx` and generates Chinese translations as `*.cn.mdx` files in the same directory using the dot parser convention.
|
|
43
|
+
|
|
44
|
+
**Prerequisites:**
|
|
45
|
+
You must set the following environment variables (in `.env` or your shell):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
OPENAI_API_KEY=sk-...
|
|
49
|
+
OPENAI_BASE_URL=https://api.openai.com/v1 # Optional
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Commands:**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Translate a specific file
|
|
56
|
+
pnpm objectdocs translate content/docs/00-intro/index.mdx
|
|
57
|
+
|
|
58
|
+
# Translate multiple files
|
|
59
|
+
pnpm objectdocs translate content/docs/00-intro/index.mdx content/docs/01-quickstart/index.mdx
|
|
60
|
+
|
|
61
|
+
# Translate all files in content/docs
|
|
62
|
+
pnpm objectdocs translate --all
|
|
63
|
+
|
|
64
|
+
# Specify a custom model (default: gpt-4o)
|
|
65
|
+
pnpm objectdocs translate --all --model gpt-4-turbo
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### CI/CD Integration
|
|
69
|
+
|
|
70
|
+
In CI environments, you can use the `CHANGED_FILES` environment variable to translate only modified files:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export CHANGED_FILES="content/docs/new-page.mdx"
|
|
74
|
+
pnpm objectdocs translate
|
|
75
|
+
```
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ObjectDocs
|
|
4
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { cac } from 'cac';
|
|
11
|
+
import 'dotenv/config';
|
|
12
|
+
import { registerTranslateCommand } from '../src/commands/translate.mjs';
|
|
13
|
+
import { registerDevCommand } from '../src/commands/dev.mjs';
|
|
14
|
+
import { registerBuildCommand } from '../src/commands/build.mjs';
|
|
15
|
+
import { registerStartCommand } from '../src/commands/start.mjs';
|
|
16
|
+
|
|
17
|
+
const cli = cac('objectdocs');
|
|
18
|
+
|
|
19
|
+
registerTranslateCommand(cli);
|
|
20
|
+
registerDevCommand(cli);
|
|
21
|
+
registerBuildCommand(cli);
|
|
22
|
+
registerStartCommand(cli);
|
|
23
|
+
|
|
24
|
+
cli.help();
|
|
25
|
+
cli.version('0.0.1');
|
|
26
|
+
|
|
27
|
+
cli.parse();
|
|
28
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectdocs/cli",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"objectdocs": "./bin/cli.mjs"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@types/node": "^25.0.8",
|
|
10
|
+
"cac": "^6.7.14",
|
|
11
|
+
"dotenv": "^16.4.5",
|
|
12
|
+
"openai": "^4.0.0",
|
|
13
|
+
"typescript": "^5.9.3",
|
|
14
|
+
"@objectdocs/site": "0.2.1"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "node ./bin/cli.mjs dev ../../content/docs",
|
|
18
|
+
"build": "node ./bin/cli.mjs build ../../content/docs",
|
|
19
|
+
"translate": "node ./bin/cli.mjs translate",
|
|
20
|
+
"test": "echo \"No test specified\" && exit 0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectDocs
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
export function registerBuildCommand(cli) {
|
|
20
|
+
cli
|
|
21
|
+
.command('build [dir]', 'Build static documentation site')
|
|
22
|
+
.action(async (dir, options) => {
|
|
23
|
+
// 1. Resolve user's docs directory
|
|
24
|
+
const docsDir = dir ? path.resolve(process.cwd(), dir) : path.resolve(process.cwd(), 'content/docs');
|
|
25
|
+
|
|
26
|
+
// 2. Resolve the Next.js App directory
|
|
27
|
+
let nextAppDir;
|
|
28
|
+
try {
|
|
29
|
+
nextAppDir = path.dirname(require.resolve('@objectdocs/site/package.json'));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Fallback for local development
|
|
32
|
+
nextAppDir = path.resolve(__dirname, '../../../site');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Copy user config and assets to nextAppDir
|
|
36
|
+
const userConfigPath = path.resolve(process.cwd(), 'content/docs.site.json');
|
|
37
|
+
if (fs.existsSync(userConfigPath)) {
|
|
38
|
+
console.log(` Copying config from ${userConfigPath}`);
|
|
39
|
+
fs.cpSync(userConfigPath, path.join(nextAppDir, 'docs.site.json'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const userPublicPath = path.resolve(process.cwd(), 'public');
|
|
43
|
+
if (fs.existsSync(userPublicPath)) {
|
|
44
|
+
console.log(` Copying public assets from ${userPublicPath}`);
|
|
45
|
+
const targetPublicDir = path.join(nextAppDir, 'public');
|
|
46
|
+
if (!fs.existsSync(targetPublicDir)) {
|
|
47
|
+
fs.mkdirSync(targetPublicDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
fs.cpSync(userPublicPath, targetPublicDir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`Building docs site...`);
|
|
53
|
+
console.log(` Engine: ${nextAppDir}`);
|
|
54
|
+
console.log(` Content: ${docsDir}`);
|
|
55
|
+
|
|
56
|
+
const env = {
|
|
57
|
+
...process.env,
|
|
58
|
+
DOCS_DIR: docsDir
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const nextCmd = 'npm';
|
|
62
|
+
const args = ['run', 'build'];
|
|
63
|
+
|
|
64
|
+
const child = spawn(nextCmd, args, {
|
|
65
|
+
stdio: 'inherit',
|
|
66
|
+
env,
|
|
67
|
+
cwd: nextAppDir // CRITICAL: Run in the Next.js app directory
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
child.on('close', (code) => {
|
|
71
|
+
if (code === 0) {
|
|
72
|
+
// Copy output to project root
|
|
73
|
+
const src = path.join(nextAppDir, 'out');
|
|
74
|
+
const dest = path.join(process.cwd(), 'out');
|
|
75
|
+
|
|
76
|
+
if (fs.existsSync(src)) {
|
|
77
|
+
console.log(`\nMoving build output to ${dest}...`);
|
|
78
|
+
if (fs.existsSync(dest)) {
|
|
79
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
82
|
+
console.log(`Build successfully output to: ${dest}`);
|
|
83
|
+
} else {
|
|
84
|
+
// Check for .next directory (dynamic build)
|
|
85
|
+
const srcNext = path.join(nextAppDir, '.next');
|
|
86
|
+
const destNext = path.join(process.cwd(), '.next');
|
|
87
|
+
|
|
88
|
+
if (fs.existsSync(srcNext) && srcNext !== destNext) {
|
|
89
|
+
console.log(`\nLinking .next build output to ${destNext}...`);
|
|
90
|
+
if (fs.existsSync(destNext)) {
|
|
91
|
+
fs.rmSync(destNext, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
// Use symlink instead of copy to preserve internal symlinks in .next (pnpm support)
|
|
94
|
+
fs.symlinkSync(srcNext, destNext, 'dir');
|
|
95
|
+
console.log(`Build successfully linked to: ${destNext}`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`\nNo 'out' directory generated in ${src}.`);
|
|
98
|
+
console.log(`This is expected if 'output: export' is disabled.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
process.exit(code);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectDocs
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
export function registerDevCommand(cli) {
|
|
20
|
+
cli
|
|
21
|
+
.command('dev [dir]', 'Start development server')
|
|
22
|
+
.option('--port <port>', 'Port to listen on', { default: 7777 })
|
|
23
|
+
.action(async (dir, options) => {
|
|
24
|
+
// 1. Resolve user's docs directory (Absolute path)
|
|
25
|
+
const docsDir = dir ? path.resolve(process.cwd(), dir) : path.resolve(process.cwd(), 'content/docs');
|
|
26
|
+
|
|
27
|
+
// 2. Resolve the Next.js App directory
|
|
28
|
+
let nextAppDir;
|
|
29
|
+
try {
|
|
30
|
+
nextAppDir = path.dirname(require.resolve('@objectdocs/site/package.json'));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// Fallback for local development
|
|
33
|
+
nextAppDir = path.resolve(__dirname, '../../../site');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`Starting docs server...`);
|
|
37
|
+
console.log(` Engine: ${nextAppDir}`);
|
|
38
|
+
console.log(` Content: ${docsDir}`);
|
|
39
|
+
|
|
40
|
+
const env = {
|
|
41
|
+
...process.env,
|
|
42
|
+
DOCS_DIR: docsDir,
|
|
43
|
+
PORT: options.port
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const nextCmd = 'npm';
|
|
47
|
+
const args = ['run', 'dev', '--', '-p', options.port];
|
|
48
|
+
|
|
49
|
+
let child;
|
|
50
|
+
let isRestarting = false;
|
|
51
|
+
let debounceTimer;
|
|
52
|
+
|
|
53
|
+
const startServer = () => {
|
|
54
|
+
// Sync config and assets before starting
|
|
55
|
+
const userConfigPath = path.resolve(process.cwd(), 'content/docs.site.json');
|
|
56
|
+
if (fs.existsSync(userConfigPath)) {
|
|
57
|
+
fs.cpSync(userConfigPath, path.join(nextAppDir, 'docs.site.json'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const userPublicPath = path.resolve(process.cwd(), 'public');
|
|
61
|
+
if (fs.existsSync(userPublicPath)) {
|
|
62
|
+
const targetPublicDir = path.join(nextAppDir, 'public');
|
|
63
|
+
if (!fs.existsSync(targetPublicDir)) {
|
|
64
|
+
fs.mkdirSync(targetPublicDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
fs.cpSync(userPublicPath, targetPublicDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
child = spawn(nextCmd, args, {
|
|
70
|
+
stdio: 'inherit',
|
|
71
|
+
env,
|
|
72
|
+
cwd: nextAppDir // CRITICAL: Run in the Next.js app directory
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.on('close', (code) => {
|
|
76
|
+
if (isRestarting) {
|
|
77
|
+
isRestarting = false;
|
|
78
|
+
startServer();
|
|
79
|
+
} else {
|
|
80
|
+
// Only exit if we are not restarting.
|
|
81
|
+
// Null code means killed by signal (like our kill() call), but we handle that via flag.
|
|
82
|
+
process.exit(code || 0);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
startServer();
|
|
88
|
+
|
|
89
|
+
// Watch for config changes
|
|
90
|
+
const configFile = path.resolve(process.cwd(), 'content/docs.site.json');
|
|
91
|
+
if (fs.existsSync(configFile)) {
|
|
92
|
+
console.log(`Watching config: ${configFile}`);
|
|
93
|
+
fs.watch(configFile, (eventType) => {
|
|
94
|
+
if (eventType === 'change') {
|
|
95
|
+
clearTimeout(debounceTimer);
|
|
96
|
+
debounceTimer = setTimeout(() => {
|
|
97
|
+
console.log('\nConfig changed. Restarting server...');
|
|
98
|
+
isRestarting = true;
|
|
99
|
+
child.kill();
|
|
100
|
+
}, 500);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectDocs
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
export function registerStartCommand(cli) {
|
|
20
|
+
cli.command('start [dir]', 'Start the production server')
|
|
21
|
+
.action((dir) => {
|
|
22
|
+
// 1. Resolve Next.js App directory
|
|
23
|
+
let nextAppDir;
|
|
24
|
+
try {
|
|
25
|
+
nextAppDir = path.dirname(require.resolve('@objectdocs/site/package.json'));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
nextAppDir = path.resolve(__dirname, '../../../site');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Check config
|
|
31
|
+
let isStatic = false;
|
|
32
|
+
try {
|
|
33
|
+
const configPath = path.resolve(process.cwd(), 'content/docs.site.json');
|
|
34
|
+
if (fs.existsSync(configPath)) {
|
|
35
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
36
|
+
if (config.build?.output === 'export') {
|
|
37
|
+
isStatic = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isStatic || (dir && dir !== 'out')) {
|
|
45
|
+
// Static Mode
|
|
46
|
+
const targetDir = dir ? path.resolve(process.cwd(), dir) : path.resolve(process.cwd(), 'out');
|
|
47
|
+
console.log(`Serving static site from: ${targetDir}`);
|
|
48
|
+
|
|
49
|
+
const child = spawn('npx', ['serve', targetDir], {
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
shell: true
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
child.on('error', (err) => {
|
|
55
|
+
console.error('Failed to start server:', err);
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
// Dynamic Mode (Next.js start)
|
|
59
|
+
console.log('Starting Next.js production server...');
|
|
60
|
+
console.log(` Engine: ${nextAppDir}`);
|
|
61
|
+
|
|
62
|
+
const docsDir = path.resolve(process.cwd(), 'content/docs');
|
|
63
|
+
|
|
64
|
+
const env = {
|
|
65
|
+
...process.env,
|
|
66
|
+
DOCS_DIR: docsDir
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const child = spawn('npm', ['start'], {
|
|
71
|
+
cwd: nextAppDir,
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
env: env
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectDocs
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import OpenAI from 'openai';
|
|
12
|
+
import { getAllMdxFiles, resolveTranslatedFilePath, translateContent, getSiteConfig } from '../utils/translate.mjs';
|
|
13
|
+
|
|
14
|
+
export function registerTranslateCommand(cli) {
|
|
15
|
+
cli
|
|
16
|
+
.command('translate [files...]', 'Translate documentation files')
|
|
17
|
+
.option('--all', 'Translate all files in content/docs')
|
|
18
|
+
.option('--model <model>', 'OpenAI model to use', { default: 'gpt-4o' })
|
|
19
|
+
.action(async (files, options) => {
|
|
20
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
21
|
+
const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL;
|
|
22
|
+
|
|
23
|
+
if (!OPENAI_API_KEY) {
|
|
24
|
+
console.error('Error: Missing OPENAI_API_KEY environment variable.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const openai = new OpenAI({
|
|
29
|
+
apiKey: OPENAI_API_KEY,
|
|
30
|
+
baseURL: OPENAI_BASE_URL,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Get language configuration
|
|
34
|
+
const config = getSiteConfig();
|
|
35
|
+
console.log(`Translation target: ${config.defaultLanguage} -> ${config.targetLanguage}`);
|
|
36
|
+
console.log(`Configured languages: ${config.languages.join(', ')}\n`);
|
|
37
|
+
|
|
38
|
+
let targetFiles = [];
|
|
39
|
+
|
|
40
|
+
if (options.all) {
|
|
41
|
+
console.log('Scanning for all .mdx files in content/docs...');
|
|
42
|
+
targetFiles = getAllMdxFiles('content/docs');
|
|
43
|
+
} else if (files && files.length > 0) {
|
|
44
|
+
targetFiles = files;
|
|
45
|
+
} else if (process.env.CHANGED_FILES) {
|
|
46
|
+
targetFiles = process.env.CHANGED_FILES.split(',');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (targetFiles.length === 0) {
|
|
50
|
+
console.log('No files to translate.');
|
|
51
|
+
console.log('Usage:');
|
|
52
|
+
console.log(' objectdocs translate content/docs/file.mdx');
|
|
53
|
+
console.log(' objectdocs translate --all');
|
|
54
|
+
console.log(' (CI): Set CHANGED_FILES environment variable');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`Processing ${targetFiles.length} files...`);
|
|
59
|
+
|
|
60
|
+
for (const file of targetFiles) {
|
|
61
|
+
const enFilePath = path.resolve(process.cwd(), file);
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(enFilePath)) {
|
|
64
|
+
console.log(`File skipped (not found): ${file}`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const zhFilePath = resolveTranslatedFilePath(enFilePath);
|
|
69
|
+
|
|
70
|
+
if (zhFilePath === enFilePath) {
|
|
71
|
+
console.log(`Skipping: Source and destination are the same for ${file}`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`Translating: ${file} -> ${path.relative(process.cwd(), zhFilePath)}`);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const content = fs.readFileSync(enFilePath, 'utf-8');
|
|
79
|
+
const translatedContent = await translateContent(content, openai, options.model);
|
|
80
|
+
|
|
81
|
+
const dir = path.dirname(zhFilePath);
|
|
82
|
+
if (!fs.existsSync(dir)) {
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fs.writeFileSync(zhFilePath, translatedContent);
|
|
87
|
+
console.log(`✓ Automatically translated: ${zhFilePath}`);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`✗ Failed to translate ${file}:`, error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectDocs
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import OpenAI from 'openai';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Load site configuration from docs.site.json
|
|
15
|
+
* @returns {object} - The site configuration
|
|
16
|
+
*/
|
|
17
|
+
function loadSiteConfig() {
|
|
18
|
+
const configPath = path.resolve(process.cwd(), 'content/docs.site.json');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(configPath)) {
|
|
21
|
+
console.warn(`Warning: docs.site.json not found at ${configPath}, using defaults`);
|
|
22
|
+
// Fallback matches the default configuration in packages/site/lib/site-config.ts
|
|
23
|
+
return {
|
|
24
|
+
i18n: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
defaultLanguage: 'en',
|
|
27
|
+
languages: ['en', 'cn']
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
34
|
+
return JSON.parse(configContent);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error loading docs.site.json:', error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Load configuration
|
|
42
|
+
const siteConfig = loadSiteConfig();
|
|
43
|
+
const languages = siteConfig.i18n?.languages || ['en', 'cn'];
|
|
44
|
+
const defaultLanguage = siteConfig.i18n?.defaultLanguage || 'en';
|
|
45
|
+
|
|
46
|
+
// Generate language suffixes dynamically from config
|
|
47
|
+
// e.g., ['en', 'cn'] -> ['.en.mdx', '.cn.mdx']
|
|
48
|
+
const LANGUAGE_SUFFIXES = languages.map(lang => `.${lang}.mdx`);
|
|
49
|
+
|
|
50
|
+
// Target language is the first non-default language
|
|
51
|
+
const targetLanguage = languages.find(lang => lang !== defaultLanguage) || languages[0];
|
|
52
|
+
const TARGET_LANGUAGE_SUFFIX = `.${targetLanguage}.mdx`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the current site configuration
|
|
56
|
+
* @returns {object} - Configuration object with languages info
|
|
57
|
+
*/
|
|
58
|
+
export function getSiteConfig() {
|
|
59
|
+
return {
|
|
60
|
+
languages,
|
|
61
|
+
defaultLanguage,
|
|
62
|
+
targetLanguage,
|
|
63
|
+
languageSuffixes: LANGUAGE_SUFFIXES,
|
|
64
|
+
targetLanguageSuffix: TARGET_LANGUAGE_SUFFIX,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a file has a language suffix
|
|
70
|
+
* @param {string} filePath - The file path to check
|
|
71
|
+
* @returns {boolean} - True if file has a language suffix
|
|
72
|
+
*/
|
|
73
|
+
function hasLanguageSuffix(filePath) {
|
|
74
|
+
return LANGUAGE_SUFFIXES.some(suffix => filePath.endsWith(suffix));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getAllMdxFiles(dir) {
|
|
78
|
+
let results = [];
|
|
79
|
+
if (!fs.existsSync(dir)) return results;
|
|
80
|
+
|
|
81
|
+
const list = fs.readdirSync(dir);
|
|
82
|
+
list.forEach(file => {
|
|
83
|
+
file = path.join(dir, file);
|
|
84
|
+
const stat = fs.statSync(file);
|
|
85
|
+
if (stat && stat.isDirectory()) {
|
|
86
|
+
results = results.concat(getAllMdxFiles(file));
|
|
87
|
+
} else {
|
|
88
|
+
// Only include .mdx files that don't have language suffix
|
|
89
|
+
if (file.endsWith('.mdx') && !hasLanguageSuffix(file)) {
|
|
90
|
+
results.push(path.relative(process.cwd(), file));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resolveTranslatedFilePath(enFilePath) {
|
|
98
|
+
// Strategy: Use dot parser convention
|
|
99
|
+
// content/docs/path/to/file.mdx -> content/docs/path/to/file.{targetLang}.mdx
|
|
100
|
+
// Target language is determined from docs.site.json configuration
|
|
101
|
+
// Skip files that already have language suffix
|
|
102
|
+
const absPath = path.resolve(enFilePath);
|
|
103
|
+
|
|
104
|
+
// Skip if already has a language suffix
|
|
105
|
+
if (hasLanguageSuffix(absPath)) {
|
|
106
|
+
return absPath;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Replace .mdx with target language suffix
|
|
110
|
+
if (absPath.endsWith('.mdx')) {
|
|
111
|
+
return absPath.replace(/\.mdx$/, TARGET_LANGUAGE_SUFFIX);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return absPath;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function translateContent(content, openai, model) {
|
|
118
|
+
const prompt = `
|
|
119
|
+
You are a technical documentation translator for "ObjectStack".
|
|
120
|
+
Translate the following MDX documentation from English to Chinese (Simplified).
|
|
121
|
+
|
|
122
|
+
Rules:
|
|
123
|
+
1. Preserve all MDX frontmatter (keys and structure). only translate the values if they are regular text.
|
|
124
|
+
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.
|
|
125
|
+
3. Use professional software terminology (e.g. "ObjectStack", "ObjectQL", "ObjectUI" should strictly remain in English).
|
|
126
|
+
4. "Local-First" translate to "本地优先".
|
|
127
|
+
5. "Protocol-Driven" translate to "协议驱动".
|
|
128
|
+
6. Maintain the original markdown formatting (links, bold, italics).
|
|
129
|
+
|
|
130
|
+
Content to translate:
|
|
131
|
+
---
|
|
132
|
+
${content}
|
|
133
|
+
---
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await openai.chat.completions.create({
|
|
138
|
+
model: model,
|
|
139
|
+
messages: [{ role: 'user', content: prompt }],
|
|
140
|
+
temperature: 0.1,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return response.choices[0].message.content.trim();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Translation failed:', error);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|