@llm-translate/cli 1.0.0-next.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/.dockerignore +51 -0
- package/.env.example +33 -0
- package/.github/workflows/docs-pages.yml +57 -0
- package/.github/workflows/release.yml +49 -0
- package/.translaterc.json +44 -0
- package/CLAUDE.md +243 -0
- package/Dockerfile +55 -0
- package/README.md +371 -0
- package/RFC.md +1595 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4494 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1152 -0
- package/dist/index.js +3841 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.yml +56 -0
- package/docs/.vitepress/config.ts +161 -0
- package/docs/api/agent.md +262 -0
- package/docs/api/engine.md +274 -0
- package/docs/api/index.md +171 -0
- package/docs/api/providers.md +304 -0
- package/docs/changelog.md +64 -0
- package/docs/cli/dir.md +243 -0
- package/docs/cli/file.md +213 -0
- package/docs/cli/glossary.md +273 -0
- package/docs/cli/index.md +129 -0
- package/docs/cli/init.md +158 -0
- package/docs/cli/serve.md +211 -0
- package/docs/glossary.json +235 -0
- package/docs/guide/chunking.md +272 -0
- package/docs/guide/configuration.md +139 -0
- package/docs/guide/cost-optimization.md +237 -0
- package/docs/guide/docker.md +371 -0
- package/docs/guide/getting-started.md +150 -0
- package/docs/guide/glossary.md +241 -0
- package/docs/guide/index.md +86 -0
- package/docs/guide/ollama.md +515 -0
- package/docs/guide/prompt-caching.md +221 -0
- package/docs/guide/providers.md +232 -0
- package/docs/guide/quality-control.md +206 -0
- package/docs/guide/vitepress-integration.md +265 -0
- package/docs/index.md +63 -0
- package/docs/ja/api/agent.md +262 -0
- package/docs/ja/api/engine.md +274 -0
- package/docs/ja/api/index.md +171 -0
- package/docs/ja/api/providers.md +304 -0
- package/docs/ja/changelog.md +64 -0
- package/docs/ja/cli/dir.md +243 -0
- package/docs/ja/cli/file.md +213 -0
- package/docs/ja/cli/glossary.md +273 -0
- package/docs/ja/cli/index.md +111 -0
- package/docs/ja/cli/init.md +158 -0
- package/docs/ja/guide/chunking.md +271 -0
- package/docs/ja/guide/configuration.md +139 -0
- package/docs/ja/guide/cost-optimization.md +30 -0
- package/docs/ja/guide/getting-started.md +150 -0
- package/docs/ja/guide/glossary.md +214 -0
- package/docs/ja/guide/index.md +32 -0
- package/docs/ja/guide/ollama.md +410 -0
- package/docs/ja/guide/prompt-caching.md +221 -0
- package/docs/ja/guide/providers.md +232 -0
- package/docs/ja/guide/quality-control.md +137 -0
- package/docs/ja/guide/vitepress-integration.md +265 -0
- package/docs/ja/index.md +58 -0
- package/docs/ko/api/agent.md +262 -0
- package/docs/ko/api/engine.md +274 -0
- package/docs/ko/api/index.md +171 -0
- package/docs/ko/api/providers.md +304 -0
- package/docs/ko/changelog.md +64 -0
- package/docs/ko/cli/dir.md +243 -0
- package/docs/ko/cli/file.md +213 -0
- package/docs/ko/cli/glossary.md +273 -0
- package/docs/ko/cli/index.md +111 -0
- package/docs/ko/cli/init.md +158 -0
- package/docs/ko/guide/chunking.md +271 -0
- package/docs/ko/guide/configuration.md +139 -0
- package/docs/ko/guide/cost-optimization.md +30 -0
- package/docs/ko/guide/getting-started.md +150 -0
- package/docs/ko/guide/glossary.md +214 -0
- package/docs/ko/guide/index.md +32 -0
- package/docs/ko/guide/ollama.md +410 -0
- package/docs/ko/guide/prompt-caching.md +221 -0
- package/docs/ko/guide/providers.md +232 -0
- package/docs/ko/guide/quality-control.md +137 -0
- package/docs/ko/guide/vitepress-integration.md +265 -0
- package/docs/ko/index.md +58 -0
- package/docs/zh/api/agent.md +262 -0
- package/docs/zh/api/engine.md +274 -0
- package/docs/zh/api/index.md +171 -0
- package/docs/zh/api/providers.md +304 -0
- package/docs/zh/changelog.md +64 -0
- package/docs/zh/cli/dir.md +243 -0
- package/docs/zh/cli/file.md +213 -0
- package/docs/zh/cli/glossary.md +273 -0
- package/docs/zh/cli/index.md +111 -0
- package/docs/zh/cli/init.md +158 -0
- package/docs/zh/guide/chunking.md +271 -0
- package/docs/zh/guide/configuration.md +139 -0
- package/docs/zh/guide/cost-optimization.md +30 -0
- package/docs/zh/guide/getting-started.md +150 -0
- package/docs/zh/guide/glossary.md +214 -0
- package/docs/zh/guide/index.md +32 -0
- package/docs/zh/guide/ollama.md +410 -0
- package/docs/zh/guide/prompt-caching.md +221 -0
- package/docs/zh/guide/providers.md +232 -0
- package/docs/zh/guide/quality-control.md +137 -0
- package/docs/zh/guide/vitepress-integration.md +265 -0
- package/docs/zh/index.md +58 -0
- package/package.json +91 -0
- package/release.config.mjs +15 -0
- package/schemas/glossary.schema.json +110 -0
- package/src/cli/commands/dir.ts +469 -0
- package/src/cli/commands/file.ts +291 -0
- package/src/cli/commands/glossary.ts +221 -0
- package/src/cli/commands/init.ts +68 -0
- package/src/cli/commands/serve.ts +60 -0
- package/src/cli/index.ts +64 -0
- package/src/cli/options.ts +59 -0
- package/src/core/agent.ts +1119 -0
- package/src/core/chunker.ts +391 -0
- package/src/core/engine.ts +634 -0
- package/src/errors.ts +188 -0
- package/src/index.ts +147 -0
- package/src/integrations/vitepress.ts +549 -0
- package/src/parsers/markdown.ts +383 -0
- package/src/providers/claude.ts +259 -0
- package/src/providers/interface.ts +109 -0
- package/src/providers/ollama.ts +379 -0
- package/src/providers/openai.ts +308 -0
- package/src/providers/registry.ts +153 -0
- package/src/server/index.ts +152 -0
- package/src/server/middleware/auth.ts +93 -0
- package/src/server/middleware/logger.ts +90 -0
- package/src/server/routes/health.ts +84 -0
- package/src/server/routes/translate.ts +210 -0
- package/src/server/types.ts +138 -0
- package/src/services/cache.ts +899 -0
- package/src/services/config.ts +217 -0
- package/src/services/glossary.ts +247 -0
- package/src/types/analysis.ts +164 -0
- package/src/types/index.ts +265 -0
- package/src/types/modes.ts +121 -0
- package/src/types/mqm.ts +157 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/tokens.ts +116 -0
- package/tests/fixtures/glossaries/ml-glossary.json +53 -0
- package/tests/fixtures/input/lynq-installation.ko.md +350 -0
- package/tests/fixtures/input/lynq-installation.md +350 -0
- package/tests/fixtures/input/simple.ko.md +27 -0
- package/tests/fixtures/input/simple.md +27 -0
- package/tests/unit/chunker.test.ts +229 -0
- package/tests/unit/glossary.test.ts +146 -0
- package/tests/unit/markdown.test.ts +205 -0
- package/tests/unit/tokens.test.ts +81 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +34 -0
- package/vitest.config.ts +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@llm-translate/cli",
|
|
3
|
+
"version": "1.0.0-next.1",
|
|
4
|
+
"description": "CLI-based document translation tool powered by LLMs with glossary enforcement and quality-aware refinement",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"llm-translate": "./dist/cli/index.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"start": "node ./dist/cli/index.js",
|
|
21
|
+
"serve": "node ./dist/cli/index.js serve",
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"test:run": "vitest run",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"lint": "eslint src --ext .ts",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"docs:dev": "vitepress dev docs",
|
|
29
|
+
"docs:build": "vitepress build docs",
|
|
30
|
+
"docs:preview": "vitepress preview docs",
|
|
31
|
+
"docs:translate": "npm run docs:translate:ko && npm run docs:translate:ja && npm run docs:translate:zh",
|
|
32
|
+
"docs:translate:ko": "node ./dist/cli/index.js dir ./docs ./docs/ko --target-lang ko --glossary ./docs/glossary.json --parallel 8 --no-cache",
|
|
33
|
+
"docs:translate:ja": "node ./dist/cli/index.js dir ./docs ./docs/ja --target-lang ja --glossary ./docs/glossary.json --parallel 8 --no-cache",
|
|
34
|
+
"docs:translate:zh": "node ./dist/cli/index.js dir ./docs ./docs/zh --target-lang zh --glossary ./docs/glossary.json --parallel 8 --no-cache"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"translation",
|
|
38
|
+
"llm",
|
|
39
|
+
"cli",
|
|
40
|
+
"markdown",
|
|
41
|
+
"glossary",
|
|
42
|
+
"ai",
|
|
43
|
+
"claude",
|
|
44
|
+
"openai"
|
|
45
|
+
],
|
|
46
|
+
"author": "Tim Kang",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/selenehyun/llm-translate.git"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20.0.0"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@ai-sdk/anthropic": "^1.0.0",
|
|
60
|
+
"@ai-sdk/openai": "^1.0.0",
|
|
61
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
62
|
+
"@hono/node-server": "^1.14.0",
|
|
63
|
+
"@hono/zod-validator": "^0.4.3",
|
|
64
|
+
"ai": "^4.0.0",
|
|
65
|
+
"chalk": "^5.3.0",
|
|
66
|
+
"cheerio": "^1.0.0",
|
|
67
|
+
"commander": "^12.1.0",
|
|
68
|
+
"cosmiconfig": "^9.0.0",
|
|
69
|
+
"hono": "^4.7.0",
|
|
70
|
+
"ora": "^8.1.0",
|
|
71
|
+
"remark-gfm": "^4.0.0",
|
|
72
|
+
"remark-parse": "^11.0.0",
|
|
73
|
+
"remark-stringify": "^11.0.0",
|
|
74
|
+
"unified": "^11.0.5",
|
|
75
|
+
"unist-util-visit": "^5.0.0",
|
|
76
|
+
"zod": "^3.23.8"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@types/mdast": "^4.0.4",
|
|
80
|
+
"semantic-release": "^24.0.0",
|
|
81
|
+
"@types/node": "^22.10.1",
|
|
82
|
+
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
|
83
|
+
"@typescript-eslint/parser": "^8.17.0",
|
|
84
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
85
|
+
"eslint": "^9.16.0",
|
|
86
|
+
"tsup": "^8.3.5",
|
|
87
|
+
"typescript": "^5.7.2",
|
|
88
|
+
"vitepress": "^1.6.4",
|
|
89
|
+
"vitest": "^2.1.8"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type {import('semantic-release').GlobalConfig}
|
|
3
|
+
*/
|
|
4
|
+
export default {
|
|
5
|
+
branches: [
|
|
6
|
+
'main',
|
|
7
|
+
{ name: 'develop', channel: 'next', prerelease: 'next' },
|
|
8
|
+
],
|
|
9
|
+
plugins: [
|
|
10
|
+
'@semantic-release/commit-analyzer',
|
|
11
|
+
'@semantic-release/release-notes-generator',
|
|
12
|
+
'@semantic-release/npm',
|
|
13
|
+
'@semantic-release/github',
|
|
14
|
+
],
|
|
15
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/selenehyun/llm-translate/schemas/glossary.schema.json",
|
|
4
|
+
"title": "llm-translate Glossary",
|
|
5
|
+
"description": "Schema for llm-translate glossary files",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["metadata", "terms"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "JSON Schema reference"
|
|
12
|
+
},
|
|
13
|
+
"metadata": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"description": "Glossary metadata",
|
|
16
|
+
"required": ["name", "sourceLang", "targetLangs", "version"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"name": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Unique identifier for the glossary"
|
|
21
|
+
},
|
|
22
|
+
"sourceLang": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Source language code (e.g., 'en', 'ko')",
|
|
25
|
+
"pattern": "^[a-z]{2}(-[A-Z]{2})?$"
|
|
26
|
+
},
|
|
27
|
+
"targetLangs": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"description": "List of target language codes",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"pattern": "^[a-z]{2}(-[A-Z]{2})?$"
|
|
33
|
+
},
|
|
34
|
+
"minItems": 1
|
|
35
|
+
},
|
|
36
|
+
"version": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "Glossary version (semver recommended)",
|
|
39
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
|
40
|
+
},
|
|
41
|
+
"domain": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Domain or category of the glossary (e.g., 'technical-documentation', 'legal')"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"additionalProperties": false
|
|
47
|
+
},
|
|
48
|
+
"terms": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"description": "List of glossary terms",
|
|
51
|
+
"items": {
|
|
52
|
+
"$ref": "#/$defs/term"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"$defs": {
|
|
58
|
+
"term": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"description": "A single glossary term",
|
|
61
|
+
"required": ["source", "targets"],
|
|
62
|
+
"properties": {
|
|
63
|
+
"source": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Source term to match",
|
|
66
|
+
"minLength": 1
|
|
67
|
+
},
|
|
68
|
+
"targets": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"description": "Target translations keyed by language code",
|
|
71
|
+
"additionalProperties": {
|
|
72
|
+
"type": "string"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"context": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Context or usage hint for the translator"
|
|
78
|
+
},
|
|
79
|
+
"caseSensitive": {
|
|
80
|
+
"type": "boolean",
|
|
81
|
+
"description": "Whether matching should be case-sensitive. When false (default), 'API' matches 'api', 'Api', 'API', etc.",
|
|
82
|
+
"default": false
|
|
83
|
+
},
|
|
84
|
+
"doNotTranslate": {
|
|
85
|
+
"type": "boolean",
|
|
86
|
+
"description": "Keep the source term unchanged in all languages",
|
|
87
|
+
"default": false
|
|
88
|
+
},
|
|
89
|
+
"doNotTranslateFor": {
|
|
90
|
+
"type": "array",
|
|
91
|
+
"description": "Keep the source term unchanged for specific languages",
|
|
92
|
+
"items": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"pattern": "^[a-z]{2}(-[A-Z]{2})?$"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"partOfSpeech": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"description": "Part of speech for the term",
|
|
100
|
+
"enum": ["noun", "verb", "adjective", "other"]
|
|
101
|
+
},
|
|
102
|
+
"notes": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "Additional notes for translators"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"additionalProperties": false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { resolve, dirname, relative, join } from 'node:path';
|
|
4
|
+
import type { DirCommandOptions } from '../options.js';
|
|
5
|
+
import { defaults } from '../options.js';
|
|
6
|
+
import { loadConfig, mergeConfig } from '../../services/config.js';
|
|
7
|
+
import { createTranslationEngine } from '../../core/engine.js';
|
|
8
|
+
import { logger, configureLogger } from '../../utils/logger.js';
|
|
9
|
+
import { TranslationError, getExitCode } from '../../errors.js';
|
|
10
|
+
import type { DocumentFormat, DocumentResult } from '../../types/index.js';
|
|
11
|
+
|
|
12
|
+
export const dirCommand = new Command('dir')
|
|
13
|
+
.description('Translate all files in a directory')
|
|
14
|
+
.argument('<input>', 'Input directory path')
|
|
15
|
+
.argument('<output>', 'Output directory path')
|
|
16
|
+
.option('-s, --source-lang <lang>', 'Source language code')
|
|
17
|
+
.option('-t, --target-lang <lang>', 'Target language code')
|
|
18
|
+
.option('-g, --glossary <path>', 'Path to glossary file')
|
|
19
|
+
.option(
|
|
20
|
+
'-p, --provider <name>',
|
|
21
|
+
'LLM provider (claude|openai|ollama)',
|
|
22
|
+
defaults.provider
|
|
23
|
+
)
|
|
24
|
+
.option('-m, --model <name>', 'Model name')
|
|
25
|
+
.option(
|
|
26
|
+
'--quality <0-100>',
|
|
27
|
+
'Quality threshold',
|
|
28
|
+
String(defaults.quality)
|
|
29
|
+
)
|
|
30
|
+
.option(
|
|
31
|
+
'--max-iterations <n>',
|
|
32
|
+
'Max refinement iterations',
|
|
33
|
+
String(defaults.maxIterations)
|
|
34
|
+
)
|
|
35
|
+
.option('-f, --format <fmt>', 'Force output format (md|html|txt)')
|
|
36
|
+
.option('--dry-run', 'Show what would be translated')
|
|
37
|
+
.option('--json', 'Output results as JSON')
|
|
38
|
+
.option(
|
|
39
|
+
'--chunk-size <tokens>',
|
|
40
|
+
'Max tokens per chunk',
|
|
41
|
+
String(defaults.chunkSize)
|
|
42
|
+
)
|
|
43
|
+
.option(
|
|
44
|
+
'--parallel <n>',
|
|
45
|
+
'Parallel file processing',
|
|
46
|
+
String(defaults.parallel)
|
|
47
|
+
)
|
|
48
|
+
.option('--no-cache', 'Disable translation cache')
|
|
49
|
+
.option('--context <text>', 'Additional context for translation')
|
|
50
|
+
.option('--include <patterns>', 'File patterns to include (comma-separated)', '*.md,*.markdown')
|
|
51
|
+
.option('--exclude <patterns>', 'File patterns to exclude (comma-separated)')
|
|
52
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
53
|
+
.option('-q, --quiet', 'Suppress non-error output')
|
|
54
|
+
.action(async (input: string, output: string, options: DirCommandOptions & { include?: string; exclude?: string }) => {
|
|
55
|
+
try {
|
|
56
|
+
// Configure logger
|
|
57
|
+
configureLogger({
|
|
58
|
+
level: options.verbose ? 'debug' : 'info',
|
|
59
|
+
quiet: options.quiet ?? false,
|
|
60
|
+
json: options.json ?? false,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Validate required options
|
|
64
|
+
if (!options.targetLang) {
|
|
65
|
+
console.error('Error: Target language (-t, --target-lang) is required');
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Resolve paths
|
|
70
|
+
const inputDir = resolve(input);
|
|
71
|
+
const outputDir = resolve(output);
|
|
72
|
+
|
|
73
|
+
// Parse include/exclude patterns
|
|
74
|
+
const includePatterns = options.include?.split(',').map(p => p.trim()) ?? ['*.md', '*.markdown'];
|
|
75
|
+
const excludePatterns = options.exclude?.split(',').map(p => p.trim()) ?? [];
|
|
76
|
+
|
|
77
|
+
// Find all matching files (exclude output directory and locale directories)
|
|
78
|
+
const files = await findFiles(inputDir, includePatterns, excludePatterns, outputDir);
|
|
79
|
+
|
|
80
|
+
if (files.length === 0) {
|
|
81
|
+
console.log('No files found matching the specified patterns');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!options.quiet) {
|
|
86
|
+
logger.info(`Found ${files.length} file(s) to translate`);
|
|
87
|
+
logger.info(`Input: ${inputDir}`);
|
|
88
|
+
logger.info(`Output: ${outputDir}`);
|
|
89
|
+
logger.info(`Target language: ${options.targetLang}`);
|
|
90
|
+
if (options.glossary) {
|
|
91
|
+
logger.info(`Glossary: ${resolve(options.glossary)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Dry run - just show what would be done
|
|
96
|
+
if (options.dryRun) {
|
|
97
|
+
console.log('\nDry run mode - no translation will be performed\n');
|
|
98
|
+
console.log('Files to translate:');
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const relativePath = relative(inputDir, file);
|
|
101
|
+
const outputPath = join(outputDir, relativePath);
|
|
102
|
+
console.log(` ${relativePath} → ${relative(process.cwd(), outputPath)}`);
|
|
103
|
+
}
|
|
104
|
+
console.log(`\nTotal: ${files.length} file(s)`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Load configuration
|
|
109
|
+
const baseConfig = await loadConfig({ configPath: options.config });
|
|
110
|
+
const config = mergeConfig(baseConfig, {
|
|
111
|
+
sourceLang: options.sourceLang,
|
|
112
|
+
targetLang: options.targetLang,
|
|
113
|
+
provider: options.provider,
|
|
114
|
+
model: options.model,
|
|
115
|
+
quality: options.quality ? parseInt(options.quality, 10) : undefined,
|
|
116
|
+
maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : undefined,
|
|
117
|
+
chunkSize: options.chunkSize ? parseInt(options.chunkSize, 10) : undefined,
|
|
118
|
+
glossary: options.glossary,
|
|
119
|
+
noCache: options.cache === false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Create translation engine
|
|
123
|
+
const engine = createTranslationEngine({
|
|
124
|
+
config,
|
|
125
|
+
verbose: options.verbose,
|
|
126
|
+
noCache: options.cache === false,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Process files
|
|
130
|
+
const parallelCount = typeof options.parallel === 'string'
|
|
131
|
+
? parseInt(options.parallel, 10)
|
|
132
|
+
: (options.parallel ?? defaults.parallel);
|
|
133
|
+
|
|
134
|
+
if (!options.quiet) {
|
|
135
|
+
logger.info(`Parallel processing: ${parallelCount} file(s) at a time`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const results = await processFiles(
|
|
139
|
+
files,
|
|
140
|
+
inputDir,
|
|
141
|
+
outputDir,
|
|
142
|
+
engine,
|
|
143
|
+
options,
|
|
144
|
+
parallelCount
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Output results
|
|
148
|
+
outputResults(results, options);
|
|
149
|
+
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof TranslationError) {
|
|
152
|
+
console.error(`Error: ${error.message}`);
|
|
153
|
+
process.exit(getExitCode(error));
|
|
154
|
+
}
|
|
155
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Types
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
interface FileResult {
|
|
165
|
+
inputPath: string;
|
|
166
|
+
outputPath: string;
|
|
167
|
+
relativePath: string;
|
|
168
|
+
success: boolean;
|
|
169
|
+
error?: string;
|
|
170
|
+
result?: DocumentResult;
|
|
171
|
+
duration: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface DirResults {
|
|
175
|
+
files: FileResult[];
|
|
176
|
+
totalDuration: number;
|
|
177
|
+
successCount: number;
|
|
178
|
+
failCount: number;
|
|
179
|
+
totalTokensInput: number;
|
|
180
|
+
totalTokensOutput: number;
|
|
181
|
+
totalCacheRead: number;
|
|
182
|
+
totalCacheWrite: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Helper Functions
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Find all files matching the include patterns and not matching exclude patterns
|
|
191
|
+
*/
|
|
192
|
+
async function findFiles(
|
|
193
|
+
dir: string,
|
|
194
|
+
includePatterns: string[],
|
|
195
|
+
excludePatterns: string[],
|
|
196
|
+
outputDir?: string
|
|
197
|
+
): Promise<string[]> {
|
|
198
|
+
const files: string[] = [];
|
|
199
|
+
|
|
200
|
+
// If output is inside input dir, exclude it
|
|
201
|
+
const outputRelative = outputDir ? relative(dir, outputDir) : null;
|
|
202
|
+
const isOutputInsideInput = outputRelative && !outputRelative.startsWith('..');
|
|
203
|
+
|
|
204
|
+
async function scan(currentDir: string): Promise<void> {
|
|
205
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
206
|
+
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
const fullPath = join(currentDir, entry.name);
|
|
209
|
+
const relativePath = relative(dir, fullPath);
|
|
210
|
+
|
|
211
|
+
// Skip hidden files and directories
|
|
212
|
+
if (entry.name.startsWith('.')) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (entry.isDirectory()) {
|
|
217
|
+
// Skip output directory if it's inside input directory
|
|
218
|
+
if (isOutputInsideInput && relativePath === outputRelative) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Also skip common locale directories (2-letter codes like ko, ja, zh)
|
|
223
|
+
if (/^[a-z]{2}(-[A-Z]{2})?$/.test(entry.name)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check if directory should be excluded
|
|
228
|
+
if (!matchesPatterns(relativePath + '/', excludePatterns)) {
|
|
229
|
+
await scan(fullPath);
|
|
230
|
+
}
|
|
231
|
+
} else if (entry.isFile()) {
|
|
232
|
+
// Check if file matches include patterns and not exclude patterns
|
|
233
|
+
if (
|
|
234
|
+
matchesPatterns(entry.name, includePatterns) &&
|
|
235
|
+
!matchesPatterns(relativePath, excludePatterns)
|
|
236
|
+
) {
|
|
237
|
+
files.push(fullPath);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await scan(dir);
|
|
244
|
+
return files.sort();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if a path matches any of the given glob patterns
|
|
249
|
+
*/
|
|
250
|
+
function matchesPatterns(path: string, patterns: string[]): boolean {
|
|
251
|
+
for (const pattern of patterns) {
|
|
252
|
+
if (matchGlob(path, pattern)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Simple glob matching (supports * and **)
|
|
261
|
+
*/
|
|
262
|
+
function matchGlob(path: string, pattern: string): boolean {
|
|
263
|
+
// Convert glob pattern to regex
|
|
264
|
+
const regexPattern = pattern
|
|
265
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special chars
|
|
266
|
+
.replace(/\*\*/g, '{{DOUBLESTAR}}') // Temp placeholder for **
|
|
267
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
268
|
+
.replace(/{{DOUBLESTAR}}/g, '.*') // ** matches anything including /
|
|
269
|
+
.replace(/\?/g, '.'); // ? matches single char
|
|
270
|
+
|
|
271
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
272
|
+
return regex.test(path);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Process files with parallel execution using worker pool pattern.
|
|
277
|
+
* As soon as one file completes, the next file starts immediately.
|
|
278
|
+
*/
|
|
279
|
+
async function processFiles(
|
|
280
|
+
files: string[],
|
|
281
|
+
inputDir: string,
|
|
282
|
+
outputDir: string,
|
|
283
|
+
engine: ReturnType<typeof createTranslationEngine>,
|
|
284
|
+
options: DirCommandOptions & { include?: string; exclude?: string },
|
|
285
|
+
parallelCount: number
|
|
286
|
+
): Promise<DirResults> {
|
|
287
|
+
const startTime = Date.now();
|
|
288
|
+
const results: FileResult[] = new Array(files.length);
|
|
289
|
+
let completed = 0;
|
|
290
|
+
let nextIndex = 0;
|
|
291
|
+
|
|
292
|
+
// Process a single file and return the result
|
|
293
|
+
const processFile = async (inputPath: string, _index: number): Promise<FileResult> => {
|
|
294
|
+
const relativePath = relative(inputDir, inputPath);
|
|
295
|
+
const outputPath = join(outputDir, relativePath);
|
|
296
|
+
const fileStartTime = Date.now();
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// Read input file
|
|
300
|
+
const content = await readFile(inputPath, 'utf-8');
|
|
301
|
+
|
|
302
|
+
// Translate
|
|
303
|
+
const result = await engine.translateContent({
|
|
304
|
+
content,
|
|
305
|
+
sourceLang: options.sourceLang,
|
|
306
|
+
targetLang: options.targetLang!,
|
|
307
|
+
format: mapFormat(options.format),
|
|
308
|
+
glossaryPath: options.glossary,
|
|
309
|
+
qualityThreshold: options.quality ? parseInt(options.quality, 10) : undefined,
|
|
310
|
+
maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : undefined,
|
|
311
|
+
context: options.context,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Ensure output directory exists
|
|
315
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
316
|
+
|
|
317
|
+
// Write output
|
|
318
|
+
await writeFile(outputPath, result.content, 'utf-8');
|
|
319
|
+
|
|
320
|
+
completed++;
|
|
321
|
+
if (!options.quiet && !options.json) {
|
|
322
|
+
const progress = `[${completed}/${files.length}]`;
|
|
323
|
+
logger.success(`${progress} ${relativePath}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
inputPath,
|
|
328
|
+
outputPath,
|
|
329
|
+
relativePath,
|
|
330
|
+
success: true,
|
|
331
|
+
result,
|
|
332
|
+
duration: Date.now() - fileStartTime,
|
|
333
|
+
};
|
|
334
|
+
} catch (error) {
|
|
335
|
+
completed++;
|
|
336
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
337
|
+
|
|
338
|
+
if (!options.quiet && !options.json) {
|
|
339
|
+
const progress = `[${completed}/${files.length}]`;
|
|
340
|
+
logger.error(`${progress} ${relativePath}: ${errorMessage}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
inputPath,
|
|
345
|
+
outputPath,
|
|
346
|
+
relativePath,
|
|
347
|
+
success: false,
|
|
348
|
+
error: errorMessage,
|
|
349
|
+
duration: Date.now() - fileStartTime,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Worker function: continuously process files until none remain
|
|
355
|
+
const worker = async (): Promise<void> => {
|
|
356
|
+
while (true) {
|
|
357
|
+
const index = nextIndex++;
|
|
358
|
+
if (index >= files.length) break;
|
|
359
|
+
|
|
360
|
+
const inputPath = files[index];
|
|
361
|
+
if (!inputPath) break;
|
|
362
|
+
|
|
363
|
+
const result = await processFile(inputPath, index);
|
|
364
|
+
results[index] = result;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Start workers (up to parallelCount or file count, whichever is smaller)
|
|
369
|
+
const workerCount = Math.min(parallelCount, files.length);
|
|
370
|
+
const workers = Array.from({ length: workerCount }, () => worker());
|
|
371
|
+
|
|
372
|
+
// Wait for all workers to complete
|
|
373
|
+
await Promise.all(workers);
|
|
374
|
+
|
|
375
|
+
// Calculate totals
|
|
376
|
+
const successResults = results.filter(r => r.success && r.result);
|
|
377
|
+
const totalTokensInput = successResults.reduce((sum, r) => sum + (r.result?.metadata.tokensUsed.input ?? 0), 0);
|
|
378
|
+
const totalTokensOutput = successResults.reduce((sum, r) => sum + (r.result?.metadata.tokensUsed.output ?? 0), 0);
|
|
379
|
+
const totalCacheRead = successResults.reduce((sum, r) => sum + (r.result?.metadata.tokensUsed.cacheRead ?? 0), 0);
|
|
380
|
+
const totalCacheWrite = successResults.reduce((sum, r) => sum + (r.result?.metadata.tokensUsed.cacheWrite ?? 0), 0);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
files: results,
|
|
384
|
+
totalDuration: Date.now() - startTime,
|
|
385
|
+
successCount: results.filter(r => r.success).length,
|
|
386
|
+
failCount: results.filter(r => !r.success).length,
|
|
387
|
+
totalTokensInput,
|
|
388
|
+
totalTokensOutput,
|
|
389
|
+
totalCacheRead,
|
|
390
|
+
totalCacheWrite,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Output results summary
|
|
396
|
+
*/
|
|
397
|
+
function outputResults(results: DirResults, options: DirCommandOptions): void {
|
|
398
|
+
if (options.json) {
|
|
399
|
+
console.log(JSON.stringify({
|
|
400
|
+
success: results.failCount === 0,
|
|
401
|
+
totalFiles: results.files.length,
|
|
402
|
+
successCount: results.successCount,
|
|
403
|
+
failCount: results.failCount,
|
|
404
|
+
totalDuration: results.totalDuration,
|
|
405
|
+
tokensUsed: {
|
|
406
|
+
input: results.totalTokensInput,
|
|
407
|
+
output: results.totalTokensOutput,
|
|
408
|
+
cacheRead: results.totalCacheRead,
|
|
409
|
+
cacheWrite: results.totalCacheWrite,
|
|
410
|
+
},
|
|
411
|
+
files: results.files.map(f => ({
|
|
412
|
+
input: f.relativePath,
|
|
413
|
+
output: f.outputPath,
|
|
414
|
+
success: f.success,
|
|
415
|
+
error: f.error,
|
|
416
|
+
duration: f.duration,
|
|
417
|
+
quality: f.result?.metadata.averageQuality ?? 0,
|
|
418
|
+
tokens: f.result ? {
|
|
419
|
+
input: f.result.metadata.tokensUsed.input,
|
|
420
|
+
output: f.result.metadata.tokensUsed.output,
|
|
421
|
+
} : undefined,
|
|
422
|
+
})),
|
|
423
|
+
}, null, 2));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (options.quiet) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
console.log('');
|
|
432
|
+
console.log('─'.repeat(60));
|
|
433
|
+
console.log(' Translation Summary');
|
|
434
|
+
console.log('─'.repeat(60));
|
|
435
|
+
console.log(` Files: ${results.successCount} succeeded, ${results.failCount} failed`);
|
|
436
|
+
console.log(` Duration: ${(results.totalDuration / 1000).toFixed(1)}s`);
|
|
437
|
+
console.log(` Tokens: ${results.totalTokensInput.toLocaleString()} input / ${results.totalTokensOutput.toLocaleString()} output`);
|
|
438
|
+
|
|
439
|
+
if (results.totalCacheRead > 0 || results.totalCacheWrite > 0) {
|
|
440
|
+
console.log(` Cache: ${results.totalCacheRead.toLocaleString()} read / ${results.totalCacheWrite.toLocaleString()} write`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (results.failCount > 0) {
|
|
444
|
+
console.log('');
|
|
445
|
+
console.log(' Failed files:');
|
|
446
|
+
for (const file of results.files.filter(f => !f.success)) {
|
|
447
|
+
console.log(` - ${file.relativePath}: ${file.error}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log('─'.repeat(60));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function mapFormat(format: string | undefined): DocumentFormat | undefined {
|
|
455
|
+
if (!format) return undefined;
|
|
456
|
+
|
|
457
|
+
switch (format.toLowerCase()) {
|
|
458
|
+
case 'md':
|
|
459
|
+
case 'markdown':
|
|
460
|
+
return 'markdown';
|
|
461
|
+
case 'html':
|
|
462
|
+
return 'html';
|
|
463
|
+
case 'txt':
|
|
464
|
+
case 'text':
|
|
465
|
+
return 'text';
|
|
466
|
+
default:
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
}
|