@aigne/doc-smith 0.9.8 → 0.9.9-beta.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 +19 -0
- package/agents/create/aggregate-document-structure.mjs +21 -0
- package/agents/create/analyze-diagram-type-llm.yaml +1 -2
- package/agents/create/analyze-diagram-type.mjs +160 -2
- package/agents/create/generate-diagram-image.yaml +31 -0
- package/agents/create/generate-structure.yaml +1 -12
- package/agents/create/replace-d2-with-image.mjs +12 -27
- package/agents/create/utils/merge-document-structures.mjs +9 -3
- package/agents/localize/index.yaml +4 -0
- package/agents/localize/save-doc-translation-or-skip.mjs +18 -0
- package/agents/localize/set-review-content.mjs +58 -0
- package/agents/localize/translate-diagram.yaml +62 -0
- package/agents/localize/translate-document-wrapper.mjs +34 -0
- package/agents/localize/translate-multilingual.yaml +15 -9
- package/agents/localize/translate-or-skip-diagram.mjs +52 -0
- package/agents/publish/translate-meta.mjs +58 -6
- package/agents/update/generate-diagram.yaml +25 -8
- package/agents/update/index.yaml +1 -8
- package/agents/update/save-and-translate-document.mjs +5 -1
- package/agents/update/update-single/update-single-document-detail.mjs +52 -10
- package/agents/utils/analyze-feedback-intent.mjs +197 -80
- package/agents/utils/check-detail-result.mjs +14 -1
- package/agents/utils/choose-docs.mjs +3 -43
- package/agents/utils/save-doc-translation.mjs +2 -33
- package/agents/utils/save-doc.mjs +3 -37
- package/aigne.yaml +2 -2
- package/package.json +1 -1
- package/prompts/detail/diagram/generate-image-user.md +49 -0
- package/utils/d2-utils.mjs +10 -3
- package/utils/delete-diagram-images.mjs +3 -3
- package/utils/diagram-version-utils.mjs +14 -0
- package/utils/file-utils.mjs +40 -5
- package/utils/image-compress.mjs +1 -1
- package/utils/sync-diagram-to-translations.mjs +3 -3
- package/utils/translate-diagram-images.mjs +790 -0
- package/agents/update/check-sync-image-flag.mjs +0 -55
- package/agents/update/sync-images-and-exit.mjs +0 -148
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import { readFileContent } from "./docs-finder-utils.mjs";
|
|
2
|
+
import { debug } from "./debug.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import { copyFile } from "node:fs/promises";
|
|
6
|
+
import { diagramImageFullRegex } from "./d2-utils.mjs";
|
|
7
|
+
import { calculateImageTimestamp } from "./diagram-version-utils.mjs";
|
|
8
|
+
import { getFileName } from "./utils.mjs";
|
|
9
|
+
import { compressImage } from "./image-compress.mjs";
|
|
10
|
+
|
|
11
|
+
// Constants
|
|
12
|
+
const DEFAULT_DIAGRAM_TYPE = "architecture";
|
|
13
|
+
const DEFAULT_ASPECT_RATIO = "16:9";
|
|
14
|
+
const DEFAULT_ALT_TEXT = "Diagram";
|
|
15
|
+
const DEFAULT_IMAGE_QUALITY = 85;
|
|
16
|
+
const DEFAULT_IMAGE_SIZE = "1K";
|
|
17
|
+
const DEFAULT_MIME_TYPE = "image/jpeg";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Find translation files for a document, filtered by selected languages
|
|
21
|
+
* @param {string} docPath - Document path (e.g., "/guides/getting-started")
|
|
22
|
+
* @param {string} docsDir - Documentation directory
|
|
23
|
+
* @param {string} locale - Main language locale (e.g., "en")
|
|
24
|
+
* @param {Array<string>} selectedLanguages - Array of selected language codes to translate (e.g., ["zh", "ja"])
|
|
25
|
+
* @returns {Promise<Array<{language: string, fileName: string}>>} - Array of translation file info
|
|
26
|
+
*/
|
|
27
|
+
async function findTranslationFiles(docPath, docsDir, locale, selectedLanguages = null) {
|
|
28
|
+
// Convert path to flat filename format
|
|
29
|
+
const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const files = await fs.readdir(docsDir);
|
|
33
|
+
const translationFiles = [];
|
|
34
|
+
const mainFileName = locale === "en" ? `${flatName}.md` : `${flatName}.${locale}.md`;
|
|
35
|
+
|
|
36
|
+
// Filter files to find translation files matching the pattern
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
if (!file.endsWith(".md")) continue;
|
|
39
|
+
if (file === mainFileName) continue; // Skip main language file
|
|
40
|
+
|
|
41
|
+
// Case 1: File without language suffix (xxx.md) - this is English translation when main language is not English
|
|
42
|
+
if (file === `${flatName}.md` && locale !== "en") {
|
|
43
|
+
translationFiles.push({
|
|
44
|
+
language: "en",
|
|
45
|
+
fileName: file,
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Case 2: File with language suffix (xxx.{lang}.md) - all other translations
|
|
51
|
+
if (file.startsWith(`${flatName}.`) && file.match(/\.\w+(-\w+)?\.md$/)) {
|
|
52
|
+
const langMatch = file.match(/\.(\w+(-\w+)?)\.md$/);
|
|
53
|
+
if (langMatch && langMatch[1] !== locale) {
|
|
54
|
+
const fileLanguage = langMatch[1];
|
|
55
|
+
// If selectedLanguages is provided, only include files for selected languages
|
|
56
|
+
if (selectedLanguages === null || selectedLanguages.includes(fileLanguage)) {
|
|
57
|
+
translationFiles.push({
|
|
58
|
+
language: fileLanguage,
|
|
59
|
+
fileName: file,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return translationFiles;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
debug(`⚠️ Could not read translation files from ${docsDir}: ${error.message}`);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract diagram images with timestamp from content
|
|
75
|
+
* Supports both new format (with timestamp) and old format (without timestamp)
|
|
76
|
+
* Uses a single regex to extract all information in one pass
|
|
77
|
+
* @param {string} content - Document content
|
|
78
|
+
* @returns {Array<{type: string, aspectRatio: string, timestamp: string|null, path: string, altText: string, fullMatch: string, index: number}>} - Array of diagram image info
|
|
79
|
+
*/
|
|
80
|
+
function extractDiagramImagesWithTimestamp(content) {
|
|
81
|
+
const images = [];
|
|
82
|
+
|
|
83
|
+
// Use unified regex to match both old and new formats in one pass
|
|
84
|
+
// Captures: type, aspectRatio, optional timestamp, altText, path, fullMatch
|
|
85
|
+
const matches = Array.from((content || "").matchAll(diagramImageFullRegex));
|
|
86
|
+
for (const match of matches) {
|
|
87
|
+
images.push({
|
|
88
|
+
type: match[1] || DEFAULT_DIAGRAM_TYPE, // Diagram type (e.g., "architecture", "guide")
|
|
89
|
+
aspectRatio: match[2] || DEFAULT_ASPECT_RATIO, // Aspect ratio (e.g., "16:9", "4:3")
|
|
90
|
+
timestamp: match[3] || null, // Timestamp (null for old format)
|
|
91
|
+
altText: match[4] || DEFAULT_ALT_TEXT, // Alt text from markdown
|
|
92
|
+
path: match[5] || "", // Image path
|
|
93
|
+
fullMatch: match[0] || "", // Full matched block
|
|
94
|
+
index: match.index || 0, // Position in document
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sort by position in document
|
|
99
|
+
images.sort((a, b) => a.index - b.index);
|
|
100
|
+
|
|
101
|
+
return images;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Convert diagram info to mediaFile format for image generation
|
|
106
|
+
* @param {Object} diagramInfo - Diagram info with path
|
|
107
|
+
* @param {string} docPath - Document path
|
|
108
|
+
* @param {string} docsDir - Documentation directory
|
|
109
|
+
* @returns {Promise<Object|null>} - MediaFile object or null if file doesn't exist
|
|
110
|
+
*/
|
|
111
|
+
async function convertDiagramInfoToMediaFile(diagramInfo, docPath, docsDir) {
|
|
112
|
+
if (!diagramInfo || !diagramInfo.path) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const imagePath = diagramInfo.path;
|
|
118
|
+
|
|
119
|
+
// Resolve absolute path
|
|
120
|
+
let absolutePath;
|
|
121
|
+
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
122
|
+
// Remote URL, cannot convert to local file
|
|
123
|
+
return null;
|
|
124
|
+
} else if (path.isAbsolute(imagePath)) {
|
|
125
|
+
absolutePath = imagePath;
|
|
126
|
+
} else {
|
|
127
|
+
// Relative path resolution:
|
|
128
|
+
// - If path starts with "../", it's relative to the document directory
|
|
129
|
+
// - Otherwise, it's relative to docsDir (e.g., "assets/diagram/...")
|
|
130
|
+
if (imagePath.startsWith("../")) {
|
|
131
|
+
// Relative to document directory
|
|
132
|
+
const docDir = path.dirname(docPath);
|
|
133
|
+
const imageRelativePath = path.join(docDir, imagePath).replace(/\\/g, "/");
|
|
134
|
+
absolutePath = path.join(process.cwd(), docsDir, imageRelativePath);
|
|
135
|
+
} else {
|
|
136
|
+
// Relative to docsDir (most common case: "assets/diagram/...")
|
|
137
|
+
absolutePath = path.join(process.cwd(), docsDir, imagePath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Normalize path
|
|
142
|
+
const normalizedPath = path.normalize(absolutePath);
|
|
143
|
+
|
|
144
|
+
// Check if file exists
|
|
145
|
+
if (!(await fs.pathExists(normalizedPath))) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get file extension for mimeType detection
|
|
150
|
+
const ext = path.extname(normalizedPath).toLowerCase();
|
|
151
|
+
let mimeType = DEFAULT_MIME_TYPE;
|
|
152
|
+
if (ext === ".png") mimeType = "image/png";
|
|
153
|
+
else if (ext === ".gif") mimeType = "image/gif";
|
|
154
|
+
else if (ext === ".webp") mimeType = "image/webp";
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
type: "local",
|
|
158
|
+
path: normalizedPath,
|
|
159
|
+
filename: path.basename(normalizedPath),
|
|
160
|
+
mimeType,
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
debug(`Failed to convert diagram info to mediaFile: ${error.message}`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generate translated image filename with language suffix
|
|
170
|
+
* @param {string} originalPath - Original image path (e.g., "assets/diagram/overview-diagram-0.jpg")
|
|
171
|
+
* @param {string} language - Target language code (e.g., "zh")
|
|
172
|
+
* @returns {string} - Translated image path (e.g., "assets/diagram/overview-diagram-0.zh.jpg")
|
|
173
|
+
*/
|
|
174
|
+
function generateTranslatedImagePath(originalPath, language) {
|
|
175
|
+
const pathParts = originalPath.split(".");
|
|
176
|
+
if (pathParts.length < 2) {
|
|
177
|
+
// No extension, just append language
|
|
178
|
+
return `${originalPath}.${language}`;
|
|
179
|
+
}
|
|
180
|
+
// Insert language before extension
|
|
181
|
+
const ext = pathParts.pop();
|
|
182
|
+
const base = pathParts.join(".");
|
|
183
|
+
return `${base}.${language}.${ext}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Compress and copy generated image to target location
|
|
188
|
+
* Tries compression first, falls back to copying if compression fails
|
|
189
|
+
* @param {Object} generatedImage - Generated image object with path
|
|
190
|
+
* @param {string} targetPath - Target absolute path for the image
|
|
191
|
+
* @param {string} fileName - Translation file name for error reporting
|
|
192
|
+
* @returns {Promise<void>}
|
|
193
|
+
*/
|
|
194
|
+
async function compressAndCopyImage(generatedImage, targetPath, fileName = null) {
|
|
195
|
+
try {
|
|
196
|
+
const compressedPath = await compressImage(generatedImage.path, {
|
|
197
|
+
quality: DEFAULT_IMAGE_QUALITY,
|
|
198
|
+
outputPath: targetPath,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (compressedPath === generatedImage.path) {
|
|
202
|
+
// Compression failed, copy original
|
|
203
|
+
await copyFile(generatedImage.path, targetPath);
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(`copy original image ${fileName} to ${targetPath}`);
|
|
207
|
+
debug(`copy original image ${fileName} to ${targetPath}`, error);
|
|
208
|
+
await copyFile(generatedImage.path, targetPath);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function ensureImageTimestamp(imageInfo, docPath, docsDir) {
|
|
213
|
+
if (imageInfo.timestamp) {
|
|
214
|
+
return imageInfo.timestamp;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const existingImage = await convertDiagramInfoToMediaFile(
|
|
218
|
+
{ path: imageInfo.path },
|
|
219
|
+
docPath,
|
|
220
|
+
docsDir,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return existingImage
|
|
224
|
+
? await calculateImageTimestamp(existingImage.path)
|
|
225
|
+
: Math.floor(Date.now() / 1000).toString();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createImageMarkdown(diagramType, aspectRatio, timestamp, altText, imagePath) {
|
|
229
|
+
const type = diagramType || DEFAULT_DIAGRAM_TYPE;
|
|
230
|
+
const ratio = aspectRatio || DEFAULT_ASPECT_RATIO;
|
|
231
|
+
const alt = altText || DEFAULT_ALT_TEXT;
|
|
232
|
+
return `<!-- DIAGRAM_IMAGE_START:${type}:${ratio}:${timestamp} -->\n\n<!-- DIAGRAM_IMAGE_END -->`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const processedDocuments = new Set();
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Translate diagram images for translation documents
|
|
239
|
+
* Only translates images when timestamp differs between main and translation documents
|
|
240
|
+
* If main document images don't have timestamp (old format), generates timestamps and updates both main and translation documents
|
|
241
|
+
* @param {string} mainContent - Main document content (with timestamp in markers)
|
|
242
|
+
* @param {string} docPath - Document path
|
|
243
|
+
* @param {string} docsDir - Documentation directory
|
|
244
|
+
* @param {string} locale - Main language locale
|
|
245
|
+
* @param {Object} options - Options object with context for invoking agents
|
|
246
|
+
* @param {Array<string>} selectedLanguages - Selected languages to translate
|
|
247
|
+
* @returns {Promise<{updated: number, skipped: number, errors: Array, mainContentUpdated: string|null}>} - Translation result with updated main content
|
|
248
|
+
*/
|
|
249
|
+
export async function translateDiagramImages(
|
|
250
|
+
mainContent,
|
|
251
|
+
docPath,
|
|
252
|
+
docsDir,
|
|
253
|
+
locale = "en",
|
|
254
|
+
options = {},
|
|
255
|
+
selectedLanguages = null,
|
|
256
|
+
) {
|
|
257
|
+
// Avoid duplicate processing when called multiple times in iterate_on context
|
|
258
|
+
const documentKey = `${docPath}:${docsDir}:${locale}`;
|
|
259
|
+
if (processedDocuments.has(documentKey)) {
|
|
260
|
+
debug(`⏭️ Diagram images for ${docPath} already processed, skipping`);
|
|
261
|
+
return { updated: 0, skipped: 0, errors: [] };
|
|
262
|
+
}
|
|
263
|
+
processedDocuments.add(documentKey);
|
|
264
|
+
|
|
265
|
+
const result = {
|
|
266
|
+
updated: 0,
|
|
267
|
+
skipped: 0,
|
|
268
|
+
errors: [],
|
|
269
|
+
mainContentUpdated: null, // Will contain updated main content if timestamps were added
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// Find translation files, filtered by selected languages
|
|
274
|
+
const translationFiles = await findTranslationFiles(
|
|
275
|
+
docPath,
|
|
276
|
+
docsDir,
|
|
277
|
+
locale,
|
|
278
|
+
selectedLanguages,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (translationFiles.length === 0) {
|
|
282
|
+
debug("ℹ️ No translation files found, skipping image translation");
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const mainImages = extractDiagramImagesWithTimestamp(mainContent);
|
|
287
|
+
let updatedMainContent = mainContent;
|
|
288
|
+
let mainContentNeedsUpdate = false;
|
|
289
|
+
|
|
290
|
+
if (mainImages.length === 0) {
|
|
291
|
+
debug("ℹ️ No diagram images in main content, skipping translation");
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (let i = 0; i < mainImages.length; i++) {
|
|
296
|
+
const mainImage = mainImages[i];
|
|
297
|
+
if (!mainImage.timestamp) {
|
|
298
|
+
const newTimestamp = await ensureImageTimestamp(mainImage, docPath, docsDir);
|
|
299
|
+
mainImages[i] = { ...mainImage, timestamp: newTimestamp };
|
|
300
|
+
updatedMainContent = updatedMainContent.replace(
|
|
301
|
+
mainImage.fullMatch,
|
|
302
|
+
createImageMarkdown(
|
|
303
|
+
mainImage.type,
|
|
304
|
+
mainImage.aspectRatio,
|
|
305
|
+
newTimestamp,
|
|
306
|
+
mainImage.altText,
|
|
307
|
+
mainImage.path,
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
mainContentNeedsUpdate = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (mainContentNeedsUpdate) {
|
|
315
|
+
const mainFileName = getFileName(docPath, locale);
|
|
316
|
+
await fs.writeFile(path.join(docsDir, mainFileName), updatedMainContent, "utf8");
|
|
317
|
+
result.mainContentUpdated = updatedMainContent;
|
|
318
|
+
debug(`✅ Updated main document with timestamps: ${mainFileName}`);
|
|
319
|
+
}
|
|
320
|
+
for (const { language, fileName } of translationFiles) {
|
|
321
|
+
try {
|
|
322
|
+
const translationFilePath = path.join(docsDir, fileName);
|
|
323
|
+
const translationContent = await readFileContent(docsDir, fileName);
|
|
324
|
+
|
|
325
|
+
if (translationContent === null || translationContent === undefined) {
|
|
326
|
+
debug(`⚠️ Could not read translation file: ${fileName}`);
|
|
327
|
+
result.skipped++;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const translationImages = extractDiagramImagesWithTimestamp(translationContent);
|
|
332
|
+
let hasChanges = false;
|
|
333
|
+
let updatedContent = translationContent;
|
|
334
|
+
for (let i = 0; i < mainImages.length; i++) {
|
|
335
|
+
const mainImage = mainImages[i];
|
|
336
|
+
const translationImage = translationImages[i];
|
|
337
|
+
|
|
338
|
+
const translationImagePath = translationImage?.path || "";
|
|
339
|
+
const hasLanguageSuffix =
|
|
340
|
+
translationImagePath.includes(`.${language}.`) ||
|
|
341
|
+
translationImagePath.endsWith(`.${language}`);
|
|
342
|
+
|
|
343
|
+
const needsTranslation =
|
|
344
|
+
mainImage &&
|
|
345
|
+
(!translationImage ||
|
|
346
|
+
!hasLanguageSuffix ||
|
|
347
|
+
!translationImage.timestamp ||
|
|
348
|
+
translationImage.timestamp !== mainImage.timestamp);
|
|
349
|
+
|
|
350
|
+
if (!needsTranslation) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const existingImage = await convertDiagramInfoToMediaFile(
|
|
355
|
+
{ path: mainImage.path },
|
|
356
|
+
docPath,
|
|
357
|
+
docsDir,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
if (!existingImage) {
|
|
361
|
+
debug(
|
|
362
|
+
`⏭️ Main image not found: ${mainImage.path}, skipping translation for ${language}`,
|
|
363
|
+
);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const translateDiagramAgent = options.context?.agents?.["translateDiagram"];
|
|
369
|
+
if (!translateDiagramAgent) {
|
|
370
|
+
debug(`⚠️ translateDiagram agent not found, skipping translation`);
|
|
371
|
+
result.errors.push({
|
|
372
|
+
file: fileName,
|
|
373
|
+
imageIndex: i,
|
|
374
|
+
error: "translateDiagram agent not found",
|
|
375
|
+
});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Call translateDiagram agent with translation parameters
|
|
380
|
+
const imageResult = await options.context.invoke(translateDiagramAgent, {
|
|
381
|
+
existingImage: [existingImage], // Pass the main document image for translation
|
|
382
|
+
ratio: mainImage.aspectRatio || DEFAULT_ASPECT_RATIO, // Aspect ratio from main image
|
|
383
|
+
size: DEFAULT_IMAGE_SIZE, // Image clarity/size
|
|
384
|
+
locale: language, // Target language code for translation
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
let generatedImage = null;
|
|
388
|
+
if (
|
|
389
|
+
imageResult?.images &&
|
|
390
|
+
Array.isArray(imageResult.images) &&
|
|
391
|
+
imageResult.images.length > 0
|
|
392
|
+
) {
|
|
393
|
+
generatedImage = imageResult.images[0];
|
|
394
|
+
} else if (imageResult?.image || imageResult?.imageUrl || imageResult?.path) {
|
|
395
|
+
generatedImage = {
|
|
396
|
+
path: imageResult.image || imageResult.imageUrl || imageResult.path,
|
|
397
|
+
filename: path.basename(
|
|
398
|
+
imageResult.image || imageResult.imageUrl || imageResult.path,
|
|
399
|
+
),
|
|
400
|
+
mimeType: imageResult.mimeType || DEFAULT_MIME_TYPE,
|
|
401
|
+
type: "local",
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!generatedImage) {
|
|
406
|
+
debug(`⚠️ No image generated for ${language} diagram ${i}`);
|
|
407
|
+
result.errors.push({
|
|
408
|
+
file: fileName,
|
|
409
|
+
imageIndex: i,
|
|
410
|
+
error: "No image generated from agent",
|
|
411
|
+
});
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const translatedImagePath = generateTranslatedImagePath(mainImage.path, language);
|
|
416
|
+
const translatedImageAbsolutePath = path.join(
|
|
417
|
+
process.cwd(),
|
|
418
|
+
docsDir,
|
|
419
|
+
translatedImagePath,
|
|
420
|
+
);
|
|
421
|
+
await fs.ensureDir(path.dirname(translatedImageAbsolutePath));
|
|
422
|
+
await compressAndCopyImage(generatedImage, translatedImageAbsolutePath, fileName);
|
|
423
|
+
|
|
424
|
+
const altText = translationImage?.altText || mainImage.altText || DEFAULT_ALT_TEXT;
|
|
425
|
+
const newImageMarkdown = createImageMarkdown(
|
|
426
|
+
mainImage.type,
|
|
427
|
+
mainImage.aspectRatio,
|
|
428
|
+
mainImage.timestamp,
|
|
429
|
+
altText,
|
|
430
|
+
translatedImagePath,
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (translationImage) {
|
|
434
|
+
updatedContent = updatedContent.replace(translationImage.fullMatch, newImageMarkdown);
|
|
435
|
+
} else {
|
|
436
|
+
const lastImageIndex =
|
|
437
|
+
translationImages.length > 0
|
|
438
|
+
? translationImages[translationImages.length - 1].index +
|
|
439
|
+
translationImages[translationImages.length - 1].fullMatch.length
|
|
440
|
+
: updatedContent.length;
|
|
441
|
+
updatedContent =
|
|
442
|
+
updatedContent.slice(0, lastImageIndex) +
|
|
443
|
+
"\n\n" +
|
|
444
|
+
newImageMarkdown +
|
|
445
|
+
"\n\n" +
|
|
446
|
+
updatedContent.slice(lastImageIndex);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
hasChanges = true;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error(`❌ Error translating diagram image ${fileName} for ${language}`);
|
|
452
|
+
debug(`❌ Error translating diagram image ${fileName} for ${language}`, error);
|
|
453
|
+
result.errors.push({
|
|
454
|
+
file: fileName,
|
|
455
|
+
imageIndex: i,
|
|
456
|
+
error: error.message,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (hasChanges) {
|
|
462
|
+
await fs.writeFile(translationFilePath, updatedContent, "utf8");
|
|
463
|
+
result.updated++;
|
|
464
|
+
} else {
|
|
465
|
+
result.skipped++;
|
|
466
|
+
}
|
|
467
|
+
} catch (error) {
|
|
468
|
+
debug(`❌ Error processing translation file ${fileName}: ${error.message}`);
|
|
469
|
+
result.errors.push({
|
|
470
|
+
file: fileName,
|
|
471
|
+
error: error.message,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return result;
|
|
477
|
+
} finally {
|
|
478
|
+
// Clear processed documents after completion (even on error) to allow re-processing if needed
|
|
479
|
+
processedDocuments.delete(documentKey);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Cache diagram images for translation (before document translation)
|
|
485
|
+
* This function checks if images need translation and caches the translated image info
|
|
486
|
+
* @param {string} mainContent - Main document content
|
|
487
|
+
* @param {string} translationContent - Current translation document content (may be empty for new translations)
|
|
488
|
+
* @param {string} docPath - Document path
|
|
489
|
+
* @param {string} docsDir - Documentation directory
|
|
490
|
+
* @param {string} locale - Main language locale
|
|
491
|
+
* @param {string} language - Target language code
|
|
492
|
+
* @param {Object} options - Options object with context for invoking agents
|
|
493
|
+
* @param {boolean} shouldTranslateDiagramsOnly - Whether to translate diagrams only (from --diagram flag)
|
|
494
|
+
* @returns {Promise<Array<{originalMatch: string, translatedMarkdown: string, index: number}>|null>} - Cached image info or null
|
|
495
|
+
*/
|
|
496
|
+
export async function cacheDiagramImagesForTranslation(
|
|
497
|
+
mainContent,
|
|
498
|
+
translationContent,
|
|
499
|
+
docPath,
|
|
500
|
+
docsDir,
|
|
501
|
+
locale = "en",
|
|
502
|
+
language,
|
|
503
|
+
options = {},
|
|
504
|
+
shouldTranslateDiagramsOnly = false,
|
|
505
|
+
) {
|
|
506
|
+
try {
|
|
507
|
+
const mainImages = extractDiagramImagesWithTimestamp(mainContent);
|
|
508
|
+
let updatedMainContent = mainContent;
|
|
509
|
+
let mainContentNeedsUpdate = false;
|
|
510
|
+
|
|
511
|
+
if (mainImages.length === 0) {
|
|
512
|
+
debug("ℹ️ No diagram images in main content");
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (let i = 0; i < mainImages.length; i++) {
|
|
517
|
+
const mainImage = mainImages[i];
|
|
518
|
+
if (!mainImage.timestamp) {
|
|
519
|
+
const newTimestamp = await ensureImageTimestamp(mainImage, docPath, docsDir);
|
|
520
|
+
mainImages[i] = { ...mainImage, timestamp: newTimestamp };
|
|
521
|
+
updatedMainContent = updatedMainContent.replace(
|
|
522
|
+
mainImage.fullMatch,
|
|
523
|
+
createImageMarkdown(
|
|
524
|
+
mainImage.type,
|
|
525
|
+
mainImage.aspectRatio,
|
|
526
|
+
newTimestamp,
|
|
527
|
+
mainImage.altText,
|
|
528
|
+
mainImage.path,
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
mainContentNeedsUpdate = true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (mainContentNeedsUpdate) {
|
|
536
|
+
const mainFileName = getFileName(docPath, locale);
|
|
537
|
+
await fs.writeFile(path.join(docsDir, mainFileName), updatedMainContent, "utf8");
|
|
538
|
+
debug(`✅ Updated main document with timestamps: ${mainFileName}`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const translationImages = translationContent
|
|
542
|
+
? extractDiagramImagesWithTimestamp(translationContent)
|
|
543
|
+
: [];
|
|
544
|
+
|
|
545
|
+
const cachedImages = [];
|
|
546
|
+
|
|
547
|
+
for (let i = 0; i < mainImages.length; i++) {
|
|
548
|
+
const mainImage = mainImages[i];
|
|
549
|
+
const translationImage = translationImages[i];
|
|
550
|
+
|
|
551
|
+
let needsTranslation = false;
|
|
552
|
+
|
|
553
|
+
if (shouldTranslateDiagramsOnly) {
|
|
554
|
+
// When --diagram flag is set, always regenerate translation images regardless of existing images
|
|
555
|
+
needsTranslation = true;
|
|
556
|
+
debug(
|
|
557
|
+
`🔄 --diagram flag set: forcing regeneration of translation for ${language} diagram ${i}`,
|
|
558
|
+
);
|
|
559
|
+
} else {
|
|
560
|
+
// Normal mode: check if translation is needed based on timestamp and language suffix
|
|
561
|
+
const translationImagePath = translationImage?.path || "";
|
|
562
|
+
const hasLanguageSuffix =
|
|
563
|
+
translationImagePath.includes(`.${language}.`) ||
|
|
564
|
+
translationImagePath.endsWith(`.${language}`);
|
|
565
|
+
needsTranslation =
|
|
566
|
+
!translationImage ||
|
|
567
|
+
!hasLanguageSuffix ||
|
|
568
|
+
!translationImage.timestamp ||
|
|
569
|
+
translationImage.timestamp !== mainImage.timestamp;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!needsTranslation) {
|
|
573
|
+
// No translation needed, but we should cache the existing translation image
|
|
574
|
+
// to ensure it's preserved in the final document (this applies to both normal and --diagram mode)
|
|
575
|
+
if (translationImage) {
|
|
576
|
+
cachedImages.push({
|
|
577
|
+
originalMatch: translationImage.fullMatch,
|
|
578
|
+
translatedMarkdown: translationImage.fullMatch, // Keep existing markdown
|
|
579
|
+
index: translationImage.index,
|
|
580
|
+
mainImageIndex: mainImage.index,
|
|
581
|
+
});
|
|
582
|
+
debug(
|
|
583
|
+
`💾 Cached existing diagram image for ${language} diagram ${i} (timestamp matches, no translation needed)`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const existingImage = await convertDiagramInfoToMediaFile(
|
|
590
|
+
{ path: mainImage.path },
|
|
591
|
+
docPath,
|
|
592
|
+
docsDir,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
if (!existingImage) {
|
|
596
|
+
debug(`⏭️ Main image not found: ${mainImage.path}, skipping translation for ${language}`);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const translateDiagramAgent = options.context?.agents?.["translateDiagram"];
|
|
602
|
+
if (!translateDiagramAgent) {
|
|
603
|
+
debug(`⚠️ translateDiagram agent not found, skipping translation`);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const imageResult = await options.context.invoke(translateDiagramAgent, {
|
|
608
|
+
existingImage: [existingImage],
|
|
609
|
+
ratio: mainImage.aspectRatio || DEFAULT_ASPECT_RATIO,
|
|
610
|
+
size: DEFAULT_IMAGE_SIZE,
|
|
611
|
+
locale: language,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
let generatedImage = null;
|
|
615
|
+
if (
|
|
616
|
+
imageResult?.images &&
|
|
617
|
+
Array.isArray(imageResult.images) &&
|
|
618
|
+
imageResult.images.length > 0
|
|
619
|
+
) {
|
|
620
|
+
generatedImage = imageResult.images[0];
|
|
621
|
+
} else if (imageResult?.image || imageResult?.imageUrl || imageResult?.path) {
|
|
622
|
+
generatedImage = {
|
|
623
|
+
path: imageResult.image || imageResult.imageUrl || imageResult.path,
|
|
624
|
+
filename: path.basename(imageResult.image || imageResult.imageUrl || imageResult.path),
|
|
625
|
+
mimeType: imageResult.mimeType || DEFAULT_MIME_TYPE,
|
|
626
|
+
type: "local",
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (!generatedImage) {
|
|
631
|
+
debug(`⚠️ No image generated for ${language} diagram ${i}`);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const translatedImagePath = generateTranslatedImagePath(mainImage.path, language);
|
|
636
|
+
const translatedImageAbsolutePath = path.join(process.cwd(), docsDir, translatedImagePath);
|
|
637
|
+
await fs.ensureDir(path.dirname(translatedImageAbsolutePath));
|
|
638
|
+
const translationFileName = getFileName(docPath, language);
|
|
639
|
+
await compressAndCopyImage(
|
|
640
|
+
generatedImage,
|
|
641
|
+
translatedImageAbsolutePath,
|
|
642
|
+
translationFileName,
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const altText = translationImage?.altText || mainImage.altText || DEFAULT_ALT_TEXT;
|
|
646
|
+
const newImageMarkdown = createImageMarkdown(
|
|
647
|
+
mainImage.type,
|
|
648
|
+
mainImage.aspectRatio,
|
|
649
|
+
mainImage.timestamp,
|
|
650
|
+
altText,
|
|
651
|
+
translatedImagePath,
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
cachedImages.push({
|
|
655
|
+
originalMatch: translationImage?.fullMatch || null,
|
|
656
|
+
translatedMarkdown: newImageMarkdown,
|
|
657
|
+
index: translationImage?.index || mainImage.index,
|
|
658
|
+
mainImageIndex: mainImage.index,
|
|
659
|
+
});
|
|
660
|
+
} catch (error) {
|
|
661
|
+
const translationFileName = getFileName(docPath, language);
|
|
662
|
+
console.error(`❌ Error translating diagram image ${translationFileName} for ${language}`);
|
|
663
|
+
debug(`❌ Error translating diagram image ${translationFileName} for ${language}`, error);
|
|
664
|
+
// Continue processing other images even if one fails
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return cachedImages.length > 0 ? cachedImages : null;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
debug(`❌ Error caching diagram images: ${error.message}`);
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Agent wrapper for caching diagram images during translation workflow
|
|
677
|
+
* This agent is called BEFORE translate-document-wrapper.mjs to cache image info
|
|
678
|
+
* @param {Object} input - Input parameters
|
|
679
|
+
* @param {string} input.path - Document path
|
|
680
|
+
* @param {string} input.docsDir - Documentation directory
|
|
681
|
+
* @param {string} input.locale - Main language locale
|
|
682
|
+
* @param {boolean} input.shouldTranslateDiagramsOnly - Whether to translate diagrams only (from --diagram flag)
|
|
683
|
+
* @param {string} input.language - Current language being translated
|
|
684
|
+
* @param {Object} options - Options with context for invoking agents
|
|
685
|
+
* @returns {Promise<Object>} - Result object with cached image info
|
|
686
|
+
*/
|
|
687
|
+
export default async function translateDiagramImagesAgent(input, options) {
|
|
688
|
+
// Extract parameters from input
|
|
689
|
+
const docPath = input.path;
|
|
690
|
+
const docsDir = input.docsDir;
|
|
691
|
+
const locale = input.locale || "en";
|
|
692
|
+
const currentLanguage = input.language;
|
|
693
|
+
const shouldTranslateDiagramsOnly = input.shouldTranslateDiagramsOnly || false;
|
|
694
|
+
|
|
695
|
+
if (!docPath || !docsDir || !currentLanguage) {
|
|
696
|
+
debug(
|
|
697
|
+
"⚠️ Missing required parameters for diagram image translation (path, docsDir, or language)",
|
|
698
|
+
);
|
|
699
|
+
debug(` - path: ${docPath}`);
|
|
700
|
+
debug(` - docsDir: ${docsDir}`);
|
|
701
|
+
debug(` - language: ${currentLanguage}`);
|
|
702
|
+
return { ...input, cachedDiagramImages: null };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
// Read main document content
|
|
707
|
+
const mainFileName = getFileName(docPath, locale);
|
|
708
|
+
const mainContent = await readFileContent(docsDir, mainFileName);
|
|
709
|
+
|
|
710
|
+
if (!mainContent) {
|
|
711
|
+
debug(`⚠️ Could not read main document: ${mainFileName}`);
|
|
712
|
+
return { ...input, cachedDiagramImages: null };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Read current translation file content (if exists) to check timestamps
|
|
716
|
+
const translationFileName = getFileName(docPath, currentLanguage);
|
|
717
|
+
const translationContent = await readFileContent(docsDir, translationFileName);
|
|
718
|
+
|
|
719
|
+
// Cache diagram images for translation
|
|
720
|
+
// This function will:
|
|
721
|
+
// 1. If --diagram flag is set, always translate images
|
|
722
|
+
// 2. Otherwise, check if main document image timestamps match translation document timestamps
|
|
723
|
+
// 3. Cache the image info that needs to be inserted into translated document
|
|
724
|
+
const cachedImages = await cacheDiagramImagesForTranslation(
|
|
725
|
+
mainContent,
|
|
726
|
+
translationContent || "",
|
|
727
|
+
docPath,
|
|
728
|
+
docsDir,
|
|
729
|
+
locale,
|
|
730
|
+
currentLanguage,
|
|
731
|
+
options,
|
|
732
|
+
shouldTranslateDiagramsOnly,
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
if (cachedImages && cachedImages.length > 0) {
|
|
736
|
+
// Check if any images were actually translated (not just cached existing ones)
|
|
737
|
+
const translatedCount = cachedImages.filter(
|
|
738
|
+
(img) => img.translatedMarkdown !== img.originalMatch,
|
|
739
|
+
).length;
|
|
740
|
+
if (translatedCount > 0) {
|
|
741
|
+
debug(
|
|
742
|
+
`✅ Cached ${cachedImages.length} diagram image(s) for ${currentLanguage} (${translatedCount} translated)`,
|
|
743
|
+
);
|
|
744
|
+
} else {
|
|
745
|
+
debug(
|
|
746
|
+
`ℹ️ Cached ${cachedImages.length} existing diagram image(s) for ${currentLanguage} (no translation needed)`,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
debug(`ℹ️ No diagram images found or cached for ${currentLanguage}`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// In --diagram mode:
|
|
754
|
+
// - If translation document exists: use existing translation content (skip document translation, only replace images)
|
|
755
|
+
// - If translation document doesn't exist: allow document translation first, then replace images
|
|
756
|
+
let finalTranslation = input.translation;
|
|
757
|
+
let finalIsApproved = input.isApproved;
|
|
758
|
+
|
|
759
|
+
if (shouldTranslateDiagramsOnly) {
|
|
760
|
+
if (translationContent) {
|
|
761
|
+
// Translation document exists: use existing content, skip document translation
|
|
762
|
+
finalTranslation = translationContent;
|
|
763
|
+
finalIsApproved = true; // Skip document translation
|
|
764
|
+
debug(
|
|
765
|
+
`ℹ️ --diagram mode: using existing translation content for ${currentLanguage} (will only replace diagram images)`,
|
|
766
|
+
);
|
|
767
|
+
} else {
|
|
768
|
+
// Translation document doesn't exist: allow document translation first
|
|
769
|
+
finalTranslation = undefined; // Let translate-document-wrapper.mjs handle translation
|
|
770
|
+
finalIsApproved = false; // Allow document translation
|
|
771
|
+
debug(
|
|
772
|
+
`ℹ️ --diagram mode: translation document not found for ${currentLanguage}, will translate document first, then replace diagram images`,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
...input,
|
|
779
|
+
translation: finalTranslation,
|
|
780
|
+
isApproved: finalIsApproved,
|
|
781
|
+
cachedDiagramImages: cachedImages || null,
|
|
782
|
+
};
|
|
783
|
+
} catch (error) {
|
|
784
|
+
// Don't fail the translation if image translation fails
|
|
785
|
+
debug(`⚠️ Failed to cache diagram images: ${error.message}`);
|
|
786
|
+
return { ...input, cachedDiagramImages: null };
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
translateDiagramImagesAgent.task_render_mode = "hide";
|