specweave 0.23.0 → 0.23.2
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/.claude-plugin/marketplace.json +0 -88
- package/CLAUDE.md +100 -0
- package/bin/fix-marketplace-errors.sh +8 -8
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +5 -17
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/core/repo-structure/repo-id-generator.d.ts +20 -0
- package/dist/src/core/repo-structure/repo-id-generator.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-id-generator.js +44 -0
- package/dist/src/core/repo-structure/repo-id-generator.js.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +5 -2
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +10 -0
- package/plugins/specweave/commands/specweave-archive.md +51 -15
- package/plugins/specweave/hooks/post-edit-spec.sh +62 -9
- package/plugins/specweave/hooks/post-metadata-change.sh +160 -0
- package/plugins/specweave/hooks/post-write-spec.sh +62 -8
- package/plugins/specweave/lib/hooks/auto-transition.js.bak +50 -0
- package/plugins/specweave/lib/hooks/auto-transition.ts.bak +84 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.d.ts.bak +89 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.js.bak +142 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.ts.bak +269 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.d.ts.bak +60 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.js.bak +155 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.ts.bak +264 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.d.ts.bak +42 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.js.bak +110 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.ts.bak +178 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.d.ts.bak +45 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.js.bak +92 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.ts.bak +156 -0
- package/plugins/specweave/lib/hooks/reflection-parser.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-parser.d.ts.bak +33 -0
- package/plugins/specweave/lib/hooks/reflection-parser.js.bak +301 -0
- package/plugins/specweave/lib/hooks/reflection-parser.ts.bak +484 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.d.ts.bak +56 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.js.bak +182 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.ts.bak +306 -0
- package/plugins/specweave/lib/hooks/reflection-storage.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-storage.d.ts.bak +64 -0
- package/plugins/specweave/lib/hooks/reflection-storage.js.bak +231 -0
- package/plugins/specweave/lib/hooks/reflection-storage.ts.bak +369 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.d.ts.bak +43 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.js.bak +132 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.ts.bak +258 -0
- package/plugins/specweave/lib/hooks/sync-cache.js.bak +294 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.d.js.bak +1 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.d.ts.bak +27 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.js.bak +339 -0
- package/plugins/specweave/lib/hooks/sync-us-tasks.js.bak +476 -0
- package/plugins/specweave/lib/hooks/translate-file.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/translate-file.d.ts.bak +59 -0
- package/plugins/specweave/lib/hooks/translate-file.js.bak +289 -0
- package/plugins/specweave/lib/hooks/translate-file.ts.bak +428 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.d.ts.bak +13 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.js.bak +119 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.ts.bak +224 -0
- package/plugins/specweave/lib/hooks/update-ac-status.js.bak +51 -0
- package/plugins/specweave/lib/hooks/update-ac-status.ts.bak +103 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.d.js.bak +1 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.d.ts.bak +29 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.js.bak +296 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.ts.bak +489 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +6225 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import {
|
|
3
|
+
detectLanguage,
|
|
4
|
+
prepareTranslation,
|
|
5
|
+
postProcessTranslation,
|
|
6
|
+
validateTranslation,
|
|
7
|
+
getLanguageName,
|
|
8
|
+
formatCost
|
|
9
|
+
} from "../../../../dist/src/utils/translation.js";
|
|
10
|
+
async function translateFile(options) {
|
|
11
|
+
const { filePath, targetLang, preview, verbose } = options;
|
|
12
|
+
if (!await fs.pathExists(filePath)) {
|
|
13
|
+
throw new Error(`File not found: ${filePath}`);
|
|
14
|
+
}
|
|
15
|
+
if (verbose) {
|
|
16
|
+
console.log(`\u{1F4C4} Reading file: ${filePath}`);
|
|
17
|
+
}
|
|
18
|
+
const originalContent = await fs.readFile(filePath, "utf-8");
|
|
19
|
+
const detectionResult = detectLanguage(originalContent);
|
|
20
|
+
const sourceLanguage = detectionResult.language;
|
|
21
|
+
if (verbose) {
|
|
22
|
+
console.log(`\u{1F50D} Detected language: ${getLanguageName(sourceLanguage)} (confidence: ${(detectionResult.confidence * 100).toFixed(0)}%)`);
|
|
23
|
+
}
|
|
24
|
+
if (sourceLanguage === targetLang) {
|
|
25
|
+
if (verbose) {
|
|
26
|
+
console.log(`\u2705 File already in ${getLanguageName(targetLang)}, skipping translation`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
filePath,
|
|
31
|
+
sourceLanguage,
|
|
32
|
+
targetLanguage: targetLang,
|
|
33
|
+
warnings: [`Already in ${getLanguageName(targetLang)}`],
|
|
34
|
+
cost: 0,
|
|
35
|
+
tokensUsed: 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (sourceLanguage === "unknown") {
|
|
39
|
+
if (verbose) {
|
|
40
|
+
console.warn(`\u26A0\uFE0F Could not detect language, assuming English`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
filePath,
|
|
45
|
+
sourceLanguage: "unknown",
|
|
46
|
+
targetLanguage: targetLang,
|
|
47
|
+
warnings: ["Language detection failed - file may already be in English or mixed language"],
|
|
48
|
+
cost: 0,
|
|
49
|
+
tokensUsed: 0
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (verbose) {
|
|
53
|
+
console.log(`\u{1F310} Translating from ${getLanguageName(sourceLanguage)} to ${getLanguageName(targetLang)}...`);
|
|
54
|
+
console.log(`\u{1F4B0} Estimated cost: ${formatCost(3e-3)} (using Haiku)`);
|
|
55
|
+
}
|
|
56
|
+
const prepared = prepareTranslation(originalContent, sourceLanguage, targetLang);
|
|
57
|
+
const translatedContent = await invokeLLMTranslation(prepared.prompt, verbose);
|
|
58
|
+
const finalContent = postProcessTranslation(translatedContent, prepared.preserved);
|
|
59
|
+
const warnings = validateTranslation(originalContent, finalContent);
|
|
60
|
+
if (warnings.length > 0 && verbose) {
|
|
61
|
+
console.warn(`\u26A0\uFE0F Translation warnings:`);
|
|
62
|
+
warnings.forEach((w) => console.warn(` - ${w}`));
|
|
63
|
+
}
|
|
64
|
+
if (preview) {
|
|
65
|
+
if (verbose) {
|
|
66
|
+
console.log(`
|
|
67
|
+
\u{1F4CB} PREVIEW (first 500 chars):
|
|
68
|
+
`);
|
|
69
|
+
console.log(finalContent.substring(0, 500));
|
|
70
|
+
console.log(`
|
|
71
|
+
... (${finalContent.length} total characters)
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
filePath,
|
|
77
|
+
sourceLanguage,
|
|
78
|
+
targetLanguage: targetLang,
|
|
79
|
+
warnings,
|
|
80
|
+
cost: prepared.estimatedCost,
|
|
81
|
+
tokensUsed: prepared.estimatedTokens,
|
|
82
|
+
preview: finalContent
|
|
83
|
+
};
|
|
84
|
+
} else {
|
|
85
|
+
await fs.writeFile(filePath, finalContent, "utf-8");
|
|
86
|
+
if (verbose) {
|
|
87
|
+
console.log(`\u2705 Translation complete: ${filePath}`);
|
|
88
|
+
console.log(` Tokens used: ${prepared.estimatedTokens.toLocaleString()}`);
|
|
89
|
+
console.log(` Cost: ${formatCost(prepared.estimatedCost)}`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
success: true,
|
|
93
|
+
filePath,
|
|
94
|
+
sourceLanguage,
|
|
95
|
+
targetLanguage: targetLang,
|
|
96
|
+
warnings,
|
|
97
|
+
cost: prepared.estimatedCost,
|
|
98
|
+
tokensUsed: prepared.estimatedTokens
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function invokeLLMTranslation(prompt, verbose) {
|
|
103
|
+
const contentMatch = prompt.match(/SOURCE DOCUMENT[^\n]*:\n---\n([\s\S]*?)\n---/);
|
|
104
|
+
const contentToTranslate = contentMatch ? contentMatch[1] : "";
|
|
105
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
106
|
+
if (apiKey) {
|
|
107
|
+
if (verbose) {
|
|
108
|
+
console.log(`
|
|
109
|
+
\u{1F916} Translating via Anthropic API (Haiku model)...`);
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const Anthropic = await import("@anthropic-ai/sdk").then((m) => m.default);
|
|
113
|
+
const anthropic = new Anthropic({
|
|
114
|
+
apiKey
|
|
115
|
+
});
|
|
116
|
+
const message = await anthropic.messages.create({
|
|
117
|
+
model: "claude-3-haiku-20240307",
|
|
118
|
+
max_tokens: 8e3,
|
|
119
|
+
messages: [
|
|
120
|
+
{
|
|
121
|
+
role: "user",
|
|
122
|
+
content: prompt
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
});
|
|
126
|
+
const translatedContent = message.content[0].type === "text" ? message.content[0].text : contentToTranslate;
|
|
127
|
+
if (verbose) {
|
|
128
|
+
console.log(`\u2705 Translation complete via API`);
|
|
129
|
+
console.log(` Model: claude-3-haiku-20240307`);
|
|
130
|
+
console.log(` Input tokens: ${message.usage.input_tokens}`);
|
|
131
|
+
console.log(` Output tokens: ${message.usage.output_tokens}`);
|
|
132
|
+
console.log(` Cost: ~$${((message.usage.input_tokens * 0.25 + message.usage.output_tokens * 1.25) / 1e6).toFixed(4)}`);
|
|
133
|
+
}
|
|
134
|
+
return translatedContent;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`
|
|
137
|
+
\u274C API translation failed: ${error.message}`);
|
|
138
|
+
console.error(` Falling back to manual translation instructions
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const isInteractive = process.stdout.isTTY && process.env.CLAUDE_CODE_SESSION;
|
|
143
|
+
if (isInteractive) {
|
|
144
|
+
if (verbose) {
|
|
145
|
+
console.log(`
|
|
146
|
+
\u{1F916} Invoking Claude Code translator skill...`);
|
|
147
|
+
console.log(` (Tip: Set ANTHROPIC_API_KEY for fully automatic translation)
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
console.log("\n" + "=".repeat(80));
|
|
151
|
+
console.log("TRANSLATION REQUEST (translator skill will auto-activate):");
|
|
152
|
+
console.log("=".repeat(80));
|
|
153
|
+
console.log(prompt);
|
|
154
|
+
console.log("=".repeat(80) + "\n");
|
|
155
|
+
return `<!-- \u26A0\uFE0F TRANSLATION IN PROGRESS - Manual translation required via translator skill -->
|
|
156
|
+
|
|
157
|
+
${contentToTranslate}`;
|
|
158
|
+
} else {
|
|
159
|
+
if (verbose) {
|
|
160
|
+
console.log(`
|
|
161
|
+
\u{1F916} Generating translation (automated mode)...`);
|
|
162
|
+
}
|
|
163
|
+
console.error("\n\u26A0\uFE0F AUTO-TRANSLATION REQUIRES MANUAL STEP:");
|
|
164
|
+
console.error(" Option A (Recommended): Set ANTHROPIC_API_KEY environment variable");
|
|
165
|
+
console.error(" Option B: Run /specweave:translate <file-path>");
|
|
166
|
+
console.error(" Option C: Manually translate the content\n");
|
|
167
|
+
return `<!-- \u26A0\uFE0F AUTO-TRANSLATION PENDING -->
|
|
168
|
+
<!-- Set ANTHROPIC_API_KEY for automatic translation -->
|
|
169
|
+
<!-- Or run: /specweave:translate to complete -->
|
|
170
|
+
<!-- Original content below -->
|
|
171
|
+
|
|
172
|
+
${contentToTranslate}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function batchTranslateFiles(filePaths, targetLang = "en", preview = false, verbose = false) {
|
|
176
|
+
const results = [];
|
|
177
|
+
if (verbose) {
|
|
178
|
+
console.log(`
|
|
179
|
+
\u{1F504} Batch translating ${filePaths.length} file(s) to ${getLanguageName(targetLang)}...
|
|
180
|
+
`);
|
|
181
|
+
}
|
|
182
|
+
for (const filePath of filePaths) {
|
|
183
|
+
try {
|
|
184
|
+
const result = await translateFile({
|
|
185
|
+
filePath,
|
|
186
|
+
targetLang,
|
|
187
|
+
preview,
|
|
188
|
+
verbose
|
|
189
|
+
});
|
|
190
|
+
results.push(result);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (verbose) {
|
|
193
|
+
console.error(`\u274C Error translating ${filePath}: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
results.push({
|
|
196
|
+
success: false,
|
|
197
|
+
filePath,
|
|
198
|
+
sourceLanguage: "unknown",
|
|
199
|
+
targetLanguage: targetLang,
|
|
200
|
+
warnings: [error.message],
|
|
201
|
+
cost: 0,
|
|
202
|
+
tokensUsed: 0
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (verbose) {
|
|
207
|
+
const successful = results.filter((r) => r.success).length;
|
|
208
|
+
const totalCost = results.reduce((sum, r) => sum + r.cost, 0);
|
|
209
|
+
const totalTokens = results.reduce((sum, r) => sum + r.tokensUsed, 0);
|
|
210
|
+
console.log(`
|
|
211
|
+
\u{1F4CA} Batch Translation Summary:`);
|
|
212
|
+
console.log(` Successful: ${successful}/${filePaths.length}`);
|
|
213
|
+
console.log(` Total tokens: ${totalTokens.toLocaleString()}`);
|
|
214
|
+
console.log(` Total cost: ${formatCost(totalCost)}`);
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
function parseArgs() {
|
|
219
|
+
const args = process.argv.slice(2);
|
|
220
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
221
|
+
console.log(`
|
|
222
|
+
Translation CLI Utility
|
|
223
|
+
|
|
224
|
+
Usage:
|
|
225
|
+
node translate-file.js <file-path> [options]
|
|
226
|
+
|
|
227
|
+
Options:
|
|
228
|
+
--target-lang <code> Target language (default: en)
|
|
229
|
+
--preview Preview translation without writing to file
|
|
230
|
+
--verbose, -v Show detailed output
|
|
231
|
+
--help, -h Show this help message
|
|
232
|
+
|
|
233
|
+
Supported Languages:
|
|
234
|
+
en (English), ru (Russian), es (Spanish), zh (Chinese),
|
|
235
|
+
de (German), fr (French), ja (Japanese), ko (Korean),
|
|
236
|
+
pt (Portuguese), ar (Arabic), he (Hebrew)
|
|
237
|
+
|
|
238
|
+
Examples:
|
|
239
|
+
# Translate Russian file to English
|
|
240
|
+
node translate-file.js .specweave/increments/0001/spec.md
|
|
241
|
+
|
|
242
|
+
# Preview translation
|
|
243
|
+
node translate-file.js spec.md --preview --verbose
|
|
244
|
+
|
|
245
|
+
# Translate to Spanish
|
|
246
|
+
node translate-file.js plan.md --target-lang es
|
|
247
|
+
`.trim());
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
const filePath = args[0];
|
|
251
|
+
let targetLang = "en";
|
|
252
|
+
let preview = false;
|
|
253
|
+
let verbose = false;
|
|
254
|
+
for (let i = 1; i < args.length; i++) {
|
|
255
|
+
const arg = args[i];
|
|
256
|
+
if (arg === "--target-lang" && args[i + 1]) {
|
|
257
|
+
targetLang = args[i + 1];
|
|
258
|
+
i++;
|
|
259
|
+
} else if (arg === "--preview") {
|
|
260
|
+
preview = true;
|
|
261
|
+
} else if (arg === "--verbose" || arg === "-v") {
|
|
262
|
+
verbose = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
filePath,
|
|
267
|
+
targetLang,
|
|
268
|
+
preview,
|
|
269
|
+
verbose
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function main() {
|
|
273
|
+
try {
|
|
274
|
+
const options = parseArgs();
|
|
275
|
+
const result = await translateFile(options);
|
|
276
|
+
process.exit(result.success ? 0 : 1);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error(`\u274C Translation failed: ${error.message}`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
283
|
+
if (isMainModule) {
|
|
284
|
+
main();
|
|
285
|
+
}
|
|
286
|
+
export {
|
|
287
|
+
batchTranslateFiles,
|
|
288
|
+
translateFile
|
|
289
|
+
};
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Translation CLI Utility
|
|
3
|
+
*
|
|
4
|
+
* Translates a single file from detected source language to target language
|
|
5
|
+
* using the translation utilities and LLM invocation.
|
|
6
|
+
*
|
|
7
|
+
* This script is called from:
|
|
8
|
+
* - Post-increment-planning hook (auto-translate spec.md, plan.md, tasks.md)
|
|
9
|
+
* - Post-task-completion hook (auto-translate living docs)
|
|
10
|
+
* - Manual /specweave:translate command
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node translate-file.js <file-path> [--target-lang en] [--preview]
|
|
14
|
+
*
|
|
15
|
+
* @see src/utils/translation.ts
|
|
16
|
+
* @see .specweave/increments/0006-llm-native-i18n/reports/DESIGN-POST-GENERATION-TRANSLATION.md
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs-extra';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import {
|
|
22
|
+
detectLanguage,
|
|
23
|
+
prepareTranslation,
|
|
24
|
+
postProcessTranslation,
|
|
25
|
+
validateTranslation,
|
|
26
|
+
getLanguageName,
|
|
27
|
+
formatCost,
|
|
28
|
+
type SupportedLanguage,
|
|
29
|
+
} from '../../../../dist/src/utils/translation.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* CLI options
|
|
33
|
+
*/
|
|
34
|
+
interface CLIOptions {
|
|
35
|
+
filePath: string;
|
|
36
|
+
targetLang: SupportedLanguage;
|
|
37
|
+
preview: boolean;
|
|
38
|
+
verbose: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Translation result
|
|
43
|
+
*/
|
|
44
|
+
interface FileTranslationResult {
|
|
45
|
+
success: boolean;
|
|
46
|
+
filePath: string;
|
|
47
|
+
sourceLanguage: SupportedLanguage;
|
|
48
|
+
targetLanguage: SupportedLanguage;
|
|
49
|
+
warnings: string[];
|
|
50
|
+
cost: number;
|
|
51
|
+
tokensUsed: number;
|
|
52
|
+
preview?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Main translation function
|
|
57
|
+
*
|
|
58
|
+
* @param options - CLI options
|
|
59
|
+
* @returns Translation result
|
|
60
|
+
*/
|
|
61
|
+
export async function translateFile(options: CLIOptions): Promise<FileTranslationResult> {
|
|
62
|
+
const { filePath, targetLang, preview, verbose } = options;
|
|
63
|
+
|
|
64
|
+
// 1. Validate file exists
|
|
65
|
+
if (!await fs.pathExists(filePath)) {
|
|
66
|
+
throw new Error(`File not found: ${filePath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.log(`📄 Reading file: ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Read original content
|
|
74
|
+
const originalContent = await fs.readFile(filePath, 'utf-8');
|
|
75
|
+
|
|
76
|
+
// 3. Detect source language
|
|
77
|
+
const detectionResult = detectLanguage(originalContent);
|
|
78
|
+
const sourceLanguage = detectionResult.language;
|
|
79
|
+
|
|
80
|
+
if (verbose) {
|
|
81
|
+
console.log(`🔍 Detected language: ${getLanguageName(sourceLanguage)} (confidence: ${(detectionResult.confidence * 100).toFixed(0)}%)`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 4. Check if already in target language
|
|
85
|
+
if (sourceLanguage === targetLang) {
|
|
86
|
+
if (verbose) {
|
|
87
|
+
console.log(`✅ File already in ${getLanguageName(targetLang)}, skipping translation`);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
filePath,
|
|
92
|
+
sourceLanguage,
|
|
93
|
+
targetLanguage: targetLang,
|
|
94
|
+
warnings: [`Already in ${getLanguageName(targetLang)}`],
|
|
95
|
+
cost: 0,
|
|
96
|
+
tokensUsed: 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 5. Check if source language is unknown
|
|
101
|
+
if (sourceLanguage === 'unknown') {
|
|
102
|
+
if (verbose) {
|
|
103
|
+
console.warn(`⚠️ Could not detect language, assuming English`);
|
|
104
|
+
}
|
|
105
|
+
// Assume English if detection fails
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
filePath,
|
|
109
|
+
sourceLanguage: 'unknown',
|
|
110
|
+
targetLanguage: targetLang,
|
|
111
|
+
warnings: ['Language detection failed - file may already be in English or mixed language'],
|
|
112
|
+
cost: 0,
|
|
113
|
+
tokensUsed: 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 6. Prepare translation
|
|
118
|
+
if (verbose) {
|
|
119
|
+
console.log(`🌐 Translating from ${getLanguageName(sourceLanguage)} to ${getLanguageName(targetLang)}...`);
|
|
120
|
+
console.log(`💰 Estimated cost: ${formatCost(0.003)} (using Haiku)`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const prepared = prepareTranslation(originalContent, sourceLanguage, targetLang);
|
|
124
|
+
|
|
125
|
+
// 7. Invoke LLM for translation
|
|
126
|
+
// NOTE: This is where we call the actual LLM
|
|
127
|
+
// For now, we'll create a simple prompt that can be used with Claude Code's Task tool
|
|
128
|
+
const translatedContent = await invokeLLMTranslation(prepared.prompt, verbose);
|
|
129
|
+
|
|
130
|
+
// 8. Post-process translation
|
|
131
|
+
const finalContent = postProcessTranslation(translatedContent, prepared.preserved);
|
|
132
|
+
|
|
133
|
+
// 9. Validate translation
|
|
134
|
+
const warnings = validateTranslation(originalContent, finalContent);
|
|
135
|
+
|
|
136
|
+
if (warnings.length > 0 && verbose) {
|
|
137
|
+
console.warn(`⚠️ Translation warnings:`);
|
|
138
|
+
warnings.forEach(w => console.warn(` - ${w}`));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 10. Preview or write
|
|
142
|
+
if (preview) {
|
|
143
|
+
if (verbose) {
|
|
144
|
+
console.log(`\n📋 PREVIEW (first 500 chars):\n`);
|
|
145
|
+
console.log(finalContent.substring(0, 500));
|
|
146
|
+
console.log(`\n... (${finalContent.length} total characters)\n`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
filePath,
|
|
151
|
+
sourceLanguage,
|
|
152
|
+
targetLanguage: targetLang,
|
|
153
|
+
warnings,
|
|
154
|
+
cost: prepared.estimatedCost,
|
|
155
|
+
tokensUsed: prepared.estimatedTokens,
|
|
156
|
+
preview: finalContent,
|
|
157
|
+
};
|
|
158
|
+
} else {
|
|
159
|
+
// Write translated content back to file
|
|
160
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
161
|
+
|
|
162
|
+
if (verbose) {
|
|
163
|
+
console.log(`✅ Translation complete: ${filePath}`);
|
|
164
|
+
console.log(` Tokens used: ${prepared.estimatedTokens.toLocaleString()}`);
|
|
165
|
+
console.log(` Cost: ${formatCost(prepared.estimatedCost)}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
filePath,
|
|
171
|
+
sourceLanguage,
|
|
172
|
+
targetLanguage: targetLang,
|
|
173
|
+
warnings,
|
|
174
|
+
cost: prepared.estimatedCost,
|
|
175
|
+
tokensUsed: prepared.estimatedTokens,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Invokes LLM for translation using Anthropic API
|
|
182
|
+
*
|
|
183
|
+
* PRODUCTION IMPLEMENTATION:
|
|
184
|
+
* 1. Checks for ANTHROPIC_API_KEY in environment
|
|
185
|
+
* 2. If available: Uses Anthropic API directly (fully automatic)
|
|
186
|
+
* 3. If not available: Provides clear instructions for manual translation
|
|
187
|
+
*
|
|
188
|
+
* @param prompt - Translation prompt
|
|
189
|
+
* @param verbose - Show detailed output
|
|
190
|
+
* @returns Translated content
|
|
191
|
+
*/
|
|
192
|
+
async function invokeLLMTranslation(prompt: string, verbose: boolean): Promise<string> {
|
|
193
|
+
// Extract the content to translate (between --- markers)
|
|
194
|
+
const contentMatch = prompt.match(/SOURCE DOCUMENT[^\n]*:\n---\n([\s\S]*?)\n---/);
|
|
195
|
+
const contentToTranslate = contentMatch ? contentMatch[1] : '';
|
|
196
|
+
|
|
197
|
+
// Check if ANTHROPIC_API_KEY is available
|
|
198
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
199
|
+
|
|
200
|
+
if (apiKey) {
|
|
201
|
+
// Fully automatic translation using Anthropic API
|
|
202
|
+
if (verbose) {
|
|
203
|
+
console.log(`\n🤖 Translating via Anthropic API (Haiku model)...`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Dynamic import of Anthropic SDK (allows graceful fallback if not installed)
|
|
208
|
+
const Anthropic = await import('@anthropic-ai/sdk').then(m => m.default);
|
|
209
|
+
|
|
210
|
+
const anthropic = new Anthropic({
|
|
211
|
+
apiKey,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const message = await anthropic.messages.create({
|
|
215
|
+
model: 'claude-3-haiku-20240307',
|
|
216
|
+
max_tokens: 8000,
|
|
217
|
+
messages: [
|
|
218
|
+
{
|
|
219
|
+
role: 'user',
|
|
220
|
+
content: prompt,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Extract translated content from response
|
|
226
|
+
const translatedContent = message.content[0].type === 'text'
|
|
227
|
+
? message.content[0].text
|
|
228
|
+
: contentToTranslate;
|
|
229
|
+
|
|
230
|
+
if (verbose) {
|
|
231
|
+
console.log(`✅ Translation complete via API`);
|
|
232
|
+
console.log(` Model: claude-3-haiku-20240307`);
|
|
233
|
+
console.log(` Input tokens: ${message.usage.input_tokens}`);
|
|
234
|
+
console.log(` Output tokens: ${message.usage.output_tokens}`);
|
|
235
|
+
console.log(` Cost: ~$${((message.usage.input_tokens * 0.25 + message.usage.output_tokens * 1.25) / 1000000).toFixed(4)}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return translatedContent;
|
|
239
|
+
} catch (error: any) {
|
|
240
|
+
console.error(`\n❌ API translation failed: ${error.message}`);
|
|
241
|
+
console.error(` Falling back to manual translation instructions\n`);
|
|
242
|
+
// Fall through to manual instructions
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Fallback: Manual translation instructions
|
|
247
|
+
const isInteractive = process.stdout.isTTY && process.env.CLAUDE_CODE_SESSION;
|
|
248
|
+
|
|
249
|
+
if (isInteractive) {
|
|
250
|
+
// Interactive mode: Output prompt for Claude to process
|
|
251
|
+
if (verbose) {
|
|
252
|
+
console.log(`\n🤖 Invoking Claude Code translator skill...`);
|
|
253
|
+
console.log(` (Tip: Set ANTHROPIC_API_KEY for fully automatic translation)\n`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Output the translation prompt
|
|
257
|
+
// The translator skill should auto-activate on this prompt
|
|
258
|
+
console.log('\n' + '='.repeat(80));
|
|
259
|
+
console.log('TRANSLATION REQUEST (translator skill will auto-activate):');
|
|
260
|
+
console.log('='.repeat(80));
|
|
261
|
+
console.log(prompt);
|
|
262
|
+
console.log('='.repeat(80) + '\n');
|
|
263
|
+
|
|
264
|
+
// In interactive mode, we expect the user/Claude to provide translation
|
|
265
|
+
// For now, return a marker indicating manual intervention needed
|
|
266
|
+
return `<!-- ⚠️ TRANSLATION IN PROGRESS - Manual translation required via translator skill -->\n\n${contentToTranslate}`;
|
|
267
|
+
} else {
|
|
268
|
+
// Non-interactive/automated mode: Provide clear instructions
|
|
269
|
+
if (verbose) {
|
|
270
|
+
console.log(`\n🤖 Generating translation (automated mode)...`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.error('\n⚠️ AUTO-TRANSLATION REQUIRES MANUAL STEP:');
|
|
274
|
+
console.error(' Option A (Recommended): Set ANTHROPIC_API_KEY environment variable');
|
|
275
|
+
console.error(' Option B: Run /specweave:translate <file-path>');
|
|
276
|
+
console.error(' Option C: Manually translate the content\n');
|
|
277
|
+
|
|
278
|
+
// Return original content with clear marker
|
|
279
|
+
return `<!-- ⚠️ AUTO-TRANSLATION PENDING -->\n<!-- Set ANTHROPIC_API_KEY for automatic translation -->\n<!-- Or run: /specweave:translate to complete -->\n<!-- Original content below -->\n\n${contentToTranslate}`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Batch translate multiple files
|
|
285
|
+
*
|
|
286
|
+
* @param filePaths - Array of file paths to translate
|
|
287
|
+
* @param targetLang - Target language
|
|
288
|
+
* @param preview - Preview mode
|
|
289
|
+
* @param verbose - Verbose output
|
|
290
|
+
* @returns Array of translation results
|
|
291
|
+
*/
|
|
292
|
+
export async function batchTranslateFiles(
|
|
293
|
+
filePaths: string[],
|
|
294
|
+
targetLang: SupportedLanguage = 'en',
|
|
295
|
+
preview: boolean = false,
|
|
296
|
+
verbose: boolean = false
|
|
297
|
+
): Promise<FileTranslationResult[]> {
|
|
298
|
+
const results: FileTranslationResult[] = [];
|
|
299
|
+
|
|
300
|
+
if (verbose) {
|
|
301
|
+
console.log(`\n🔄 Batch translating ${filePaths.length} file(s) to ${getLanguageName(targetLang)}...\n`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const filePath of filePaths) {
|
|
305
|
+
try {
|
|
306
|
+
const result = await translateFile({
|
|
307
|
+
filePath,
|
|
308
|
+
targetLang,
|
|
309
|
+
preview,
|
|
310
|
+
verbose,
|
|
311
|
+
});
|
|
312
|
+
results.push(result);
|
|
313
|
+
} catch (error: any) {
|
|
314
|
+
if (verbose) {
|
|
315
|
+
console.error(`❌ Error translating ${filePath}: ${error.message}`);
|
|
316
|
+
}
|
|
317
|
+
results.push({
|
|
318
|
+
success: false,
|
|
319
|
+
filePath,
|
|
320
|
+
sourceLanguage: 'unknown',
|
|
321
|
+
targetLanguage: targetLang,
|
|
322
|
+
warnings: [error.message],
|
|
323
|
+
cost: 0,
|
|
324
|
+
tokensUsed: 0,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Summary
|
|
330
|
+
if (verbose) {
|
|
331
|
+
const successful = results.filter(r => r.success).length;
|
|
332
|
+
const totalCost = results.reduce((sum, r) => sum + r.cost, 0);
|
|
333
|
+
const totalTokens = results.reduce((sum, r) => sum + r.tokensUsed, 0);
|
|
334
|
+
|
|
335
|
+
console.log(`\n📊 Batch Translation Summary:`);
|
|
336
|
+
console.log(` Successful: ${successful}/${filePaths.length}`);
|
|
337
|
+
console.log(` Total tokens: ${totalTokens.toLocaleString()}`);
|
|
338
|
+
console.log(` Total cost: ${formatCost(totalCost)}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Parse CLI arguments
|
|
346
|
+
*/
|
|
347
|
+
function parseArgs(): CLIOptions {
|
|
348
|
+
const args = process.argv.slice(2);
|
|
349
|
+
|
|
350
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
351
|
+
console.log(`
|
|
352
|
+
Translation CLI Utility
|
|
353
|
+
|
|
354
|
+
Usage:
|
|
355
|
+
node translate-file.js <file-path> [options]
|
|
356
|
+
|
|
357
|
+
Options:
|
|
358
|
+
--target-lang <code> Target language (default: en)
|
|
359
|
+
--preview Preview translation without writing to file
|
|
360
|
+
--verbose, -v Show detailed output
|
|
361
|
+
--help, -h Show this help message
|
|
362
|
+
|
|
363
|
+
Supported Languages:
|
|
364
|
+
en (English), ru (Russian), es (Spanish), zh (Chinese),
|
|
365
|
+
de (German), fr (French), ja (Japanese), ko (Korean),
|
|
366
|
+
pt (Portuguese), ar (Arabic), he (Hebrew)
|
|
367
|
+
|
|
368
|
+
Examples:
|
|
369
|
+
# Translate Russian file to English
|
|
370
|
+
node translate-file.js .specweave/increments/0001/spec.md
|
|
371
|
+
|
|
372
|
+
# Preview translation
|
|
373
|
+
node translate-file.js spec.md --preview --verbose
|
|
374
|
+
|
|
375
|
+
# Translate to Spanish
|
|
376
|
+
node translate-file.js plan.md --target-lang es
|
|
377
|
+
`.trim());
|
|
378
|
+
process.exit(0);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const filePath = args[0];
|
|
382
|
+
let targetLang: SupportedLanguage = 'en';
|
|
383
|
+
let preview = false;
|
|
384
|
+
let verbose = false;
|
|
385
|
+
|
|
386
|
+
// Parse options
|
|
387
|
+
for (let i = 1; i < args.length; i++) {
|
|
388
|
+
const arg = args[i];
|
|
389
|
+
|
|
390
|
+
if (arg === '--target-lang' && args[i + 1]) {
|
|
391
|
+
targetLang = args[i + 1] as SupportedLanguage;
|
|
392
|
+
i++;
|
|
393
|
+
} else if (arg === '--preview') {
|
|
394
|
+
preview = true;
|
|
395
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
396
|
+
verbose = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
filePath,
|
|
402
|
+
targetLang,
|
|
403
|
+
preview,
|
|
404
|
+
verbose,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* CLI entry point
|
|
410
|
+
*/
|
|
411
|
+
async function main(): Promise<void> {
|
|
412
|
+
try {
|
|
413
|
+
const options = parseArgs();
|
|
414
|
+
const result = await translateFile(options);
|
|
415
|
+
|
|
416
|
+
// Exit with appropriate code
|
|
417
|
+
process.exit(result.success ? 0 : 1);
|
|
418
|
+
} catch (error: any) {
|
|
419
|
+
console.error(`❌ Translation failed: ${error.message}`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if running as main module (ESM)
|
|
425
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
426
|
+
if (isMainModule) {
|
|
427
|
+
main();
|
|
428
|
+
}
|
|
File without changes
|