@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 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
+ }